笔记摘要:
这里主要介绍了java5中线程锁技术以外的其他同步工具,首先介绍semaphore:一个计数信号量。用于控制同时访问资源的线程个数,
CyclicBarrier同步辅助类:从字面意思看是路障,这里用于线程之间的相互等待,到达某点后,继续向下执行,CountDownLatch同步辅
助类:在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。犹如倒计时计数器,然后是Exchangeer:实现两个
对象之间数据交换,可阻塞队列:ArrayBlockingQueue,通过阻塞队列间的通信来演示其作用,最后介绍了几个同步集合。
一、Semaphore实现信号灯
1.、Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许
的并发访问数。Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
2、Semaphore实现的功能就像:银行办理业务,一共有5个窗口,但一共有10个客户,一次性最多有5个客户可以进行办理,其他的人必须等候,
当5个客户中的任何一个离开后,在等待的客户中有一个人可以进行业务办理。
3、Semaphore提供了两种规则:
一种是公平的:获得资源的先后,按照排队的先后。在构造函数中设置true实现
一种是野蛮的:谁有本事抢到资源,谁就可以获得资源的使用权。
4、与传统的互斥锁的异同:
单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁“,再由另外一个线程释放”锁“,
这可以应用于死锁恢复的一些场合。
5、应用场景:共享资源的争夺,例如游戏中选手进入房间的情况。
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
- public class SemaphoreTest {
- public static void main(String[] args) {
- //创建一个可根据需要创建新线程的线程池
- ExecutorService service = Executors.newCachedThreadPool();
- final Semaphore sp = new Semaphore(3);
- //创建10个线程
- for(int i=0;i<10;i++){
- Runnable runnable = new Runnable(){
- public void run(){
- try {
- sp.acquire(); //获取灯,即许可权
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- System.out.println("线程" + Thread.currentThread().getName() +
- "进入,当前已有" + (3-sp.availablePermits()) + "个并发");
- try {
- Thread.sleep((long)(Math.random()*10000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将离开");
- sp.release(); // 释放一个许可,将其返回给信号量
- //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
- System.out.println("线程" + Thread.currentThread().getName() +
- "已离开,当前已有" + (3-sp.availablePermits()) + "个并发");
- }
- };
- service.execute(runnable);
- }
- }
- }
二、其他同步工具类:
CyclicBarrier
1、一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,
这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
2、CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障
点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作 很有用。
示例:
3个线程到达某个集合点后再向下执行,使用await方法实现
- import java.util.concurrent.CyclicBarrier;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class CyclicBarrierTest {
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final CyclicBarrier cb = new CyclicBarrier(3);
- for(int i=0;i<3;i++){
- Runnable runnable = new Runnable(){
- public void run(){
- try {
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点1,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();//在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- service.execute(runnable);
- }
- service.shutdown();
- }
- }
CountDownLatch
1、一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。犹如倒计时计数器,调用CountDownLatch对象
的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
2、可以实现一个人(也可以是多个人)等待其他所有人都来通知他,也可以实现一个人通知多个人的效果,类似裁判一声口令,运动员开始奔跑
(一对多),或者所有运送员都跑到终点后裁判才可以公布结果(多对一)。
3、用指定的计数 初始化 CountDownLatch。在调用 countDown() 方法之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所
有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
示例:
实现运动员比赛的效果
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.CyclicBarrier;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class CountdownLatchTest {
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- //构造一个用给定计数初始化的 CountDownLatch,相当于裁判的口哨
- final CountDownLatch cdOrder = new CountDownLatch(1);
- //相当于定义3个运行员
- final CountDownLatch cdAnswer = new CountDownLatch(3);
- for (int i = 0; i < 3; i++) {
- Runnable runnable = new Runnable() {
- public void run() {
- try {
- System.out.println("线程"
- + Thread.currentThread().getName() + "正准备接受命令");
- // 等待发令枪
- cdOrder.await();//使当前线程在锁存器倒计数至零之前一直等待
- System.out.println("线程"
- + Thread.currentThread().getName() + "已接受命令");
- Thread.sleep((long) (Math.random() * 10000));
- System.out
- .println("线程"
- + Thread.currentThread().getName()
- + "回应命令处理结果");
- // 各个运动员完报告成绩之后,通知裁判
- cdAnswer.countDown();//递减锁存器的计数,如果计数到达零,则释放所有等待的线程
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- service.execute(runnable);
- }
- try {
- Thread.sleep((long) (Math.random() * 10000));
- System.out.println("线程" + Thread.currentThread().getName()
- + "即将发布命令");
- // 发令枪打响,比赛开始
- cdOrder.countDown();
- System.out.println("线程" + Thread.currentThread().getName()
- + "已发送命令,正在等待结果");
- // 裁判等待各个运动员的结果
- cdAnswer.await();
- // 裁判公布获得所有运动员的成绩
- System.out.println("线程" + Thread.currentThread().getName()
- + "已收到所有响应结果");
- } catch (Exception e) {
- e.printStackTrace();
- }
- service.shutdown();
- }
- }
Exchanger
1、用于实现两个对象之间的数据交换,每个对象在完成一定的事务后想与对方交换数据,第一个先拿出数据的对象将一直等待第二个对象拿着数据
到来时,彼此才能交换数据。
2、方法:exchange(V x)
等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
3、应用:使用 Exchanger 在线程间交换缓冲区
示例:
模拟毒品交易情景
- import java.util.concurrent.Exchanger;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ExchangerTest {
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final Exchanger exchanger = new Exchanger();
- service.execute(new Runnable(){
- public void run() {
- try {
- String data1 = "毒品";
- System.out.println("线程" + Thread.currentThread().getName() +
- "正在把: " + data1 +" 交易出去");
- Thread.sleep((long)(Math.random()*10000));
- String data2 = (String)exchanger.exchange(data1);
- System.out.println("线程" + Thread.currentThread().getName() +
- "换得了: " + data2);
- }catch(Exception e){
- }
- }
- });
- service.execute(new Runnable(){
- public void run() {
- try {
- String data1 = "美金";
- System.out.println("线程" + Thread.currentThread().getName() +
- "正在把: " + data1 +" 交易出去");
- Thread.sleep((long)(Math.random()*10000));
- String data2 = (String)exchanger.exchange(data1);
- System.out.println("线程" + Thread.currentThread().getName() +
- "换得了: " + data2);
- }catch(Exception e){
- }
- }
- });
- }
- }
ArrayBlockingQueue
1、一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列包含固定长度的队列和不固定长度的队列。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能
再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
通俗的讲:
当指定队列大小,如果已经放满,其他存入数据的线程就阻塞,等着该队列中有空位,才能放进去。当取的比较快,队列中没有数据,
取数据的线程阻塞,等队列中放入了数据,才可以取。
2、ArrayBlockingQueue中只有put和take方法才具有阻塞功能。方法类型如下
| 抛出异常 | 特殊值 | 阻塞 | 超时 |
插入 | ||||
移除 | ||||
检查 | 不可用 | 不可用 |
示例:
用3个空间的队列来演示向阻塞队列中存取数据的效果。
- 通俗的将:
- 当指定队列大小,如果已经放满,其他存入数据的线程就阻塞,等着该队列中有空位,才能放进去。当取的比较快,队列中没有数据,取数据的线程阻塞,等队列中放入了数据,才可以取。
- ArrayBlockingQueue中只有put和take方法才具有阻塞功能。方法类型如下
- 抛出异常 特殊值 阻塞 超时
- 插入 add(e) offer(e) put(e) offer(e, time, unit)
- 移除 remove() poll() take() poll(time, unit)
- 检查 element() peek() 不可用 不可用
- 用3个空间的队列来演示向阻塞队列中存取数据的效果。
- package cn.xushuai.thread;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- public class BlockingQueueTest {
- public static void main(String[] args) {
- final BlockingQueue queue = new ArrayBlockingQueue(3);
- for(int i=0;i<2;i++){
- new Thread(){
- public void run(){
- while(true){
- try {
- Thread.sleep((long)(Math.random()*1000));
- System.out.println(Thread.currentThread().getName() + "准备放数据!");
- queue.put(1); //放进去后,可能立即执行“准备取数据”
- System.out.println(Thread.currentThread().getName() + "已经放了数据," +
- "队列目前有" + queue.size() + "个数据");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- }
- new Thread(){
- public void run(){
- while(true){
- try {
- //将此处的睡眠时间分别改为100和1000,观察运行结果
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName() + "准备取数据!");
- queue.take(); //取出后可能來不及执行下面的打印语句,就跑到了“准备放数据”,
- System.out.println(Thread.currentThread().getName() + "已经取走数据," +
- "队列目前有" + queue.size() + "个数据");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- }
- }
阻塞队列间的通信
A队列向空间中存数据,B从空间里取数据,A存入后,通知B去取,B取过之后,通知A去放,依次循环
示例:
子线程先循环10次,接着主线程循环100次,接着又回到子线程,循环10次,再回到主线程又循环100,如此循环50次。
说明:
这里通过使 用两个具有1个空间的队列来实现同步通知的功能(实现了锁和condition的功能),以便实现队列间的通信,其中使用到了
构造代码块为主队列先存入一个数据,以使其先阻塞,子队列先执行。
使用构造代码块的原因:
成员变量在创建类的实例对象时,才分配空间,才能有值,所以创建一个构造方法来给main_quene赋值,这里不可以使用静态代码块,因为静态
在还没创建对象就存在, 而sub_quene和main_quene是对象创建以后的成员变量,所以这里用匿名构造方法,它的运行时期在任何构造方法之前,
创建几个对象就执行几次
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- public class BlockingQueueCommunication {
- public static void main(String[] args){
- final Business business = new Business();
- new Thread(new Runnable(){
- @Override
- public void run() {
- for(int i=1;i<=50;i++){
- business.sub(i);
- }
- }
- }).start();
- //主线程外部循环
- for(int i=1;i<=50;i++){
- business.main(i);
- }
- }
- //业务类
- static class Business{
- BlockingQueue<Integer> sub_quene = new ArrayBlockingQueue<Integer>(1);
- BlockingQueue<Integer> main_quene = new ArrayBlockingQueue<Integer>(1);
- {
- //为了让子队列先走,所以在一开始就往主队列中存入一个对象,使其阻塞。
- try {
- main_quene.put(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //子队列先走
- public void sub(int i){
- try {
- sub_quene.put(1); //子队列第一次存入,可以执行,但由于只有1个空间,已经存满,所以只有在执行后要等到take之后才能继续下次执行
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //子队列循环执行
- for(int j=1;j<=10;j++){
- System.out.println("sub thread sequence of"+i+",loop of "+j);
- }
- try {
- main_quene.take(); //让主队列从已经填满的队列中取出数据,使其开始第一次执行
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- public void main(int i){
- try {
- main_quene.put(1); //主队列先前放过1个空间,现在处于阻塞状态,等待子队列通知,即子线程中的main_quene.take();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //主队列循环执行
- for(int j=1;j<=100;j++){
- System.out.println("main thread sequence of"+i+", loop of "+j);
- }
- try {
- sub_quene.take(); //让子队列从已经填满的队列中取出数据,使其执行
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
三、Java5中的一些同步集合类:
1、ConcurrentHashMap(同步的HashMap)
支持获取的完全并发和更新的所期望可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。
不过,尽管所有操作都是线程安全的,但获取操作不 必锁定,并且不 支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable
进行互操作,这取决于其线程安全,而与其同步细节无关。
内部原理:
其实内部使用了代理模式,你给我一个HashMap,我就给你一个同步的HashMap。同步的HashMap在调用方法时,是去分配给原始的HashMap只是在去
调用方法的同时加上了Synchronized,以此实现同步效果
2、ConcurrentSkipListSet(类似于TreeSet)
一个基于 ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的
Comparator 进行排序,具体取决于使用的构造方法。
3、CopyOnWriteArrayList
ArrayList 的一个线程安全的变体,可解决线程安全问题,在遍历的时候,同时进行添加操作。其中所有可变操作(add、set 等等)都是通过对底层数
组进行一次新的复制来实现的。
对其所有操作使用内部 CopyOnWriteArrayList 的 Set。