ConcurrentProgramming:wait()和notify()和synchronized_synchronized和wait-程序员宅基地

技术标签: synchronized  wait  JUC、数据结构、算法、JVM、Linxu  notify  

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 关键字解决的是多个线程之间访问资源的同步性。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chuanchengdabing/article/details/120165713

智能推荐

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

深度神经网络在训练初期的“梯度消失”或“梯度爆炸”的问题解决:数据标准化(Data Standardization),权重初始化(Weight Initialization),Dropout正则化等_在人工神经网络研究的初始阶段,辛顿针对训练过程中常出现的梯度消失现象, 提供相-程序员宅基地

文章浏览阅读101次。1986年,深度学习(Deep Learning)火爆,它提出了一个名为“深层神经网络”(Deep Neural Networks)的新型机器学习模型。随后几年,神经网络在图像、文本等领域取得了惊艳成果。但是,随着深度学习的应用范围越来越广泛,神经网络在遇到新的任务时出现性能下降或退化的问题。这主要是由于深度神经网络在训练初期面临着“梯度消失”或“梯度爆炸”的问题。_在人工神经网络研究的初始阶段,辛顿针对训练过程中常出现的梯度消失现象, 提供相

kill进程的几种方式_如何kill掉一个进程-程序员宅基地

文章浏览阅读461次。我们会先使用 ps、top 等命令获得进程的 PID,然后使用 kill 命令来杀掉该进程。killall和pkill是相似的,不过如果给出的进程名不完整,killall会报错。当然我们可以向进程发送一个终止运行的信号,此时的 kill 命令才是名至实归。,这样结束掉的进程不会进行资源的清理工作,所以如果你用它来终结掉 vim 的进程,就会发现临时文件 *.swp 没有被删除。命令:pid of xx进程,显示进程的进程号,同上pgrep。这是 kill 命令最主要的用法,也是本文要介绍的内容。_如何kill掉一个进程

随便推点

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

vscode打开markdown文件 不显示图片 预览markdown文件_vscodemarkdown图片无法显示-程序员宅基地

文章浏览阅读3.2k次,点赞3次,收藏4次。vscode打开markdown文件 不显示图片 预览markdown文件_vscodemarkdown图片无法显示

推荐文章

热门文章

相关标签