zoukankan      html  css  js  c++  java
  • 【Java线程】锁机制:synchronized、Lock、Condition(转)

    原文地址

    1、synchronized

    把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)

    1.1 原子性

    原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

    1.2 可见性

    可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

    作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

    原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。

    一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。

    ——volatile只保证可见性,不保证原子性!

    1.3 何时要同步?

    可见性同步的基本规则是在以下情况中必须同步: 

    1. 读取上一次可能是由另一个线程写入的变量 
    2. 写入下一次可能由另一个线程读取的变量

    一致性同步:当修改多个相关值时,您想要其它线程原子地看到这组更改—— 要么看到全部更改,要么什么也看不到。

    这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。

    在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:

    1. 由静态初始化器(在静态字段上或 static{} 块中的初始化器)
    2. 初始化数据时 
    3. 访问 final 字段时 ——final对象呢?
    4. 在创建线程之前创建对象时 
    5. 线程可以看见它将要处理的对象时

    1.4 synchronize的限制

    synchronized是不错,但它并不完美。它有一些功能性的限制:

    1. 它无法中断一个正在等候获得锁的线程;
    2. 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
    3. 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

    2、ReentrantLock

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

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

    [java] view plain copy
     
    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内!!

    3、读写锁ReadWriteLock

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

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

    [java] view plain copy
     
    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. }    


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

    [java] view plain copy
     
    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.     }    


    运行结果:

    [plain] view plain copy
     
    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;

    [java] view plain copy
     
    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. }    

    测试结果:

    [plain] view plain copy
     
    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 是使用读-写锁定的理想候选者。

    4、线程间通信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的功能

    [java] view plain copy
     
    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不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

  • 相关阅读:
    专业实训日志01
    《河北省重大技术需求征集系统》可用性及可修改性战术分析
    基于淘宝网的软件质量属性分析
    架构漫谈读后感之软件架构师如何工作
    模型-视图-控制器模式(MVC模式,10种常见体系架构模式之一)
    列举实例分析采用的可用性和可修改性战术
    以淘宝网为例的质量属性分析
    《架构漫谈》读后感 之“关于软件架构师如何工作”
    《软件需求(第二版)》阅读笔记06
    《软件需求(第二版)》阅读笔记05
  • 原文地址:https://www.cnblogs.com/yixianyixian/p/8243964.html
Copyright © 2011-2022 走看看