current position:Home>Deep understanding of volatile, Synchronized, and already the underlying implementation principle

Deep understanding of volatile, Synchronized, and already the underlying implementation principle

2022-08-06 17:33:12austin

前沿

我们都知道,Writing correct concurrent programs can be challenging for programmers,Because we have a lot of reasons Java并发机制的底层实现原理 Not enough understanding and awareness,Therefore, a fast and precise solution is required并发类的疑难杂症,It is necessary to understand the nature of concurrent programming,追本溯源,In-depth analysis of the underlying principles of the concurrency mechanism.

这些年,To improve the performance of the machine,我们的CPU、内存、I/OContinuous iteration of equipment,但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异.为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用.

And it happens that we are enjoying the good performance experience brought by concurrent programming,At the same time, we also have to face the problems caused by concurrent programming.,And the basis for these weird questions comes from here!

image.png

一、There are three main sources of concurrency problems

  • 源头之一:缓存导致的可见性问题
  • 源头之二:线程切换带来的原子性问题
  • 源头之三:编译优化带来的有序性问题

只要我们能够深刻理解可见性、原子性、有序性在并发场景下的原理,Many concurrency problems will emerge.

二、如何解决可见性和有序性问题

导致可见性的原因是缓存,导致有序性的原因是编译优化(Caused by reordering of instructions),那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是试想,If such a solution is adopted,If our application performance is worrying?

合理的方案应该是按需禁用缓存以及编译优化,为了解决可见性和有序性问题,JavaThe memory model provides some specifications,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法.具体来说,这些方法包括volatilesynchronizedfinal三个关键字,以及六项Happens-Before规则.

三、volatile

3.1 volatile关键字作用

  • 保证变量写操作的可见性;
  • 保证变量前后代码的执行顺序;

3.2 volatile底层实现原理

1. 内存可见性,保证变量的可见性: 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果.当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中.当一个线程用到被volatileWhen the value of the key is modified,虚拟机会强制要求它从主内存中读取.

2. 禁止JVM指令重排序(防止JVM编译源码生成classreordering): 指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致.这在单线程中不会构成问题,但是在多线程中就会出现问题.A very classic example is to add fields at the same time in the double-checked create singleton methodvolatile,就是为了防止指令重排序.

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
复制代码

image.png

由上可以看到,instance变量被 volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性.这是因为 instance = new Singleton(); 这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

  • instance分配内存
  • 初始化instance
  • 将instance变量指向分配的内存空间

3.2 volatile的写-读内存语义

  • 当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存.
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,Read the latest value of shared variable from main memory between threads.

3.3 volatile总结

简而言之,volatileThe variable itself has the following properties:

1️⃣ 可见性:对一个volatile变量的读,总能看到(任意线程)对这个volatileThe latest value that the variable was last modified to write.
2️⃣ 原子性:对任意单个volatile变量的读/写具有原子性,但是类型volatile++This compound operation is not atomic.

即:volatileBy disabling instruction reordering,保证了有序性,但是不能保证原子性.

四、JVM锁技术:synchronized

所谓原子性就是:一个或者多个操作在CPU执行的过程中不被中断的特性,称为“原子性”.The source of the atomicity problem is thread switching,Wouldn't it solve this problem if I could disable thread switching??And the operating system does thread switching depends on CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换.

在单核CPU的场景下,同一时刻只有一个线程执行,禁止 CPU 中断,意味着操作系统不会重新调度线程,也就是禁止了线程切换,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性.

但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1上,一个线程执行在 CPU-2上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,It's likely atomic concurrent programming.

同一时刻只有一个线程执行 这个条件非常重要,我们称之为互斥. 如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了.

When it comes to mutual exclusion,First must have thought of that killer solution:加锁,At the same time, the following models also appear in the brain:

image.png

4.1 JavaProvide lock technology:synchronized

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现.synchronized 关键字可以用来修饰方法,也可以用来修饰代码块,Its usage examples are basically as follows:

public class MainTest {
    //修饰非静态方法
    synchronized void synFoo(){
        //临界区
    }

    //修饰静态方法
    synchronized static void synStaticFoo(){
        //临界区
    }

    //修饰代码块
    Object obj = new Object();
    void synCodeBlock(){
        synchronized (obj){

        }
    }
}
复制代码

利用synchronized可以实现同步,Java中的每一个对象都可以作为锁,具体表现为3种形式:

1️⃣ 对应普通同步方法,锁的是当前实例对象.
2️⃣ 对于静态同步方法,锁的是当前类的Class对象.
3️⃣ 对于同步方法块,锁的是synchronized括号里配置的对象.

即synchronizedLock types can be divided into:类锁,对象锁,代码块锁,同步方法锁,What do these locks mean?,The code will be explained below!

4.2 synchronizedSeveral usages and analysis of locks

1️⃣ 代码块锁,新建一个对象:Object lock = new Object(),The lock here is under this objectObject对象,Most people like to use,thisis to lock the entire object,范围比较大,May cause other locking methods in the object to be interfered,So you can use this method to prevent deadlocks when large class objects are used.

private Object lock = new Object();
/** * 锁对象lock */
public void methodA(){
    synchronized(lock){
        try {
            Thread.sleep(3000);
            System.out.println("---- 锁对象lock ---- ");
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

2️⃣ 代码块锁,lock class object lock itself,different objects isolated from each other,互不干扰.

/** * 锁对象本身 */
public void methodB(){
    synchronized(this){
        try {
            Thread.sleep(3000);
            System.out.println("---- 代码块锁,lock class object lock itself ---- ");
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

3️⃣ 实例方法锁

/** * 实例方法锁,同一个实例,多线程阻塞 * 不同的实例,互不干扰,Because the lock is not an object * 不同的实例,Different instance method locks,Multithreading is also blocking waiting,因为是一个对象 */
public synchronized void methodC(){
    try {
        Thread.sleep(3000);
        System.out.println("---- 实例方法锁 ---- ");
        
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
复制代码

4️⃣ 静态方法锁:

/** * static方法,类级别的锁,more objectthis无关, * Static method lock in the same class,多线程阻塞,Waiting to acquire class lock to execute * Different static method locks in the same class,多线程阻塞,Also need to wait for kind of lock to perform */
public synchronized static void methodD(){
    try {
        Thread.sleep(3000);
        System.out.println("---- 静态方法锁,类级别锁,锁的是当前类Class对象 ---- ");
        
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
复制代码

4.3 可重入性

当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,如果当前锁是重入性,请求将会成功,如果当前锁不是可重入性,会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得,就会产生死锁,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,还有就是当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法,这就是synchronized的可重入性.

可重入原理:加锁次数计数器

  • JVM负责跟踪对象被加锁的次数
  • 线程第一次给对象加锁的时候,计数变为1.Whenever this same thread acquires the lock again on the object,计数会递增
  • 每当任务离开时,计数递减,当计数为0的时候,lock will be fully released

4.4 synchronized的缺点

  1. 效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
  2. 不够灵活(读写锁更灵活:读不加,write before adding):加锁和释放锁的时机单一,Only a single add per lock(某个对象),可能是不够的
  3. 无法知道是否成功获取到锁(lockYou can try and do some logical business successfully,Failed to do some other logic)

4.5 ‍*️synchronized总结

  • synchronizedKeywords implement variable visibility and atomicity.
  • synchronizedThe locking mechanism implements atomic operations,另外JavaAtomic operations are implemented inCAS的方式.
  • synchronizedLock is the same object,是一个对象,Then the next thread needs to wait for the previous thread to release the object lock
  • synchronized虽然能保证线程安全,但是在并发场景下,会影响性能,For example, in a panic buying scenario.Is there a way to lock or lock the code block is a very time-consuming process,Or a public way,A lot of business logic is used,但是在并发环境下,基本行不通,There should be no interference in different business scenarios.

五、ReentrantLock

ReentrantLock是可重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,除此之外,ReentrantLockAlso supports access to lock the fairness and the justice of choice,with one of its inner classesSync有关,通过FairSync、NonfairSync的lock()The method realizes the acquisition of fair locks and unfair locks.

ReentrantLock.png

ReentrantLock中,调用lock()方法获得锁;调用unlock()方法释放锁;ReentrantLock的实现依赖于Java的同步器框架AbstractQueuedSynchronizer(简称AQS),AQSused a shapedvolatile变量 (命名为state)来维护同步状态,而这个被volatile修饰的state变量是ReentrantLock内存语义实现的关键.

ReentrantLockImplement locked entry

Re-entry is that only any thread can acquire the lock again after acquiring the lock without being blocked by the lock,The red thread of this feature needs to solve two problems:

1️⃣ 线程再次获得锁. 锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取.
2️⃣ 锁的最终释放. 线程重复n次获取了锁,随后第nafter release,Other threads can get to the lock,锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0的时候表示锁已经成功释放.

That's all for this article,如有不恰当的地方,欢迎在评论区指出,互相学习,我是austin,我们下期再见.

copyright notice
author[austin],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/218/202208061702145168.html

Random recommended