Java多线程与线程并发库高级应用笔记2版

-------android培训java培训、java基础学习技术博客、期待与您交流! ----------

8.1 传统的线程

8.1.1 启动传统线程

1. 创建和启动线程的两种传统方式
(1)继承Thread类,重写run方法;通过Thread类的start方法启动线程
(2)在传递给Thread对象的Runnable对象的run方法中编写代码
2. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Thread thread = new Thread(){  
  2.     public void run() {  
  3.         while(true){  
  4.             try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}  
  5.             System.out.println("1:" + Thread.currentThread().getName());  
  6.         }  
  7.     }  
  8. };  
  9. thread.start();       
  10.           
  11. Thread thread2 = new Thread(new Runnable(){  
  12.     public void run() {  
  13.         while(true){  
  14.             try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}  
  15.             System.out.println("1:" + Thread.currentThread().getName());  
  16.         }  
  17.     }  
  18. });  
  19. thread2.start();  

3.备注:
(1)查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
(2)如果在Thread子类覆盖的run方法中编写了运行代码,也为Thread子类对象传递了一个Runnable对象,那么,线程运行时的执行代码是子类的run方法的代码。匿名内部类对象的构造方法如何调用父类的非默认构造方法。

8.1.2 传统线程定时器

传统的线程定时器类是Timer类
1.Timer的常用方法

(1)void schedule(TimerTask task,Date time);//在指定的时间指定任务。
(2)void schedule(TimerTask task,Date firstTime,long period);//在firstTime执行任务,然后每隔period在执行一次。
(3)void schedule(TimerTask task,long delay);//在指定的延迟时间之后指定任务。
(4)void schedule(TimerTask task,long delay,long period);//在指定的延迟时间之后执行任务,之后每隔一段时间在执行一次。
(5)void scheduleAtFixedRate(TimerTask task,Date firstTime,long period);//在firstTime执行一次,然后每隔period循环执行。
(6)void scheduleAtFixedRate(TimerTask task,long delay,long period);//在delay之后执行一次,然后每隔period循环执行。
task:所要安排的任务。
time:执行任务的时间。
firstTime:首次执行任务的时间。
period:执行各后续任务之间的时间间隔,单位是毫秒。 
delay:执行任务前的延迟时间,单位是毫秒。 
2.TimkerTask的常用方法
abstract void run();//此计时器任务要执行的操作。 
3.示例代码

(1)过10秒钟后启动定时器,然后每过1秒定时器执行一次。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. new Timer().schedule(new TimerTask() {  
  2.             public void run() {  
  3.                 try {Thread.sleep(100);  
  4.                     System.out.println(new Date().getSeconds());  
  5.                     System.out.println(Thread.currentThread().getName());  
  6.                 } catch (InterruptedException e) {e.printStackTrace();}  
  7.             }  
  8.         }, 100001000);  

(2)启动定时器,2秒后执行任务,然后4秒后执行任务,接着交替执行。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class TraditionalTimerTest {  
  2.     private static int count = 0;//count初始值为0  
  3.     public static void main(String[] args) {  
  4.         class MyTimerTask extends TimerTask{  
  5.             public void run() {  
  6.                 count = (count+1)%2;//0和1交替  
  7.                 System.out.println("bombing!");  
  8.                 new Timer().schedule(  
  9.                     new MyTimerTask(),2000+2000*count);//定时时间切换  
  10.             }  
  11.         }  
  12.           
  13.         new Timer().schedule(new MyTimerTask(), 2000);//在主线程中2s以后,启动定时器  
  14.           
  15.         while(true){  
  16.             System.out.println(new Date().getSeconds());//打印当前的时间值(秒)  
  17.             try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}  
  18.         }     
  19.     }  
  20. }  

8.1.3 传统线程同步互斥

1.互斥原理
通过监视器来实现同步互斥,实现同步方式有两种:一种是synchronized代码块,另一种是synchronized函数。
2.同步函数synchronized的监视器对象
(1)如果方法是非静态的,则监视器对象是this。
(2)如果方法是静态的,则监视器对象是函数所在类在内存中的字节码文件。

8.1.4 传统的线程间通信(一)

1.线程间通信的原理
线程间的通信主要是通过读写标记flag以及wait和notifyAll来实现的。假定读取线程时,flag标记为false。
(1)bShouldSub为true,则sub线程进行操作,操作完成之后,将bShouldSub置为false,sub线程wait,notifyAll线程池中的线程,如果是main线程,则执行。
(2)bShouldSub为false,则main线程进行操作,操作完成之后,将bShouldSub置为true,main线程wait,notifyAll线程池中的线程,如果是sub线程,则执行。
2. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class TraditionalThreadCommunication {  
  2.     public static void main(String[] args) {  
  3.         final Business business = new Business();  
  4.         new Thread(new Runnable() {  
  5.             public void run() {  
  6.                 for (int i = 1; i <= 20; i++) {  
  7.                     business.sub(i);//调用子线程  
  8.                 }  
  9.             }  
  10.         }).start();  
  11.   
  12.         for (int i = 1; i <= 20; i++) {  
  13.             business.main(i);//调用主线程  
  14.         }  
  15.     }  
  16. }  
  17.   
  18. class Business {  
  19.     private boolean bShouldSub = true;  
  20.     public synchronized void sub(int i) {  
  21.         while (!bShouldSub) {//子线程执行(bShouldSub为true)  
  22.             try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}  
  23.         }  
  24.         for (int j = 1; j <= 10; j++) {  
  25.             System.out.println("sub thread sequence of " + j + ",loop of " + i);  
  26.         }  
  27.         bShouldSub = false;  
  28.         this.notify();  
  29.     }  
  30.   
  31.     public synchronized void main(int i) {  
  32.         while (bShouldSub) {//主线程执行(bShouldSub为false)  
  33.             try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}  
  34.         }  
  35.         for (int j = 1; j <= 10; j++) {  
  36.             System.out.println("main thread sequence of " + j + ",loop of " + i);  
  37.         }  
  38.         bShouldSub = true;  
  39.         this.notify();  
  40.     }  
  41. }  

8.1.4 传统的线程间通信(二)

1.重入锁 ReentrantLock
(1)概念
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。
(2)使用Lock和Condition实现线程间的通信
第一,使用Lock替换synchronized,使用Condition替换Object;
第二,使用Condition中的await替换Object中的wait,使用Condition中的signal替换Object中的notifyAll即可。可以显示唤醒指定的Condition,因此可以使用singnal唤醒需要唤醒的Condition。
(3)示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class BoundedBuffer {  
  2.    final Lock lock = new ReentrantLock();  
  3.    final Condition notFull  = lock.newCondition();   
  4.    final Condition notEmpty = lock.newCondition();   
  5.   
  6.    final Object[] items = new Object[100];  
  7.    int putptr, takeptr, count;//putptr:存放指针,takeptr:获取指针,count:当前数组元素的个数。  
  8.   
  9.    public void put(Object x) throws InterruptedException {  
  10.      lock.lock();  
  11.      try {  
  12.        while (count == items.length)   
  13.          notFull.await();  
  14.        items[putptr] = x;   
  15.        if (++putptr == items.length) putptr = 0;  
  16.        ++count;  
  17.        notEmpty.signal();  
  18.      } finally {  
  19.        lock.unlock();  
  20.      }  
  21.    }  
  22.   
  23.    public Object take() throws InterruptedException {  
  24.      lock.lock();  
  25.      try {  
  26.        while (count == 0)   
  27.          notEmpty.await();  
  28.        Object x = items[takeptr];   
  29.        if (++takeptr == items.length) takeptr = 0;  
  30.        --count;  
  31.        notFull.signal();  
  32.        return x;  
  33.      } finally {  
  34.        lock.unlock();  
  35.      }  
  36.    }   
  37.  }  

2.读写锁 ReentrantReadWriteLock
(1)概念
分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。
第一,如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;
第二,如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
总之,读的时候上读锁,写的时候上写锁!
(2)示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class CachedData {  
  2.    Object data;  
  3.    volatile boolean cacheValid;  
  4.    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();  
  5.   
  6.    void processCachedData() {  
  7.      rwl.readLock().lock();  
  8.      if (!cacheValid) {  
  9.         // 获取写锁之前,必须释放读锁。  
  10.         rwl.readLock().unlock();  
  11.         rwl.writeLock().lock();  
  12.         // 重新检查锁的状态,因此有可能此时其他所在获取请求时,该状态发生了变化  
  13.         if (!cacheValid) {  
  14.           data = ...  
  15.           cacheValid = true;  
  16.         }  
  17.         // 在释放写锁前,写锁降级为读锁  
  18.         rwl.readLock().lock();  
  19.         rwl.writeLock().unlock(); // 释放写锁,此时保持读状态。  
  20.      }  
  21.   
  22.      use(data);  
  23.      rwl.readLock().unlock();  
  24.    }  
  25.  }  

8.1.5 多线程访问共享数据总结

多个线程访问共享对象和数据的方式
1. 如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
2. 如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
(1)将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
(2)将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
(3)上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
(4)总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
4.极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。

8.2 并发库

8.2.1 ThreadLocal与线程级变量共享


1. ThreadLocal的作用和目的:
用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
2. 每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
3. ThreadLocal的应用场景:
(1)订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
(2)银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
(3)例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
4. 实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
(1)对基本类型的数据的封装,这种应用相对很少见。
(2)对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。
5. 实验案例:
定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class ThreadLocalTest {  
  2.     private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();  
  3.     public static void main(String[] args) {  
  4.         for(int i=0;i<2;i++){  
  5.             new Thread(new Runnable(){  
  6.                 public void run() {  
  7.                     int data = new Random().nextInt();  
  8.                     System.out.println(Thread.currentThread().getName() + " has put data :" + data);  
  9.                     x.set(data);  
  10.                     MyThreadScopeData.getThreadInstance().setName("name" + data);  
  11.                     MyThreadScopeData.getThreadInstance().setAge(data);  
  12.                     new A().get();  
  13.                     new B().get();  
  14.                 }  
  15.             }).start();  
  16.         }  
  17.     }  
  18.       
  19.     static class A{  
  20.         public void get(){  
  21.             int data = x.get();  
  22.             System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data);  
  23.             MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();  
  24.             System.out.println("A from " + Thread.currentThread().getName() + " getMyData: " + myData.getName() + "," + myData.getAge());  
  25.         }  
  26.     }  
  27.       
  28.     static class B{  
  29.         public void get(){  
  30.             int data = x.get();           
  31.             System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data);  
  32.             MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();  
  33.             System.out.println("B from " + Thread.currentThread().getName() + " getMyData: " + myData.getName() + "," + myData.getAge());             
  34.         }         
  35.     }  
  36. }  
  37.   
  38. class MyThreadScopeData{  
  39.     private MyThreadScopeData(){}  
  40.     public static MyThreadScopeData getThreadInstance(){  
  41.         MyThreadScopeData instance = map.get();  
  42.         if(instance == null){  
  43.             instance = new MyThreadScopeData();  
  44.             map.set(instance);  
  45.         }  
  46.         return instance;  
  47.     }  
  48.     private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();  
  49.       
  50.     private String name;  
  51.     private int age;  
  52.     public String getName() {  
  53.         return name;  
  54.     }  
  55.     public void setName(String name) {  
  56.         this.name = name;  
  57.     }  
  58.     public int getAge() {  
  59.         return age;  
  60.     }  
  61.     public void setAge(int age) {  
  62.         this.age = age;  
  63.     }  
  64. }  

8.2.2 Java5中的线程池

1. 常用的方法:
(1)创建线程池
方式1:创建固定大小的线程池
Executors.newFixedThreadPool(3).execute(Runnable command);创建固定大小的线程池,每次只能运行固定数量的线程,当这些线程运行完之后才能运行其他的线程。
方式2:创建缓存线程池
Executors.newCachedThreadPool().execute(Runnable command);创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
方式3:创建单一线程池
Executors.newSingleThreadExecutor().execute(Runnable command);创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
(2)关闭线程池(ExecutorService类)
方式1:
shutdown
启动一次顺序关闭,执行以前提交的任务,但不接受新任务。shutdown会执行完线程池中提交的所有任务,然后停止。
方式2:
shutdownNow
试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。shutdownNow会执行完固定线程池中固定的任务,然后就停止。不会执行线程池中的其他任务。
(3)线程池启动定时器
方式1:
调用ScheduledExecutorService的schedule方法,返回的ScheduleFuture对象可以取消任务。
方式2:
支持间隔重复任务的定时方式,不直接支持绝对定时方式,需要转换成相对时间方式。

2.应用场景(Tcp服务器编程)
每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,每来一个客户端连接,服务器端就要创建一个新线程。由于这样的模式下,需要频繁的创建线程和销毁线程,因此效率并不高。通常的做法是先建立一些连接,存放在线程池中,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。需要注意的是一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /*1. 用3个大小的固定线程池去执行10个内部循环10次就结束的任务,每次只有3个线程在执行, 
  2. 并看到任务前仆后继的效果。*/  
  3. ExecutorService threadPool = Executors.newFixedThreadPool(3);  
  4. //2. 缓存线程池,可以看到当前有多少个任务,就会分配多少个线程为之服务。  
  5. ExecutorService threadPool = Executors.newCachedThreadPool();  
  6. for (int i = 1; i <= 10; i++) {  
  7.     final int task = i;  
  8.     threadPool.execute(new Runnable() {  
  9.         public void run() {  
  10.             for (int j = 1; j <= 10; j++) {  
  11.                 System.out.println(Thread.currentThread().getName()  
  12.                         + " is looping of " + j + " for  task of "  
  13.                         + task);  
  14.             }  
  15.         }  
  16.     });  
  17. }  
  18.   
  19. //3.以下是一个带方法的类,它设置了 ScheduledExecutorService ,在 1 小时内每 10 秒钟蜂鸣一次:    
  20. class BeeperControl {  
  21.     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);  
  22.     public void beepForAnHour() {  
  23.         final Runnable beeper = new Runnable() {  
  24.             public void run() {  
  25.                 System.out.println("beep");  
  26.             }  
  27.         };  
  28.         final ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(  
  29.                 beeper, 1010, TimeUnit.SECONDS);  
  30.         scheduler.schedule(  
  31.                     new Runnable() {  
  32.                         public void run() {  
  33.                             beeperHandle.cancel(true);  
  34.                         }  
  35.                     },   
  36.                     60 * 60,   
  37.                     TimeUnit.SECONDS);  
  38.     }  
  39. }  

8.2.3 Callable&Future

1. 概念
java5提供的可返回结果的线程模式
(1)Future取得的结果类型和Callable返回的结果类型必须一致,这是通过泛型来实现的。
(2)Callable要采用ExecutorSevice的submit方法提交,返回的future对象可以取消任务。
(3)CompletionService用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
2. 应用场景
好比我同时种了几块地的麦子,然后就等待收割。收割时,则是那块先成熟了,则先去收割哪块麦子。
3. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class CallableAndFuture {  
  2.     public static void main(String[] args) {  
  3.         //1.ExecutorSevice提交一个任务  
  4.         ExecutorService threadPool =  Executors.newSingleThreadExecutor();  
  5.         Future<String> future =  
  6.             threadPool.submit(  
  7.                 new Callable<String>() {  
  8.                     public String call() throws Exception {  
  9.                         Thread.sleep(2000);  
  10.                         return "hello";  
  11.                     };  
  12.                 }  
  13.         );  
  14.         System.out.println("等待结果");  
  15.         try {  
  16.             System.out.println("拿到结果:" + future.get());  
  17.         } catch (Exception e) {e.printStackTrace();}  
  18.           
  19.         //2.CompletionService提交一组任务  
  20.         ExecutorService threadPool2 =  Executors.newFixedThreadPool(10);  
  21.         CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(threadPool2);  
  22.         for(int i=1;i<=10;i++){  
  23.             final int seq = i;  
  24.             completionService.submit(new Callable<Integer>() {  
  25.                 public Integer call() throws Exception {  
  26.                     Thread.sleep(new Random().nextInt(1000));  
  27.                     return seq;  
  28.                 }  
  29.             });  
  30.         }  
  31.         for(int i=0;i<10;i++){  
  32.             try {  
  33.                 System.out.println(completionService.take().get());               
  34.             } catch (Exception e) { e.printStackTrace();}  
  35.         }  
  36.     }  
  37. }  

8.3 并发库中的工具

8.3.1 Semaphore同步工具

1. 概念
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程个数。
(1)构造Semaphore对象时传入的参数选项fair的取值,用来设置等待线程获取机会的顺序。如果为真,则是按照先来后到的顺序,否则,随机获取优先顺序。
(2)单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

2. 应用场景
(1)当需要控制资源访问进程数,要求线程按照FIFO的顺序进行访问时。
(2)死锁恢复(由于Semaphore对象可以在一个线程中获取锁,另外的线程中恢复锁)。

3. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class SemaphoreTest {  
  2.     public static void main(String[] args) {  
  3.         ExecutorService service = Executors.newCachedThreadPool();  
  4.         final  Semaphore sp = new Semaphore(3);//1. 获取Semaphore同步对象  
  5.         for(int i=0;i<10;i++){  
  6.             Runnable runnable = new Runnable(){  
  7.                     public void run(){  
  8.                     try {  
  9.                         sp.acquire();//2. 获取Semaphore许可  
  10.                     } catch (InterruptedException e1) {  
  11.                         e1.printStackTrace();  
  12.                     }  
  13.                     System.out.println("线程" + Thread.currentThread().getName() +   
  14.                             "进入,当前已有" + (3-sp.availablePermits()) + "个并发");//3. 返回当前的许可数。  
  15.                     try {Thread.sleep((long)(Math.random()*10000));}   
  16.                     catch (InterruptedException e) {e.printStackTrace();}  
  17.                     System.out.println("线程" + Thread.currentThread().getName() + "即将离开");                     
  18.                     sp.release();//4. 释放Semaphore许可  
  19.                     System.out.println("线程" + Thread.currentThread().getName() +   
  20.                             "已离开,当前已有" + (3-sp.availablePermits()) + "个并发");                      
  21.                 }  
  22.             };  
  23.             service.execute(runnable);            
  24.         }  
  25.     }  
  26. }  

8.3.2 CyclicBarrier 同步工具

1. 概念
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

2. 应用场景
当多个线程需要在某一个点汇集之后,然后在去做其他事情的时候。

3. 示例代码

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class CyclicBarrierTest {  
  2.     public static void main(String[] args) {  
  3.         ExecutorService service = Executors.newCachedThreadPool();  
  4.         final  CyclicBarrier cb = new CyclicBarrier(3);  
  5.         for(int i=0;i<3;i++){  
  6.             Runnable runnable = new Runnable(){  
  7.                     public void run(){  
  8.                     try {  
  9.                         Thread.sleep((long)(Math.random()*10000));    
  10.                         System.out.println("线程" + Thread.currentThread().getName() +   
  11.                                 "即将到达集合地点1,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));                         
  12.                         cb.await();  
  13.                           
  14.                         Thread.sleep((long)(Math.random()*10000));    
  15.                         System.out.println("线程" + Thread.currentThread().getName() +   
  16.                                 "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));  
  17.                         cb.await();   
  18.                         Thread.sleep((long)(Math.random()*10000));    
  19.                         System.out.println("线程" + Thread.currentThread().getName() +   
  20.                                 "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));                       
  21.                         cb.await();                       
  22.                     } catch (Exception e) {  
  23.                         e.printStackTrace();  
  24.                     }                 
  25.                 }  
  26.             };  
  27.             service.execute(runnable);  
  28.         }  
  29.         service.shutdown();  
  30.     }  
  31. }  

8.3.3 CountDownLatch 同步工具

1. 概念
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 调用CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。

2. 应用场景
(1)将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器。在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。
(2)用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

3. 使用方式
(1)构造一个用给定计数初始化的 CountDownLatch。
CountDownLatch cdOrder = new CountDownLatch(3);
(2)使当前线程在锁存器倒计数至零之前一直等待。
cdOrder.await();
(3)递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
cdOrder.countDown(); 

8.3.4 Exchanger 同步与数据交换工具

1. 概念
可以在对中对元素进行配对和交换的线程的同步点。

2. 应用场景
用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。

3. 使用方式
(1)同步工具 Exchanger
Exchanger exchanger = new Exchanger();
(2)线程组1
String data1 = "zxx";
String data2 = (String)exchanger.exchange(data1);
(3)线程组2
String data1 = "lhm";
String data2 = (String)exchanger.exchange(data1);

8.3.5 可阻塞队列 ArrayBlockingQueue

1. 概念
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

2. 特点
(1)阻塞队列与Semaphore有些相似,但也不同,阻塞队列是一方存放数据,另一方释放数据,Semaphore通常则是由同一方设置和释放信号量。
(2)ArrayBlockingQueue只有put方法和take方法才具有阻塞功能。
(3)适用于多线程中需要固定长度缓冲区。

3. 示例代码
(1)创建一个带有给定的(固定)容量。
BlockingQueue queue = new ArrayBlockingQueue(3);
(2)指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间。
queue.put(1);
(3)获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
queue.take();

8.3.6 传统与java5中的同步集合

1. 概念
传统方式下的Collection在迭代集合时,不允许对集合进行修改。当多线程对结合进行并发访问时,就会发生安全问题,jdk1.5之前可以通过Collections工具类提供的synchronizedCollection方法来获得同步集合。Java5中提供了并发集合来解决多线程访问产生的并发问题。
2. 并发库中的集合
java.util.concurrent包含除队列外,此包还提供了设计用于多线程上下文中的 Collection 实现:
(1)ConcurrentHashMap
(2)ConcurrentSkipListMap
(3)ConcurrentSkipListSet
(4)CopyOnWriteArrayList
(5)CopyOnWriteArraySet
当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。因此,实际开发中,当集合被并发访问时,可以使用java5并发库中提供的集合。

总结:
1. 传统的多线程
(1)启动线程 Thread
继承Thread类,重写run方法;通过Thread类的start方法启动线程;在传递给Thread对象的Runnable对象的run方法中编写代码。
(2)定时器 Timer
void scheduleAtFixedRate(TimerTask task,long delay,long period);通过Timer的scheduleAtFixedRate方法,可以实现在delay延迟之后执行一个task任务,每隔period执行一次。其中task是TimerTask,通常采用匿名内部类的方式来指定需要实现的任务。
(3)同步互斥
传统线程同步互斥通过synchronized代码块或者synchronized函数来实现互斥,同时使用的监视器是同一个监视器。同步函数是非静态的,监视器对象是this;方法是静态的,则监视器对象是该类在内存中的字节码对象。
(4)线程间通信1
线程间的通信主要是通过读写标记flag以及wait和notifyAll来实现的。通过判断读写标记,标记为真,则读取线程执行,写入线程堵塞,执行完成之后,标记置为假,读取线程堵塞,叫醒写入线程;标记为假,则写入线程执行,读取线程堵塞,执行完成之后,标记置为真,写入线程堵塞,叫醒读取线程。
(5)线程间通信2
Java5中提供了更加面向对象的同步工具,就是Lock和Condition,通过将synchronized替换为Lock,将监视器Object替换为Condition,就可以实现线程间的通信。同时,提供了更加强大的功能。ReentrantLock锁中可以直接定义显示的Condition,可以直接叫醒指定的线程。同时,如果想实现读写锁,可以直接使用
ReentrantReadWriteLock,就可以直接获取读锁和写锁。
(6)多线程访问的数据
通常,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
2. 并发库
在并发编程中很常用的实用工具类。
(1)线程级变量共享 ThreadLocal 
ThreadLocal用于实现线程内的数据共享,对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。ThreadLocal对象的get方法可以获取到存储到该线程对应的值。
(2)线程池
通过Executors类的静态方法可以创建固定大小的线程池,缓存线程池以及单一线程池;然后通过返回值ExecutorService的execute,指定线程池中执行的线程代码。通过ExecutorService的shutdown可以执行完线程池中所有的线程然后,停止;而shutdownNow会执行初始线程池中的线程,然后停止。通过Executors类的newScheduledThreadPool方法,创建一个在给定延迟后运行命令或者定期地执行任务的线程池。
(3)可返回结果的线程模式
方式1:提交一个Callable任务
ExecutorSevice类的submit方法提交,返回的future对象可以取消任务。
方式2:提交一组Callable任务
CompletionService用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
(4)同步工具 Semaphore 
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程
个数。
第一,通过指定构造函数中初始化线程数来获取Semaphore同步对象;
第二,通过Semaphore中的方法acquire获取Semaphore许可;
第三,多线程中需要执行的任务代码;
第四,通过释放release方法释放Semaphore许可。
(5)同步工具 CyclicBarrier
CyclicBarrier工具允许一组线程互相等待,直到到达某个公共屏障点。通过CyclicBarrier中的await方法可以
指定公共屏障点。
第一,通过指定构造函数中初始化线程数来获取CyclicBarrier同步对象;
第二,在公共屏障点之前,填写多线程中需要执行的任务代码,通过CyclicBarrier类的getNumberWaiting方法
可以获取等待的线程数量。
第三,通过CyclicBarrier中的await方法设置公共屏障点。
(6)同步工具 CountDownLatch
CountDownLatch工具,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。调用
CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
第一,构造一个用给定计数初始化的 CountDownLatch。
CountDownLatch cdOrder = new CountDownLatch(3);
第二,使当前线程在锁存器倒计数至零之前一直等待。
cdOrder.await();
第三,递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
cdOrder.countDown(); 
(7)同步与数据交换工具 Exchanger
Exchanger 可以在对中对元素进行配对和交换的线程的同步点。
(8)可阻塞队列 ArrayBlockingQueue
ArrayBlockingQueue类是一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。通过put方法将指定元素插入到队列的结尾,通过take方法,移除此队列的头部。
(9)并发 Collection
Java5还提供了用于并发访问的集合类 Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。
第一,当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,
ConcurrentSkipListMap 通常优于同步的 TreeMap。
第二,当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值