深入理解synchronized关键字

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

概念及作用

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

用法

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

对象锁

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

public void synchronized method() {
    System.out.println("我是普通方法形式的对象锁");
}

public void method() {
    synchronized(this) {
        System.out.println("我是代码块形式的对象锁");
    }
}


 

类锁

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

 

public static void synchronized method() {
    System.out.println("我是静态方法形式的类锁");
}

public void method() {
    synchronized(*.class) {
        System.out.println("我是代码块形式的类锁");
    }
}

 

 

SimpleExample

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

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

 


 

public class SimpleExample implements Runnable {
    static SimpleExample instance1 = new SimpleExample();
    static SimpleExample instance2 = new SimpleExample();

    @Override
    public void run() {
        method1();
        method2();
        method3();
        method4();
    }

    public synchronized void method1() {
        common();
    }

    public static synchronized void method2() {
       commonStatic();
    }

    public void method3() {
        synchronized(this) {
            common();
        }
    }

    public void method4() {
        synchronized(MultiInstance.class) {
            common();
        }
    }

    public void method5() {
        common();
    }

    public void method6() {
        commonStatic();
    }

    public void common() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕");
    }

    public static void commonStatic() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(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



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

 


 

// 将run方法改为
@Override
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method2();
    }
}
// 将main方法改为
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(instance1);
    Thread t2 = new Thread(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
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method5();
    }
}
// main方法同 2
结果为:
    线程 Thread-0 正在执行
    线程 Thread-1 正在执行
    线程 Thread-0 执行完毕
    线程 Thread-1 执行完毕
    finished

 

 

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

 

 

// 将run方法改为
@Override
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method6();
    }
}
// main方法同 2
结果为:
    线程 Thread-0 正在执行
    线程 Thread-1 正在执行
    线程 Thread-0 执行完毕
    线程 Thread-1 执行完毕
    finished

 

 

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

 


 

// run方法改为
@Override
public void run() {
    if ("Thread-0".equals(Thread.currentThread().getName())) {
        method7();
    } else {
        method8();
    }
}

public synchronized void method7() {
    try {
        ...
        throw new Exception();
    }  catch (Exception e) {
        e.printStackTrace();
    }
}

public synchronized void method8() {
    common();
}

public static void main(String[] args) throws InterruptedException {
    // 同 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.可重入特性

 

 

public class SynchronizedRecursion {
    int a = 0;
    int b = 0;
    private void method1() {
        System.out.println("method1正在执行,a = " + a);
        if (a == 0) {
            a ++;
            method1();
        }
        System.out.println("method1执行结束,a = " + a);
    }

    private synchronized void method2() {
        System.out.println("method2正在执行,b = " + b);
        if (b == 0) {
            b ++;
            method2();
        }
        System.out.println("method2执行结束,b = " + b);
    }

    public static void main(String[] args) {
        SynchronizedRecursion synchronizedRecursion = new SynchronizedRecursion();
        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文件

public class SynchronizedThis {
    public void method() {
        synchronized(this) {}
    }
}

// 反编译结果
public void method();
    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: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return







public class SynchronizedMethod {
    public synchronized void method() {}
}

// 反编译结果
public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 2: 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)等等,占用内存大小与虚拟机位长一致。

Mark Word (32 bits)State 锁状态
identity_hashcode:25 | age:4 | biased_lock:1 | lock:2Normal 无锁
thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2Biased 偏向锁
ptr_to_lock_record:30            | lock:2Lightweight Locked 轻量级锁
ptr_to_heavyweight_monitor:30    | lock:2Heavyweight Locked 重量级锁
| lock:2Marked for GC    GC标记
Mark Word (64 bits)State 锁状态
unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2Normal 无锁
thread:54 |epoch:2|unused:1|age:4|biased_lock:1|lock:2Biased 偏向锁
ptr_to_lock_record:62                  | lock:2Lightweight Locked 轻量级锁
ptr_to_heavyweight_monitor:62          | lock:2Heavyweight Locked 重量级锁
| lock:2Marked for GC GC标记

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

类型指针

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

锁状态

 

biased_locklock状态
001无锁
101偏向锁
000轻量级锁
010重量级锁
011GC标记

 

JDK对synchronized的优化

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

偏向锁 baised_lock

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

synchronized与ReentrantLock的区别

 

 

区别synchronizedReentrantLock
灵活性代码简单,自动获取、释放锁相对繁琐,需要手动获取、释放锁
是否可重入
作用位置可作用在方法和代码块只能用在代码块
获取、释放锁的方式monitorenter、monitorexit、ACC_SYNCHRONIZED尝试非阻塞获取锁tryLock()、超时获取锁tryLock(long timeout,TimeUnit unit)、unlock()
获取锁的结果不知道可知,tryLock()返回boolean
使用注意事项1、锁对象不能为空(锁保存在对象头中,null没有对象头)2、作用域不宜过大1、切记要在finally中unlock(),否则会形成死锁 2、不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
 

 

扩展阅读

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

深入了解Java之虚拟机内存

深入理解正则表达式

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值