java并发编程(五)java常用的并发编程工具类

同步容器类

同步容器类包括vector和HashTable,这些同步类都是使用synchronizedXXX等工厂方法创建的,简单粗暴的将整个对象锁起来,效率很差。现在大多已经被弃用了。

并发容器类

并发容器类主要包括concurrentHashMap和copyOnWriteList。

ConcurrentHashMap

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
在jdk1.8中主要改进
1.8的并发控制使用Synchronized和CAS来操作。1.8之后Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本,新版本使用和HashMap一样的数据结构每个数组位置使用一个锁现在锁定的是一个Node头节点(注意,synchronized锁定的是头结点),减小了锁的粒度,性能和冲突都会减少。
将原先table数组+单向链表的数据结构,变更为Node数组+单向链表+红黑树的结构。
具体了解可查看之前的博客Java 集合详解

CopyOnWriteList

SynchronizedList是java.util.Collections中的一个静态内部类,其实现安全的手段稍微有一点优化,就是把Vector加在方法上的synchronized关键字,移到了方法里面变成了同步块而不是同步方法从而把锁的范围缩小了。
CopyOnWriteArrayList这个类对于写来说是基于重入锁互斥的,对于读操作来说是无锁的。
CopyOnWriteArrayLis的缺点

  • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的。因为每次add()、set()、remove()这些增删改操作都要复制一个数组出来。先获得锁,然后拷贝元素组并将新元素加入,再替换掉原来的数组。我们会发现这种实现方式非常不适合频繁修改的操作。
  • 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用setArray()了)。但是线程A迭代出来的是原有的数据。因为B是在拷贝出来的数据做操作,只有替换原数组时其他线程才能看见更新数据。

使用场景
整体来说CopyOnWriteArrayList是另类的线程安全的实现,但并一定是高效的,适合用在读取和遍历多的场景下,并不适合写并发高的场景,因为数组的拷贝也是非常耗时的,尤其是数据量大的情况下。

总结

  • CopyOnWriteArrayList基于可重入锁机制,增删改操作需要加锁,读操作不需要加锁;
  • CopyOnWriteArrayList适合用在读取和遍历多的场景下,并不适合写并发高的场景;
  • 基于fail-safe机制,不会抛出CurrentModifyException;
  • 另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。

阻塞方法

线程在执行过程可能会阻塞或暂停执行,比如等待I/O,等待获得锁,等待sleep结束,或者等待其他线程的计算结果等等。

InterruptedException 异常
当一个方法后面声明可能会抛出InterruptedException 异常时,说明该方法是可能会花一点时间,但是可以取消的方法。
抛InterruptedException的代表方法有:

  1. Java.lang.Object 类的 wait 方法
  2. java.lang.Thread 类的 sleep 方法
  3. java.lang.Thread 类的 join 方法

当这些方法抛出InterruptedException我们可以通过interrupt方法来取消这些线程。取消后会继续抛出InterruptedException以便通知调用这个线程的线程这个线程已经阻塞并进行处理。
注意任何一个线程都不能强制停止另一个线程转而执行其他线程。只有当线程阻塞而抛出InterruptedException 异常时interrupt方法才能取消线程执行。

同步工具类

同步工具类可以是任何一个对象。简单来说同步工具类就是一个协调其他线程工作的类。java类库有很多同步工具类,如果这些类无法满足需求还可以自定义同步工具类。自定义同步工具类会在后续博客中详细讲解。下面详细说Java类库一些主要的同步工具类。
同步工具类主要类型包括阻塞队列,闭锁,信号量,栅栏。

阻塞队列

阻塞队列提供了可阻塞的put方法和take方法。如果队列满了put方法会阻塞,如果队列为空take方法会阻塞。队列可以是有界的也可以是无界的,无界阻塞队列永远不会满也就是说put方法不会阻塞。
在线程池里对阻塞队列有详细应用。

扫描二维码关注公众号,回复: 9637320 查看本文章

闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到结束。
主要有几种应用场景:

  • 确保某个线程所需的资源全部初始化才开始运行。
  • 确保某个服务所依赖的所有服务都启动之后才启动。
  • 确保某个业务的全部参与线程都准备就绪时才开始运行。

FutureTask
说到线程延迟可能我们最熟悉的就是FutureTask,FutureTask就是一种闭锁。
FutureTask通过Callable实现,相当于一种可以生成执行结果的线程。
只有当FutureTask内部的线程执行完成。通过Future.get才能获取其内部线程的执行结果。如果内部线程没有执行完。调用Future.get的线程会一直阻塞直到其完成。

当某个线程执行前需要依赖一系列的初始化操作。可以将这些初始化操作交给不同线程执行,然后在futureTask内的线程去运行这些初始化线程。外部阻塞初始化的线程只需要等待future.get正确返回结果后便可以正常运行了。

CountDownLatch
CountDownLatch是一种灵活闭锁实现,可以在上述各种闭锁情况中使用。
CountDownLatch内部包括一个计数器,它将被初始化为一个正整数。countDown方法递减计数器,每调用一次计数器减一。await方法会等待计数器到达0。这说明要等待的所有任务都执行完成。停止阻塞继续执行后续的线程。
比如声明一个需要等待3个任务执行完的闭锁。
CountDownLatch latch = new CountDownLatch(3);
3个任务执行完成后分别调用latch.countDown(); 方法
需要等待这三个任务执行完才能执行的线程 在执行前调用latch.await(); 方法
那么当3个任务执行完计数器被减为0,这个等待线程会开始执行。

信号量

计数信号量用来控制同时访问某个资源的操作数量,最典型的就是连接池,我们可以控制同一时间的连接数量。
Semaphore
Semaphore是java类库为我们提供的实现信号量的同步工具类。
Semaphore内部管理者一组虚拟的许可,可以理解为一个计数值,在初始化一个Semaphore时对许可进行初始化。两个主要方法acquire方法用于申请许可。release方法用于释放许可。当许可数量被申请完时acquire会阻塞直到有线程调用release方法释放许可。
例如一个简单的连接池,大小为10。
首先创建信号量Semaphore semaphore = new Semaphore(10);
当线程需要申请连接时,连接前申请许可semaphore.acquire();
当连接使用完成后释放许可,semaphore.release();

栅栏

栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。闭锁是阻塞一组线程直到另一组线程执行完,而栅栏是等待一批线程都就绪后一起执行。且闭锁是一次性对象而栅栏可以重复使用。
比如LOL游戏加载需要所有玩家都准备就绪才能开始游戏。
CyclicBarrier
CyclicBarrier可以使一定数量的线程在栅栏处汇集,当线程到达栅栏位置后调用await方法,线程阻塞直到所有线程都到达栅栏,所有线程都被释放,而栅栏重置。

如果对await调用超市或者被阻塞的线程中断,那么栅栏就被打破了所有等待的线程都将终止并抛出异常BraokenBarrierException。
CyclicBarrier有两种构造方法

  • 第一种创建一个需要等待3个线程一起执行的栅栏 CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
  • 第二种 声明一个大小为3的栅栏同时创建一个领导线程,当栅栏放开时先执行领导线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,new Runnable(){
		public void run(){
		}
	});

Exchanger
Exchanger也是一种栅栏。他是一种双向栅栏,两方在栅栏位置交换数据。比如缓存区,一批线程向缓存区写数据,缓存区数据写完后另一批线程从缓存区读取数据。

发布了56 篇原创文章 · 获赞 4 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/xs925048899/article/details/104637498