前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >肝了一下午的 Synchronized 解析!

肝了一下午的 Synchronized 解析!

作者头像
JavaFish
发布2020-11-03 16:09:03
发布2020-11-03 16:09:03
35300
代码可运行
举报
运行总次数:0
代码可运行

Synchronized

欢迎来到狗哥多线程系列连载。本篇是线程相关的第九篇,前八篇分别是:

创建线程到底有几种方式?

线程有多少种状态?

Runnable 一定在执行任务吗?

万字长文,Thread 类源码解析!

wait、notify/notifyAll 解析

线程之生产者消费者模式

狗哥肝了一下午的线程池

线程池的拒绝策略

线程池的阻塞队列

  • synchronized 是 Java 的一个关键字,它能够将代码块 (方法) 锁起来
  • synchronized 是 互斥锁,同一时间只能有一个线程进入被锁住的代码块(方法)
  • synchronized 通过监视器(Monitor)实现锁。java 一切皆对象,每个对象都有一个监视器(锁标记),而 synchronized 就是使用对象的监视器来将代码块 (方法) 锁定的

为什么用 Synchronized ?

我们加锁的原因是为了线程安全,而线程安全最重要就是保证原子性和可见性

  • 被 Synchronized 修饰的代码块(方法),同一时间只能有一个线程执行,从而保证原子性。
  • synchronized 通过使用监视器,来实现对变量的同步操作,保证了其他线程对变量的可见性。

怎么用 Synchronized ?

  • 修饰普通同步方法:锁是当前实例对象
  • 修饰静态同步方法:锁是当前类的 Class 对象
  • 修饰同步代码块:
修饰普通同步方法
代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    // 修饰普通同步方法,普通方法属于实例对象
    // 锁是当前实例对象 BigBigDog 的监视器
    public synchronized void testCommon(){
        // doSomething
    }

}

多个实例对象调用不会阻塞,比如:

代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    // 修饰普通同步方法,普通方法属于实例对象
    // 锁是当前实例对象 BigBigDog 的监视器
    public synchronized void testCommon() {
        int i = 0;
        do {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Common function is locked " + i);
        } while (i++ < 10);
    }

}

测试方法:

代码语言:javascript
代码运行次数:0
复制
public class Main {

    public static void main(String[] args) {
        BigBigDog bigBigDog = new BigBigDog();
        BigBigDog bigBigDog1 = new BigBigDog();
        new Thread(bigBigDog::testCommon).start();
        new Thread(bigBigDog1::testCommon).start();
    }

}

结果:异步运行,因为锁的是实例对象,也就是锁不同,所以并不会阻塞

代码语言:javascript
代码运行次数:0
复制
Common function is locked 0
Common function is locked 0
Common function is locked 1
Common function is locked 1
Common function is locked 2
Common function is locked 2
Common function is locked 3
Common function is locked 3
···
修饰静态同步方法
代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    // 修饰静态同步方法,静态方法属于类(粒度比普通方法大)
    // 锁是类的锁(类的字节码文件对象:BigBigDog.class)
    public static synchronized void testStatic() {
        // doSomething
    }

}

synchronized 修饰静态方法获取的是类锁 (类的字节码文件对象),synchronized 修饰普通方法获取的是对象锁。也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!测试下:

代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    // 修饰普通同步方法,普通方法属于实例对象
    // 锁是当前实例对象 BigBigDog 的监视器
    public synchronized void testCommon() {
        int i = 0;
        do {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Common function is locked " + i);
        } while (i++ < 10);
    }

    // 修饰静态同步方法,静态方法属于类(粒度比普通方法大)
    // 锁是类的锁(类的字节码文件对象:BigBigDog.class)
    public static synchronized void testStatic() {
        int i = 0;
        do {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Static function is locked " + i);
        } while (i++ < 10);
    }
}
代码语言:javascript
代码运行次数:0
复制
public class Main {

    public static void main(String[] args) {
        BigBigDog bigBigDog = new BigBigDog();
        new Thread(bigBigDog::testCommon).start();
        new Thread(BigBigDog::testStatic).start();
    }

}

结果:异步运行,并不冲突。

代码语言:javascript
代码运行次数:0
复制
Common function is locked 0
Static function is locked 0
Common function is locked 1
Static function is locked 1
Common function is locked 2
Static function is locked 2
Common function is locked 3
Static function is locked 3
修饰同步代码块
代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    public void test3() {
        // 修饰代码块,锁是括号内的对象
        // 这里的 this 是当前实例对象 BigBigDog 的监视器
        synchronized (this) {
            // doSomething
        }
    }
}
代码语言:javascript
代码运行次数:0
复制
public class BigBigDog {

    // 使用 object 的监视器作为锁
    private final Object object = new Object();

    public void test4() {
        // 修饰代码块,锁是括号内的对象
        // 这里是当前实例对象 object 的监视器
        synchronized (object) {
            // doSomething
        }
    }

}

除了第一种以 this 当前对象的监视器为锁的情况。对于同步代码块,Java 还支持它持有任意对象的锁,比如第二种的 object 。那么这两者有何区别?这两者并无本质区别,但是为了代码的可读性。还是更加建议用第一种(第二种,无缘无故定义一个对象)

Synchronized 的原理

有以下代码:test 是静态同步方法,test1 是普通同步方法,test2 则是同步代码块。

代码语言:javascript
代码运行次数:0
复制
public class SynchronizedTest {

    // 修饰静态方法
    public static synchronized void test() {
        // doSomething
    }

    // 修饰方法
    public synchronized void test1(){
    }

    public void test2(){
        // 修饰代码块
        synchronized (this){
        }
    }
}

通过命令看下 synchronized 关键字到底做了什么事情:首先用 cd 命令切换到 SynchronizedTest.java 类所在的路径,然后执行 javac SynchronizedTest.java,于是就会产生一个名为 SynchronizedTest.class 的字节码文件,然后我们执行 javap -c SynchronizedTest.class,就可以看到对应的反汇编内容,如下:

代码语言:javascript
代码运行次数:0
复制
Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javac -encoding UTF-8 SynchronizedTest.java

Z:\IDEAProject\review\review_java\src\main\java\com\nasus\thread\lock>javap -c SynchronizedTest.class
Compiled from "SynchronizedTest.java"
public class com.nasus.thread.lock.SynchronizedTest {
  public com.nasus.thread.lock.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static synchronized void test();
    Code:
       0: return

  public synchronized void test1();
    Code:
       0: return

  public void test2();
    Code:
       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
    Exception table:
       from    to  target type
           4     6     9   any
           9    12     9   any
}
test2 同步代码块解析

主要看 test2 同步代码块的反编译内容,可以看出 synchronized 多了 monitorenter 和 monitorexit 指令。把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0

那这里为啥只有一次 monitorenter 却有两次 monitorexit ?

  • JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁

执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

test1 普通同步方法

它并不是依靠 monitorenter 和 monitorexit 指令实现的,从上面的反编译内容可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。(在这看不出来需要看 JVM 底层实现)

当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁

PS:想要进一步深入了解 synchronized 就必须了解 monitor 对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。可以参考这篇博客:@chenssy 大神写的很好,建议拜读下。

https://blog.csdn.net/chenssy/article/details/54883355

-END-

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个优秀的废人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Synchronized
    • 为什么用 Synchronized ?
    • 怎么用 Synchronized ?
      • 修饰普通同步方法
      • 修饰静态同步方法
      • 修饰同步代码块
    • Synchronized 的原理
      • test2 同步代码块解析
      • test1 普通同步方法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档