ReentrantLock、AQS底层实现原理

ReentrantLock介绍:

在单线程情况下交替执行与队列无关,因为使用JDK级别解决同步问题。

可重入锁: 线程已经获取某个共享资源的锁之后,释放锁之前,当前线程还可以再次对于这个共享资源获取锁(锁计数器[state+1])。

在jdk1.6之前->ReentrantLock和Synchronized的区别:

ReentrantLock:一部分在JDK级别解决,一部分在OS(操作系统)的Api执行,可使用Condition(条件)锁

Synchronized【Jvm内置锁】:所有锁都在OS(操作系统)Api执行

ReentrantLock类图解析:

  • ReentrantLock 实现 Lock 和 Serializable 接口
  • ReentrantLock 内部类 Sync、NonfairSync 和 FairSync 类

Sync:继承 AbstractQueuedSynchronizer 抽象类
NonfairSync(非公平锁)与 FairSync(公平锁) : 继承 Sync 抽象类

java.util.concurrent(j.u.c) 基于AQS实现:

AQS核心三大板块:

  • CAS
  • 自旋
  • LockSupport(park,unpark)

AQS核心源码分析:

https://www.processon.com/view/link/5e841dc6e4b0a2d87025a5af

文件可能比较大,加载的慢,请耐心等候……

Synchronized底层实现原理

synchronized介绍:

synchronized 关键字在多线程环境下作为线程安全的同步锁

synchronized作用:

1.同步代码块(当前对象锁[this] 或 自定义对象锁) 
2.同步静态方法(当前类的Class实例,Class数据存在永久代中,该类全局锁)
3.同步静态方法(当前对象锁)

同步代码块:

//Java代码
public void syncronizedTest(){
    synchronized (this){
        System.out.println("hello world");
    }
}

//代码反汇编 
JVM使用monitorentermonitorexit两个指令实现同步,即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能。
monitorenter/monitorexit:
    每个对象都会与一个monitor(监视器锁)相关联,当某个monitor被拥有后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor

    1.每个monitor维护一个记录拥有次数的计数器,未被拥有monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为1
    2.当同一个线程再次获得该monitor的时候,计数器再次自增     
    3.当不同线程想要获得该monitor的时候,就会被阻塞
    4.当同一个线程释放monitor(执行monitorexit指令)时,计数器自减。当计数器减为0时,monitor将被释放,其他线程可获得该monitor    
  

同步方法(静态方法,非静态方法):

//静态方法
public static synchronized void syncronizedStaticTest(){
    System.out.println("hello world");
} 

//非静态方法
public synchronized void syncronizedTest(){
    System.out.println("hello world");
}

//静态方法与非静态方法反汇编

JVM使用acc_synchronized标识来实现,即JVM通过在方法访问标识符(flags)中加入acc_synchronized来实现同步功能

    同步方法是隐式的,一个同步方法会在运行时常量池中的method_info结构体中存放acc_synchronized标识符。      当一个线程访问方法时,会去检查是否存在acc_synchronized标识符,如果存在,则要先获得对应的monitor锁,然后执行方法,当方法执行结束(正常return或抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。

总结:

同步方法和同步代码块底层都是通过monitor来实现同步的。

同步方法和同步代码块的区别:
    同步方法是通过方法中的access_flags中设置acc_synchronized标志来实现,同步代码块是通过monitorenter和monitorexit来实现。每个对象都与monitor相关联,而monitor可以被线程拥有或释放。

Java内存模型-volatile有序性

/**
 * @Description:Java内存模型-有序性
 * @Author:chenxi
 * @Date:2020/3/22
 **/
public class JMMOrderTest {

    private static int a, b = 0;
    private static int x, y = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        for (; ; ) {
            count++;a = 0;b = 0;
            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    int block = 0;
                    do{
                        block++;
                    }while (block < 20000);

                    a = 1;
                    x = b;
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            thread_1.start();
            thread_2.start();
            thread_1.join();
            thread_2.join();

            if (x == 0 && y == 0) {
                System.err.println("execute count = " + count + "  a-b-x-y: " + a + "-" + b + "-" + x + "-" + y);
                System.err.println("x and y is 0");
                break;
            } else {
                System.out.println("execute count = " + count + "  a-b-x-y: " + a + "-" + b + "-" + x + "-" + y);
            }
        }
    }
}

假设会出现以下场景:

假设Thread_1先启动执行结束、Thread_2后启动执行、Thread_2后启动执行
结果为:a = 1;b = 1;x = 0;y = 1;
假设Thread_2先启动执行结束、Thread_1后执行
结果为:a = 1;b = 1;x = 1;y = 0;
假设Thread_1先启动执行,在代码块中a = 1执行过程中阻塞了,Thread_2执行时在Thread_1执行x = b之前先执行b = 1,那么x,y = 1
执行顺序如下
   Thread_1:
      a = 1;
  Thread_2:
      b = 1; 
  Thread_1:
      x = b; // x = b = 1;
  Thread_2:
      y = a; // y = a = 1;

控制台输出如下:

......
execute count = 1689  a-b-x-y: 1-1-0-1
execute count = 1690  a-b-x-y: 1-1-1-0
execute count = 1690  a-b-x-y: 1-1-1-1
execute count = 1690  a-b-x-y: 1-1-0-0 
x and y is 0

那么为什么会出现 1-1-0-0 ???

CPU会进行指令重排:

在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    int block = 0;
                    do{
                        block++;
                    }while (block < 20000);

                     x = b; //改变执行顺序
                     a = 1; //改变执行顺序

                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    y = a;  //改变执行顺序 
                    b = 1;  //改变执行顺序 
                }
            });
假设Thread_1先启动执行,在代码块中x = b执行过程中阻塞了,Thread_2执行时在Thread_1执行a = 1之前先执行y = a,那么x,y = 0
执行顺序如下
  Thread_1:
      x = b; // x = b = 0;
  Thread_2:
      y = a; // y = a = 0; 
   Thread_1:
      a = 1;
  Thread_2:
      b = 1; 

此时,我们使用volatile来修饰x,y变量:

 private static volatile int x, y = 0; 

执行程序,x,y = 0的结果将不会再出现

因为volatile在写后面加上了storeload(内存屏障)

volatile如何防止指令重排:

volatile关键字通过”内存屏障“来防止指令被重排序

JMM采取保守策略,基于保守策略JMM内存屏障插入策略:

  • 在每个volatile操作的前面插入一个StoreStore屏障
  • 在每个volatile操作的后面插入一个StoreLoad屏障
  • 在每个volatile操作的后面插入一个LoadLoad屏障
  • 在每个volatile操作的后面插入一个LoadStore屏障

代码分析:

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    ......//此处代码省略 

                    a = 1; //volatile写后增加storeLoad(内存屏障)
                    x = b; //先volatile读,再普通写
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1; //volatile写后增加storeLoad(内存屏障) 
                    y = a; //先volatile读,再普通写 
                }
            });

自定义内存屏障:

  private static int x, y = 0;  

  ......//此处代码省略

            Thread thread_1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //阻塞
                    ......//此处代码省略

                    a = 1;
                    //手动增加内存屏障
                    getUnsafe().storeFence();
                    x = b;
                }
            });

            Thread thread_2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    //手动增加内存屏障
                    getUnsafe().storeFence(); 
                    y = a;
                }
            }); 

private static Unsafe getUnsafe() {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    }catch (Exception e){
        e.printStackTrace();
    }
    return null;
}

openJdk hotspot下 orderAccess_linux_x86.inline.hpp 源码解析:

openJdk下载地址:https://pan.baidu.com/s/13OvQIj5RrBWMVBQ8ENyV_w
提取码:e475

path: openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\os_cpu\linux_x86\vm下 orderAccess_linux_x86.inline.hpp 文件

Java内存模型-volatile原子性

/**
 * @Description:Java内存模型-原子性
 * @Author:chenxi
 * @Date:2020/3/22
 **/
public class JMMAtomicityTest {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter++;
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);//three result: 9202 9560 9861
    }
}

以上程序运行三次返回结果如下:9202 -> 9560 -> 9861

此时2个线程分别做了counter++,实际写入主内存的值却只有1。

Java内存模型-volatile可见性

Java内存模型:

        Java线程内存模型与CPU缓存模型类似,基于CPU缓存模型建立,Java线程内存模型是标准化的,用于屏蔽各种硬件和操作系统的内存访问差异。

主要分为三大模块: 线程、工作内存、主内存

Java内存模型数据原子操作:
read(读取) 从主内存读取数据
load(载入) 将主内存读取到的数据写入工作内存
use(使用) 从工作内存读取数据来计算
assign(赋值) 将计算好的值重新赋值到工作内存中
store(存储) 将工作内存数据写入主内存
write(写入) 将store过去的变量赋值给主内存中的变量
lock(锁定) 将主内存变量加锁,标识为线程独占状态
unlock(解锁) 将主内存变量解锁,解锁后其他线程可以锁定该变量

Volatile:

        volatile是Java虚拟机提供的最轻量级的同步机制。
        volatile保证可见性(所有线程都能看到共享的最新状态)与有序性(禁止指令重排序优化),但是不保证原子性,保证原子性需借助synchronizedLock锁机制
Volatile如何保证内存可见性:

        read(读取)、load(载入)、use(使用)动作必须连续出现。
        assign(赋值)、store(存储)、write(写入)动作必须连续出现volatile关键字使变量的读、写具有了原子性。然而这种原子性仅限于变量(包括引用)基本类型的自增(如count++)等操作不是原子的。
        ②对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的

volatile内存可见性实现原理:

        底层实现主要通过汇编lock前缀指令在汇编lock指令前后加上内存屏障),它会锁定这块内存区域的缓存并回写到主内存,此操作被称为”缓存锁定“,缓存一致性机制会阻止同时修改被两个以上的处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
/**
* @Description:Java内存模型-可见性
* @Author:chenxi
* @Date:2020/3/21
**/
public class JMMVisibilityTest {

    private static volatile boolean initFlag = false;

    public static void writeInitFlag(){ 
        System.out.println("start write initFlag...");
        initFlag = true;
        System.out.println("end write initFlag...");
    }

    public static void loadInitFlag(){
        while (!initFlag) {
        }
        System.out.println("current thread " +         Thread.currentThread().getName() + " sniffing initFlag value change");
    }

    public static void main(String[] args) {
        Thread thread_1 = new Thread(()->{
            loadInitFlag();
        }, "thread-1");

        Thread thread_2 = new Thread(()->{
            writeInitFlag();
        }, "thread-2");
        
        thread_1.start();

        try {
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        thread_2.start();
    }
}

Java内存模型缓存不一致性底层机制:

Bus总线加锁(性能太低):

        cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,其他cpu没法去读取或写入这个数据,造成阻塞,直到这个cpu使用完数据释放锁之后其他cpu才能读取该数据。总线加锁使CPU性能发挥不出来。

Java代码执行底层汇编查看vm配置:

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*JMMVisibilityTest.writeInitFlag

MESI缓存一致性协议:

        多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会立马同步到主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效,在下一轮指令周期从主内存中重新load数据。
MESI协议中状态:

M:被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主内存中的数据不一致,该缓存行的内存需要在未来某个时间点(允许其他CPU读取主内存中相应数据之前)写回(write back)主内存中。 当被写回主内存后,该缓存行的状态会变为独享(Exclusive)状态
E:独享的(Exclusive) 该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主内存中数据一致,有其他CPU读取该内存时变成共享(Shared)状态。 当CPU修改该缓存行内容时,该缓存行状态变为被修改(Modified)状态
S:共享的(Shared) 该缓存可能被多个CPU缓存,并且各个缓存中的数据与主内存数据一致(clean)。 当有一个CPU修改该缓存行时,其他CPU中该缓存行内容被视为无效(Invalid)状态
I:无效的(Invalid) 该缓存无效(可能被其他CPU修改了该缓存行) 使用(use)时需重新从主内存中读取(read)数据
        注意:变量所属内存区域必须是在缓存行,不能超过缓存行大小(缓存行大小一般64字节,较大的为128字节),变量如果超过缓存行大小,缓存一致性协议无法工作,就会使用总线锁