【Java并发编程】:并发新特性—Lock锁和条件变量

时间:2021-09-19 13:28:44

简单使用Lock锁

Java5中引入了新的锁机制——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块中释放

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. private Object lock;
  3. public Buffer() {
  4. lock = this;
  5. }
  6. public void write() {
  7. synchronized (lock) {
  8. long startTime = System.currentTimeMillis();
  9. System.out.println("开始往这个buff写入数据…");
  10. for (;;)// 模拟要处理很长时间
  11. {
  12. if (System.currentTimeMillis()
  13. - startTime > Integer.MAX_VALUE) {
  14. break;
  15. }
  16. }
  17. System.out.println("终于写完了");
  18. }
  19. }
  20. public void read() {
  21. synchronized (lock) {
  22. System.out.println("从这个buff读数据");
  23. }
  24. }
  25. public static void main(String[] args) {
  26. Buffer buff = new Buffer();
  27. final Writer writer = new Writer(buff);
  28. final Reader reader = new Reader(buff);
  29. writer.start();
  30. reader.start();
  31. new Thread(new Runnable() {
  32. @Override
  33. public void run() {
  34. long start = System.currentTimeMillis();
  35. for (;;) {
  36. //等5秒钟去中断读
  37. if (System.currentTimeMillis()
  38. - start > 5000) {
  39. System.out.println("不等了,尝试中断");
  40. reader.interrupt();  //尝试中断读线程
  41. break;
  42. }
  43. }
  44. }
  45. }).start();
  46. // 我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,
  47. // 就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,
  48. // 它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,
  49. // 让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。
  50. }
  51. }
  52. class Writer extends Thread {
  53. private Buffer buff;
  54. public Writer(Buffer buff) {
  55. this.buff = buff;
  56. }
  57. @Override
  58. public void run() {
  59. buff.write();
  60. }
  61. }
  62. class Reader extends Thread {
  63. private Buffer buff;
  64. public Reader(Buffer buff) {
  65. this.buff = buff;
  66. }
  67. @Override
  68. public void run() {
  69. buff.read();//这里估计会一直阻塞
  70. System.out.println("读结束");
  71. }
  72. }

执行结果如下:

【Java并发编程】:并发新特性—Lock锁和条件变量
    我们等待了很久,后面依然没有输出,说明读线程对互斥锁的等待并没有被中断,也就是该户吃锁没有响应对读线程的中断。
    我们再将上面代码中synchronized的互斥锁改为ReentrantLock的响应中断锁,即改为如下代码:
  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class BufferInterruptibly {
  3. private ReentrantLock lock = new ReentrantLock();
  4. public void write() {
  5. lock.lock();
  6. try {
  7. long startTime = System.currentTimeMillis();
  8. System.out.println("开始往这个buff写入数据…");
  9. for (;;)// 模拟要处理很长时间
  10. {
  11. if (System.currentTimeMillis()
  12. - startTime > Integer.MAX_VALUE) {
  13. break;
  14. }
  15. }
  16. System.out.println("终于写完了");
  17. } finally {
  18. lock.unlock();
  19. }
  20. }
  21. public void read() throws InterruptedException {
  22. lock.lockInterruptibly();// 注意这里,可以响应中断
  23. try {
  24. System.out.println("从这个buff读数据");
  25. } finally {
  26. lock.unlock();
  27. }
  28. }
  29. public static void main(String args[]) {
  30. BufferInterruptibly buff = new BufferInterruptibly();
  31. final Writer2 writer = new Writer2(buff);
  32. final Reader2 reader = new Reader2(buff);
  33. writer.start();
  34. reader.start();
  35. new Thread(new Runnable() {
  36. @Override
  37. public void run() {
  38. long start = System.currentTimeMillis();
  39. for (;;) {
  40. if (System.currentTimeMillis()
  41. - start > 5000) {
  42. System.out.println("不等了,尝试中断");
  43. reader.interrupt();  //此处中断读操作
  44. break;
  45. }
  46. }
  47. }
  48. }).start();
  49. }
  50. }
  51. class Reader2 extends Thread {
  52. private BufferInterruptibly buff;
  53. public Reader2(BufferInterruptibly buff) {
  54. this.buff = buff;
  55. }
  56. @Override
  57. public void run() {
  58. try {
  59. buff.read();//可以收到中断的异常,从而有效退出
  60. } catch (InterruptedException e) {
  61. System.out.println("我不读了");
  62. }
  63. System.out.println("读结束");
  64. }
  65. }
  66. class Writer2 extends Thread {
  67. private BufferInterruptibly buff;
  68. public Writer2(BufferInterruptibly buff) {
  69. this.buff = buff;
  70. }
  71. @Override
  72. public void run() {
  73. buff.write();
  74. }
  75. }

执行结果如下:

【Java并发编程】:并发新特性—Lock锁和条件变量
    从结果中可以看出,尝试中断后输出了catch语句块中的内容,也输出了后面的“读结束”,说明线程对互斥锁的等待被中断了,也就是该互斥锁响应了对读线程的中断。

条件变量实现线程间协作

在下面将一文中的代码改为用条件变量实现,如下:

  1. import java.util.concurrent.*;
  2. import java.util.concurrent.locks.*;
  3. class Info{ // 定义信息类
  4. private String name = "name";//定义name属性,为了与下面set的name属性区别开
  5. private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开
  6. private boolean flag = true ;   // 设置标志位,初始时先生产
  7. private Lock lock = new ReentrantLock();
  8. private Condition condition = lock.newCondition(); //产生一个Condition对象
  9. public  void set(String name,String content){
  10. lock.lock();
  11. try{
  12. while(!flag){
  13. condition.await() ;
  14. }
  15. this.setName(name) ;    // 设置名称
  16. Thread.sleep(300) ;
  17. this.setContent(content) ;  // 设置内容
  18. flag  = false ; // 改变标志位,表示可以取走
  19. condition.signal();
  20. }catch(InterruptedException e){
  21. e.printStackTrace() ;
  22. }finally{
  23. lock.unlock();
  24. }
  25. }
  26. public void get(){
  27. lock.lock();
  28. try{
  29. while(flag){
  30. condition.await() ;
  31. }
  32. Thread.sleep(300) ;
  33. System.out.println(this.getName() +
  34. " --> " + this.getContent()) ;
  35. flag  = true ;  // 改变标志位,表示可以生产
  36. condition.signal();
  37. }catch(InterruptedException e){
  38. e.printStackTrace() ;
  39. }finally{
  40. lock.unlock();
  41. }
  42. }
  43. public void setName(String name){
  44. this.name = name ;
  45. }
  46. public void setContent(String content){
  47. this.content = content ;
  48. }
  49. public String getName(){
  50. return this.name ;
  51. }
  52. public String getContent(){
  53. return this.content ;
  54. }
  55. }
  56. class Producer implements Runnable{ // 通过Runnable实现多线程
  57. private Info info = null ;      // 保存Info引用
  58. public Producer(Info info){
  59. this.info = info ;
  60. }
  61. public void run(){
  62. boolean flag = true ;   // 定义标记位
  63. for(int i=0;i<10;i++){
  64. if(flag){
  65. this.info.set("姓名--1","内容--1") ;    // 设置名称
  66. flag = false ;
  67. }else{
  68. this.info.set("姓名--2","内容--2") ;    // 设置名称
  69. flag = true ;
  70. }
  71. }
  72. }
  73. }
  74. class Consumer implements Runnable{
  75. private Info info = null ;
  76. public Consumer(Info info){
  77. this.info = info ;
  78. }
  79. public void run(){
  80. for(int i=0;i<10;i++){
  81. this.info.get() ;
  82. }
  83. }
  84. }
  85. public class ThreadCaseDemo{
  86. public static void main(String args[]){
  87. Info info = new Info(); // 实例化Info对象
  88. Producer pro = new Producer(info) ; // 生产者
  89. Consumer con = new Consumer(info) ; // 消费者
  90. new Thread(pro).start() ;
  91. //启动了生产者线程后,再启动消费者线程
  92. try{
  93. Thread.sleep(500) ;
  94. }catch(InterruptedException e){
  95. e.printStackTrace() ;
  96. }
  97. new Thread(con).start() ;
  98. }
  99. }

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

姓名--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并发编程】:并发新特性—Lock锁和条件变量的更多相关文章

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

    简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...

  2. 并发编程(八)Lock锁

    一.引言 线程并发的过程中,肯定会设计到一个变量共享的概念,那么我们在多线程运行过程中,怎么保证每个先拿获取的变量信息都是最新且有序的呢?这一篇我们来专门学习一下Lock锁. 我们先来了解几个概念: ...

  3. Java并发编程系列-&lpar;4&rpar; 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  4. 并发编程学习笔记&lpar;6&rpar;----公平锁和ReentrantReadWriteLock使用及原理

    (一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...

  5. Python 3 并发编程多进程之进程同步(锁)

    Python 3 并发编程多进程之进程同步(锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,竞争带来的结果就是错乱,如何控制,就是加锁处理. 1. ...

  6. JAVA JDK1&period;5-1&period;9新特性

    1.51.自动装箱与拆箱:2.枚举(常用来设计单例模式)3.静态导入4.可变参数5.内省 1.61.Web服务元数据2.脚本语言支持3.JTable的排序和过滤4.更简单,更强大的JAX-WS5.轻量 ...

  7. Java 8 正式发布,新特性全搜罗

    经过2年半的努力.屡次的延期和9个里程碑版本,甲骨文的Java开发团队终于发布了Java 8正式版本. Java 8版本最大的改进就是Lambda表达式,其目的是使Java更易于为多核处理器编写代码: ...

  8. Java引入的一些新特性

    Java引入的一些新特性 Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本. Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程, ...

  9. 多线程&lpar;JDK1&period;5的新特性互斥锁&rpar;

    多线程(JDK1.5的新特性互斥锁)(掌握)1.同步·使用ReentrantLock类的lock()和unlock()方法进行同步2.通信·使用ReentrantLock类的newCondition( ...

随机推荐

  1. yocto系统介绍

    The Yocto Project is an open source collaboration project that provides templates, tools and methods ...

  2. Linux 使用 su 切换用户提示 Authentication Failure 的解决方法

    Ubuntu v14.04,使用 su 命令切换用户时报验证失败的错误 这个问题产生的原因是由于 ubuntu 系统默认是没有激活 root 用户的,需要我们手工进行操作,在命令行界面下,或者在终端中 ...

  3. jQuery对象与Dom对象的相互转换

    1.jQuery对象转换为Dom对象 [index] var $d = $("#id"); ]; get(index) var $d = $("#id"); ) ...

  4. Vue源码后记-更多options参数(1)

    我是这样计划的,写完这个还写一篇数据变动时,VNode是如何更新的,顺便初探一下diff算法. 至于vue-router.vuex等插件源码,容我缓一波好吧,vue看的有点伤. 其实在之前讲其余内置指 ...

  5. Twisted 安全信道

    1.安装python的SSL插件pyOpenSSL pip install pyopenssl 2.安装OpenSSL工具包 sudo apt-get install openssl sudo apt ...

  6. Android屏幕适配和方案【整理】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 这里只是根据参考资料整理下,具体内容请阅读参考资料. 原型设计图 推荐1倍效果图,即采用 720 * 360 大小( 1280 *7 ...

  7. psutil的几个例子

    python进行系统相关操作时都有点力不从心,尤其是windows下,比如获取进程的cpu.内存等等,可以通过以下方法可以达到这种要求: 1.安装pywin32.psutil这种第三方库,里面提供了很 ...

  8. 72&period;纯 CSS 创作气泡填色的按钮特效

    原文地址:https://segmentfault.com/a/1190000015560736 感想:过渡效果+xyz中一轴. HTML code: <nav> <ul> & ...

  9. js 选择指定区域

    /根据id 选择特定区域function SelectRange(id) { var div = document.getElementById(id); var controlRange; if ( ...

  10. 【vue】------浅谈vue------【William】

    ### Vue > Vue是一个前端js框架,由尤雨溪开发,是个人项目 Vue近几年来特别的受关注,三年前的时候angularJS霸占前端JS框架市场很长时间,接着react框架横空出世,因为它 ...