肝了一下午的 Synchronized 解析!

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 肝了一下午的 Synchronized 解析!
  • synchronized 是 Java 的一个关键字,它能够将代码块 (方法) 锁起来


  • synchronized 是 互斥锁,同一时间只能有一个线程进入被锁住的代码块(方法)


  • synchronized 通过监视器(Monitor)实现锁。java 一切皆对象,每个对象都有一个监视器(锁标记),而 synchronized 就是使用对象的监视器来将代码块 (方法) 锁定的


为什么用 Synchronized ?


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


  • 被 Synchronized 修饰的代码块(方法),同一时间只能有一个线程执行,从而保证原子性。


  • synchronized 通过使用监视器,来实现对变量的同步操作,保证了其他线程对变量的可见性。


怎么用 Synchronized ?


  • 修饰普通同步方法:锁是当前实例对象
  • 修饰静态同步方法:锁是当前类的 Class 对象
  • 修饰同步代码块:


修饰普通同步方法


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


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


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);
    }
}


测试方法:


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();
    }
}


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


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
···


修饰静态同步方法


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


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


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);
    }
}


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


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


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


修饰同步代码块


public class BigBigDog {
    public void test3() {
        // 修饰代码块,锁是括号内的对象
        // 这里的 this 是当前实例对象 BigBigDog 的监视器
        synchronized (this) {
            // doSomething
        }
    }
}
public class BigBigDog {
    // 使用 object 的监视器作为锁
    private final Object object = new Object();
    public void test4() {
        // 修饰代码块,锁是括号内的对象
        // 这里是当前实例对象 object 的监视器
        synchronized (object) {
            // doSomething
        }
    }
}


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


Synchronized 的原理


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


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,就可以看到对应的反汇编内容,如下:


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 大神写的很好,建议拜读下。


相关文章
|
6月前
|
存储 安全 Java
深入解析 Java 中的 Synchronized:原理、实现与性能优化
深入解析 Java 中的 Synchronized:原理、实现与性能优化
228 1
|
5月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
96 0
|
6月前
|
安全 Java 编译器
synchronized同步锁 : 原理到锁升级及历史演进的解析
synchronized同步锁 : 原理到锁升级及历史演进的解析
|
7月前
|
Java
Java中的线程同步:synchronized关键字的深度解析
【4月更文挑战第14天】在多线程环境下,线程同步是一个重要的话题。Java提供了多种机制来实现线程同步,其中最常用且最重要的就是synchronized关键字。本文将深入探讨synchronized关键字的工作原理,使用方法以及注意事项,帮助读者更好地理解和使用这一重要的线程同步工具。
|
7月前
|
安全 Java 测试技术
Java之戳中痛点之 synchronized 深度解析
Java之戳中痛点之 synchronized 深度解析
27 1
|
7月前
|
安全 Java
并发编程之synchronized的详细解析
并发编程之synchronized的详细解析
46 0
|
Java 开发者
JUC系列学习(三):ReentrantLock的使用、源码解析及与Synchronized的异同
`ReentrantLock`同`Synchronized`一样可以实现线程锁的功能,同样具有可重入性,除此之外还可以实现公平锁&非公平锁,其底层是基于`AQS`框架实现的。
|
存储 缓存 安全
基础篇:深入JMM内存模型解析volatile、synchronized的内存语义
总线锁定:当某个CPU处理数据时,通过锁定系统总线或者是内存总线,让其他CPU不具备访问内存的访问权限,从而保证了缓存的一致性
101 0
|
存储 缓存 算法
并发编程(一)| Volatile 与 Synchronized 深度解析
今天这篇是我的好朋友 evil say的投稿,这小伙现在大四,客观来说,大四有这个实力,我觉得很不错。他目前正在找实习,如果看了本文觉得他可以,有公司有坑位、愿意抛出橄榄枝的话。请联系他:hack7458@outlook.com
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2

推荐镜像

更多
下一篇
DataWorks