深入学习并发编程中的synchronized

第一章 并发编程中的三个问题

1.1 可见性

1.1.1 目标

学习什么是可见性问题

1.1.2 可见性概念

可见性(Visibility):是指当一个线程对共享变量进行修改,另一个线程要立即得到修改后的最新值。

1.1.3 案例演示

/*
    目标:演示可见性问题
        1.创建一个共享变量
        2.创建一条线程不断读取共享变量
        3.创建一条线程修改共享变量
 */
public class Test01Visibility {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(flag){

            }
        }).start();

        Thread.sleep(2000);

        new Thread(()->{
            flag = false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
}

1.1.4小结

并发编程中,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。

1.2 原子性

1.2.1 目标

学习什么是原子性问题

1.2.2 原子性概念

原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素的干扰而中断,要么所有的操作都不执行。

1.2.3 案例演示

/*
    目标:演示原子性问题
        1.定义一个共享变量number
        2.对number进行1000次++操作
        3.使用5个线程来进行
 */
public class Test02Atomicity {
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable increment = ()-> {
            for(int i=0;i<1000;i++){
                number++;
            }
        };
        List<Thread> list = new ArrayList<>();
        for(int i=0;i<5;i++){
            Thread t = new Thread(increment);
            t.start();
//            t.join();
            list.add(t);
        }
        for(Thread t : list){
            t.join();
        }
        System.out.println(number);
    }
}

使用javap反汇编class文件(javap -p -v .\Test02Atomicity.class )
得到如下字节码指令:

9: getstatic #51 // Field number:I
12: iconst_1
13: iadd
14: putstatic #51 // Field number:I

由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出问题。比如一个线程在执行13: iadd时,另一个线程又执行了getstatic。会导致两次number++,实际上只加了1。

1.2.4 小结

并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

1.3 有序性

1.3.1 目标

学习什么是有序性问题

1.3.2 有序性概念

有序性(Ordering):是指程序中代码的执行顺序,为了提高程序执行效率,java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

1.3.3 有序性演示

1.3.4 小结

程序代码在执行过程中的先后顺序,由于java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序

第二章 Java内存模型(JMM)

在介绍java内存模型之前,先来看看计算机内存模型

2.1 计算机结构

2.1.1 目标

学习计算机的主要狗组成
学习缓存的作用

2.2.2 计算机结构简介

冯诺依曼,五大组成部分

2.2 java内存模型

2.2.1 目标

学习java内存模型的概念和作用

2.2.2 Java内存模型的概念

Java Memory Molde(Java内存模型/JMM),不要与java内存结构混淆

关于“Java内存模型”的权威解释,参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

● 主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

● 工作内存
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。在这里插入图片描述

2.2.3 Java内存模型的作用

Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性和原子性的规则和保障。

2.2.4 CPU缓存,内存与Java内存模型的关系

通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行。

但ava内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。

JMM内存模型与CPU硬件内存架构的关系:
在这里插入图片描述

2.2.5 小结

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

2.3 主内存与工作内存之间的交互

2.3.1 目标

了解主内存与工作内存之间的数据交互过程

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

对应如下的流程图:
在这里插入图片描述
注意:
1.如果对一个变量执行lock操作,将会清空工作内存中此变量的值
2.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

2.3.2 小结

主内存与工作内存之间的数据交互过程

lock -> read -> load -> use -> assign -> store -> write -> unlock

第三章 synchronized保证三大特性

synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

synchronized (锁对象) {
	//受保护资源;
}

3.1 synchronized与原子性

3.1.1 目标

学习使用synchronized保证原子性的原理

3.1.2 使用synchronized保证原子性

原理:synchronized保证只有一个线程拿到锁,能够进入同步代码块。

3.2 synchronized与可见性

3.1.1 目标

学习使用synchronized保证可见新给的原理

3.1.2 使用synchronized保证可见性

案例演示:

public class Test01Visibility {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        new Thread(()->{
            while(flag){
                synchronized (obj){
                }
                //System.out.println(flag);
            }
        }).start();

        Thread.sleep(2000);

        new Thread(()->{
            flag = false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
}

使用print也会达到相同效果,因为print的原码中含有synchronized语句

3.1.3 小结

synchronized保证可见性的原理:执行synchronized时,对应lock原子操作会刷新工作内存中共享变量的值

3.3 synchronized与有序性

3.3.1 目标

学习使用synchronized保证有序性的原理

3.3.2 为什么要重排序

为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。

3.3.3 as-if-serial语义

as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。

编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

由于还不会使用jcstress并发压力测试,所以使用while循环外加一个if判断来验证是否会出现result==0的情况

public class Test03Ordering {
    private static int result;
    public static int num;
    public static boolean ready;
    public void actor2(){
        num = 2;
        ready = true;
    }
    public static void main(String[] args) throws InterruptedException {
        while(true){
            num = 0;
            ready = false;
            result = 0;
            Object obj = new Object();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (obj) {
                        if (ready) {
                            result = num + num;
                        } else {
                            result = 1;
                        }
                    }
                }
            });
            Thread t2 =  new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (obj) {
                        ready = true;
                        num = 2;
                    }
                }
            });
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println("num = " + num + "   ,ready = " + ready + "  ,result = "+ result);
            if(result == 0){
                break;
            }
        }
    }
}

第四章 synchronized的特性

4.1 可重入特性

4.1.1 目标

了解什么是可重入

了解可重入的原理

4.1.2 什么是可重入

一个线程可以多次执行synchronized,重复获取同一把锁

4.1.3 代码演示

/*
    目标:演示synchronized可重入
        1.自定义一个线程类
        2.在线程类的run方法中使用秦涛的同步代码块
        3.使用两个线程来执行
 */
public class Demo01 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
    }
}

//1.定义一个线程类
class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (MyThread.class){
            System.out.println(getName() + "进入了同步代码块1");

            synchronized (MyThread.class){
                System.out.println(getName() + "进入了同步代码块2");
            }
        }
    }
}

4.1.4 可重入原理

synchronized的锁对象中有一个计数器(recursions变量) 会记录线程获得几次锁

4.1.5 可重入的好处

1.可以避免死锁
2.可以让我们更好的来封装代码

4.1.6 小结

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁

4.2 不可中断特性

4.2.1 目标

学习synchronized 不可中断特性
学习Lock的可中断特性

4.2.2 什么是不可中断

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,在等待或阻塞中不可被中断。

4.2.3 synchronized不可中断代码演示

synchronized是不可中断,处于阻塞状态的线程会一直等待锁。

/*
    目标:演示synchronized不可中断
        1.定义一个Runnable
        2.在Runnable定义同步代码块
        3.先开启一个线程来执行同步代码块,保证不退出同步代码块
        4.后开启一个线程来执行同步代码块(阻塞状态)
        5.强行停止第二个线程
 */
public class Uninterruptible {
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        //1.定义一个Runnable
        Runnable run = () ->{
            //2.在Runnable定义同步代码块
            synchronized (obj){
                String name = Thread.currentThread().getName();
                System.out.println(name + "进入同步代码块");

                //保证不退出同步代码块
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //3.先开启一个线程来执行同步代码块
        Thread t1 = new Thread(run);
        t1.start();

        //保证第一个线程先去执行
        Thread.sleep(1000);

        //4.后开启一个线程来执行同步代码块
        Thread t2 = new Thread(run);
        t2.start();

        //5.停止第二个线程
        System.out.println("停止线程前");
        t2.interrupt();
        System.out.println("停止线程后");

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

执行结果:
在这里插入图片描述

4.2.4 ReentreantLock可中断演示

Lock有两种方式,一种是可中断一种是不可中断


/*
    目标:演示Lock不可中断和可中断
 */
public class Interruptible {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        //test01();
        test02();
    }

    //演示Lock不可中断
    public static void test01() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();

            try {
                lock.lock();
                System.out.println(name + "获得锁,进入锁执行");
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name + "释放锁");
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();

        System.out.println("停止t2线程前");
        t2.interrupt();
        System.out.println("停止t2线程后");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }

    public static void test02() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;

            try {
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if (b) {
                    System.out.println(name + "获得锁,进入锁执行");
                    Thread.sleep(88888);
                } else {
                    System.out.println(name + "在指定的时间内没有得到锁做其他操作");
                }
            }catch(InterruptedException e){
                    e.printStackTrace();
            } finally{
                if (b) {
                    lock.unlock();
                    System.out.println(name + "释放锁");
                }
            }
        };


        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();

/*        System.out.println("停止t2线程前");
        t2.interrupt();
        System.out.println("停止t2线程后");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());*/
    }
}

执行结果:
test01:
在这里插入图片描述
test02:
在这里插入图片描述

4.2.5 小结

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放,锁后一个线程会一直阻塞或等待,在阻塞和等待过程中是不可被中断的。

synchronized属于不可被中断

Lock的lock方法是不可被中断的
Lock的tryLock方法是可中断的

第五章 synchronized原理

5.1 javap反汇编

5.1.1 通过javap反汇编学习synchronized的原理

我们编写一个简单的synchronized代码,如下:

public class Demo01 {
    private static Object obj = new Object();

    public static void main(String[] args) {
        synchronized (obj){
            System.out.println("1");
        }
    }

    public synchronized void test(){
        System.out.println("a");
    }
}

通过cmd对class文件进行反汇编
得到
main方法的
在这里插入图片描述
test方法的
在这里插入图片描述

5.1.2 monitorenter

首先看一下JVM规范中对于monitorenter的描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
● If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
● If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
● If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

翻译:
每一个对象都会和一个监视器monitor关联。(这个对象也就是被我们当作锁的Object obj)
监视器被占用时会被锁住,其他线程无法来获取该monitor。
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

1.若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2.若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3.若其他线程已占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorenter小结
synchronized的锁对象会关联一个monitor(C++ 对象),这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner(拥有这把锁的线程),recursions(记录线程拥有锁的次数),当一个线程拥有monitor后其他线程只能等待

5.1.3 monitorexit

首先看一下JVM规范中对于monitorenter的描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译:
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

在这里插入图片描述
我们发现在这里有两个monitorexit,第一个monitorexit是 6、9、11、14执行都不出错时执行的,第二个monitorexit,我们观察最下面的Exception table,这个意思时当代码出错时,就执行到19,这时第二个monitorexit就保证了锁一定会被释放。

面试题:synchronized出现异常还会释放锁吗?
会释放锁,原因如上。

5.1.4 同步方法

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

接下来观察 test的反汇编代码
在这里插入图片描述
发现没有见到monitorenter与monitorexit

可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexi

5.1.5 小结

通过javap反汇编我们看到synchronized使用编程了monitorenter和monitorexit两个指令。每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁

5.1.6 面试题:synchronized与Lock的区别

1.synchronized是关键字,而Lock是一个接口。
        synchronized没有源码,是由JVM直接支持。
2.synchronized会自动释放锁,而Lock必须手动释放锁。
3.synchronized是不可中断的,Lock可以中断也可以不中断。
4.通过Lock可以知道线程有没有拿到锁,而synchronized不能。
        tryLock方法会返回一个boolean值,来告知是否获得锁
5.synchronized能锁住方法和代码块,而Lock只能锁住代码块。
6.Lock可以使用读锁提高多线程读效率。
        Lock 的一个实现类 ReentrantReadWriteLock,允许多个线程来读,允许一个线程来写,可以提高多个线程来读的效率。
7.synchronized是非公平锁,ReentreantLock可以控制是否是公平锁。
        当有多个线程在等待锁的时候,不是按照先来后到来给锁的,而是随机的
        在构造ReentreantLock锁时使用带参构造方法,传入一个boolean类型变量 fair来选择锁是公平锁。

5.2 深入JVM源码

5.2.1 目标

通过JVM源码分析synchronized的原理

5.2.2 JVM源码下载

http://openjdk.java.net

5.2.3 IDE(Clion)下载

5.2.4 monitor监视器锁

可以看出无论是synchronized代码块还是synchronized方法,其线程安全的语义实现最终依赖一个叫monitor的东西,那么这个东西是什么呢?下面让我们来详细介绍一下。

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。

_recursions //现成的重入次数
_object //存储该monitor的对象
_owner //标识拥有该monitor的线程
_waitSet //处于wait状态的线程,会被加入到_waitSet
_cxq //多线程竞争锁时的单向列表
_EntryList //处于等待锁block状态的线程,会被加入到该列表

5.2.5 monitor竞争

1.执行monitorenter时,会调用InterpreterRuntime.cpp

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");

这段代码的学习重点在最下面的if else语句中
if判断是不是用偏向锁,如果我们设置了偏向锁就会走fast_enter
如果没有设置就执行else中的语句

2.执行else语句,对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter

3.最终调用ObjectMonitor::enter (位于src/share/vm/runtime/objectMonitor.cpp)

void ObjectMonitor::enter(TRAPS) {
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD;

  //通过CAS操作尝试把monitor的_owner字段设置为当前线程
  void * cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL);
  if (cur == NULL) {
    // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
    assert(_recursions == 0, "invariant");
    assert(_owner == Self, "invariant");
    return;
  }
  // 线程重入,recursions++ 
  if (cur == Self) {
    // TODO-FIXME: check for integer overflow!  BUGID 6557169.
    _recursions++;
    return;
  }
  //如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
  if (Self->is_lock_owned ((address)cur)) {
    assert(_recursions == 0, "internal state error");
    _recursions = 1;
    // Commute owner from a thread-specific on-stack BasicLockObject address to
    // a full-fledged "Thread *".
    _owner = Self;
    return;
  }

省略一些代码
    for (;;) {
      jt->set_suspend_equivalent();
      // cleared by handle_special_suspend_equivalent_condition()
      // or java_suspend_self()
      
      //如果获取锁失败,则等待锁的释放
      EnterI(THREAD);

      if (!ExitSuspendEquivalent(jt)) break;

      _recursions = 0;
      _succ = NULL;
      exit(false, Self);

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }

此处省略锁的自旋优化等操作,统一放在后面synchronized优化中说。
以上代码的具体流程概括如下:
1.通过CAS尝试把monitor的owner字段设置为当前线程。
2.如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入的次数。
3.如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
4.如果获取锁失败,则等待锁的释放

5.2.6 monitor等待

void ObjectMonitor::EnterI(TRAPS) {
  Thread * const Self = THREAD;

  // Try the lock - TATAS
  //尝试抢救一下,看还能不能抢到锁
  if (TryLock (Self) > 0) {
    assert(_succ != Self, "invariant");
    assert(_owner == Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }
   //自旋,尝试抢救一下,看还能不能抢到锁
  if (TrySpin (Self) > 0) {
    assert(_owner == Self, "invariant");
    assert(_succ != Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }

  //当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
  ObjectWaiter node(Self);
  Self->_ParkEvent->reset();
  node._prev   = (ObjectWaiter *) 0xBAD;
  node.TState  = ObjectWaiter::TS_CXQ;
  
  //通过CAS把node节点push到_cxq列表中
  ObjectWaiter * nxt;
  for (;;) {
    node._next = nxt = _cxq;
    if (Atomic::cmpxchg_ptr(&node, &_cxq, nxt) == nxt) break;

    // Interference - the CAS failed because _cxq changed.  Just retry.
    // As an optional optimization we retry the lock.
    //尝试抢救一下,看还能不能抢到锁
    if (TryLock (Self) > 0) {
      assert(_succ != Self, "invariant");
      assert(_owner == Self, "invariant");
      assert(_Responsible != Self, "invariant");
      return;
    }
  }
  for (;;) {
	//线程在被挂起前做一下挣扎,看还能不能抢到锁
    if (TryLock(Self) > 0) break;
    assert(_owner != Self, "invariant");

    if ((SyncFlags & 2) && _Responsible == NULL) {
      Atomic::cmpxchg_ptr(Self, &_Responsible, NULL);
    }

    // park self
    //park就是挂起,park就不会执行了,就等被唤醒了
    if (_Responsible == Self || (SyncFlags & 1)) {
      TEVENT(Inflated enter - park TIMED);
      Self->_ParkEvent->park((jlong) recheckInterval);
      // Increase the recheckInterval, but clamp the value.
      recheckInterval *= 8;
      if (recheckInterval > MAX_RECHECK_INTERVAL) {
        recheckInterval = MAX_RECHECK_INTERVAL;
      }
    } else {
      TEVENT(Inflated enter - park UNTIMED);
      Self->_ParkEvent->park();
    }
    //尝试抢救一下,看还能不能抢到锁
    if (TryLock(Self) > 0) break;
  }
}

以上代码的具体流程概括如下:
1.当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
2.在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒
4.当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁

5.2.7 monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其他线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp)

退出同步代码块会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。

根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。

5.2.8 monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:
在这里插入图片描述
从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。

用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、l/O资源等。

系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如I/0调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。系统调用的过程可以简单理解为:

1.用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。

2.用户态程序执行系统调用。

3.CPU切换到内核态,并跳到位于内存指定位置的指令。

4.系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。

5.系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。

第六章 JDK6 synchronized优化

6.1 CAS

6.1.1 目标

学习CAS的使用
学习CAS的原理

6.1.2 CAS概述和作用

CAS的全称是:Compare And Swap(比较再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中,替换V。

CAS和volatile实现无锁并发

java提供了一个类 AtomicInteger 可以实现原子操作,这是因为它底层就是由CAS实现的

private static AtomicInteger number = new AtomicInteger();
number.getAndIncrement();

6.1.3 CAS原理

通过刚才AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。

Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得

Unsafe实现CAS

private static AtomicInteger number = new AtomicInteger();
   Runnable increment = ()-> {
       for(int i=0;i<1000;i++){
           number.getAndIncrement();
       }
   };


   public final int getAndIncrement() {
       return U.getAndAddInt(this, VALUE, 1);
   }


   public final int getAndAddInt(Object o, long offset, int delta) {
       int v;
       do {
           v = getIntVolatile(o, offset);
       } while (!weakCompareAndSetInt(o, offset, v, v + delta));
       return v;
   }
乐观锁和悲观锁

悲观锁从悲观的角度出发:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人只是想拿这个数据就会阻塞。因此synchronized我们也将其称为悲观锁。JDK钟ReentrantLock也是一种悲观锁。性能较差!

乐观锁从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如果没有人修改则更新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。

        1.因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
        2.但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响

6.1.4 小结

CAS的作用?Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证。

CAS的原理?CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果 内存地址值V 和 旧的预期值A 相等就修改 内存地址值为B

6.2 synchronized锁升级过程

高效并发是从JDK 1.5到JDK1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包偏向锁( Biased Locking )、括轻量级锁( Lightweight Locking )和如适应性自旋(AdaptiveSpinning)、锁消除( Lock Elimination)、锁粗化(Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁–》偏向锁–》轻量级锁-》重量级锁

6.3 Java对象的布局

6.3.1 目标

学习Java对象的布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对其填充。如下图所示:
在这里插入图片描述

6.3.2 对象头

当一个线程尝试访问synchronized修饰的代码块时,他首先要获得锁,那么这个锁到底存在哪里呢?

 
 
 
 
 
  
 

to be continued

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值