Skip to content

参考资料: https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html

synchronized的作用对象

作用对象可以是代码块、方法、类对象锁和类锁的区别? 作用于代码块和方法时,获得的是对象锁。不同的线程通过同一个对象去调用加锁的代码块和方法时,线程会同步。不同的线程通过不同的对象去调用加锁的代码块和方法时,线程不会同步 作用于类或者静态方法时,获得的是类锁。作用于整个类上。也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。(作用于静态方法也是获得类锁)

对象锁和类锁的举例

Synchronized原理

加锁与释放锁的原理

在获取锁和释放锁的过程中,通过一个锁计数器来实现锁的所有权。 首先需要明确:每一个对象在同一时间只与一个锁相关联,而一个锁在同一时间只能被一个线程获取。正是通过锁这个媒介,保证了在某一时间,线程对对象的唯一操作权。 一个线程在尝试获取与一个对象关联的锁的所有权时,锁计数器monitor的变化情况如下:

  1. 初始monitor计数器为0,说明目前这个锁没有被占用,那么这个线程就可以获得锁的所有权,并且锁计数器monitor+1。只要monitor不为0,其他的线程想要获得锁,就必须等待。
  2. 如果当前线程已经拿到锁的所有权,又重入了这把锁,那么锁计数器也会+1。每次重入,都会累加。
  3. 如果当前锁已经被其他线程占用,那么当前线程只能等待其他线程释放锁,即锁计数器为0。

再看一个线程已经获取到锁的所有权时,释放锁时锁计数器的变化情况:

  1. 每释放一次锁,锁计数器都会-1,当减完1之后,锁计数器不为0,说明对于锁的所有权是重入进来的,当前线程还是继续持有这把锁的所有权
  2. 直到锁计数器最终减为了0,此时才标识着当前线程释放了锁的所有权,其他等待此锁的线程可以成功获得锁的所有权。

什么是可重入锁

上面关于锁计数器的分析中,又出现了新的名词“重入”。 可重入:就是指若一个程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该程序不会出错”,则称其为可重入。 看到定义就能闻出浓浓的递归的气息。而可重入锁也因此得名:递归锁。可重入锁:指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。 看一个例子就能明白了:

java
public class SynchronizedDemo {

    public static void main(String[] args) {
        SynchronizedDemo demo =  new SynchronizedDemo();
        demo.method1();
    }

    private synchronized void method1() {
        System.out.println(Thread.currentThread().getId() + ": method1()");
        method2();
    }

    private synchronized void method2() {
        System.out.println(Thread.currentThread().getId()+ ": method2()");
        method3();
    }

    private synchronized void method3() {
        System.out.println(Thread.currentThread().getId()+ ": method3()");
    }
}

结合前文中加锁和释放锁的原理,不难理解: 执行monitorenter获取锁

  1. (monitor计数器=0,可获取锁)
  2. 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
  3. 执行method2()方法,monitor计数器+1 -> 2
  4. 执行method3()方法,monitor计数器+1 -> 3

执行monitorexit命令

  1. method3()方法执行完,monitor计数器-3 -> 2
  2. method2()方法执行完,monitor计数器-2 -> 1
  3. method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
  4. (monitor计数器=0,锁被释放了)

这就是Synchronized的重入性,即在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一,线程不需要再次获取同一把锁。

为什么能保证线程安全

回到我们的Java多线程理论基础中如何保证多线程的安全:

  • 保证原子性:synchronized
  • 保证有序性:volatile 和 synchronized 、Happens-Before 规则
  • 保证可见性:volatile、synchronized

举一个很经典的例子,单例中的双重检测锁写法:

java
public class DoubleCheckLockSingleton {
    private volatile static DoubleCheckLockSingleton mInstance=null;
    
    private DoubleCheckLockSingleton(){ }
    
    public static DoubleCheckLockSingleton getInstance(){
        if(mInstance==null){
            synchronized (DoubleCheckLockSingleton.class){
                if(mInstance==null){
                    mInstance=new DoubleCheckLockSingleton();
                }
            }
        }
        return mInstance;
    }
}

很明显我们的synchronized修饰的是类,所以获取的是一个类锁。 这里的mInstance是一个共享的变量,它的状态应当对所有线程可见。我们考虑这种情况,App启动,当线程1调用静态方法getInstance去获取mInstance变量,发现此时为null。那么线程1先通过synchronized获取一个类锁,这时正好线程2如果也调用了getInstance方法,因为不能获得锁,所以只能等待。 等待线程1,执行完 mInstance=new DoubleCheckLockSingleton();,唯一实例创建完毕,执行完这段代码块,便释放了锁。线程1也获得mInstance,此时它不再为空,而是一个DoubleCheckLockSingleton类对象。 此时在等待的线程2可以获得这个类锁,执行加锁的代码块,在第二个判空出就跳出选择结构了,因为线程1以及对mInstance赋值,它不再为null。直接释放锁,return mInstance。 而之后再有其他线程调用getInstance方法,直接在第一个判空出就跳出选择结构,直接来到return语句。 可见synchronized同步锁机制,让本应该并行执行的代码,因为锁的互斥,而等待串行执行。线程1对mInstance的赋值,对线程2是可见的,从而保证了线程安全。