[Thinking in JAVA] JAVA多线程的学习

1.  Thread.yield方法声明把CPU让给其他具有相同优先级的线程去执行,不过这只是一个暗示,并没有保障机制

2.  Executor

     执行器,管理Thread对象。

     语法demo:   

     ExecutorService exec=Executors.newCachedThreadPool();

     for(int i=0;i<5;i++)

           exec.execute(new  XXX());      //XXX为实现Runnable接口的类

      exec.shutdown();

     三种类型及其区别:   

           CachedThreadPool:在程序执行过程中创建与所需数量相同的线程,在回收旧线程时停止创建新线程,是Executor首选。

           FixedThreadPool:    一次性预先执行代价高昂的线程分配,可以限制线程的数量。

           SingleThreadExecutor: 如果向其提交多个任务,那么这些任务排队,每个任务都在下个任务开始前结束,所有的任务使用相同的线程。

             

3.如何创建有返回值的线程

   实现Callable接口,重写call()方法。

  exec.submit(new XXX()) 会返回Future对象,用future.get()获取值,这个值是泛型的,  取决于实现接口时的声明,如 implements Callable 则get到的是String类型

关于get: 可以先调用Future的isDone()方法来查询是否已经完成。任务完成时会具有一个结果,可以通过get来获取。如果不用isDone直接get,get会阻塞直到结果准备就绪。


4.通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(是否是后台,优先级,名称),

          比如class MyThreadFactory implements ThreadFactory{    

                             public Thread newThread(Runnable r) {
// TODO 自动生成的方法存根

                Thread t=new Thread(r);

                t.setDaemon(true);   //设置为后台线程
return ..;
}


5. 守护线程中派生的子线程默认是守护线程,当所有的非守护线程结束时,后台线程终止,一旦main()退出,JVM会关闭所有的守护线程。

当所有的非后台线程结束时,程序就终止了。同时会杀死进程中所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。



6.线程join方法:

   若在A线程中调用B.join,则A被挂起,直到B结束。在调用时也可以带上一个超时参数。       对join方法的调用可以被打断(通过在调用线程上调用interrupt()方法),这时被打断的B需要用到try-catch子句。(在run方法中,catch InterruptedException)


7.捕获线程抛出的异常

    线程抛出的异常不能被正常的try catch到,可以用Executor解决这个问题:

   如4中所示自定义一个myfactory类,在该factory的newThread方法中t.setUncaughtExceptionHandler(new XXX) ;     XXX是实现了Thread.UncaughtExceptionHandler接口的类。 然后ExecutorService exec=Executors.newCachedThreadPool(new myfactory);


8.  synchronized和Lock对象

    synchronized:如果某个任务处于一个对标记为synchronized的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。不过如果是synchronized(obj)方法块,只要obj不是相同的对象,两个方法不会因为另一个方法的同步而被阻塞

     用synchronized时代码量更小,且不会出现忘了unlock这种情况,用Lock对象需要 lock();  try{  } finally{ unlock},避免在lock之后代码出现异常导致死锁。

    显式使用lock对象可以解决更特殊的问题,比如尝试获取锁一段时间然后放弃、实现自己的调度算法等等。且在加锁释放锁方面有更细粒度的控制力。

   

    ps:synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。



9. volatile

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

如果多个任务在同时访问某个域,这个域就应该时volatile的,否则只能由同步来访问。同步也会导致向主存中刷新。因此如果一个域完全由synchronized方法或语句块来防护,就不必将其设置为volatile。

    long和double的读取写入可能不是原子的,因为long和double 64位,在32位机器上的读写操作会被当作两个分离的32位操作执行。如果使用volatile关键字,就会获得原子性。此关键字还确保了应用中的可视性。如果把一个域声明为volatile的,只要对这个域产生写操作,所有的读操作都可以看到这个修改。即便使用了缓存,volatile域会被立即写入到主存中,而读取操作发生在主存。如果域由synchronized方法或语句块防护,不必设置为volatile。使用volatile 而不是synchronized的唯一安全的情况是类中只有一个可变域。第一选择应该是synchronized。


10.同步控制块

亦称为临界区,在方法内部:

synchronized(syncObject){

}



11.ThreadLocal

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。



12.线程的暂停、继续、终止

wait,notify,exec.shutdownNow()


在Executor上调用shutdownNow(),将发送一个interrupt()调用给它启动的所有线程。(相当于xx.interrupt()) 

如果想中断某个单一任务,使用Executor.submit()而不是execute()来启动任务,返回一个Future f,可以通过调用f.cancel(true)来中断。

可以中断对sleep的调用(或者任何要求抛出InterruptedException的调用),不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程(办法是关闭任务在其上发生阻塞的底层资源)。




13.wait 和 notify


sleep和yield不释放锁,wait会释放锁


两种形式的wait:毫秒数作为参数或者不加参数


把wait、notify放在Object类中是因为这些方法操作的锁也是所有对象的一部分,所以可以把wait放进任何同步控制方法里,不需要考虑这个类是继承Thread还是实现Runnable接口。实际上,只能在同步控制方法或同步控制块里调用wait、notify。否则在运行时将得到IllegalMonitorStateException异常。调用这两个方法的任务在调用这些方法前必须拥有对象的锁。


notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。

void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。

两者的最大区别在于:

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。


可以用lock+condition来代替wait和notify,但是更复杂,只有在更加困难的多线程问题中才必需。



ReentrantLock

java.util.concurrent.lock 中的Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票定时锁等候可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

  1. class Outputter1 {    
  2.     private Lock lock = new ReentrantLock();// 锁对象    
  3.   
  4.     public void output(String name) {           
  5.         lock.lock();      // 得到锁    
  6.   
  7.         try {    
  8.             for(int i = 0; i < name.length(); i++) {    
  9.                 System.out.print(name.charAt(i));    
  10.             }    
  11.         } finally {    
  12.             lock.unlock();// 释放锁    
  13.         }    
  14.     }    
  15. }    

区别:

需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!

读写锁ReadWriteLock

上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?

例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:

  1. class syncData {        
  2.     private int data;// 共享数据        
  3.     public synchronized void set(int data) {    
  4.         System.out.println(Thread.currentThread().getName() + "准备写入数据");    
  5.         try {    
  6.             Thread.sleep(20);    
  7.         } catch (InterruptedException e) {    
  8.             e.printStackTrace();    
  9.         }    
  10.         this.data = data;    
  11.         System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
  12.     }       
  13.     public synchronized  void get() {    
  14.         System.out.println(Thread.currentThread().getName() + "准备读取数据");    
  15.         try {    
  16.             Thread.sleep(20);    
  17.         } catch (InterruptedException e) {    
  18.             e.printStackTrace();    
  19.         }    
  20.         System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
  21.     }    
  22. }    

然后写个测试类来用多个线程分别读写这个共享数据:

  1. public static void main(String[] args) {    
  2. //        final Data data = new Data();    
  3.           final syncData data = new syncData();    
  4. //        final RwLockData data = new RwLockData();    
  5.           
  6.         //写入  
  7.         for (int i = 0; i < 3; i++) {    
  8.             Thread t = new Thread(new Runnable() {    
  9.                 @Override  
  10.         public void run() {    
  11.                     for (int j = 0; j < 5; j++) {    
  12.                         data.set(new Random().nextInt(30));    
  13.                     }    
  14.                 }    
  15.             });  
  16.             t.setName("Thread-W" + i);  
  17.             t.start();  
  18.         }    
  19.         //读取  
  20.         for (int i = 0; i < 3; i++) {    
  21.             Thread t = new Thread(new Runnable() {    
  22.                 @Override  
  23.         public void run() {    
  24.                     for (int j = 0; j < 5; j++) {    
  25.                         data.get();    
  26.                     }    
  27.                 }    
  28.             });    
  29.             t.setName("Thread-R" + i);  
  30.             t.start();  
  31.         }    
  32.     }    

运行结果:

  1. Thread-W0准备写入数据  
  2. Thread-W0写入0  
  3. Thread-W0准备写入数据  
  4. Thread-W0写入1  
  5. Thread-R1准备读取数据  
  6. Thread-R1读取1  
  7. Thread-R1准备读取数据  
  8. Thread-R1读取1  
  9. Thread-R1准备读取数据  
  10. Thread-R1读取1  
  11. Thread-R1准备读取数据  
  12. Thread-R1读取1  
  13. Thread-R1准备读取数据  
  14. Thread-R1读取1  
  15. Thread-R2准备读取数据  
  16. Thread-R2读取1  
  17. Thread-R2准备读取数据  
  18. Thread-R2读取1  
  19. Thread-R2准备读取数据  
  20. Thread-R2读取1  
  21. Thread-R2准备读取数据  
  22. Thread-R2读取1  
  23. Thread-R2准备读取数据  
  24. Thread-R2读取1  
  25. Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!  
  26. Thread-R0读取1  
  27. Thread-R0准备读取数据  
  28. Thread-R0读取1  
  29. Thread-R0准备读取数据  
  30. Thread-R0读取1  
  31. Thread-R0准备读取数据  
  32. Thread-R0读取1  
  33. Thread-R0准备读取数据  
  34. Thread-R0读取1  
  35. Thread-W1准备写入数据  
  36. Thread-W1写入18  
  37. Thread-W1准备写入数据  
  38. Thread-W1写入16  
  39. Thread-W1准备写入数据  
  40. Thread-W1写入19  
  41. Thread-W1准备写入数据  
  42. Thread-W1写入21  
  43. Thread-W1准备写入数据  
  44. Thread-W1写入4  
  45. Thread-W2准备写入数据  
  46. Thread-W2写入10  
  47. Thread-W2准备写入数据  
  48. Thread-W2写入4  
  49. Thread-W2准备写入数据  
  50. Thread-W2写入1  
  51. Thread-W2准备写入数据  
  52. Thread-W2写入14  
  53. Thread-W2准备写入数据  
  54. Thread-W2写入2  
  55. Thread-W0准备写入数据  
  56. Thread-W0写入4  
  57. Thread-W0准备写入数据  
  58. Thread-W0写入20  
  59. Thread-W0准备写入数据  
  60. Thread-W0写入29  

现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??

对!读取线程不应该互斥!

我们可以用读写锁ReadWriteLock实现:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;


  1. class Data {        
  2.     private int data;// 共享数据    
  3.     private ReadWriteLock rwl = new ReentrantReadWriteLock();       
  4.     public void set(int data) {    
  5.         rwl.writeLock().lock();// 取到写锁    
  6.         try {    
  7.             System.out.println(Thread.currentThread().getName() + "准备写入数据");    
  8.             try {    
  9.                 Thread.sleep(20);    
  10.             } catch (InterruptedException e) {    
  11.                 e.printStackTrace();    
  12.             }    
  13.             this.data = data;    
  14.             System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
  15.         } finally {    
  16.             rwl.writeLock().unlock();// 释放写锁    
  17.         }    
  18.     }       
  19.   
  20.     public void get() {    
  21.         rwl.readLock().lock();// 取到读锁    
  22.         try {    
  23.             System.out.println(Thread.currentThread().getName() + "准备读取数据");    
  24.             try {    
  25.                 Thread.sleep(20);    
  26.             } catch (InterruptedException e) {    
  27.                 e.printStackTrace();    
  28.             }    
  29.             System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
  30.         } finally {    
  31.             rwl.readLock().unlock();// 释放读锁    
  32.         }    
  33.     }    
  34. }    

测试结果:

  1. Thread-W1准备写入数据  
  2. Thread-W1写入9  
  3. Thread-W1准备写入数据  
  4. Thread-W1写入24  
  5. Thread-W1准备写入数据  
  6. Thread-W1写入12  
  7. Thread-W0准备写入数据  
  8. Thread-W0写入22  
  9. Thread-W0准备写入数据  
  10. Thread-W0写入15  
  11. Thread-W0准备写入数据  
  12. Thread-W0写入6  
  13. Thread-W0准备写入数据  
  14. Thread-W0写入13  
  15. Thread-W0准备写入数据  
  16. Thread-W0写入0  
  17. Thread-W2准备写入数据  
  18. Thread-W2写入23  
  19. Thread-W2准备写入数据  
  20. Thread-W2写入24  
  21. Thread-W2准备写入数据  
  22. Thread-W2写入24  
  23. Thread-W2准备写入数据  
  24. Thread-W2写入17  
  25. Thread-W2准备写入数据  
  26. Thread-W2写入11  
  27. Thread-R2准备读取数据  
  28. Thread-R1准备读取数据  
  29. Thread-R0准备读取数据  
  30. Thread-R0读取11  
  31. Thread-R1读取11  
  32. Thread-R2读取11  
  33. Thread-W1准备写入数据  
  34. Thread-W1写入18  
  35. Thread-W1准备写入数据  
  36. Thread-W1写入1  
  37. Thread-R0准备读取数据  
  38. Thread-R2准备读取数据  
  39. Thread-R1准备读取数据  
  40. Thread-R2读取1  
  41. Thread-R2准备读取数据  
  42. Thread-R1读取1  
  43. Thread-R0读取1  
  44. Thread-R1准备读取数据  
  45. Thread-R0准备读取数据  
  46. Thread-R0读取1  
  47. Thread-R2读取1  
  48. Thread-R2准备读取数据  
  49. Thread-R1读取1  
  50. Thread-R0准备读取数据  
  51. Thread-R1准备读取数据  
  52. Thread-R0读取1  
  53. Thread-R2读取1  
  54. Thread-R1读取1  
  55. Thread-R0准备读取数据  
  56. Thread-R1准备读取数据  
  57. Thread-R2准备读取数据  
  58. Thread-R1读取1  
  59. Thread-R2读取1  
  60. Thread-R0读取1  


与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)


从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。

在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

线程间通信Condition

Condition可以替代传统的线程间通信,await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。

——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!


传统线程的通信方式,Condition都可以实现。

注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。


Condition的强大之处在于它可以为多个线程间建立不同的Condition

看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition 实例来做到这一点。

——其实就是java.util.concurrent.ArrayBlockingQueue的功能


  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;  //写索引  
  8.   int takeptr; //读索引  
  9.   int count;   //队列中数据数目  
  10.   
  11.   //写  
  12.   public void put(Object x) throws InterruptedException {  
  13.     lock.lock(); //锁定  
  14.     try {  
  15.       // 如果队列满,则阻塞<写线程>  
  16.       while (count == items.length) {  
  17.         notFull.await();   
  18.       }  
  19.       // 写入队列,并更新写索引  
  20.       items[putptr] = x;   
  21.       if (++putptr == items.length) putptr = 0;   
  22.       ++count;  
  23.   
  24.       // 唤醒<读线程>  
  25.       notEmpty.signal();   
  26.     } finally {   
  27.       lock.unlock();//解除锁定   
  28.     }   
  29.   }  
  30.   
  31.   //读   
  32.   public Object take() throws InterruptedException {   
  33.     lock.lock(); //锁定   
  34.     try {  
  35.       // 如果队列空,则阻塞<读线程>  
  36.       while (count == 0) {  
  37.          notEmpty.await();  
  38.       }  
  39.   
  40.       //读取队列,并更新读索引  
  41.       Object x = items[takeptr];   
  42.       if (++takeptr == items.length) takeptr = 0;  
  43.       --count;  
  44.   
  45.       // 唤醒<写线程>  
  46.       notFull.signal();   
  47.       return x;   
  48.     } finally {   
  49.       lock.unlock();//解除锁定   
  50.     }   
  51.   }   


优点:

假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。

那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。




14.同步队列

详见 http://wsmajunfeng.iteye.com/blog/1629354


15.管道

两个线程之间,一个拥有pepedwriter,一个拥有pipedreader(需要writer做参数)。


16.CountDownLatch和CyclicBarrier


CountDownLatch:

被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。

new CountDownLatch(int size) , 被等待的线程在执行完操作后latch.countdown(),等待的线程调用latch.await(); 计数值为0时结束等待。


CyclicBarrier:

 字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。适用于:创建一组任务,并行执行,然后再下一个步骤之前等待。用于一组或几组线程,比如一组线程需要在一个时间点上达成一致,例如同时开始一个工作。

//当await的数量到达了设定的数量后,首先执行该Runnable对象。
CyclicBarrier(int,Runnable):

//通知barrier已完成线程
await():  

  1. /**  
  2.  * 各省数据独立,分库存偖。为了提高计算性能,统计时采用每个省开一个线程先计算单省结果,最后汇总。  
  3.  *   
  4.  * @author guangbo email:[email protected]  
  5.  *   
  6.  */  
  7. public class Total {   
  8.   
  9.     // private ConcurrentHashMap result = new ConcurrentHashMap();   
  10.   
  11.     public static void main(String[] args) {   
  12.         TotalService totalService = new TotalServiceImpl();   
  13.         CyclicBarrier barrier = new CyclicBarrier(5,   
  14.                 new TotalTask(totalService));   
  15.   
  16.         // 实际系统是查出所有省编码code的列表,然后循环,每个code生成一个线程。   
  17.         new BillTask(new BillServiceImpl(), barrier, "北京").start();   
  18.         new BillTask(new BillServiceImpl(), barrier, "上海").start();   
  19.         new BillTask(new BillServiceImpl(), barrier, "广西").start();   
  20.         new BillTask(new BillServiceImpl(), barrier, "四川").start();   
  21.         new BillTask(new BillServiceImpl(), barrier, "黑龙江").start();   
  22.   
  23.     }   
  24. }   
  25.   
  26. /**  
  27.  * 主任务:汇总任务  
  28.  */  
  29. class TotalTask implements Runnable {   
  30.     private TotalService totalService;   
  31.   
  32.     TotalTask(TotalService totalService) {   
  33.         this.totalService = totalService;   
  34.     }   
  35.   
  36.     public void run() {   
  37.         // 读取内存中各省的数据汇总,过程略。   
  38.         totalService.count();   
  39.         System.out.println("=======================================");   
  40.         System.out.println("开始全国汇总");   
  41.     }   
  42. }   
  43.   
  44. /**  
  45.  * 子任务:计费任务  
  46.  */  
  47. class BillTask extends Thread {   
  48.     // 计费服务   
  49.     private BillService billService;   
  50.     private CyclicBarrier barrier;   
  51.     // 代码,按省代码分类,各省数据库独立。   
  52.     private String code;   
  53.   
  54.     BillTask(BillService billService, CyclicBarrier barrier, String code) {   
  55.         this.billService = billService;   
  56.         this.barrier = barrier;   
  57.         this.code = code;   
  58.     }   
  59.   
  60.     public void run() {   
  61.         System.out.println("开始计算--" + code + "省--数据!");   
  62.         billService.bill(code);   
  63.         // 把bill方法结果存入内存,如ConcurrentHashMap,vector等,代码略   
  64.         System.out.println(code + "省已经计算完成,并通知汇总Service!");   
  65.         try {   
  66.             // 通知barrier已经完成   
  67.             barrier.await();   
  68.         } catch (InterruptedException e) {   
  69.             e.printStackTrace();   
  70.         } catch (BrokenBarrierException e) {   
  71.             e.printStackTrace();   
  72.         }   
  73.     }   
  74.   
  75. }  


Semaphore

 Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。


 假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public  class  Test {
     public  static  void  main(String[] args) {
         int  N =  8 ;             //工人数
         Semaphore semaphore =  new  Semaphore( 5 );  //机器数目
         for ( int  i= 0 ;i
             new  Worker(i,semaphore).start();
     }
     
     static  class  Worker  extends  Thread{
         private  int  num;
         private  Semaphore semaphore;
         public  Worker( int  num,Semaphore semaphore){
             this .num = num;
             this .semaphore = semaphore;
         }
         
         @Override
         public  void  run() {
             try  {
                 semaphore.acquire();
                 System.out.println( "工人" + this .num+ "占用一个机器在生产..." );
                 Thread.sleep( 2000 );
                 System.out.println( "工人" + this .num+ "释放出机器" );
                 semaphore.release();           
             catch  (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
}

下面对上面说的三个辅助类进行一个总结:

  1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。


17.concurrenthashmap(转自http://blog.csdn.net/liuzhengkang/article/details/2916620)


 ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁。如图
 
       左边便是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段)。ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。 只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationEx ception,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

接下来,让我们看看ConcurrentHashMap中的几个重要方法,心里知道了实现机制后,使用起来就更加有底气。
       ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图可以看出之间的关系。
       get方法(请注意,这里分析的方法都是针对桶的,因为ConcurrentHashMap的最大改进就是将粒度细化到了桶上),首先判断了当前桶的数据个数是否为0,为0自然不可能get到什么,只有返回null,这样做避免了不必要的搜索,也用最小的代价避免出错。然后得到头节点(方法将在下面涉及)之后就是根据hash和key逐个判断是否是指定的值,如果是并且值非空就说明找到了,直接返回;程序非常简单,但有一个令人困惑的地方,这句return readValueUnderLock(e)到底是用来干什么的呢?研究它的代码,在锁定之后返回一个值。但这里已经有一句V v = e.value得到了节点的值,这句return readValueUnderLock(e)是否多此一举?事实上,这里完全是为了并发考虑的,这里当v为空时,可能是一个线程正在改变节点,而之前的get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值,这里不得不佩服Doug Lee思维的严密性。整个get操作只有很少的情况会锁定,相对于之前的Hashtable,并发是不可避免的啊!
        V get(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry e = getFirst(hash);
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key)) {
                        V v = e.value;
                        if (v != null)
                            return v;
                        return readValueUnderLock(e); // recheck
                    }
                    e = e.next;
                }
            }
            return null;
        }

 

        V readValueUnderLock(HashEntry e) {
            lock();
            try {
                return e.value;
            } finally {
                unlock();
            }
        }

 

    put操作一上来就锁定了整个segment,这当然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时能够rehash,而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每个segment是一个传统意义上的hashtable,如上图,从两者的结构就可以看出区别,这里就是找出需要的entry在table的哪一个位置,之后得到的entry就是这个链的第一个节点,如果e!=null,说明找到了,这是就要替换节点的值(onlyIfAbsent == false),否则,我们需要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry插入到链头,剩下的就非常容易理解了。

        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();
                HashEntry[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry first = (HashEntry) tab[index];
                HashEntry e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue;
                if (e != null) {
                    oldValue = e.value;
                    if (!onlyIfAbsent)
                        e.value = value;
                }
                else {
                    oldValue = null;
                    ++modCount;
                    tab[index] = new HashEntry(key, hash, first, value);
                    count = c; // write-volatile
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

 

    remove操作非常类似put,但要注意一点区别,中间那个for循环是做什么用的呢?(*号标记)从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。

        V remove(Object key, int hash, Object value) {
            lock();
            try {
                int c = count - 1;
                HashEntry[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry first = (HashEntry)tab[index];
                HashEntry e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue = null;
                if (e != null) {
                    V v = e.value;
                    if (value == null || value.equals(v)) {
                        oldValue = v;
                        // All entries following removed node can stay
                        // in list, but all preceding ones need to be
                        // cloned.
                        ++modCount;
                        HashEntry newFirst = e.next;
                    *    for (HashEntry p = first; p != e; p = p.next)
                    *        newFirst = new HashEntry(p.key, p.hash, 
                                                          newFirst, p.value);
                        tab[index] = newFirst;
                        count = c; // write-volatile
                    }
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

 

    static final class HashEntry {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry next;

        HashEntry(K key, int hash, HashEntry next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }
    }

 




你可能感兴趣的:(Java&JVM)