消息中心

所涉及到的知识点

方案一

1.使用双重锁机制实现的单例模式,降低synchronized带来的性能开销,但同时需要注意加上volatile修饰(通过禁止重排序来保证线程安全的延迟初始化);

http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

java中volatile关键字的含义

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

2.Java原子变量

http://www.cnblogs.com/Gordon-YangYiBao/archive/2012/08/07/2626422.html

http://blog.csdn.net/hong0220/article/details/38958121

http://www.cnblogs.com/timlearn/p/4127616.html

3.线程池

深入理解线程池:http://www.cnblogs.com/dolphin0520/p/3932921.html

线程池:http://ifeve.com/thread-pools/

http://ifeve.com/java-threadpool/

如何在线程池中寻找堆栈?

https://github.com/ConnL/littleprogram/blob/master/TraceThreadPoolExecutorMain.java

线程池与生产者-消费者模式

"Java中的线程池类其实就是一种生产者和消费者模式的实现方式,但是我觉得其实现方式更加高明。生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然慢一些。"(参考http://ifeve.com/producers-and-consumers-mode/ 清英)

4.生产者-消费者模式

经典的多线程设计模式,为多线程间的协作提供了良好的解决方案。生产者和消费者之间通过共享内存缓存区进行通信。共享内存缓存区的主要功能是数据在多线程间的共享,此外通过该缓存区,可以缓解生产者和消费者间的性能差,作为生产者和消费者间的通信桥梁,避免了生产者和消费者的直接通信,从而将生产者和消费者进行解耦。

具体参考:“语言-高并发-生产者消费者模式”

5.阻塞队列

阻塞队列的基本概念以及简单实现 http://ifeve.com/blocking-queues/

如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,让生产者和消费者能够高效率的进行通讯呢?让我们先来看看JDK是如何实现的。

使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现。

比如当执行take操作时,如果队列为空,则让当前线程等待在notEmpty上。当新元素进入队列后,需要通知等待在notEmpty上的线程,让他们继续工作。对于put操作也是一样的,当队列满时,需要让压入线程等待,当有元素从队列中被挪走时,队列中出现空位时,自然也需要通知等待入队的线程。

http://www.infoq.com/cn/articles/java-blocking-queue/

方案二

1.如何实现线程通信

传统的线程通信:

Object类提供的wait()、notify()、notify()三个方法,这三个方法必须由同步监视器对象来调用(在调用这些方法之前,线程必须获得该对象的对象级别锁),可以分为以下两种情况:

对于使用synchronize修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步方法中直接调用这三个方法;

对于使用synchronize修饰的同步代码块,同步监视器是synchronize后括号里的对象,所以必须使用该对象调用这三个方法。

wait方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒

notify方法可以随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进行可运行状态。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

notifyAll方法可以使所有正在等待队列中等待同一共享资源的“全部线程”从等待状态退出,进入可运行状态。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

wait使线程停止运行,而notify使停止的线程继续运行。

2.如何实现线程的挂起(suspend)和继续执行(resume)?

JAVA多线程机制之暂停、恢复和停止

http://blog.csdn.net/jianggujin/article/details/50458486

Java 线程暂停与继续

http://blog.csdn.net/ritterliu/article/details/7453264

3.ConcurrentHashMap以及线程安全的JDK并发容器

(1)包装方式

基本原理:使用委托,将自己所有的map功能交给传入的HashMap实现,而自己则主要负责保证线程安全。无论是对Map的读取或者写入,都需要获得mutex的锁,这会导致所有对map的操作都进入等待状态,直到mutex可用,如果并发级别不高,一般也够用。但是,在高并发环境中,我们也有必要寻求新的解决方案。

(2)一种更加专业的并发HashMap是ConcurrentHashMap

ConcurrentHashMap原理分析:https://my.oschina.net/hosee/blog/639352

不变模式:参考《Java高并发程序设计》

简要理解如下:

Hashtable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了分段锁技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

如何确定元素的存取位置?

三次hash:

  1. 对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
  2. 将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
  3. 将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。

get操作不需要加锁

不变(Immutable)和易变(Volatile)ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:

static final class HashEntry<K,V> { final K key; final int hash; volatile V value; volatile HashEntry<K,V> next; }

不变模式(immutable)是多线程安全里最简单的一种保障方式。通过volatile和final来确保数据安全。

put操作需要加锁

size操作三次尝试(做法比较巧妙)

size操作与put和get操作最大的区别在于,size操作需要遍历所有的Segment才能算出整个Map的大小,而put和get都只关心一个Segment。假设我们当前遍历的Segment为SA,那么在遍历SA过程中其他的Segment比如SB可能会被修改,于是这一次运算出来的size值可能并不是Map当前的真正大小。所以一个比较简单的办法就是计算Map大小的时候所有的Segment都Lock住,不能更新(包含put,remove等等)数据,计算完之后再Unlock。这是普通人能够想到的方案,但是牛逼的作者还有一个更好的Idea:先给3次机会,不lock所有的Segment,遍历所有Segment,累加各个Segment的大小得到整个Map的大小,如果某相邻的两次计算获取的所有Segment的更新的次数(每个Segment都有一个modCount变量,这个变量在Segment中的Entry被修改时会加一,通过这个值可以得到每个Segment的更新操作的次数)是一样的,说明计算过程中没有更新操作,则直接返回这个值。如果这三次不加锁的计算过程中Map的更新次数有变化,则之后的计算先对所有的Segment加锁,再遍历所有Segment计算Map大小,最后再解锁所有Segment。

初始化参数

  • initialCapacity表示新创建的这个ConcurrentHashMap的初始容量,也就是上面的结构图中的Entry数量。默认值为static final int DEFAULT_INITIAL_CAPACITY = 16;
  • loadFactor表示负载因子,就是当ConcurrentHashMap中的元素个数大于loadFactor * 最大容量时就需要rehash,扩容。默认值为static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • concurrencyLevel表示并发级别,这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的Segment中,则这些线程能够无竞争地自由访问(因为他们不需要竞争同一把锁),达到同时访问的效果。这也是为什么这个参数起名为“并发级别”的原因。

hash算法:

这里用到了Wang/Jenkins hash算法的变种,主要的目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

4.关于单元测试,mock测试

编写更好的http://blog.csdn.net/qq_35101189/article/details/53611041

http://blog.csdn.net/chjttony/article/details/14522771

https://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/

两种方案限流及主要思路

方案一

限流在消息服务器端实现,通过轮询,控制在INTERVAL(60000毫秒)时间内从服务器端阻塞队列中取出最多max条消息,创建对应的消息发送器MessageSender(实现Runnable接口)然后提交给线程池(newFixedThreadPool,线程数量为5)去执行消息的发送(模拟实现)。而消息中心(MessageCenter)主要实现对外提供消息的添加(push)、消息发送器注册(register)、消息分发(dispatchMessage)、消息中心开启(start)以及消息中心关闭(shutdown),其中添加即往消息中心阻塞队列塞各类消息,注册即往map(需考虑线程安全)中放messagetype-messageServer对,分发即把消息中心阻塞队列中的消息向各自的服务器阻塞队列中进行添加,开启和关闭借助标志isRunning(AtomicBoolean类型)实现。各类消息服务器和消息中心均为单例(使用双重锁机制实现,需注意加volatile修饰保证线程安全的延迟初始化)。

方案二

限流在消息中心实现,在消息中心中通过创建专门的发送线程sendThread从消息中心阻塞队列中取出消息,然后进行发送操作,限流完全借助sendThread的sleep(sendSpan)(sendSpan为消息发送时间间隔,如每分钟发送的消息数量为count,则二者的关系为:sendSpan = 60 * 1000 / count)来实现。消息中心对外提供:setSendExecutors(重新设置消息发送线程池,用于优化发送速度)、setMsgSendCountPerMinute(设置每分钟的消息发送数量,从而改变默认的sendSpan值用于不同类型消息的限流)、registerOrReplaceSender(在ConcurrentHashMap中注册或者更换消息发送器)、addMsg(向消息中心传递消息,采用非阻塞方式,当消息池满后抛出异常)、start(启动/重启消息发送服务,通过对sendThread的开启和恢复来实现)、suspend(暂停发送消息,通过对sendThread的挂起实现)、sendMsg(私有方法,根据消息获取对应的消息发送器,创建一个Runnable对象然后提交给线程池执行)。这里面sendThread的挂起(suspend)和恢复(resume)需借助传统的线程通信wait和notifyAll来实现可靠的线程挂起和恢复。

猜你喜欢

转载自my.oschina.net/u/2939155/blog/1632912
今日推荐