【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

本文深入探讨Java中的Lock锁机制,包括ReentrantLock、读写锁的使用,以及与synchronized关键字的性能和用途比较。详细介绍了Lock锁的高级功能,如可中断锁、公平锁、条件变量等,通过示例代码展示如何实现线程间的协作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单使用Lock锁

    Java 5中引入了新的锁机制——java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作。Lock接口有3个实现它的类:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。lock必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化。为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁,尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。因此,采用lock加锁和释放锁的一般形式如下:

 

 
  1. Lock lock = new ReentrantLock();//默认使用非公平锁,如果要使用公平锁,需要传入参数true

  2. ........

  3. lock.lock();

  4. try {

  5. //更新对象的状态

  6. //捕获异常,必要时恢复到原来的不变约束

  7. //如果有return语句,放在这里

  8. } finally {

  9. lock.unlock(); //锁必须在finally块中释放

  10. }

 

ReetrankLock与synchronized比较

 

    性能比较

 

    在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。


    下面浅析以下两种锁机制的底层的实现策略。

    互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。synchronized采用的便是这种并发策略。

    随着指令集的发展,我们有了另一种选择:基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步。ReetrantLock采用的便是这种并发策略。

    在乐观的并发策略中,需要操作和冲突检测这两个步骤具备原子性,它靠硬件指令来保证,这里用的是CAS操作(Compare and Swap)。JDK1.5之后,Java程序才可以使用CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState,这里其实就是调用的CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起。

    Java 5中引入了注入AutomicInteger、AutomicLong、AutomicReference等特殊的原子性变量类,它们提供的如:compareAndSet()、incrementAndSet()和getAndIncrement()等方法都使用了CAS操作。因此,它们都是由硬件指令来保证的原子方法。

 

   用途比较

    基本语法上,ReentrantLock与synchronized很相似,它们都具备一样的线程重入特性,只是代码写法上有点区别而已,一个表现为API层面的互斥锁(Lock),一个表现为原生语法层面的互斥锁(synchronized)。ReentrantLock相对synchronized而言还是增加了一些高级功能,主要有以下三项:

    1、等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间非常上的同步块很有帮助。而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的。

    2、可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁时非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁。

    3、锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程)。

 

可中断锁

    ReetrantLock有两种锁:忽略中断锁和响应中断锁。忽略中断锁与synchronized实现的互斥锁一样,不能响应中断,而响应中断锁可以响应中断。

    如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,如果此时ReetrantLock提供的是忽略中断锁,则它不会去理会该中断,而是让线程B继续等待,而如果此时ReetrantLock提供的是响应中断锁,那么它便会处理中断,让线程B放弃等待,转而去处理其他事情。

  获得响应中断锁的一般形式如下:

 

 
  1. ReentrantLock lock = new ReentrantLock();

  2. ...........

  3. lock.lockInterruptibly();//获取响应中断锁

  4. try {

  5. //更新对象的状态

  6. //捕获异常,必要时恢复到原来的不变约束

  7. //如果有return语句,放在这里

  8. }finally{

  9. lock.unlock(); //锁必须在finally块中释放

  10. }

    这里有一个不错的分析中断的示例代码(摘自网上)

    当用synchronized中断对互斥锁的等待时,并不起作用,该线程依然会一直等待,如下面的实例:

 
  1. public class Buffer {

  2.  
  3. private Object lock;

  4.  
  5. public Buffer() {

  6. lock = this;

  7. }

  8.  
  9. public void write() {

  10. synchronized (lock) {

  11. long startTime = System.currentTimeMillis();

  12. System.out.println("开始往这个buff写入数据…");

  13. for (;;)// 模拟要处理很长时间

  14. {

  15. if (System.currentTimeMillis()

  16. - startTime > Integer.MAX_VALUE) {

  17. break;

  18. }

  19. }

  20. System.out.println("终于写完了");

  21. }

  22. }

  23.  
  24. public void read() {

  25. synchronized (lock) {

  26. System.out.println("从这个buff读数据");

  27. }

  28. }

  29.  
  30. public static void main(String[] args) {

  31. Buffer buff = new Buffer();

  32.  
  33. final Writer writer = new Writer(buff);

  34. final Reader reader = new Reader(buff);

  35.  
  36. writer.start();

  37. reader.start();

  38.  
  39. new Thread(new Runnable() {

  40.  
  41. @Override

  42. public void run() {

  43. long start = System.currentTimeMillis();

  44. for (;;) {

  45. //等5秒钟去中断读

  46. if (System.currentTimeMillis()

  47. - start > 5000) {

  48. System.out.println("不等了,尝试中断");

  49. reader.interrupt(); //尝试中断读线程

  50. break;

  51. }

  52.  
  53. }

  54.  
  55. }

  56. }).start();

  57. // 我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,

  58. // 就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,

  59. // 它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,

  60. // 让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。

  61. }

  62. }

  63.  
  64. class Writer extends Thread {

  65.  
  66. private Buffer buff;

  67.  
  68. public Writer(Buffer buff) {

  69. this.buff = buff;

  70. }

  71.  
  72. @Override

  73. public void run() {

  74. buff.write();

  75. }

  76. }

  77.  
  78. class Reader extends Thread {

  79.  
  80. private Buffer buff;

  81.  
  82. public Reader(Buffer buff) {

  83. this.buff = buff;

  84. }

  85.  
  86. @Override

  87. public void run() {

  88.  
  89. buff.read();//这里估计会一直阻塞

  90.  
  91. System.out.println("读结束");

  92.  
  93. }

  94. }

    执行结果如下:

 

 

    我们等待了很久,后面依然没有输出,说明读线程对互斥锁的等待并没有被中断,也就是该户吃锁没有响应对读线程的中断。

    我们再将上面代码中synchronized的互斥锁改为ReentrantLock的响应中断锁,即改为如下代码: 

 

 
  1. import java.util.concurrent.locks.ReentrantLock;

  2.  
  3. public class BufferInterruptibly {

  4.  
  5. private ReentrantLock lock = new ReentrantLock();

  6.  
  7. public void write() {

  8. lock.lock();

  9. try {

  10. long startTime = System.currentTimeMillis();

  11. System.out.println("开始往这个buff写入数据…");

  12. for (;;)// 模拟要处理很长时间

  13. {

  14. if (System.currentTimeMillis()

  15. - startTime > Integer.MAX_VALUE) {

  16. break;

  17. }

  18. }

  19. System.out.println("终于写完了");

  20. } finally {

  21. lock.unlock();

  22. }

  23. }

  24.  
  25. public void read() throws InterruptedException {

  26. lock.lockInterruptibly();// 注意这里,可以响应中断

  27. try {

  28. System.out.println("从这个buff读数据");

  29. } finally {

  30. lock.unlock();

  31. }

  32. }

  33.  
  34. public static void main(String args[]) {

  35. BufferInterruptibly buff = new BufferInterruptibly();

  36.  
  37. final Writer2 writer = new Writer2(buff);

  38. final Reader2 reader = new Reader2(buff);

  39.  
  40. writer.start();

  41. reader.start();

  42.  
  43. new Thread(new Runnable() {

  44.  
  45. @Override

  46. public void run() {

  47. long start = System.currentTimeMillis();

  48. for (;;) {

  49. if (System.currentTimeMillis()

  50. - start > 5000) {

  51. System.out.println("不等了,尝试中断");

  52. reader.interrupt(); //此处中断读操作

  53. break;

  54. }

  55. }

  56. }

  57. }).start();

  58.  
  59. }

  60. }

  61.  
  62. class Reader2 extends Thread {

  63.  
  64. private BufferInterruptibly buff;

  65.  
  66. public Reader2(BufferInterruptibly buff) {

  67. this.buff = buff;

  68. }

  69.  
  70. @Override

  71. public void run() {

  72.  
  73. try {

  74. buff.read();//可以收到中断的异常,从而有效退出

  75. } catch (InterruptedException e) {

  76. System.out.println("我不读了");

  77. }

  78.  
  79. System.out.println("读结束");

  80.  
  81. }

  82. }

  83.  
  84. class Writer2 extends Thread {

  85.  
  86. private BufferInterruptibly buff;

  87.  
  88. public Writer2(BufferInterruptibly buff) {

  89. this.buff = buff;

  90. }

  91.  
  92. @Override

  93. public void run() {

  94. buff.write();

  95. }

  96.  
  97. }

    执行结果如下:

 

 

    从结果中可以看出,尝试中断后输出了catch语句块中的内容,也输出了后面的“读结束”,说明线程对互斥锁的等待被中断了,也就是该互斥锁响应了对读线程的中断。

 

条件变量实现线程间协作

 

    在生产者——消费者模型一文中,我们用synchronized实现互斥,并配合使用Object对象的wait()和notify()或notifyAll()方法来实现线程间协作。Java 5之后,我们可以用Reentrantlock锁配合Condition对象上的await()和signal()或signalAll()方法来实现线程间协作。在ReentrantLock对象上newCondition()可以得到一个Condition对象,可以通过在Condition上调用await()方法来挂起一个任务(线程),通过在Condition上调用signal()来通知任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。另外,如果使用了公平锁,signalAll()的与Condition关联的所有任务将以FIFO队列的形式获取锁,如果没有使用公平锁,则获取锁的任务是随机的,这样我们便可以更好地控制处在await状态的任务获取锁的顺序。与notifyAll()相比,signalAll()是更安全的方式。另外,它可以指定唤醒与自身Condition对象绑定在一起的任务。

 

    下面将生产者——消费者模型一文中的代码改为用条件变量实现,如下:

 
  1. import java.util.concurrent.*;

  2. import java.util.concurrent.locks.*;

  3.  
  4. class Info{ // 定义信息类

  5. private String name = "name";//定义name属性,为了与下面set的name属性区别开

  6. private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开

  7. private boolean flag = true ; // 设置标志位,初始时先生产

  8. private Lock lock = new ReentrantLock();

  9. private Condition condition = lock.newCondition(); //产生一个Condition对象

  10. public void set(String name,String content){

  11. lock.lock();

  12. try{

  13. while(!flag){

  14. condition.await() ;

  15. }

  16. this.setName(name) ; // 设置名称

  17. Thread.sleep(300) ;

  18. this.setContent(content) ; // 设置内容

  19. flag = false ; // 改变标志位,表示可以取走

  20. condition.signal();

  21. }catch(InterruptedException e){

  22. e.printStackTrace() ;

  23. }finally{

  24. lock.unlock();

  25. }

  26. }

  27.  
  28. public void get(){

  29. lock.lock();

  30. try{

  31. while(flag){

  32. condition.await() ;

  33. }

  34. Thread.sleep(300) ;

  35. System.out.println(this.getName() +

  36. " --> " + this.getContent()) ;

  37. flag = true ; // 改变标志位,表示可以生产

  38. condition.signal();

  39. }catch(InterruptedException e){

  40. e.printStackTrace() ;

  41. }finally{

  42. lock.unlock();

  43. }

  44. }

  45.  
  46. public void setName(String name){

  47. this.name = name ;

  48. }

  49. public void setContent(String content){

  50. this.content = content ;

  51. }

  52. public String getName(){

  53. return this.name ;

  54. }

  55. public String getContent(){

  56. return this.content ;

  57. }

  58. }

  59. class Producer implements Runnable{ // 通过Runnable实现多线程

  60. private Info info = null ; // 保存Info引用

  61. public Producer(Info info){

  62. this.info = info ;

  63. }

  64. public void run(){

  65. boolean flag = true ; // 定义标记位

  66. for(int i=0;i<10;i++){

  67. if(flag){

  68. this.info.set("姓名--1","内容--1") ; // 设置名称

  69. flag = false ;

  70. }else{

  71. this.info.set("姓名--2","内容--2") ; // 设置名称

  72. flag = true ;

  73. }

  74. }

  75. }

  76. }

  77. class Consumer implements Runnable{

  78. private Info info = null ;

  79. public Consumer(Info info){

  80. this.info = info ;

  81. }

  82. public void run(){

  83. for(int i=0;i<10;i++){

  84. this.info.get() ;

  85. }

  86. }

  87. }

  88. public class ThreadCaseDemo{

  89. public static void main(String args[]){

  90. Info info = new Info(); // 实例化Info对象

  91. Producer pro = new Producer(info) ; // 生产者

  92. Consumer con = new Consumer(info) ; // 消费者

  93. new Thread(pro).start() ;

  94. //启动了生产者线程后,再启动消费者线程

  95. try{

  96. Thread.sleep(500) ;

  97. }catch(InterruptedException e){

  98. e.printStackTrace() ;

  99. }

  100.  
  101. new Thread(con).start() ;

  102. }

  103. }

    执行后,同样可以得到如下的结果:

姓名--1 --> 内容--1
姓名--2 --> 内容--2
姓名--1 --> 内容--1
姓名--2 --> 内容--2
姓名--1 --> 内容--1
姓名--2 --> 内容--2
姓名--1 --> 内容--1
姓名--2 --> 内容--2
姓名--1 --> 内容--1
姓名--2 --> 内容--2
    从以上并不能看出用条件变量的await()、signal()、signalAll()方法比用Object对象的wait()、notify()、notifyAll()方法实现线程间协作有多少优点,但它在处理更复杂的多线程问题时,会有明显的优势。所以,Lock和Condition对象只有在更加困难的多线程问题中才是必须的。

    

 

读写锁

 

    另外,synchronized获取的互斥锁不仅互斥读写操作、写写操作,还互斥读读操作,而读读操作时不会带来数据竞争的,因此对对读读操作也互斥的话,会降低性能。Java 5中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:

 

 
  1. ReadWriteLock rwl = new ReentrantReadWriteLock();

  2. rwl.writeLock().lock() //获取写锁

  3. rwl.readLock().lock() //获取读锁

   用读锁来锁定读操作,用写锁来锁定写操作,这样写操作和写操作之间会互斥,读操作和写操作之间会互斥,但读操作和读操作就不会互斥。

 

   《Java并发编程实践》一书给出了使用 ReentrantLock的最佳时机:

    当你需要以下高级特性时,才应该使用:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized。  

--------------------- 作者:兰亭风雨 来源:优快云 原文:https://blog.youkuaiyun.com/ns_code/article/details/17487337?utm_source=copy 版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值