`

【Java线程】锁机制:synchronized、Lock、Condition

 
阅读更多

原文地址:http://blog.csdn.net/vking_wang/article/details/9952063

 

 

  • http://www.infoq.com/cn/articles/java-memory-model-5  深入理解Java内存模型(五)——锁 
  • http://www.ibm.com/developerworks/cn/java/j-jtp10264/  Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制
  • http://blog.csdn.net/ghsau/article/details/7481142

 

 

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 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

 

  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,则代码如下:

 

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

 

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的功能

 

 

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

分享到:
评论
1 楼 JueLie 2016-11-13  
规划规划

相关推荐

    java中的Lock类和Condition类.docx

    在jdk1.5以后,JAVA提供了Lock类来实现和synchronized一样的功能,并且还提供了Condition来显示线程间通信。 Lock类是Java类来提供的功能,丰富的api使得Lock类的同步功能比synchronized的同步更强大。本文章的所有...

    locks框架:接口.pdf

    可重入性和重入锁: 解释 Lock 接口的可重入性,讲解同一个线程多次获取锁的机制,避免死锁。介绍 ReentrantLock 的实现原理。 Condition 条件变量: 介绍 Lock 接口中的 Condition,它可以实现更复杂的线程等待和...

    Lock锁的底层原理完整版

    Lock锁,一种线程同步机制,其主要功能是防止多个线程同时访问同一代码块,从而避免因并发问题引发的数据不一致或其他错误。Lock锁的灵活性相比synchronized更高,它支持手动获取和释放锁,能够中断的获取锁以及超时...

    JUC多线程学习个人笔记

    锁机制:JUC提供了Lock接口和Condition接口,可以实现更细粒度的锁控制和线程的等待和唤醒机制。 并发工具类:JUC提供了一些并发编程的工具类,如Semaphore、CountDownLatch、CyclicBarrier等,可以实现线程间的协作...

    龙果java并发编程完整视频

    第14节synchronized保证线程安全的原理(理论层面)00:13:59分钟 | 第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题与线程安全性深入解析00:27:15分钟 | 第17节理解自旋锁,死锁...

    龙果 java并发编程原理实战

    第13节从Java字节码的角度看线程安全性问题00:25:43分钟 | 第14节synchronized保证线程安全的原理(理论层面) 00:13:59分钟 | 第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题...

    Java线程指南

    Java线程指南 线程安全与不安全 线程同步synchronized和volatile 线程协作-生产者/消费者模式 Timer和TimerTask 线程池 Callable和Future 锁对象Lock-同步问题更完美的处理方式 Condition-线程通信更高效的方式

    Java 并发编程原理与实战视频

    第13节从Java字节码的角度看线程安全性问题00:25:43分钟 | 第14节synchronized保证线程安全的原理(理论层面) 00:13:59分钟 | 第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题...

    java并发编程

    第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题与线程安全性深入解析00:27:15分钟 | 第17节理解自旋锁,死锁与重入锁00:24:58分钟 | 第18节深入理解volatile原理与使用00:28:30...

    JUC知识点总结(三)ReentrantLock与ReentrantReadWriteLock源码解析

    ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 它是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等 避免多线程死锁的方法。 尝试非...

    【2018最新最详细】并发多线程教程

    12.详解Condition的await和signal等待通知机制 13.LockSupport工具 14.并发容器之ConcurrentHashMap(JDK 1.8版本) 15.并发容器之ConcurrentLinkedQueue 16.并发容器之CopyOnWriteArrayList 17.并发容器之ThreadLocal...

    Java并发编程原理与实战

    从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入...

    这就是标题—— JUC.pdf

    Synchronized / Lock 线程通讯 wait()、notify()和notifyAll() 虚假唤醒 Condition 定制化通信 多线程锁 并发下的集合类 List Set Map Callable接口 线程创建的方式 callable / runnable FutureTask JUC常用辅助类 ...

    JAVA高质量并发详解,多线程并发深入讲解

    接着,深入讲解了Java并发编程的核心API,如synchronized关键字、Lock接口、Condition接口、Semaphore等,帮助读者掌握Java并发编程的基本工具和方法。 除了基础知识和API的讲解,本书还重点介绍了Java并发编程的...

    Java 7并发编程实战手册

    《Java 7并发编程实战手册》是Java 7并发编程的实战指南,介绍了Java 7并发API中大部分重要而有用的机制。全书分为9章,涵盖了线程管理、线程同步、线程执行器、Fork/Join框架、并发集合、定制并发类、测试并发应用...

    Java并发编程实战

    本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则...

    JAVA多线程实现2个producer和一个Consumer把整数放入到一个环形缓冲Circle Buffer中

    采用同步机制synchronized/wait(notify)或者lock(unlock)/condition variable实现两个producer和一个consumer之间协调运行。运行结果输出格式为:Put(or Get) number {[content] length start_index end_index} 包含...

    Java JDK 7学习笔记(国内第一本Java 7,前期版本累计销量5万册)

    11.2.1 lock、readwritelock与condition 349 11.2.2 使用executor 357 11.2.3 并行collection简介 370 11.3 重点复习 373 11.4 课后练习 375 chapter12 通用api 377 12.1 日志 378 12.1.1 日志api简介...

Global site tag (gtag.js) - Google Analytics