深入理解synchronized关键字

synchronized是并发编程中重要的使用工具之一,我们必须学会使用并且掌握它的原理。

概念及作用

JVM自带的关键字,可在需要线程安全的业务场景中使用,来保证线程安全。

用法

按照锁的对象区分可以分为对象锁类锁按照在代码中的位置区分可以分为方法形式代码块形式

对象锁

锁对象为当前this或者说是当前类的实例对象

publicvoidsynchronizedmethod(){

System.out.println("我是普通方法形式的对象锁");

}

publicvoidmethod(){

synchronized(this) {

System.out.println("我是代码块形式的对象锁");

}

}

类锁

锁的是当前类或者指定类的Class对象。一个类可能有多个实例对象,但它只可能有一个Class对象。

publicstaticvoidsynchronizedmethod(){

System.out.println("我是静态方法形式的类锁");

}

publicvoidmethod(){

synchronized(*.class) {

System.out.println("我是代码块形式的类锁");

}

}

SimpleExample

最基本的用法在上一个标题用法中已将伪代码列出,这里列举在以上基础上稍微变化一些的用法

多个实例,对当前实例加锁,同步执行,对当前类Class对象加锁,异步执行。

publicclassSimpleExampleimplementsRunnable{

staticSimpleExample instance1 =newSimpleExample();

staticSimpleExample instance2 =newSimpleExample();

@Override

publicvoidrun(){

method1();

method2();

method3();

method4();

}

publicsynchronizedvoidmethod1(){

common();

}

publicstaticsynchronizedvoidmethod2(){

commonStatic();

}

publicvoidmethod3(){

synchronized(this) {

common();

}

}

publicvoidmethod4(){

synchronized(MultiInstance.class) {

common();

}

}

publicvoidmethod5(){

common();

}

publicvoidmethod6(){

commonStatic();

}

publicvoidcommon(){

System.out.println("线程 "+ Thread.currentThread().getName() +" 正在执行");

try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}

System.out.println("线程 "+ Thread.currentThread().getName() +" 执行完毕");

}

publicstaticvoidcommonStatic(){

System.out.println("线程 "+ Thread.currentThread().getName() +" 正在执行");

try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}

System.out.println("线程 "+ Thread.currentThread().getName() +" 执行完毕");

}

publicstaticvoidmain(String[] args)throwsInterruptedException{

Thread t1 =newThread(instance1);

Thread t2 =newThread(instance2);

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println("finished");

}

}

method1()、method3()结果为:

线程 Thread-0正在执行

线程 Thread-1正在执行

线程 Thread-0执行完毕

线程 Thread-1执行完毕

finished

method2()、method4()执行结果为:

线程 Thread-0 正在执行

线程 Thread-0 执行完毕

线程 Thread-1 正在执行

线程 Thread-1 执行完毕

finished

对象锁和类锁,锁的对象不一样,互不影响,所以异步执行

// 将run方法改为

@Override

publicvoidrun()

{

if("Thread-0".equals(Thread.currentThread().getName())) {

method1();

}else{

method2();

}

}

// 将main方法改为

publicstaticvoidmain(String[] args) throws InterruptedException{

Thread t1 =newThread(instance1);

Thread t2 =newThread(instance1);

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println("finished");

}

结果为:

线程 Thread-0正在执行

线程 Thread-1正在执行

线程 Thread-1执行完毕

线程 Thread-0执行完毕

finished

3.对象锁和无锁得普通方法,普通方法不需要持有锁,所以异步执行

// 将run方法改为

@Override

publicvoidrun()

{

if("Thread-0".equals(Thread.currentThread().getName())) {

method1();

}else{

method5();

}

}

// main方法同 2

结果为:

线程 Thread-0正在执行

线程 Thread-1正在执行

线程 Thread-0执行完毕

线程 Thread-1执行完毕

finished

类锁和无锁静态方法,异步执行

// 将run方法改为

@Override

publicvoidrun()

{

if("Thread-0".equals(Thread.currentThread().getName())) {

method1();

}else{

method6();

}

}

// main方法同 2

结果为:

线程 Thread-0正在执行

线程 Thread-1正在执行

线程 Thread-0执行完毕

线程 Thread-1执行完毕

finished

方法抛出异常,synchronized锁自动释放

// run方法改为

@Override

publicvoidrun(){

if("Thread-0".equals(Thread.currentThread().getName())) {

method7();

}else{

method8();

}

}

publicsynchronizedvoidmethod7(){

try{

...

thrownewException();

}catch(Exception e) {

e.printStackTrace();

}

}

publicsynchronizedvoidmethod8(){

common();

}

publicstaticvoidmain(String[] args)throwsInterruptedException{

// 同 2

}

结果为:

线程 Thread-0正在执行

java.lang.Exception

at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.method7(SynchronizedException.java:26)

at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.run(SynchronizedException.java:15)

at java.lang.Thread.run(Thread.java:748)

线程 Thread-0执行结束

线程 Thread-1正在执行

线程 Thread-1执行结束

finished

// 这说明抛出异常后持有对象锁的method7()方法释放了锁,这样method8()才能获取到锁并执行。

6.可重入特性

publicclassSynchronizedRecursion{

inta =0;

intb =0;

privatevoidmethod1(){

System.out.println("method1正在执行,a = "+ a);

if(a ==0) {

a ++;

method1();

}

System.out.println("method1执行结束,a = "+ a);

}

privatesynchronizedvoidmethod2(){

System.out.println("method2正在执行,b = "+ b);

if(b ==0) {

b ++;

method2();

}

System.out.println("method2执行结束,b = "+ b);

}

publicstaticvoidmain(String[] args){

SynchronizedRecursion synchronizedRecursion =newSynchronizedRecursion();

synchronizedRecursion.method1();

synchronizedRecursion.method2();

}

}

结果为:

method1正在执行,a =0

method1正在执行,a =1

method1执行结束,a =1

method1执行结束,a =1

method2正在执行,b =0

method2正在执行,b =1

method2执行结束,b =1

method2执行结束,b =1

可以看到method1()与method2()的执行结果一样的,method2()在获取到对象锁以后,在递归调用时不需要等上一次调用先释放后再获取,而是直接进入,这说明了synchronized的可重入性。

当然,除了递归调用,调用同类的其它同步方法,调用父类同步方法,都是可重入的,前提是同一对象去调用,这里就不一一列举了.

总结一下

一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待;

每个实例都对应有自己的一把锁,不同实例之间互不影响;

锁对象是*.class以及synchronized修饰的static方法时,所有对象共用一把类锁;

无论是方法正常执行完毕或者方法抛出异常,都会释放锁;

使用synchronized修饰的方法都是可重入的。

synchronized的实现原理

monitorenter和monitorexit

将下面两段代码分别用 javac *.java编译成.class文件,再反编译 javap -verbose *.class文件

publicclassSynchronizedThis{

publicvoidmethod(){

synchronized(this) {}

}

}

// 反编译结果

publicvoidmethod();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=3, args_size=1

0: aload_0

1: dup

2: astore_1

3: monitorenter

4: aload_1

5: monitorexit

6:goto14

9: astore_2

10: aload_1

11: monitorexit

12: aload_2

13: athrow

14:return

publicclassSynchronizedMethod{

publicsynchronizedvoidmethod(){}

}

// 反编译结果

publicsynchronizedvoidmethod();

descriptor: ()V

flags: ACC_PUBLIC, ACC_SYNCHRONIZED

Code:

stack=0, locals=1, args_size=1

0:return

LineNumberTable:

line2:0

可以看到:

synchronized加在代码块上,JVM是通过monitorentermonitorexit来控制锁的获取的释放的;

synchronized加在方法上,JVM是通过ACC_SYNCHRONIZED来控制的,但本质上也是通过monitorenter和monitorexit指令控制的。

对象头

上面我们提到monitor,这是什么鬼? 其实,对象在内存是这样存储的,包括对象头实例数据对齐填充padding,其中对象头包括Mark Word和类型指针。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(identity_hashcode)、GC分代年龄(age)、锁状态标志(lock)、线程持有的锁、偏向线程ID(thread)、偏向时间戳(epoch)等等,占用内存大小与虚拟机位长一致。

可以看到,monitor就存在Mark Word中。

类型指针

类型指针指向对象的类元数据metadata,虚拟机通过这个指针确定该对象是哪个类的实例。

锁状态


JDK对synchronized的优化


jdk1.6之前synchronized是很重的,所以并不被开发者偏爱,随着后续版本jdk对synchronized的优化使其越来越轻量,它还是很好用的,甚至ConcurrentHashMap在jdk的put方法都在jdk1.8时从ReetrantLock.tryLock()改为用synchronized来实现同步。 并且还引入了偏向锁,轻量级锁等概念。

偏向锁 baised_lock

如果一个线程获取了偏向锁,那么如果在接下来的一段时间里,如果没有其他线程来抢占锁,那么获取锁的线程在下一次进入方法时不需要重新获取锁。

synchronized与ReentrantLock的区别

扩展阅读

死磕 Java 并发:深入分析 synchronized 的实现原理

深入了解Java之虚拟机内存

深入理解正则表达式

Java并发编程之volatile关键字解析

缓存在高并发场景下的常见问题

来源:https://juejin.im/post/5c34aca86fb9a049c2329c5e

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容