「多线程与并发」- 浅谈 synchronized 关键字

210 阅读5分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

1. 前言

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

无论是开源框架,还是 JDK 源码,都大量使用了 synchronized 关键字,因此了解 synchronized 关键字的原理与机制是非常有必要的


2. synchronized 修饰对象与作用域

synchronized 可以修饰方法与代码块,其中,修饰方法又可分为修饰静态方法与普通实例方法

1)修饰实例方法:修饰实例方法会作用于当前的实例对象,进入对象的同步方法前需要获得当前对象实例的锁

synchronized void method() {
}

2)修饰静态方法:修饰静态方法作用于当前类的所有实例,因为静态方法不属于任何一个对象,而是属于类成员;进入同步代码块时需要获取当前类的锁

synchronized static void method() {
}

3)修饰代码块:指定加锁对象,对指定的 类 / 对象 加锁;比如 synchronized(this) 就是对当前对象加锁,而 synchronized(类名.class) 就是给指定的类加锁

synchronized(this) {
    //需要同步的代码
}

synchronized(Test.class) {
    //需要同步的代码
}

小结

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
  • synchronized 关键字加到实例方法上是给对象实例上锁。
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
  • 如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,原因是获取的锁不一样,一个是对象实例锁,一个是类的锁

3. 使用 synchronized 手写单例模式

传送门:掘金:教你手写单例模式

public class DCLSingleton {
    private volatile static DCLSingleton dclSingleton;

    private DCLSingleton() { }

    public static DCLSingleton getInstance() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) dclSingleton = new DCLSingleton();
            }
        }
        return dclSingleton;
    }
}

4. synchronized 底层原理

4.1 synchronized 修饰代码块时

synchronized (this) {
    System.out.println("test method");
}

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置;这两个指令会使其锁计数器加1或者减1

每一个对象在同一时间只与一个 monitor 相关联,而一个 monitor 在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的 Monitor 锁的所有权的时候,monitorenter 指令会发生如下3中情况之一:

  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于 monitor 的所有权,释放过程很简单,就是将 monitor 的计数器减1,如果减完以后,计数器不是 0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成 0,则代表当前线程不再拥有该monitor的所有权,即释放锁


4.2 修饰方法的情况

synchronized 修饰方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中

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


5. synchronized 同步的缺点

  • 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
  • 无法知道是否成功获得锁:相对而言,Lock可以拿到状态

6. 总结

  • synchronized 是通过 JVM 来实现同步的,即使 JDK 后加入了 Lock,任然被广泛使用
  • synchronized 实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待
  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错,而且要注意避免死锁
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错