ConcurrentProgramming:wait()和notify()和synchronized


关键词

  • 同一个对象,使用synchronized同步信息
  • wait和synchronized同时使用(不释放锁,死锁)
  • wait和notify缺陷(只能作用于对象,无法判断队列空满状态)
  • synchronized(对象监视器 monitor 的获取,执行期间任何线程都无法再获得同一个monitor对象
    1. Block转为Running状态,过程涉及到操作系统用户模式和内核模式的切换,代价比较高
    2. Java1.6为 synchronized 做了优化,从偏向锁到轻量级锁再到重量级锁的过度

一、wait()和notify()必须和synchronized一起使用?

在Java里面,wait()和notify()是Object的成员函数,是基础中的基础。为什么Java要把wait()和notify()放在如此基础的类里面,而不是作为像Thread一类的成员函数,或者其他类的成员函数呢?

先看为什么wait()和notify()必须和synchronized一起使用?请看下面的代码:

​ 等待什么?唤醒什么?什么时候等待和唤醒?

class MyClass1 {private Object obj1 = new Object();public void method1() {synchronized(obj1) {//...obj1.wait();//...}}public void method2() {synchronized(obj1) {//...obj1.notify();//...}}
}

或者下面的代码:

public class MyClass1 {public void synchronized method1() {//...this.wait();//...}public void synchronized method2() {//...this.notify();//...}
}

然后,开两个线程,线程A调用method1(),线程B调用method2()。

答案已经很明显

两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),该对象本身就需要同步(通过加锁的方式,进行协调,谁来操作)!所以,在调用wait()、notify()之前,要先通过synchronized关键字同步给对象,也就是给该对象加锁。

synchronized关键字可以加在任何对象的实例方法上面,任何对象都可能成为锁。因此,wait()和notify()只能放在Object里面了。

总结:

对于同一个对象来说,两个线程之间要通信,通过加锁的方式,进行协调,谁来操作

wait/notify 等方法也依赖于 monitor 对象,只有在同步的块或者方法中才能调用 wait/notify 等方法

二、为什么wait()的时候必须释放锁

当线程A进入synchronized(obj1)中之后,也就是对obj1上了锁。此时,调用wait()进入阻塞状态,一直不能退出synchronized代码块;那么,线程B永远无法进入synchronized(obj1)同步块里,永远没有机会调用notify(),发生死锁

这就涉及一个关键的问题:在wait()的内部,会先释放锁obj1,然后进入阻塞状态之后,它被另外一个线程用notify()唤醒重新获取锁!其次,wait()调用完成后,执行后面的业务逻辑代码,然后退出synchronized同步块,再次释放锁。

wait()内部的伪代码如下:

wait() {// 释放锁// 阻塞,等待被其他线程notify// 重新获取锁
}

如此则可以避免死锁

三、wait()与notify()的问题

以上述的生产者-消费者模型来看,其伪代码大致如下:

public void enqueue() {synchronized(queue) {while (queue.full()) {queue.wait();}//... 数据入列queue.notify();  // 通知消费者,队列中有数据了。}
}
public void dequeue() {synchronized(queue) {while (queue.empty()) {queue.wait();}// 数据出队列queue.notify(); // 通知生产者,队列中有空间了,可以继续放数据了。}
}     

多线程环境下,生产者在通知消费者的同时,也通知了其他的生产者;消费者在通知生产者的同时,也通知了其他消费者。原因在于wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题

补充在当前线程中调用park(),该线程就会被阻塞;在另外一个线程中,调用unpark(Thread thread),传入一个被阻塞的线程,就可以唤醒阻塞在park()地方的线程。

  • unpark(Thread thread),它实现了一个线程对另外一个线程的“精准唤醒”。

  • notify也只是唤醒某一个线程,但无法指定具体唤醒哪个线程。

四、 synchronized 关键字的底层原理

同步语句块


从上⾯我们可以看出:

  • synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令
  • monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置

当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

另外, wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。

在执行 monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为止。

同步方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是⼀个同步方法。JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个方法是否声明为同步方法,从而执行相应的同步调用。

总结

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是⼀个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取

在Java1.6之后,sychronized在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中。

  • 偏向锁:在只有一个线程访问同步块时使用,通过CAS操作获取锁
  • 轻量级锁:当存在多个线程交替访问同步块,偏向锁就会升级为轻量级锁。当线程获取轻量级锁失败,说明存在着竞争,轻量级锁会膨胀成重量级锁,当前线程会通过自旋(通过CAS操作不断获取锁),后面的其他获取锁的线程则直接进入阻塞状态。
  • 重量级锁:锁获取失败则线程直接阻塞,因此会有线程上下文的切换,性能最差。

锁优化-适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。

其中解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少

锁优化-锁粗化(Lock Coarsening)

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

public class StringBufferTest {StringBuffer stringBuffer=newStringBuffer();public void append(){stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("c");}
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁优化-锁消除(Lock Elimination)

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

publicclassSynchronizedTest02 {public static void main(String[] args) {SynchronizedTest02 test02=newSynchronizedTest02();        for (inti=0; i<10000; i++) {i++;         }longstart=System.currentTimeMillis();for (inti=0; i<100000000; i++) {test02.append("abc", "def");         }System.out.println("Time="+ (System.currentTimeMillis() -start));     }public void append(Stringstr1, Stringstr2) {StringBuffer sb=newStringBuffer();sb.append(str1).append(str2);     }
}

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。

缺点:

  • Sychronized会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。
  • Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

五、锁的本质

如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。
锁是一个“对象”,作用如下:

  1. 这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
  2. 如果这个对象被某个线程占用,记录这个线程的thread ID。
  3. 这个对象维护一个thread id list,记录其他所有阻塞的、等待获取拿这个锁的线程。在当前线程释放锁之后从这个thread id list里面取一个线程唤醒。

要访问的共享资源本身也是一个对象,例如前面的对象myClass,这两个对象可以合成一个对象。代码就变成synchronized(this) {…},要访问的共享资源是对象a,锁加在对象a上。当然,也可以另外新建一个对象,代码变成synchronized(obj1) {…}。这个时候,访问的共享资源是对象a,而锁加在新建的对象obj1上。

资源和锁合二为一,使得在Java里面,synchronized关键字可以加在任何对象的成员上面。这意味着,这个对象既是共享资源,同时也具备“锁”的功能!

锁的 面试题

多个线程竞争一个资源,当某个线程拿到资源并上锁后,当释放资源时,其他线程是怎么知道该资源是可以竞争的?非自旋锁;执行unlock操作后是怎么通知的?

解决

如果是synchronized的,在对象头上,每个资源都有一个monitor变量,当 Monitor 对象被某个线程持有后,就会处于锁定状态,Monitor 也有两个队列。用来存放进入及等待获取锁的线程。

如果是aqs,aqs中有一个volatile修是的state变量,当前线程拿到锁的时候,state是1,其他线程在同步队列中。其他线程判断state状态状态进行资源竞争

看unparkSuccessor方法,unparkSuccessor(…)方法唤醒队列中的后继者

六、为什么调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执行线程的相应准备⼯作,然后自动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执行 run() ⽅法,会把 run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。

总结: 调⽤ start() 方法 可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

七、synchronized 关键字和 volatile 关键字的区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,⽽不是对⽴的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定⽐ synchronized 关键字要好。但是 volatile 关键字只能⽤于变量⽽ synchronized 关键字可以修饰⽅法以及代码块。
  • volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两者都能保证。
  • volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性。

ConcurrentProgramming:wait()和notify()和synchronized相关推荐

  1. JAVA 的wait(), notify()与synchronized同步机制

    转自:http://blog.csdn.net/zyplus/article/details/6672775 在JAVA中,是没有类似于PV操作.进程互斥等相关的方法的.JAVA的进程同步是通过syn ...

  2. 为什么wait和notify只能在synchronized中?

    前言 wait和notify必须在synchronized块中,否则会抛出IllegalMonitorStateException. 原因 代码示例 class BlockingQueue {Queu ...

  3. 为什么wait和notify必须放在synchronized中?

    作者 | 磊哥 来源 | Java面试真题解析(ID:aimianshi666) 转载请联系授权(微信ID:GG_Stone) 在多线程编程中,wait 方法是让当前线程进入休眠状态,直到另一个线程调 ...

  4. 为什么wait/notify必须要和synchronized一起使用?

    作者 | 磊哥 来源 | Java面试真题解析(ID:aimianshi666) 转载请联系授权(微信ID:GG_Stone) 在多线程编程中,wait 方法是让当前线程进入休眠状态,直到另一个线程调 ...

  5. java多线程之synchronized与lock、wait与notify

    一.synchronized与lock synchronized在java很早的版本就已经有了,它的作用只要是同步代码,解决线程的安全问题,但是随着java的发展,以及开发业务的不断提高,synchr ...

  6. java同步锁售票_Java基础学习笔记: 多线程,线程池,同步锁(Lock,synchronized )(Thread类,ExecutorService ,Future类)(卖火车票案例)...

    学习多线程之前,我们先要了解几个关于多线程有关的概念. 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线程:线程是 ...

  7. 高并发编程-线程通信_使用wait和notify进行线程间的通信

    文章目录 概述 场景 引子 synchronized wait/notify机制 synchronized wait/notify 改造 问题 概述 Java中线程通信协作的最常见的两种方式: syn ...

  8. Java线程的挂起与恢复 wait(), notify()方法介绍

    一, 什么是线程的挂起与恢复 从字面理解也很简单. 所谓线程挂起就是指暂停线程的执行(阻塞状态). 而恢复时就是让暂停的线程得以继续执行.(返回就绪状态) 二, 为何需要挂起和恢复线程. 我们来看1个 ...

  9. Thread线程notify方法的自我理解

    感谢博主:http://zy19982004.iteye.com/blog/1626916 这篇博文给予我线程知识很大的帮助 知识背景:(1)wait().notify()均是Object的方法,故每 ...

最新文章

  1. java编写一个通讯录_java写的通讯录(小玩意)
  2. 脑机接口应用于驾驶员的疲劳状态检测综述
  3. Java基础--通过反射获取私有的成员方法示例代码
  4. [2020-11-30 contest]数列(矩阵加速),秘密通道(dijkstra最短路)小X游世界树(换根dp),划分(数学)
  5. gorm 密码字段隐藏_【财富密码】第1期:《LSTM大战上证指数-PyTorch版》
  6. 微服务:更愉快还是更嘈杂?
  7. 【扩频通信】基于matlab GUI扩频通信系统仿真(带面板)【含Matlab源码 1587期】
  8. java 设计模式的相似_聊聊Java中几种常用的设计模式
  9. 金蝶系统无法建立数据可连接服务器,金蝶K3打开,提示无法建立数据链接
  10. 济南月薪一万是什么水平?
  11. RabbitMQ问题解决:TCP connection succeeded but Erlang distribution failed
  12. 苹果手机的微信记录怎么恢复?奉上超好用的恢复技巧,拿走不谢
  13. 常见端口对应服务及入侵方式
  14. 抖音拍摄脚本怎么写,掌握这几点快速拍摄爆款视频丨国仁网络资讯
  15. csapp-bomblab
  16. Unity Labs:AutoLOD –试验自动性能改进
  17. 跨时钟域的亚稳态处理、为什么要打两拍不是打一拍、为什么打两拍能有效?...
  18. 上善若水——甲骨文——文字文化
  19. c语言机器人编程软件,诺宝RC|免费版机器人编程软件下载_最火软件站
  20. CSS权威指南(五)字体

热门文章

  1. Linux /proc目录详解
  2. 用Mindjet MindManager 15 打开文件后停止响应的解决方法
  3. WebStorm 8.0.3下简单运行pomelo项目
  4. 关于 # 符号的使用 - 给 厨师 的回复
  5. 基于springMVC的页面跳转、转发、重定向等
  6. UVA 11992 - Fast Matrix Operations(段树)
  7. Go语言实例系列【 获得url实例】
  8. 浅谈line-height
  9. 事务隔离级别神话与误解
  10. OSChina 周五乱弹 —— 你专业是啥,被叫去搬砖了吗?