【多线程】浅说Synchronized
一、前言
synchronized关键字用来保证在同一时刻只有一个线程可以执行被它修饰的变量或者代码块。
这一篇中,只涉及synchronized的底层实现原理,不涉及对synchronized效率以及如何优化的讨论。
二、使用方式
(1)给静态方法加锁
public class Main {public static synchronized void staticSynPrint(String str) {System.out.println(str);}}
静态方法不属于任何一个实例,而是属于该类。不管该类被实例化多少次,静态成员只有一份。在同一时刻,不管是使用实例.staticSynPrint方式还是直接类名.staticSynPrint的方式,都会进行同步处理。
(2)给静态变量加锁
同(1),他们都是该类的静态成员。
(3)synchronized(xxx.class)
public class Main {public void classSynPrint(String str) {synchronized (Main.class) {System.out.println(str);}}}
给当前类加锁(注意是当前类,不是实例对象),会作用于该类的所有实例对象,多个线程访问Main类中的所有同步方法,都需要先进行同步处理。
(4)synchronized(this)
public class Main {public void thisSynPrint(String str) {synchronized (this) {System.out.println(str);}}}
this代表实例对象,因此现在锁住的是当前实例对象,因此多个线程访问不同实例的同步方法不需要进行同步。
(5)给实例方法加锁
public class Main {public synchronized void synPrint(String str) {System.out.println(str);}}
不同线程访问同一个实例底下的该方法,才会需要进行同步。
三、实际使用方式之一:单例模式中的双重检验锁
更多单例模式的种类可以参考我的另外一篇博文【设计模式】单例模式
public class SingletonDCL {private volatile static SingletonDCL instance;private SingletonDCL() {}public static SingletonDCL getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new SingletonDCL();}}}return instance;}}
有几个疑问:
(1)这里为什么要检验两次null?
最初的想法,是直接利用synchronized将整个getInstance方法锁起来,但这样效率太低,考虑到实际代码更为复杂,我们应当缩小锁的范围。
在单例模式下,要的就是一个单例,new SingletonDCL()只能被执行一次。因此,现在初步考虑成以下的这种方式:
public static SingletonDCL getInstance() {if (instance == null) {synchronized (Singleton.class) {//一些耗时的操作instance = new SingletonDCL();}}return instance;}
但这样,存在一个问题。线程1判断instance为null,然后拿到锁,执行到了耗时的操作,阻塞了一会儿,还没有对instance进行实例化,instance还是为null。线程2判断instance为null,尝试去获取锁。线程1实例化instance之后,释放了锁。而线程2获取锁之后,同样进行了实例化操作。线程1和线程2拿到了两个不同的对象,违背了单例的原则。
因此,在获取锁之后,又进行了一次null检验。
(2)为什么使用volatile 修饰单例变量?
关于volatie和synchronized的区别,可以先参考我的另外一篇文章【JAVA】volatile和synchronized的区别
这段代码,instance = new SingletonDCL(),在虚拟机层面,其实分为了3个指令:
- 为instance分配内存空间,相当于堆中开辟出来一段空间
- 实例化instance,相当于在上一步开辟出来的空间上,放置实例化好的SingletonDCL对象
- 将instance变量引用指向第一步开辟出来的空间的首地址
但由于虚拟机做出的某些优化,可能会导致指令重排序,由1->2->3变成1->3->2。这种重新排序在单线程下不会有任何问题,但出于多线程的情况下,可能会出现以下的问题:
线程1获取锁之后,执行到了instance = new SingletonDCL()阶段,此时,刚好由于虚拟机进行了指令重排序,先进行了第1步开辟内存空间,然后执行了第3步,instance指向空间首地址,第2步还没来得及执行,此时恰好有线程2执行getInstance方法,最外层判断instance不为null(instance已经指向了某一段地址,因此不为null),直接返回了单例对象,接着线程2在获取单例对象属性的时候,出现了空指针错误!
因此使用volatile 修饰单例变量,可以避免由于虚拟机的指令重排序机制可能导致的空指针异常。
四、实现原理
这里可以分两种情况讨论:
(1)同步语句块
public class Main {public static final Object object = new Object();public void print() {synchronized (object) {System.out.println("123");}}}
使用java Main.java,之后使用javap -c Main.class(-c代表反汇编)得到:
public class com.yang.testSyn.Main {public static final java.lang.Object object;public com.yang.testSyn.Main();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic void print();Code:0: getstatic #2 // Field object:Ljava/lang/Object;3: dup4: astore_15: monitorenter6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;9: ldc #4 // String 12311: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V14: aload_115: monitorexit16: goto 2419: astore_220: aload_121: monitorexit22: aload_223: athrow24: returnException table:from to target type6 16 19 any19 22 19 anystatic {};Code:0: new #6 // class java/lang/Object3: dup4: invokespecial #1 // Method java/lang/Object."<init>":()V7: putstatic #2 // Field object:Ljava/lang/Object;10: return
}
其中print方法中的第5行、15行出现了monitorenter和monitorexit,而这两行其中的字节码代表的正是同步语句块里的内容。
当线程执行到monitorenter时,代表即将进入到同步语句块中,线程首先需要去获得Object的对象锁,而对象锁处于每个java对象的对象头中,对象头中会有一个锁的计数器,当线程查询对象头中计数器,发现内容为0时,则代表该对象没有被任何线程所占有,此时该线程可以占有此对象,计数器于是加1。
线程占有该对象后,也就是拿到该对象的锁,可以执行同步语句块里面的方法。此时,如果有其他线程进来,查询对象头发现计数器不为0,于是进入该对象的锁等待队列中,一直阻塞到计数器为0时,方可继续执行。
第一个线程执行到enterexit后,释放了Object的对象锁,此时第二个线程可以继续执行。
这边依然有几个问题:
[1]为什么有一个monitorenter指令,却有两个monitorexit指令?
因为编译器必须保证,无论同步代码块中的代码以何种方式结束(正常 return 或者异常退出),代码中每次调用 monitorenter 必须执行对应的 monitorexit 指令。为了保证这一点,编译器会自动生成一个异常处理器,这个异常处理器的目的就是为了同步代码块抛出异常时能执行 monitorexit。这也是字节码中,只有一个 monitorenter 却有两个 monitorexit 的原因。
当然这一点,也可以从Exception table(异常表)中看出来,字节码中第6(from)到16(to)的偏移量中如果出现任何类型(type)的异常,都会跳转到第19(target)行。
(2)同步方法
public class Main {public synchronized void print(String str) {System.out.println(str);}}
使用javap -v Main.class查看
-v 选项可以显示更加详细的内容,比如版本号、类访问权限、常量池相关的信息,是一个非常有用的参数。
public class com.yang.testSyn.Mainminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #5.#14 // java/lang/Object."<init>":()V#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;#3 = Methodref #17.#18 // java/io/PrintStream.println:(Ljava/lang/String;)V#4 = Class #19 // com/yang/testSyn/Main#5 = Class #20 // java/lang/Object#6 = Utf8 <init>#7 = Utf8 ()V#8 = Utf8 Code#9 = Utf8 LineNumberTable#10 = Utf8 print#11 = Utf8 (Ljava/lang/String;)V#12 = Utf8 SourceFile#13 = Utf8 Main.java#14 = NameAndType #6:#7 // "<init>":()V#15 = Class #21 // java/lang/System#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;#17 = Class #24 // java/io/PrintStream#18 = NameAndType #25:#11 // println:(Ljava/lang/String;)V#19 = Utf8 com/yang/testSyn/Main#20 = Utf8 java/lang/Object#21 = Utf8 java/lang/System#22 = Utf8 out#23 = Utf8 Ljava/io/PrintStream;#24 = Utf8 java/io/PrintStream#25 = Utf8 println
{public com.yang.testSyn.Main();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public synchronized void print(java.lang.String);descriptor: (Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=2, args_size=20: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: aload_14: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V7: returnLineNumberTable:line 32: 0line 33: 7
}
只看最后两个方法,第一个方法是编译后自动生成的默认构造方法,第二个方法则是我们的同步方法,可以看到同步方法比默认的构造方法多了一个ACC_SYNCHRONIZED的标志位。
与同步语句块不同,虚拟机不会在字节码层面实现锁同步,而是会先观察该方法是否含有ACC_SYNCHRONIZED标志。如果含有,则线程会首先尝试获取锁。如果是实例方法,则会尝试获取实例锁;如果是静态方法(类方法),则会尝试获取类锁。最后不管方法执行是否出现异常,都会释放锁。
【多线程】浅说Synchronized相关推荐
- Java多线程:synchronized | Volatile 和Lock和ReadWriteLock多方位剖析(一)
前言 本文站在多线程初中级学习者的角度,较为全面系统的带你一起了解多线程与锁相关的知识点.带你一起解开与锁相关的各种概念.用法.利弊等.比如:synchronized.Volatile.Lock.Re ...
- 多线程和Synchronized在其中的使用
多线程和Synchronized在其中的使用 一.多线程 1.适用场景 需要提高任务执行效率,有多个任务且任务量大,或者多个任务中有会阻塞的情况 2.线程状态 1. 新建状态(New): 线程对象被创 ...
- 对java多线程里Synchronized的思考
Synchronized这个关键字在多线程里经常会出现,哪怕做到架构师级别了,在考虑并发分流时,也经常会用到它.在本文里,将通过一些代码实验来验证它究竟是"锁"什么. 在启动多个线 ...
- Java多线程:synchronized关键字和Lock
一.synchronized synchronized关键字可以用于声明方法,也可以用来声明代码块,下面分别看一下具体的场景(摘抄自<大型网站系统与Java中间件实践>) 案例一:其中fo ...
- java代码读取dbsequence的值_MongoDB自增序列实现 - Java多线程同步 synchronized 用法
在使用MongoDB的时候 (基于spring-mongo) ,我想在插入对象时获取有序自增的主键 ,但是MongoDB的默认规则是生成一串无序 (大致有序) 的字串 .而Spring Data提供的 ...
- Java 多线程:synchronized 关键字用法(修饰类,方法,静态方法,代码块)
前言 在 多线程生成的原因(Java内存模型与i++操作解析) 中,介绍了Java的内存模型,从而可能导致的多线程问题.synchronized就是避免这个问题的解决方法之一.除了 synchroni ...
- Java 多线程之 synchronized 和 volatile 的比较
概述 在做多线程并发处理时,经常需要对资源进行可见性访问和互斥同步操作.有时候,我们可能从前辈那里得知我们需要对资源进行 volatile 或是 synchronized 关键字修饰处理.可是,我们却 ...
- Java多线程同步Synchronized深入解析
(转自:http://www.51testing.com/html/03/n-827703.html) 同步的概念: 同步分为同步方法和同步块两种方式. 锁定的内容分为锁定类的某个特定实例和锁定类对象 ...
- JUC多线程:synchronized锁机制原理 与 Lock锁机制
前言: 线程安全是并发编程中的重要关注点,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案,当存在多 ...
最新文章
- Python网络爬虫 - 一个简单的爬虫例子
- VSLAM中的特征点三角化
- 如何用纯 CSS 创作一个方块旋转动画
- Ruby中,:(ampersand colon)的用法
- ccna实验配置个人总结
- Qt C++属性类型提供给 QML调用(二)
- 利用R和Octave求解线性方程组
- html段落自动删除,利用JS代码自动删除稿件的普通弹幕功能
- Java 是如何优雅地实现接口数据校验的?
- 详解 ZooKeeper 数据持久化
- android中edittext监听文字变化,使用TextWatcher监听EditText变化
- Quartus II 13.0安装和破解教程
- 浅谈单片机工程师职业规划
- 电商运营学习成长目录
- python不是5的倍数_查找所有低于1000的数字之和,这是Python中3或5的倍数
- SmartWin++笔记
- 怪物猎人世界取得服务器信息,怪物猎人世界 Steam好友联机服务器选择工具V1.2...
- 蜗牛星际C单下实现黑群的局域网唤醒
- 南京大学计算机研究生复试面试题
- java线上查看死锁