Java面试热点问题,synchronized原理剖析与优化

前言

观看笔记:https://www.bilibili.com/video/BV1aJ411V763?from=search&seid=6293835933701781647

观看了这个视频之后,我建议啊😉,可以去看下那个《深入java虚拟机》这本书,因为这个课讲的内容跟这本书当中的第13章 线程安全与锁优化,内容十分相似;所以我认为可以当做是课后复习书的那种;
我也是经过了对比两者的内容才这么觉得;因为真的很相似;
包括后面举的string的那个例子;
为了我更深刻的记忆;我决定我还是仔细看一遍这一部分内容;
(现在看来其实就是第五部分 高效并发 跟该课程视频讲的 非常非常相同了;)

为了做完这点屁笔记,熬的我真是老眼昏花;
我可真是太讨厌做笔记了;

课程介绍

typora-root-url: img
typora-copy-images-to: img

深入学习并发编程中的synchronized

课程背景

第一并发编程是java知识体系当中比较重要而且比较是比较难的一块内容。

因为并发编程涉及的知识面比较广,然后比较抽象不好理解,
因此如果我们想很好的掌握并发编程这块内容,其实是有一定难度的。
synchronized的原理以及其优化了解少。

第二并发编程在实际企业开发当中也是会遇到的,一个比较重要的比较棘手的问题。

举个例子,铁道售票的12306网站,
一个时间段内,可能有大量的用户过来进行买票,那么此时就需要进行保证卖出去的票的数量正确的,既不能超卖不能少卖
另外还要保证整个执行过程的卖票的执行效率是比较的。

那么可以通过synchronized来进行保证卖票的数量正确的,既不会超卖不会少卖

但是又要去考虑这个性能问题,那么就可以看到synchronized在实际的企业开发当中也是一个棘手的问题。

第三并发编程现在是一个热点面试题。

synchronized当中出现了异常,会不会释放锁?
synchronized和Lock有什么区别?
synchronized和volatile有什么区别?
etc...

面试官通过并发面试题来考查面试者的并发编程掌握情况,
来判断面试者是否能够满足企业需要,
另外也能够判断面试者的技术水平。

课程介绍

深入学习并发编程中的synchronized

  • 第一章:并发编程中的三个问题
    • 可见性
      • 案例:
        • 共享变量;
        • 一个线程A不断地来读这个共享变量的值;
        • 再用另一个线程B对该共享变量的取值进行修改;
        • 可以观测到另一个线程B对该共享变量的修改;
        • A线程并不能够感知得到;
        • 这就出现了可见性问题
      • 目标
        • 学习什么是可见性问题
      • 可见性概念
        • 可见性(VIsibility):是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
      • 可见性演示
        • 案例演示:
          • 一个A线程根据boolean类型的标记flag;while循环;
          • 另一个B线程改变这个flag变量的值;
          • 而第一个while循环的A线程并不会停止循环。
      • 小结
        • 什么是可见性?
          可见性(Visibility):是指当一个线程对共享变量进行了修改,那么另外的线程可以理解看到修改后的最新值
    • 原子性
      • 案例:
        使用i++,
        通过5个线程分别来进行执行1000次i++,
        最终可以发现加出来的效果并非是5000,
        可能会少于5000,
        那么这个问题的原因就在于i++不是一个原子操作
        到时会通过java反汇编的方式来进行演示分析这个i++其实有4条指令
      • 目标
        • 学习什么是原子性问题
      • 原子性概念
        • 原子性(Atomicity):指在一次操作或多次操作中,呀么所有的操作全部得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
      • 原子性演示
        • 案例演示:5个线程各执行1000次i++
    • 有序性
      • 一般来想的是程序会按照编写的代码的顺序来进行执行,
        那么实际上程序会去做一些优化措施
        为了让代码的执行效率更高一点,
        会做编译器和运行期的优化操作,
        这其中也是用到了一个案例,
        有序性问题有可能会被重排序
        那么导致在多线程的情况下,
        数据会出现错乱
      • 目标
        • 学习什么是有序性问题
      • 有序性概念
        • 有序性(Ordering): 是指程序代码在执行过程中的先后顺序,由于java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
      • 有序性演示
        • jcstress是java并发压测工具…
  • 第二章:java内存模型JMM)(开始解决问题)
    • 计算机结构
      • CPU内存缓存由此来引出java内存模型主内存工作内存如何操作变量
    • java内存模型
      • 主内存工作内存之间的交互
  • 第三章:synchronized保证三大特性
    • synchronized原子性
      • 目标
      • 使用synchronized保证原子性
      • synchronized保证原子性的原理
      • 小结
    • synchronized可见性
    • synchronized有序性
  • 第四章:synchronized特性同步锁机制synchronized作为锁的特性)
    • 可重入特性
      • 指的是当一个线程进入到一个同步代码块当中时,
      • 获取了某一个锁之后,
      • 还能够再次进入同步代码块获取同一把锁。
      • 即可以重新再进入。
    • 不可中断特性
  • 第五章:synchronized的原理
    • javap 反汇编
    • 深入JVM源码
      • 目标
      • monitor监视器锁
      • monitor竞争
      • monitor等待
      • monitor释放
      • monitor重量级锁
  • 第六章:JDK6 synchronized优化
    • CAS
    • java对象布局
      • 锁升级过程
    • 偏向锁
    • 轻量级锁
    • 重量级锁
    • 锁消除
    • 锁粗化
    • 平时写代码如何对synchronized优化
      • 减少synchronized的范围
      • 降低synchronized锁的粒度
      • 读写分离

可见性问题

第一章:并发编程中的三个问题 (可见性、原子性、有序性)

在使用多线程进行并发编程的时候,如果存在有多个线程操作共享数据
那么很有可能这个共享数据的值会出现错乱
那么以上称之为 线程安全问题
那么导致 线程安全问题根本原因有这三种:

  • 可见性
  • 原子性
  • 有序性

在讲解可见性概念之前,要注意几个前提条件
1、 如果只有一个线程操作,那么就肯定不会存在线程之间的可见性问题。
2、 还需要存在有共享数据,如果没有共享数据,那么也不会存在有可见性问题。
可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值。

目标

学习什么是可见性问题

可见性概念

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

可见性演示

案例演示:
一个线程A根据boolean类型的标记flag,while循环;
另一个线程B改变这个flag变量的值;
那么线程A并不会停止循环。

package com.xxx.concurrent_problem;

/**
  案例演示:
          一个线程对共享变量的修改,另一个线程不能立即得到最新值
*/
public class Test01Visibility{
    //多个线程都会访问的数据,我们成为线程的共享数据
    private static boolean run = false;

    public static void main(String[] args) throws InterruptedException{
      //t1线程不断的来读取run共享变量的取值
      Thread t1 = new Thread(() -> {
        while(run){

        }
      });
      t1.start();

      Thread.sleep(1000);

      //t2线程对该共享变量的取值进行修改
      Thread t2 = new Thread(() -> {
        run =  false;
        System.out.println("时间到,线层2设置为false");
      });
      t2.start();

      //可以观测得到t2线程对run共享变量的修改,t1线程并不能够读取到更改了之后的值;
      //这就出现了可见性问题
    }
}
package com.xxx.demo01_concurrent_problem;

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

  public static void main(String[] args){
    /* 2. 创建一条线程不断读取共享变量
          采用lambda表达式的方式进行创建线程
    */
    new Thread(()->{
      while(flag){
        /* 循环,如果该布尔类型变量的值为true则一直循环否则结束循环;
           在循环当中千万不要进行打印,打印了的话就看不到效果了
        */

      }
    }).start();

    /**
    沉睡两秒钟;
    让效果更加明显;
    这样则更加明显的来分析问题
    */
    Thread.sleep(2000);

    /* 3. 创建一条线程修改共享变量
       放到lambda表达式中
    */
    new Thread(()->{

      // 将flag改为false;并且输出打印;
      flag = false;
      System.out.println("线程修改了变量的值为false");
    }).start();
  
  /**
  分析一下这段代码:
  程序从main方法开始执行;
  开启了线程A不断读取共享变量的取值进行循环;
  开启了线程B去进行修改共享变量的值并打印;
  
  多线程执行具有 fu jin/fu ji(我没听清)性,
  有可能先进行跑A线程也有可能跑B线程;
  如果先跑B线程那么则看不到A线程当中的循环;
  为了让这个效果更佳明显一点;
  所以后加了一个Thread.sleep(2000);
  
  首先执行main方法的时候;
  会创建出一个线程A;
  这个线程A会来进行读取共享变量A的取值;
  则读取到flag共享变量的取值为true;
  那么该while循环则将会一直进行循环;
  那么当主线程沉睡了两秒之后,又会启动一个新线程B;
  新线程B将flag共享变量的取值变为了false;
  
  问题就在于分析
  线程A当中通过flag来进行while循环当中的该flag共享变量
  是否也从一开始的取值true到后期的线程B当中对共享变量flag取值进行修改为false
  是否也是同时进行了更改取值true为false;
  如果线程A当中的共享变量flag的取值与线程B操作共享变量flag的取值同时进行了更改;
  那么线程A当中根据flag取值进行while循环的循环操作就会停下来;
  如果没有进行修改线程A当中的flag共享变量
  那么线程A当中的flag共享变量的取值就将还会是true,
  即while循环根据flag共享变量的取值true继续其循环操作;
  
  效果:
  等待两秒之后,输出了线程B当中的打印 “线程修改了变量的值为false”;
  然而但是运行Run的的红灯仍然开启显示在运行着;

  那么这个就意味着;
  上面的那个线程A还在while(true)执行当中;
  也就是线程A当中的共享变量
  没有受到线程B当中操作共享变量flag取值从true变为false操作的影响;
  flag在线程A当中取值依然还是true;
  所以while循环根据该flag共享变量的取值依旧在进行着循环操作。
  即线程A当中认为flag共享变量依旧是true,所以并没有去进行停止while循环。
  
  这个时候就可以进行观察得到 可见性问题;
  下面的线程B对共享变量flag取值的修改,而上面的线程A并没有立即得到最新的结果;
  
  最后做个小结:
  当并发编程时,如果有多线程来进行操作共享变量;
  一个线程来进行读取操作;
  一个线程来进行写操作;
  那么这个当中就可能会出现 线程安全问题;
  即一个线程B进行修改,而另外一个线程A并没有得到修改后的最新取值;
  */
  }
}

小结

什么是可见性

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

原子性问题

现在来看并发编程中的第二个问题:原子性问题;
前期讲的是并发编程中的第一个问题可见性问题;
后期要讲的是并发编程中的第三个问题有序性问题;
学习分两步第一步介绍原子性的概念
第二步通过一个案例来进行演示原子性问题

目标

学习什么是原子性问题

原子性概念

原子性的前提:

1. 需要存在有多个线程;如果是一个线程,没有竞争的这种情况是看不出来问题所在的;
2. 依然还是需要存在共享变量;即到时候多个线程来对共享变量来进行操作;

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

原子性演示

案例演示:5个线程各执行1000次i++

package com.xxx.demo01_concurrent_problem;

import java.util.ArrayList;

/**
    案例演示:5个线程各执行1000次 i++;
*/
public class Test02Atomicity{
  private static int number = 0;
  public static void main(String[] args) throws InterruptedException{

    //5个线程都执行1000次i++
    Runnable increment = () -> {
      for( int i = 0 ; i < 1000; i++){
        number++;
      }
    };

    //5个线程
    ArrayList<Thread> ts = new ArrayList<>();
    for(int i = 0; i < 5 ; i++){
      Thread t = new Thread(increment);
      t.start();
      ts.add(t);
    }

    for(Thread t : ts){
      t.join();
    }

    /* 最终的效果即,加出来的效果不是5000,可能会少于5000
        那么原因就在于i++并不是一个原子操作
        到时候会通过java反汇编的方式来进行演示和分析,这个i++其实有4条指令
    */
    System.out.println("number = "+ number);
  }
}
package com.xxx.demo01_concurrent_problem;

/*
  目标:演示原子性问题
      1. 定义一个共享变量 number
      2. 对number进行 1000次的++操作
      3. 使用5个线程来进行操作
*/
public class Test02Atomicity{
  // 1. 定义一个共享变量 number;先赋值为0
  private static int number = 0;

  public static void main(String[] args) throws InterruptedException{

  /* 存在有5个线程需要来对number共享变量1000次的++操作
     2. 对number进行1000的++操作
     做任务;使用lambda表达式来进行编写
  */
  Runnable increment = () -> {
    for(int i=0; i< 1000; i++){
      number++;
    }
  };

  List<Thread> ts = new ArrayList<Thread>();

  // 3. 使用5个线程来进行
  for(int i = 0; i< 5;i++){

    // 该5个线程所做的事情即为 上面的increment所实现的run()
    Thread t = new Thread(increment);
    t.start();

    ts.add(t);
  }

    for(Thread t : list){
      t.join();
    }

    // 打印number的取值
    System.out.println("number = "+ number);
  
    /**
    由于有可能存在 主线程跑得更快;
    所以就有可能出现5个线程其run()当中的for循环没有跑完就有可能去执行输出number的取值了;
    为了一定要让5个线程当中的for循环跑完然后再来取number的取值;
    这个时候使用join的操作;
    先把这个5个线程放置到ArrayList集合ts当中;
    最后在打印之前遍历一下list集合ts;
    得到每个线程让其执行join();
    按照分析;每个线程都执行1000次number++;
    正常来说最后输出打印这个number时的打印结果应当为5000;
    那么这个运行结果有可能是5000也有可能会小于5000;

    效果:
    number = 5000
    多运行几次
    number = 4542(这次运行之后就会发现number的取值少了很多)
    那么为什么会出现number取值少了很多的这种情况呢?
    那么这是由于 i++(number++)这是多个操作;
    而且其是通过多线程来进行操作的;
    并没有来进行保证 i++(number++)这个操作的一个原子性;
    那么这个时候通过javap反汇编的方式来查看i++(number++)到底是由几个部分来组成的;
    找到编译后的结果(工程名/target/classes/com.xxx.demo01_concurrent_problem/Test02Atomicity.class)
    找到该文件之后通过使用Windows PowerShell打开或者是通过CMD等命令行进行打开也可以;
    键入命令:javap 可以对该字节码文件Test02Atomicity.class进行反汇编;
    从而看到一些字节码的指令;

    javap -p -v .\Test02Atomicity.class
    # -p 即显示私有的;-v 即详细信息也显示出来;

    通过反汇编之后可以看到很多的代码;
    */
  }
}

使用javap 反汇编class文件,得到下面的 字节码指令:

private static void lambda$main();
  Code:
     0: iconst_0
     1: istore_0
     2: iload_0
     3: sipush        1000
     6: if_           23
     9: getstatic     #12               // Field number:I
    12: iconst_1
    13: iadd
    14: putstatic     #12               // Field number:I
    17: linc          0, 1
    20: goto
    23: return

反汇编内容:

    offset_delta = 27
  Exception:
    throws java.lang.InterruptedException

# 通过反汇编可以看到lambda表达式的代码在此处
private static void lambda$main$0()
  descriptor: ()V
  flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
  Code:
    stack=2, loclas=1, args_size=0
        0: iconst_0
        1: istore_0
        2: iload_0
        3: sipush       1000
        6: if_icmpge    23
        9: getstatic    #18         // Field number:I
       12: iconst_1
       13: iadd
       14: putstatic    #18         // Field number:I
       17: iinc         0,1
       20: goto
       23: return
      LineNumberTable:
        line 18: 0
        line 19: 9
        line 18: 17
        line 21: 23
      LocalVariableTable:
        Start  Length   Slot   Name   Signature
            2      21      0      1   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 20
  static {}:
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1,  locals=0, args_size=0
          0: iconst_0
          1: putstatic  #18         // Field number:I
          4: return
...

其中,对于 number++ 而言(number为静态变量 ),实际会产生如下的JVM字节码指令:

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

代码当中的
number++;
对应反汇编代码当中的4句
9: getstatic #18 // Field number:I
12: iconst_1
13: iadd
14: putstatic #18 // Field number:I

number++是由四条字节码指令组成的;
那么其中在一个线程下是没有问题的;
但如果是放在多线程的情况下那么就是有问题的;
当前来进行分析下这个原子性问题
程序有主方法main方法开始进行执行的;

public class Test02Atomicity{
  // 1. 定义一个共享变量number
  private static int number = 0;

  public static void main(String[] args) throws InterruptedException{
    // 2. 对number进行1000的++操作
    Runnable increment = ()->{
      for(int i = 0; i< 1000; i++){

        number++;

/**
-------------             ------------------------------------------
|           |             |                                        |
|           |             | 9: getstatic    #18  // Field number:I |
| number++; |===========》|12: iconst_1                            |
|           |             |13: iadd                                |
|           |             |14: putstatic    #18  // Field number:I |
|           |             |                                        |
-------------             ------------------------------------------
*/

      }
    };

    List<Thread> list = new ArrayList<Thread>();
    // 3. 使用5个线程来进行
    for(int i = 0 ; i< 5; i++){
      Thread t = new Thread(increment);
      t.start();
      list.add(t);
    }

    for(Thread t: list){
      t.join();
    }

    System.out.println("number = " + number);
  }


  /**
  以下进行约定:红色的箭头代表主线程
  那么主线程先会进行执行(main());
  Runnable该语句先不会进行执行(Runnable increment=()->{...};);
  然后走下面的执行语句创建list集合(List<Thread> list=new ArrayList<Thread>();))
  以及for循环5次创建生成5个Thread线程;
  然后这个时候每个线程才会去执行上面的Runnable;
  5个线程分析起来有点麻烦;
  当前在for()循环创建生成5个线程途中,
  那么此时当前假设已经创建生成线程Thread A与线程Thread B;
  
  那么此时在Thread A与Thread B同时都运行start()方法的时候
  那么就都将会去执行Runnable当中的run()方法即循环1000次的number++操作;
  那么假设此时的number取值为0;
  假设线程Thread A先进行走,那么即循环1000次执行number++;
  那么这每一次的number++都是在执行字节码的那4条指令;
  即
  9:  getstatic #18
  12: iconst_1
  13: iadd
  14: putstatic #18
  #9: getstatic该指令即为取到共享变量number的取值,此时为0
  #12: 字节码指令继续往下执行一步,iconst_1该字节码指令的含义为是在准备一个常量1
  #13: 假设再往下走一步执行字节码指令iadd;那么该指令执行后会让12: iconst_1 准备的常量1与9: getstatic #18 获取得到的number 该共享变量的取值进行相加操作;最终的结果是1;
  
  但是注意假设此时并没有真正发生赋值操作,即运算出结果为1;
  但是并没有赋值给number该共享变量的取值上;
  然后此时CPU切换到另外一个线程上面去即线程B上去执行了;
  那么此时另外一个线程,即线程B,也进入了for循环来执行number++操作;
  那么线程B也有四条字节码指令需要进行执行;
  
  先执行字节码指令的第一条指令 9: getstatic #18 获取得到共享变量number的取值,
  目前该共享变量的取值是没有线程进行改变的;
  即也就是说线程A刚刚在操作number++字节码指令操作的步骤三时只是运算得出运算结果为1;
  但是并没有进行赋值就进行了CPU切换到了线程B上,
  所以相对于线程B当前的共享变量number来说,线程B认为number共享变量的取值是0;
  那么当getstatic获取得到共享变量number的取值之后,
  然后执行字节码指令的第二条指令 12: iconst_1 即同样是准备一个常量1;
  再执行字节码指令的第三条指令 13: iadd 这个时候同样是将number++操作当中的第一条字节码指令当中9: getstatic #18 获取得到的 共享变量number的取值与 number++操作当中的第二条指令 12: iconst_1 所准备的常量1 这两者进行相加操作;
  运算结果得到1;
  假设线程B再继续往下走,
  那么此时到了number++操作所对应字节码指令的第四个指令了;
  即14: putstatic   #18 ;
  那么该指令执行之后就会将指令三当中得到的结果1赋值给共享变量number的取值,
  而number变量的取值此时从0变为1;
  那么此时该线程的一次number++执行完成;

  假设CPU又切换到前一条线程,即线程A;
  那么在切换到线程B之前,
  线程A的number++操作的4条字节码指令已经执行完了前三条即iadd执行运算得出运算结果为1;
  那么此时当CPU又切换回线程A则继续执行number++操作字节码指令的第四条指令即14: putstatic #18该指令;
  则该指令同样是需要进行put即给共享变量number进行赋值操作;
  即将A线程运算得到的1赋值给已经被线程B之前赋值好number为1的共享变量取值为1;
  所以当前共享变量number的取值依旧是1;
  那么这个时候就看到了,两个线程执行number++;
  按道理其值应该是2;
  那么因为number++的字节码指令这4条字节码指令没有保证其一个原子性;
  所以发现导致最后的结果number只加了1;
  就让数据产生了错误;
  那么这个问题的原因就在于让两个线程来进行操作number++;
  而number++的字节码指令又是多条指令(4条指令);
  其中一个线程执行到一半的时候;
  CPU又切换到另外一个线程,即另外一个线程又来执行了;
  即第二个线程干扰了第一个线程的执行从而导致执行结果的错误;
  即没有保证原子性;(即没有使得应该的结果正确)

  小结:
  在并发编程的时候,很有可能会出现原子性问题;
  当一个线程对共享变量操作到一半的时候,
  另外一个线程也有可能来对共享变量来进行操作;
  那么此时另外一个线程就有可能会干扰前一个线程的操作;
  让前一个线程的操作没有保证其原子性;
  */
}

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

小结

什么是原子性

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

有序性问题

目标

学习什么是有序性问题

学习分成两步;
第一步:学习有序性的概念;
第二步:通过一个案例来演示有序性问题
有序性(Ordering):是指程序中代码的执行顺序
一般会认为编写代码的顺序就是代码最终的执行顺序;
那么实际上并不一定是这样的;
为了提高程序的执行效率;java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是编写代码时的顺序。
接下来通过一个案例来演示有序性问题;

有序性概念

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

有序性演示

jcstress 是java并发压测工具:

https://wiki.openjdk.java.net/display/CodeTools/jcstress

修改pom文件,添加依赖:

<dependency>
  <groupId>org.openjdk.jcstress</groupId>
  <artifactId>jcstress-core</artifactId>
  <version>${jcstress.version</version>
</dependency>

代码
Test03Orderliness.java

package com.xxx.concurrent_problem;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@OutCome(id = {"1" , "4"}, expect =  Expect.ACCEPTABLE, desc = "ok")
@OutCome(id = 0, expect = EXPECT.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Orderliness{
    int num = 0;
    boolean ready = false;

    /* 线程一 执行的代码;先进行判断ready的值然后进行相关操作;
       I_Result为并发压测工具自带的类;
       @Actor注解:表示到时候有多个线程来执行这两个方法;
       @JCStressTest注解:表示用这个并发压测工具来对这个类的方法进行测试
       @OutCome注解:对输出结果的处理;
       如果当id为{"1","4"}的时候,表示这种结果是我们所预期所接受的结果,则打印信息"ok";
       如果程序最终I_Result当中保存的结果是0;则也认为结果是可接受感兴趣的;然后打印信息"danger"
       接下来分析下有几种运行结果;
    */
    @Actor
    public void actor1(I_Result r){
      if(ready){
        r.r1 = num + num;
      }else{
        r.r1 = 1;
      }
    }

    //线程二 执行的代码;对两个变量进行相应的修改;
    @Actor
    public void actor2(I_Result r){
      num = 2;
      ready = true;
    }

    /**
      画两个箭头代表两个线程;
      (蓝色箭头与紫色箭头)
      而实际上这标有@Actor注解的两个方法有很多的线程来执行;
      那么为了演示方法,一个线程一个方法也是可以的;
      那么这里存在有几种情况;
      分有线程A与线程B分别执行actor1(I_Result r)与actor2(I_Result r);
      第一种情况是上面的线程A先走;
      执行actor1(I_Result r)方法;
      则获取得到共享变量 ready取值为false;
      则此时走else块;
      将1赋值给I_Result r当中的成员变量r1,即将r.r1即I_Result成员变量r1属性的取值修改为1;

      记录一下r.r1(I_Result中r1属性取值的变化):
      结果1:  1

      接着假设第二种情况下面的线程B先走;
      执行actor2(I_Result r)方法;
      执行代码语句给共享变量num以及ready重新赋值;
      即num该数值变为了2;
      ready该布尔类型取值变为了true;

      接着CPU又切换到上面的线程A当中进行执行;
      由于线程B修改num以及ready这两个共享变量成功了;
      即num取值为2;ready取值为true;
      则此时线程A再一次进行判断ready该变量的取值时此时ready为true;
      则进入actor1(I_Result r)当中的if块中;
      执行赋值语句,此时num取值为2;
      那么此时I_Result r.r1取值又被重新赋值为num+num,即运算结果为4;
      r.r1取值重新赋值为4;

      记录一下r.r1(I_Result中r1属性取值的变化):
      结果2:  4

      第三种可能性:
      依然还是假设下面的线程先走即先走线程B;
      执行actor2(I_Result r)方法;
      执行到代码语句 num = 2; 时,CPU又切换到线程A中去执行actor1(I_Result r)方法了;
      那么这个时候就是由线程A去执行actor1(I_Result r)方法;
      此时线程A获取得到ready变量的取值:为false;
      由于是执行线程B执行到num=2;赋值完成之后但是并未执行ready=true该语句之前CPU进行切换到了线程A的操作上去了;
      所以此时线程A去进行获取ready变量时,ready变量的取值依旧是false;
      所以此时不会进入if块当中而是进入else块当中执行操作使得r.r1=1;
      即对I_Result r当中的成员变量r1进行重新赋值为1;

      记录一下r.r1(I_Result中r1属性取值的变化):
      结果3:  4

      第四种可能性:
      很难发现;这是由于java在编译时和运行时的优化;
      就可能会对actor2(I_Result r)当中的代码语句
      num = 2;
      ready = true;
      进行重排序;
      比如说有可能会被排成这样:
      ready = true;
      num = 2;
      因为这两句代码并没有什么直接间接的一个因果关系;
      如果说通过编译时和运行时的优化代码变成了
      ready = true;
      num = 2;
      的这种顺序则再来进行分析一下:
      依然还是假设下面的线程B先走,即执行actor2(I_Result r);
      则执行了第一句ready = true;
      那么此时假设CPU正好在ready 赋值为 true之后以及num = 2赋值之前又切换到了上面的那个线程,即线程A即执行actor1(I_Result r)方法;
      那么此时线程A在执行actor1(I_Result r)时首先会去获取ready该共享变量的取值,
      则此时的ready变量的取值是在线程B的操作actor2(I_Result r)时进行了修改了的,并且赋值成功了;那么这个时候线程A获取得到ready的取值为true则进入if块当中执行对I_Result r.r1的赋值语句,r.r1=num+num;那么此时的num在线程B的操作actor2(I_Result r)并没有进行执行,即没有赋值成功;所以此时线程A获取得到num的取值依然为0;
      则此时赋值给I_Result r.r1成员变量的取值为num+num=0;即赋值给r1的取值为0;

      记录一下r.r1(I_Result中r1属性取值的变化):
      结果4:  0

      此时出现结果4: 0的原因就是因为actor2(I_Result r)中的两句代码的执行顺序被重排序过了从而导致的;

      效果:
      通过jcstress来进行检测结果;
      打开idea终端;即Terminal;
      键入命令行:mvn clean install
      完成之后会在target目录当中形成两个jar包(前提是安装了jcstress的依赖;) - jcstress.jar、Synchronized-1.0-SNAPSHOT.jar
      此时运行jcstress.jar该jar包;
      C:\Users\13666\IdeaProjects\XXX\Synchronized> java -jar target/jcstress.jar
      运行之后,进行多轮的压力测试;

    */
}

压力测试结果(代码重排序所导致state结果出现了0):

    [OK] com.xxx.demo02_concurrent_probleam.Test03Ordering
  (JVM args: [-XX:-TieredCompilation])
Observed state Occurrences         Expectation     Interpretation
             0       5,630  ACCEPTABLE_INTERESTING  danger
             1 150,985,557          ACCEPTABLE      ok
             4  41,594,004          ACCEPTABLE      ok


    [OK] com.xxx.demo02_concurrent_probleam.Test03Ordering
  (JVM args: [])
Observed state Occurrences         Expectation     Interpretation
             0       2,787  ACCEPTABLE_INTERESTING  danger
             1 113,540,468          ACCEPTABLE      ok
             4  46,388,436          ACCEPTABLE      ok

I_Result.class
jcstressjava并发压测工具当中并不只有I_Result这一个类;还存在有多个类似的类来进行保存各种不同的结果。

package org.openjdk.jcstress.infra.results;

import ...

@Result
public class I_Result implements Serializable {

  /**
    成员变量r1 来保存一个int类型的结果
  */
  @Contended
  @jdk.internal.vm.annotation.Contended
  public int r1;

  public I_Result(){

  }

  public int hashCode(){
    int result = 0;
    int result = 31 * result + this.r1;
    return result;
  }

  public boolean equals(Object o){
  ...
}

(有序性问题,有可能重排序从而导致在多线程的情况下,数据出现错乱问题。)

I_Result 是一个对象,有一个属性r1 用来保存结果,在多线程情况下可能出现几种结果?

情况1:线程1先执行actor1,这时 ready=false,所以进入else分支结果为1。
情况2:线程2执行到actor2,执行了num=2;和ready=true,线程1执行,这回进入if分支,结果为4;
情况3:线程2先执行actor2,只执行num=2; 但没来得及执行ready=true; 线程1执行,还是进入else分支,结果为1;
还有一种结果为0

运行测试:

mvn clean install
java -jar target/jcstress.jar

小结

程序代码在执行工程中的 先后顺序,
由于java在 编译期以及运行期的优化(为了提高执行效率),
导致了 代码的执行顺序 未必就是 开发者编写代码的顺序。(最终程序的执行顺序可能跟编写的顺序不一样)

最后也就能够了解到并发编程当中存在的三个问题;
可见性、原子性、有序性;这三个问题有可能会导致共享数据错乱;
会出现线程安全问题;

计算机结构

第二章:Java内存模型(JMM)

在介绍 Java内存模型之前,先来看一下到底什么是计算机内存模型。

当使用多线程并发访问共享资源的时候,会出现可见性、原子性、有序性等线程安全问题;
为什么会出现这三种问题以及出现这三种问题如何来进行解决呢?

这也是第二章所需要学习的内容;java内存模型(JMM);
在第二章中分成三部分来进行学习;
首先介绍计算机的结构;那么就需要了解知道计算机有哪些重要的组成部分;
第二来进行学习java内存模型;需要知道java内存模型的概念和作用;
最后来进行学习java内存模型当中的主内存和工作内存之间是如何进行数据交互的;

目标

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

计算机结构简介

1945年6月,美籍 匈牙利 科学家 冯 诺依曼 最先提出把 计算机分作 五部分计算器控制器存储器输入和输出设备
由于他对 现代计算机技术的特殊贡献,因此 冯 诺依曼 又被称为 “现代计算机之父”。

冯诺依曼,提出计算机由五大组成部分,输入设备、输出设备、存储器、控制器、运算器

  • 输入设备:键盘、鼠标、扫描仪、etc.
  • 输出设备:显示器、打印机、etc.
  • 存储器:内存条
  • 控制器:控制器+运算器–→CPU
  • 运算器:控制器+运算器–→CPU

--------------     -----------------        -------------
| 输入设备| ------→ |    存储器    | -------→ | 输出设备 |
------------       -----------------         ------------
     ↑                |  ↑    |  ↑                 ↑
     |                |  |    |  |                 |
     |       ---------|--|----|--|----------       |
     |       |        |  |    |  |         |       |
     |       |        ↓  |    ↓  |         |       |
     |       |   ----------  -----------   |       |
     |       |   | 运算器 |   | 控制器 |----|-------|
     |       |   ----------  -----------   |
     |       |                    |        |
     --------|---------------------        |
             |                        CPU  |
             -------------------------------

CPU

中央处理器CPUcentral processing unit)是计算机系统的运算控制核心。(相当于人类的大脑)
计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。
程序最终都会变成 指令CPU 去执行,处理程序中的数据。

内存

计算机中所有程序运行 都是在 内存中 进行的, 内存的作用是 用于暂时存放 CPU的运算数据,以及与 硬盘等 外部存储器 交换的数据。

CPU自产生以来,在逻辑结构、运行效率以及功能外延上取得了巨大发展。
但受制于 制造工艺以及成本等的限制,计算机的内存反倒在访问速度上并没有多大的突破,
因此CPU的处理速度和内存的访问速度之间的差距越拉越大,通常这种差距可以达到上千倍,极端情况下甚至会在上万倍以上。
这就导致CPU每次操作内存都要耗费很多等待时间。
内存的读写速度成为了计算机运行的瓶颈。

(程序都是在内存中运行的,内存 会保存 程序运行时的数据,供CPU处理;CPU运行的时候,是需要到内存当中读取数据进行相关处理的;)

缓存

由于CPU和内存两边速度 严重的不对等,会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是就有了在CPU与主内存之间增加缓存的设计,现在缓存的数量都可以增加到3级了,最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存,CPU缓存模型如图 下图所示。

CPU自从产生以来,在运行速度运行效率上得到了巨大的发展;
但是内存由于制造工艺制造成本的控制在访问速度上面并没有多大的提升。
因此CPU的运行速度和内存的读写速度其差距就将变得越来越大;
这样也就导致了CPU在运算的时候要花上很长的时间去等待内存的读取;
那么也就是说内存的读写速度成为了计算机运行的一个瓶颈;
那么如何来解决这个两面速度不对等的问题?
于是人们就在CPU与主内存之间增加了缓存的设计;

打开任务管理器[CTRL + ALT + DEL],点开[性 能]菜单栏,可以看到;

内核:      6             # 我电脑只有4
逻辑处理器: 12            # 我电脑只有8
虚拟化:    已启用
L1缓存:    384 KB
L2缓存:    1.5 MB
L3缓存:    12.0 MB

其中缓存L1缓存最小;L2缓存较之L1缓存较大;L3缓存最大;
CPU的缓存是内置在CPU当中的;
首先离CPU最近的是L1缓存;其空间比较小但是速度比较快;价格也比较昂贵;
L2缓存空间稍大;速度稍慢些;价格的话也会更加便宜些;
L3缓存空间更大;速度较之前两者更慢,价格也更便宜些;

看图说话:
CPU 在操作内存的时候有59.4 ns(纳秒)的一个延迟(Latency)
CPU 在操作L1缓存的时候大约 1.2 ns(纳秒)
CPU 在操作L2缓存的时候大约 5.5 ns(纳秒)
CPU 在操作L3缓存的时候大约 15.9 ns(纳秒)

从中可以看到的是内存的速度要比缓存的速度慢很多;
当CPU有了缓存之后,其数据是如何进行处理的呢?
首先CPU运算的时候需要数据,那么CPU直接去一级缓存L1Cache当中找要查找的数据看是否能够查找得到;
如果命中了一级缓存则直接从一级缓存当中进行读取数据到CPU,处理完成之后就会将CPU当中处理后的结果又接着放回到缓存L1、L2、L3中以及内存中;
那么如果CPU需要数据时,但是此时并没有在一级缓存L1 Cache当中命中缓存;
那么这个时候CPU就会去进行读取二级缓存L2 Cache当中所需要查找的数据;
如果二级缓存L2 Cache当中也没有找到所需要查找得到的数据,即没有命中缓存;
那么这个时候CPU就会去进行读取三级缓存L3 Cache当中所需要查找的数据;
如果此时三级缓存L3 Cache当中也没有找到所需要查找得到的数据,即也没有命中缓存;
那么这个时候就会去查找内存Memory;从内存当中拿取到相应的所需要查找的数据之后,在CPU中运行计算之后;另外相应的其处理结果也会保存在缓存当中;

                    单CPU双核的缓存结构
------------------------------------------------------------
|--------------------------------------------|
|      -----------          -------------    |
|      |  Core1  |           |  Core 2  |    |
|      -----------          -------------    |
|           ↓                     ↓          |
|      -------------         -------------   |
|      |  L1 Cache |         | L1 Cache |    |
|      -------------         -------------   |
|           ↓                    ↓           |
|      -------------         -------------   |
|      |  L2 Cache |         | L2 Cache |    |
|      -------------         -------------   |
|           ↓                    ↓           |
|     ------------------------------------   |
|      |          L3   Cache             |   |
|     ------------------------------------   |
|                       |                    |
|-----------------------|---------------------
                        ↓
     ------------------------------------
     |              Memory              |
     ------------------------------------

CPU Cache 分成了三个级别:L1、L2、L3。
级别越小越接近CPU速度也更快,同时也代表着容量越小

1. L1 Cache最接近CPU的,它容量最小,例如32k,速度最快,每个核上都有一个L1 Cache。
2. L2 Cache 更大一些,例如256k,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache。
3. L3 Cache是三级缓存中最大的一级,例如12MB,同时也是缓存中最慢的一级,在同一个CPU插槽之间的核共享一个L3 Cache。

Cache的出现是为了解决 CPU直接访问内存效率低下问题的
程序在运行的过程中,CPU接收到指令后,
它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,
如果命中缓存,CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,
当运算结束之后,再将CPU Cache中的最新数据刷新到主内存当中,
CPU 通过直接访问Cache的方式 代替 直接访问主内存的方式 极大地提高CPU的吞吐能力

但是由于 一级缓存(L1 Cache) 容量较小
所以不可能每次都命中,
这时 CPU 会继续向下一级的二级缓存(L2 Cache) 寻找,
同样的道理,当所需要的数据在二级缓存中也没有的话,
会继续转向L3 Cache内存(主存)硬盘

小结

计算机的主要组成 CPU内存输入设备输出设备

CPU:计算机的核心;用来控制和处理的
内存:用来保存正在运行的这些程序的数据;
输入设备
输出设备
缓存
CPU的运算速度比内存的访问速度快很多;
那么如果CPU直接从内存当中进行读取数据进行相关处理的话;
则内存读取时间花费开销大就会导致拖累CPU的运算速度;
所以在CPU与内存之间增加了缓存;
缓存的读写速度较之内存的读写速度要快很多;
因此可以让CPU的执行速度高一点;

1. 说出计算机的主要组成

CPU
内存
缓存

2. 为什么会出现缓存?

缓存是为了解决CPU直接访问内存效率低下问题的

java内存模型

目标

全称:Java Memory Model(翻译成中文名为:Java内存模型;一般简称为JMM)
Java 内存模型 JMM千万不要和Java内存结构混淆;
以前学习Java的时候知道JVM对内存进行一个划分;会划分成方法区、etc.那么这个是指的是Java内存结构
那么java内存模型 JMM是一套规范;关于Java内存模型的权威解释可以查看网址;(Oracle提供的纯英文版本,即Java内存模型的详细的说明文档,其实即一套规范,那么这套规范主要描述了两个关键字;一个是synchronized;一个是volatile;)

学习java内存模型的概念和作用
java内存模型(即 java Memory Model,简称JMM)。
(java内存模型 和 以前学习的栈、堆、方法区、原空间这样的java内存结构是不一样的)
很多小伙伴 将 “java内存结构” 与 “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是一门跨平台的语言;可以在不同的操作系统上运行;
那么其底层是依赖JVM虚拟机来进行实现的跨平台这一特性;
每一个平台都有其对应的一个虚拟机;
那么此时Java程序就可以跑在不同的操作系统上;
那么java内存模型是java虚拟机规范当中的一部分;
它主要是用来屏蔽java运行在不同操作系统上的一些细节问题;
那么java程序仅仅只需要关注java内存模型即可;

Java 内存模型,是 Java虚拟机规范 中 所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
JMM是一套规范, 描述了 Java程序中 各种变量(线程共享变量)的访问规则,以及在 JVM中 将变量 存储到 内存和从内存中读取变量 这样的底层细节,具体如下。

(学习主内存和工作内存,以及它们是如何操作这些共享变量的)
Java内存模型主要分作两部分来看;
一部分叫做主内存;
另一部分叫做工作内存;
首先来看主内存,java当中的共享变量;都放在主内存当中;
比如说类的成员变量也称之为实例变量;还有静态的成员变量;或者说叫类变量;都是存储在主内存当中的;
那么每一个线程都可以来进行访问主内存;
接着来看第二部分工作内存;每一个线程都有其自己的工作内存;
当线程要执行代码的时候,就必须在工作内存当中来进行处理完成;

假设现在一个线程A要访问主内存当中的一个共享变量X;
要对该共享变量X进行操作;
那么线程是不能够在主内存当中来进行直接操作共享变量X的;
即该线程只能够将该共享变量先进行复制一份放到线程自己的工作内存当中,
然后才去进行数据的相关处理;
当线程在其工作内存对该复制过来的共享变量相应处理完成之后再将处理完成的结果同步回主内存当中去;

假设线程A想要对共享变量x操作;
此时共享变量x的取值在主内存当中为 int x = 10; 这样一个取值;
那么首先线程A需要将该共享变量x的取值从主内存当中进行拷贝一份放到自己的工作内存当中去;
然后进行相应处理,比方说这里处理为重新赋值为9,
即int x = 9;那么假设处理完成之后,
那么这个时候该线程A就需要将对该共享变量重新赋值的结果即9需要同步回主内存当中;
那么其他的线程也是一样的,
也是先要进行将共享变量先要从主内存当中进行拷贝一份放到自己的工作内存当中去;
然后再去进行操作for example:int x = 8;
最后相应操作(赋值)完成之后再由线程从工作内存同步至主内存当中去;

  • 主内存
    • 主内存是 所有线程共享的,都能访问的。所有的共享变量存储于主内存
    • 共享变量主要包括类当中的成员变量,以及一些静态变量等;线程的局部变量是不会出现在主内存当中的;因为线程的局部变量只能够自己该线程进行使用;
  • 工作内存
    • 每一个线程有自己的工作内存,工作内存只存储 该线程 对共享变量的副本。 线程对变量的所有的操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问 对方工作内存中的 变量
    • 线程对共享变量的操作都是对其副本进行操作,操作完成之后再同步回主内存当中去;
                    Java Memory Model(Java 内存模型)
---------------------------------------------------------------------

    --------------------------          ----------------------------
    |        线程1           |           |          线程2           |
    |                        |           |                          |
    |  -------------------   |           |  --------------------    |
    |  |  线程1工作内存    |  |           |  |  线程2工作内存    |    |
    |  |                  |  |           |  |                   |   |
    |  | ---------------- |  |           |  | ----------------  |   |
    |  | | 共享变量x副本 | |  |           |  | | 共享变量x副本 |  |   |
    |  | ---------------- |  |           |  | ----------------- |   |
    |  |                  |  |           |  |                   |   |
    |  --------------------  |           |  ---------------------   |
                ↑                                      ↑
                |                                      |
                |←---------------JMM控制--------------→|
                |                                      |
                ↓                                      ↓
   --------------------------------------------------------------------
   |                            主内存                                |
   |  ------------------      ------------------     --------------   |
   |  |   共享变量x    |       |   共享变量 y   |      | 共享变量z  |  |
   |  ------------------      -------------------    ---------------  |
   |                                                                  |
   --------------------------------------------------------------------

Java内存模型的作用

java内存模型是一套规范;
主要的目的就是在多线程对共享变量进行读写时,来保证共享变量的可见性、有序性、原子性;
java内存模型该规范当中主要阐述了两个关键字;
一个是synchronized;一个是volatile;
那么在编程当中也是通过这两个关键字来进行保证共享变量的三个特性即可见性、有序性、原子性;

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

java内存模型与真实的计算机结构有什么关系?

CPU缓存、内存与Java内存模型的关系

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

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

JMM内存模型与CPU硬件内存架构的关系:

图的右边是真实的CPU硬件内存架构;有CPU、CPU当中存在有CPU寄存器、CPU缓存、内存(RAM);
图的左边是Java内存模型:线程、线程当中存在有工作内存、主内存;
Java内存模型是一套抽象出来的规范,抽象的概念,是一组规则;
java内存模型当中的线程中的工作内存有可能对应着 硬件内存架构当中的CPU寄存器也有可能对应着CPU缓存也有可能对应内存(RAM);
java内存模型当中的主内存也有可能对应着 硬件内存架构当中的CPU寄存器、CPU缓存、内存(RAM);

小结

java内存模型是一套规范(主要是让java程序来可以实现跨平台这一特性;又不需要去关注平台的底层细节),
描述了 java程序中各种变量 (线程共享变量)的访问规则,
以及在 JVM中将变量 存储到内存和从内存中读取变量这样的 底层细节,
java内存模型是对共享数据的可见性、有序性、原子性的规则和保障。

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

java内存模型有主内存、工作内存之分;
当线程A需要进行操作一个共享变量X的时候,
需要将存在在主内存的共享变量X复制拷贝一份放到线程A自己的工作内存当中;
线程A即对自己工作内存当中拷贝过来的共享变量副本进行操作,
操作处理完成之后;
然后再将结果从工作内存同步回主内存当中去;
那么该过程当中其中详细的细节是怎样的?

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

目标

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

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

对应的流程图如下:
这8个原子操作先不关心 Lock与Unlock操作;除此之外还余六个操作;
假设现在线程1想要来进行访问主内存当中的共享变量x,即当前主内存当中的共享变量x的取值为 boolean x = true;

那么该线程1首先会做一个原子操作叫做Read,那么也就是读取主内存当中的共享变量x的取值即boolean x = true的这样一个取值;
那么接下来就是执行一个操作叫做Load,即将该在主内存当中读取到的共享变量加载到了工作内存当中;
那么接着会做一个Use操作,也就是说如果该线程1需要对该共享变量x进行操作,即会取到这个从主内存当中加载过来的共享变量x的取值去进行一些操作;
那么操作之后会有一个新的结果进行返回;那么假设这个操作的新的结果令这个共享变量的取值变为了false;那么即给这个共享变量x进行赋值操作,即完成操作Assign;那么操作完成之后;就需要同步回主内存;
同步回主内存首先会完成一个 Store的这样一个原子操作;表示要来保存这个处理结果;
然后接着执行Write操作,即把在工作内存当中处理完成之后最新的取值,即Assign赋值给共享变量的值同步到主内存当中;即主内存中共享变量取值x由true更改为false;
另外还有两个操作即Lock与Unlock;这个是与锁相关的操作;
比如说加了synchronized,即加了锁;才会产生有lock与unlock操作;
如果共享变量的操作没有加这个synchronized即没有加锁;
那么也就不会产生有lock与unlock操作;

注意;
(对于lock有一些特殊的情况;)

  1. 如果对一个变量执行 lock操作,将会清空工作内存中 此变量的值。
  2. 对一个变量执行unlock操作,必须先把此变量同步到主内存中。

即如果线程1中在Read主内存当中的共享变量之前,线程1的工作内存当中已经存在有该共享变量的副本;那么又有lock操作的话则将会将该线程1当中的共享变量的副本进行清空掉,然后再去进行Read读取主内存当中共享变量取值的操作;即读取主内存当中有关该共享变量最新的取值;

unlock操作也需要注意;假设线程1当中的工作内存当中存在有该共享变量的副本;那么在执行unlock之前一定会先将工作内存当中该共享变量副本的取值同步到主内存当中去;然后再进行unlock操作;

synchronized如何保证可见性其实就与lock、unlock这两个原子操作有关;

小结

主内存 与 工作内存 之间的 数据交互过程(即主内存与工作内存的交互过程中是通过这8个原子操作来进行保证数据的正确性;)

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

synchronized保证原子性

(通过synchronized关键字和内存模型来详细的分析原子性问题,以及如何来进行解决原子性问题,如何解决可见性问题以及有序性问题等)

第三章:synchronized保证三大特性(即synchronized是如何进行来保证可见性、原子性、有序性)

回顾synchronized的使用

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

//到时候只有一个线程能够拿到获取锁进入同步代码块当中来;其他的线程拿不到获取不到锁;只能够在同步代码块外进行等待
synchronized( 锁对象 ){
  // 受保护资源 / 临界区资源
}

synchronized 与 原子性

目标

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

使用 synchronized 保证原子性

案例演示:5个线程各执行1000次 i++;
(回顾之前原子性问题的代码,产生原子性问题的原因

/**
  案例演示:
    1. 定义一个共享变量number
    2. 对number进行1000的++操作
    3. 使用5个线程来进行
*/
public class Test02Atomicity{
  // 1. 定义一个共享变量number
  private static int number = 0;

  public static void main(String[] args)throws InterruptedException{
    // 2. 对number进行1000的++操作
    // Runnable当中执行1000次的循环;每次循环使得number++
    Runnable increment =  () ->{
      for(int i = 0; i< 1000; i++){
        number++;
      }
    };

    List<Thread> list = new ArrayList<Thread>();
    // 3. 使用5个线程来进行
    // 创建5个线程都去执行Runnable当中的run()
    for(int i = 0 ; i< 5 ; i++){
      Thread t = new Thread(increment);
      t.start();
      list.add(t);
    }

    for(Thread t : list){
      t.join();
    }
    System.out.println("number = " + number);
  }
  /**
    之前存在线程安全问题;
    即多次运行测试发现数据可能会小于5000;
    number = 5000;
    number = 4935;
    导致这种结果的原因是因为number++是由4条字节码指令进行组成的;
    并没有保证这4条字节码指令的原子性操作;
    接下来使用synchronized;synchronized需要一把锁对象;
    则创建一把锁;Object obj即可;
    那么使用了synchronized之后就可以保证number++是一个原子操作;
    再次多次运行就会发现number的取值不会进行变动了;而是直接就是5000;
  */
}

使用synchronized来进行保证number++的原子性操作;即进行加锁

再怎么多次运行得到的number的结果也会是5000;因为synchronized保证了number++的原子性;那么数据就不会错乱;

package com.xxx.demo02_concurrent_problem;

import java.util.ArrayList;

/**
  案例演示:5个线程各执行1000次 i++;
  */
  public class Test01Atomicity{
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException{
      Runnable increment = new Runnable(){
        @Override
        public void run(){
          for(int i = 0; i < 1000; i++){
            synchronized(Test01Atomicity.class){
              number++;
            }
          }
        }
      };

      ArrayList<Thread> ts = new ArrayList();
      for( int i = 0;  i < 50 ; i++){
        Thread t = new Thread(increment);
        t.start();
        ts.add(t);
      }

      for(Thread t : ts){
        t.join();
      }

      System.out.println("number = " + number);
    }
  }
for ( int i = 0; i < 1000; i++){
    synchronized( Test01Atomicity.class ){
      number++;
    }
}
/**
  案例演示
    1. 定义一个共享变量number
    2. 对number进行1000次的++操作
    3. 使用5个线程来进行
*/
public class Test02Atomicity{
  // 1. 定义一个共享变量number
  private static int number = 0;

  priavte Object obj = new Object();//对象锁
  public static void main(String[] args)throws InterruptedException{
    // 2. 对number进行1000次的++操作
    Runnable increment = ()->{
      for( int i = 0; i < 5000; i++){
        synchronized(obj){
          number++;
        }
      }
    };

    List<Thread> list = new ArrayList<Thread>();
    // 3. 使用5个线程来进行
    for(int i = 0; i < 5; i++){
      Thread t = new Thread(increment);
      t.start();
      list.add(t);
    }

    for(Thread t : list){
      t.join();
    }
    System.out.println("number = " + number);
  }
}

/**
  进行分析;为什么加了synchronized之后就可以保证number++是一个原子操作;
  还是通过javap反汇编的方式来进行查看number++这一块的字节码指令的变化;
  通过命令行(Windows PowerShell、CMD等)打开文件;
  目录(工程名/target/classes/com/xxx/demo02_concurrent_probleam/Test02Atomicity.class)
  通过键入命令javap对字节码文件进行反汇编;查看到字节码指令;
  >PS C:\Users\13666\IdeaProjects\XXX\Synchronized\target\classes\com\xxx\demo02_concurrent_probleam> javap -p -v .\Test02Atomicity.class

  接着依旧是分析下synchronized保证原子性操作的原理;
  在第三步骤时创建了5个线程分别都来进行执行Runnable当中的run()操作;
  即进行循环1000次number++操作;
  分析的时候就以两个线程为例;线程A与线程B;
  假设第一个线程A先进行执行Runnable当中的1000次number++的循环;
  当线程A进入到synchronized的时候,如果发现对象锁obj没有线程在使用的话,
  那么线程A就会拿着对象锁obj进入同步代码块当中;
  那么进来之后就将执行number++操作;
  而number++操作所对应的字节码指令有4条字节码指令;
  那么首先来进行执行读取静态共享变量number的取值指令,
  即15: getstatic     #18   //Field number:I该指令;
  当前读取得到number的取值为0;那么读取之后就继续拿着对象锁obj往下进行执行;
  执行指令准备一个常量1即指令18: iconst_1
  准备一个常量1完成之后又接着往下继续执行;
  接着进行一个相加的操作即指令19: iadd的操作即指令15: getstatic当中获取得到静态共享变量number的取值0与18: iconst_1所准备的常量1进行相加得出运算结果为0+1=1;
  假设执行完该相加操作之后,即19: iadd操作完成之后CPU又切换到了第二个线程,即线程B;
  注意此时线程是在19: iadd执行之后但是在20: putstatic指令执行之前进行了CPU的切换,
  即共享变量并未被重新赋值之前只是得出了运算结果的时候进行了CPU切换到线程B上去了;
  
  第二个线程即线程B也是要来进行执行同步代码块synchronized(){}当中的代码;
  但是此时的这个对象锁obj已经被线程A锁获取得到了即线程A此时还并没有进行释放锁unlock的操作;所以导致尽管CPU切换到了线程B上但是由于线程A没有释放锁;
  线程B没有办法获取得到锁,
  就只能在synchronized同步代码块的外侧进行等待;
  
  那么CPU最终还是会切换到第一个线程即线程A上面来继续完成number++所对应的未完成的字节码指令的第四条指令即20: putstatic   #18   //Field number:I
  执行putstatic那么就会将19: iadd所运算得出的取值结果1赋值给静态共享变量number取值;
  最后线程A执行外字节码指令之后就会出同步代码块,
  就会将对象锁obj给还回去;
  那么此时CPU又切换回第二个线程,线程B当中上来了;
  线程B又来执行同步代码块synchronized(){};
  这次就能够获取得到对像锁了;
  基于线程A完成了其在synchronized同步代码块当中的number++操作之后释放了锁,
  CPU又切换到了线程B上,
  所以此时线程B可以拿到获取得到对象锁obj;
  
  那么此时线程B也拿着对象锁obj进入同步代码块synchronized当中执行number++操作即所对应的4个字节码指令(getstatic、iconst_1、iadd、putstatic);
  那么线程B先来进行执行number++操作的第一条字节码指令:15: getstatic    #18     // Field number:I 操作获取得到此时共享变量number的取值,
  那么此时线程B来获取共享变量number的取值时,
  共享变量number变量在之前的线程A当中的操作当中已经由0变为了1,
  所以此时线程B获取得到number共享变量的取值为1;
  
  接着执行number++操作的第二个字节码指令准备一个常量1即18: iconst_1操作;
  在往下继续执行number操作的第三个字节码指令19: iadd,即获取得到静态共享变量取值number为1 与 所准备的常量1进行相加得到运算结果 1+1=2;
  那么最后走到第四步进行执行20:putstatic   # 18   // Field number:I操作
  那么就会把从第三步字节码指令所运算得出的结果 2 赋值给共享变量number取值;
  那么此时静态共享变量的取值就由1更改为2了;
  这个时候可以看到两个线程进行执行number++操作;
  最终得到的number的结果为2;
  数据没有出现错乱;
  那么这里最根本的原因就是有了同步代码块之后;
  
  当第一个线程进来执行number++所对应的其四个字节码操作指令;
  执行到一半,就算CPU切换到了第二个线程进行执行相同的操作也会由于没有对象锁而无法进入同步代码块只能够在同步代码块外侧进行等待第一个线程释放锁第二个线程才能够获取锁从而进入同步代码块;
  所以也就能够保证第一个线程在执行number++所对应的四个字节码指令时,
  不会受到其他线程的干扰,
  从而也就保证了操作的原子性。
  所以数据才不会出现错乱。
*/

字节码指令:

      frame_type = 250 /* chop */
          offset_delta = 27
    Exceptions:
      thows java.lang.InterruptedException
  
  private static void lambda$main$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=3, args_size=0
          0: iconst_0
          1: istore_0
          2: iload_0
          3: sipush          1000
          6: if_icmpge       39
          9: getstatic       #22             // Field obj:Ljava/lang/Object;
         12: dup
         13: astore_1
         14: monitorenter
         15: getstatic       #18             // Field number:I
         18: iconst_1
         20: putstatic       #18             // Field number:I
         23: aload_1
         24: monitorexit
         25: goto            33
         28: astore_2
         29: aload_1
         30: monitorexit
         31: aload_2
         32: athrow
         33: iinc            0,1
         36: goto
         39: return
      Exception table:
          from     to     target   type
            15     25         28     any
            28     31         28     any
      LineNumberTable:
        line 19: 0
        line 20: 9
        line 21: 15
        line 22: 23
        line 19: 33
....

number++操作依旧对应着这四条字节码指令

15: getstatic         #18       // Field number:I
18: iconst_1
19: iadd
20: putstatic         #18       // Field number:I

而此时包含着这四条字节码指令的还有monitorenter以及monitorexit这两条指令;即;

14: monitorenter
15: getstatic        #18        // Field number:I
18: iconst_1
19: iadd
20: putstatic        #18        // Field number:I
23: aload_1
24: monitorexit

synchronized 保证原子性的原理

对 number++ ,增加同步代码块后,保证同一时间 只有 一个线程操作 number++; 就不会出现安全问题。

小结

synchronized保证原子性的原理: synchronized保证只有一个线程拿到锁,能够进入同步代码块中,当第一个线程进入了同步代码块当中即使操作到了一半没有操作完由于CPU切换,切换到了其他线程,其他线程也会由于没有对象锁的缘故无法进入同步代码块当中只能进行等待;即其他线程不会来干扰第一个线程的操作;就能够保证同步代码块当中的代码是一个原子性的操作,不会受到其他线程的干扰;

synchronized 保证原子性的原理

对 number++; 增加同步代码块后,保证同一时间只有一个线程操作 number++;

synchronized保证可见性

目标

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

使用synchronized保证可见性

案例演示: 一个线程根据 boolean类型的标记flag,while循环,另一个线程改变这个flag变量的值,根据boolean 类型的标记flag进行while循环的那个线程 并不会停止循环。

回顾之前的代码(可见性问题产生的原因):

/**
  案例演示:
    1. 创建一个共享变量
    2. 创建一条线程不断读取共享变量
    3. 创建一条线程修改共享变量
*/
public class Test01Visibility{
  // 1. 创建一个共享变量;
  // boolea布尔类型flag默认取值为true
  private static boolean flag = true;

  public static void main(String[] args) throws InterruptedException{
    // 2. 创建一条线程不断读取共享变量;
    // 开启一条线程进行循环;
    // 如果flag变量一直为true则一直循环否则停止循环;
    new Thread(()->{
      while(flag){

      }
    }).start();
  
    Thread.sleep(2000);// 主线程沉睡两秒

    // 3. 创建一条线程修改共享变量flag取值为false
    new Thread(()->{
      flag = false;
      System.out.println("线程修改了变量的值为false");
    });
  
    /**
      之前的效果为;两秒之后进行打印了语句“线程修改了变量的值为false”;
      但是另外一个线程并没有获取得到最新值,
      也就是根据flag变量进行while循环的那个线程并没有获取得到最新flag取值从而也就导致这个线程一直在while(flag)/while(true)运行中;
      Run一直为红灯。
      接着来进行分析其原因以及如何来进行解决。
      为什么会出现可见性问题分析:

      首先Test01Visibility当中存在有一个成员变量flag取值为true;
      那么该成员变量是两个线程都会来进行操作的;
      线程A与线程B;
      线程A负责执行根据flag取值变化从而while循环的线程;
      线程B则是负责修改成员变量flag取值的线程;
      那么该成员变量位于主内存当中;即线程A与线程B的共享变量;

      红色的箭头代表主线程,主线程从main()开始执行;
      执行Test01Visibility当中mian()中的代码,创建线程A并启动线程A中的run();
      接着画一个新线程,该新线程用绿色箭头表示,表示的即为线程A,
      那么该A线程就会去进行执行while(flag){}循环;
      当线程A进行执行的时候就需要用到共享变量flag的取值;
      而该共享变量位于主内存当中;
      那么这个时候线程A就会从主内存当中进行复制一份共享变量flag的取值boolean flag=true的副本放到线程A自己的工作内存当中来;
      那么当前进行循环这个flag为true那么也就会导致一直在进行循环的操作;

      主线程继续往下进行执行,Thread.sleep(2000);主线程沉睡两秒钟;

      那么当主线程沉睡完两秒之后,又会进行启动一个新的线程即线程B,也就是那个修改共享变量flag取值的那个线程;
      再画一个紫色的箭头来代表一个新的线程即线程B;
      那么线程B则对应着图中的右边的那块工作内存;
      那么线程B启动之后也是需要来进行执行线程B当中的run()当中的代码即修改共享变量flag的取值为false并打印输出日志;
      所以此时线程B需要做的是也是从主内存当中去进行读取复制拷贝获取得到一份共享变量flag的副本即boolean flag = true;到线程B自己的工作内存当中来;
      另外在线程B自己的工作内存当中对其共享变量副本进行相关操作即进行重新赋值为false;
      那么当线程B修改工作内存当中共享变量flag副本取值之后会将工作内存当中该共享变量副本的取值同步回主内存当中的共享变量取值;
      也就是主内存当中该共享变量flag取值就由true变为了false;
      那么接着线程B再去打印“线程修改了变量的值为false”信息;
      那么该线程B执行至此就结束了。
      那么可以看到的是线程B所进行修改线程B自己工作内存当中的共享变量副本flag的取值为false并且同步回到了主内存当中去了;
      那么此时的线程A并不知道主内存当中共享变量flag的取值被更改了,线程A依然在进行循环操作;
      即while(true)的循环操作;
      线程A使用到的共享变量依然还是线程A自己工作内存当中之前从主内存中复制拷贝过来的flag共享变量也就是那个flag=true那个时候的取值;
      所以线程A就一直循环一直循环根本停不下来;

      那么造成这种现象的原因就是因为:
      一个线程进行修改了共享变量副本的取值并且同步回了主内存当中;
      第二个线程即根据该共享变量副本的取值做while循环的线程并没有感知到主内存当中共享变量取值发生的变化,
      因为第二个线程当中使用的共享变量副本的取值并没有随着一同主内存当中的共享变量取值改变;

      那么解决方案有两种;先简单介绍下;有一个关键字叫做volatile;
      用该关键字volatile来进行修饰共享变量就可以解决可见性问题;
    */
  }
}

使用volatile关键字来进行解决可见性问题

/**
  案例演示:
    1. 创建一个共享变量
    2. 创建一条线程不断读取共享变量
    3. 创建一条线程修改共享变量
*/
public class Test01Visibility{
  // 1. 创建一个共享变量
  private static volatile boolean flag = true;

  public static void main(String[] args)throws InterruptedException{
    // 2. 创建一条线程不断读取共享变量
    new Thread(()->{
      while(flag){

      }
    }).start();

    Thread.sleep(2000);//主线程沉睡两秒

    new Thread(()->{
      flag =  false;
      System.out.println("线程修改了变量的值为false");
    });
  
    /**
    那么此时就需要进行分析 volatile关键字是如何进行解决可见性问题的?
    简单介绍下;
    当共享变量flag添加了 volatile关键字之后,线程B进行修改了共享变量flag副本取值为false之后同步回主内存时;由于volatile关键字的修饰,会有一个缓存一致性协议;会把其他线程当中的工作内存中的该共享变量flag的副本全部进行设置为失效状态;那么这个时候其他线程由于线程自己内部工作内存中需要用到的共享变量flag数据失效的缘故就会重新到主内存当中来进行读取最新的共享变量取值;这是采用volatile的规则;

    那么现在不使用volatile,使用synchronized也可以解决这个可见性问题;
    */
  }
}

使用 synchronized 关键字来进行解决可见性问题

package com.xxx.demo02_concurrent_problem;

/**
    案例演示:
      一个线程根据 boolean 类型的标记flag,while循环,另一个线程改变这个flag变量的值,
      根据boolean 类型的标记flag进行while循环的那个线程 并不会停止循环。
  */
  public class Test01Visibility {
    // 多个线程都会访问的数据,我们称为 线程的共享变量
    private static boolean run = true;
    public static void main(String[] args) throws InterruptedException{
      Thread t1 = new Thread(() -> {
        while(run){
          //增加对象共享数据的打印,println是同步方法
          System.out.println("run = " + run);
        }
      });

      t1.start();

      Thread.sleep(1000);

      Thread t2 = new Thread(() -> {
        run = false;
        System.out.println("时间到,线程2设置为 false");
      });
    }
  }
/**
  案例演示:
    1. 创建一个共享变量
    2. 创建一条线程不断读取共享变量
    3. 创建一条线程修改共享变量
*/
public class Test01Visibility{
  // 1. 创建一个共享变量
  private static boolean flag = true;

  private static Object obj =  new Object();

  public static void main(String[] args) throws InterruptedException{
    // 2. 创建一条线程不断读取共享变量
    new Tread(()->{
      while(flag){
        synchronized(obj){

        }
      }
    }).start();

    Thread.sleep(2000);// 主线程沉睡两秒

    // 3. 创建一条线程修改共享变量
    new Thread(()->{
      flag = false;
      System.out.println("线程修改了变量的值为false")
    });
  
  /**
  分析synchronized是如何进行解决的可见性问题?
  那么synchronized关键字其实会变成8个原子操作当中的lock与unlock原子操作;
  即8个原子操作(主内存与工作内存之间具体的交互协议)为
  lock --→read --→ load--→ use--→ assign--→ store--→ write--→ unlock  
  那么这个lock原子操作之前就会让线程A当中的工作内存进行去刷新,
  也就是如果线程A中存在有该共享变量的副本会被清除,
  然后再去获取最新的共享变量flag取值;
  也就可以得到最新的false取值;
  由线程B进行修改共享变量副本并同步回主内存后的那个最新值;
  
  所以这个时候的效果就是线程A当中由于接收到了主内存当中共享变量最新取值的副本flag=false;所以就会不再执行while循环;
  另外可以不用进行写 synchronized(obj){} ;
  可以直接进行打印也是可以做到解决可见性问题的 System.out.println(flag);
  
  而分析下为什么通过打印语句System.out.println(flag);也可以使得while循环停下来呢?
  原因在于PrintStream.java中的println(boolean x)方法中也使用到了synchronized,
  所以也就导致也会去刷新线程A当中工作内存当中的变量从而去获取主内存当中最新的共享变量的取值:
  -------------------------------------------------------------------
  /**
    Prints a boolean and then terminate the line.
    This method behaves as though it invokes
    <code>{@link #print(boolean)}</code> and then
    <code>{@link #println()}</code>

    @param x The <code>boolean</code> to be printed
  */
  public void println(boolean x){
    synchronized(this){
      print(x);
      newLine();
    }
  }
  -------------------------------------------------------------------
  */
  }
}

synchronized 保证可见性的原理

synchronized同步的时候会对应8个原子操作当中的lock与unlock这两个原子操作;
那么lock操作执行时回去刷新该线程工作内存当中共享变量的取值;
从而该线程就会去主内存中去获取得到最新的值;
也就是说synchronized会刷新工作内存中的变量得到主内存中最新共享变量取值的副本;
从而保证可见性;

小结

synchronized保证可见性的原理

执行synchronized时,其对应的lock原子操作会刷新工作内存中共享变量的值

synchronized保证有序性

synchronized 与 有序性

目标

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

为什么要重排序

为了提高程序的执行效率,有可能所写的代码对CPU来说其执行效率并不高;
它可能经过重排序之后执行的效率更高一点;所以编译器和CPU会对程序中的代码进行重排序;(编译器和CPU不会进行乱排,会满足某种规则;所以就有一个as-if-serial语义

重排序 是指 编译器 和 处理器 为了优化程序性能 而对 指令序列 进行 重新排序 的一种手段。

as-if-serial语义

as-if-serial 语义的意思是:

(不管编译器和CPU如何重排序。必须保证在单线程情况下程序的结果是正确的;那么在多线程的情况下就有可能是有问题存在问题的)
不管怎么 重排序(编译器和处理器 为了提高 并行度),单线程程序的执行结果不能被改变。
编译器、runtime和处理器 都必须遵守 as-if-serial 语义。

以下数据有依赖关系,不能 重排序。

写后读:

int a = 1; # 对一个变量进行写操作
int b = a; # 在对一个变量写操作完成之后又将该变量的取值读取出来

# 那么在这种情况下是不能够进行重排序的;
# 即不能够换成
# int b = a;
# int a = 1;
# 那么在执行 int b = a;的时候,a是没有取值的;

写后写:

int a = 1; # 先给变量a进行赋值操作即写操作;
int a = 2; # 再来给变量a进行赋值操作即写操作;

# 那么在这种情况下也是不能够进行重排序的;
# 即不能够换成
# int a = 2;
# int a = 1;
# 的这样一种情况,
# 因为在执行原来没有重排序的代码时最终获取得到a变量的结果为2;
# 而经过重排序之后a变量的取值被重新赋值为了1;
# 导致数据最终结果的不正确性;
# 所以在这种情况下也是不能够进行重排序;

读后写:

int a = 1; # 给变量a进行赋值写操作
int b = a; # 将变量a的取值进行读取出来
int a = 2; # 再将变量a的取值进行重新赋值

# 这种情况下也是不能够进行重排序的;
# 即不能够换成
# int a = 1;
# int a = 2;
# int b = a;
# 这样一种顺序;
# 如果是这样排序的话;将会导致变量b取值的变化;
# 在原来没有进行重排序的时候变量b原本正确赋有的值应该是1;
# 而当重新排序之后变量b的取值发生了改变,即变成了2;
# 同样是导致了数据的最终不正确性;
# 所以在这种情况下也是不能够进行重排序的;

编译器 和 处理器不会对 存在数据依赖关系的 操作 做重排序,
因为这种 重排序 会改变执行结果。

但是,如果操作之间 不存在数据依赖关系,这些操作就可能被 编译器和处理器 重排序。

(在有些时候是可以进行重排序的;只要没有影响到单线程执行运行结果的正确性;比如说下面这种情况就是可以进行重排序的;即int a =1;int b=2;这两句代码是可以进行互换位置的;彼此互不影响;即int b = 2; int a=1;但是int c =a +b;一定要处于最下面;否则不能得到变量a与变量b的取值;)

int a = 1;
int b = 2;
int c = a + b;

上面3个操作的数据依赖关系如图所示:

-------------------------------------------
                                  ---------
-------                            |     |
|  a  |---------------------------→|     |
-------                            |     |
                                   |  c  |
-------                            |     |
|  b  |---------------------------→|     |
-------                            |     |
                                  ---------
-------------------------------------------
/**
即 变量c 依赖 变量a与变量b 的取值;
但是 变量a 与 变量b 并没有直接的依赖关系;
所以这种情况下是可以进行重排序的;
*/

如上图所示 a和c 之间存在数据依赖关系,
同时 b和c 之间也存在数据依赖关系。
因此在最终执行的指令序列中,
c 不能被重排序到 a和b 的前面。
但 a和b 之间没有数据依赖关系,
编译器和处理器 可以重排序 a和b 之间的执行顺序。
下图是该程序的两种执行顺序。

---------------------------------------
-------         -------         -------
|  a  |--------→|  b  |--------→|  c  |
-------         -------         -------

-------         -------         -------
|  b  |--------→|  a  |--------→|  c  |
-------         -------         -------
----------------------------------------

可以这样:
int a = 1;
int b = 2;
int c = a + b;

也可可以重排序这样:
int b = 2;
int a = 1;
int c = a + b;

使用synchronized保证有序性

Test03Ordering.java

package com.xxx.concurrent_problem;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.results.I_Result;

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@OUtcome(id = "0", expect =  Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering{
  int num = 0;
  boolean ready = false;
  // 线程一 执行的代码
  @Actor
  public void actor1(I_Result r){
    if(ready){
      r.r1 = num + num;
    }else{
      r.r1 = 1;
    }
  }

  // 线程二 执行的代码
  @Actor
  public void actor2(I_Result r){
    num = 2;
    ready = true;
  }
}

回顾代码分析有序性问题产生的原因

@JCStressTest
@Outcome( id = {"1", "4"}, expect =  Expect.ACCEPTABLE, desc="ok")
@Outcome( id = "0", expect =  Expect.ACCEPTABLE_INTERESTING, desc="danger")
@State
public class Test03Ordering{

  int num = 0;
  boolean ready = false;

  // 线程一 执行的代码
  @Actor
  public void actor1(I_Result r){
    if(ready){
      r.r1 = num + num;
    }else{
      r.r1 = 1;
    }
  }

  //线程二 执行的代码
  @Actor
  public void actor2(I_Result r){
    num = 2;
    ready = true;
  }

  /**
  以前会观察得到4种可能性;3种结果为0,1,4
  分析回顾下:
  假设有两个线程在执行;分别是线程A与线程B;
  线程A执行actor1(I_Result r);线程B执行actor2(I_Result r);
  当初出现0的结果是这样来的:

  是actor2(I_Result r)当中的代码
  num = 2;
  ready = true;
  经过了重排序变成了
  ready = true;
  num = 2;
  的一个顺序;

  假设下面的这个线程,也就是执行actor2(I_Result r)的线程B先走;
  那么走了第一句也就是执行了第一句ready=true该句话;
  但是注意并没有执行num=2;
  所以此时共享变量当中的num仍然为0;而ready为true;
  然后此时CPU切换线程到了第二个线程当中来了;
  那么这个时候线程A进行执行actor1(I_Result r)方法;
  那么这个时候线程A就会进入到判断if(ready){}代码块中;
  因为此时获取得到的ready变量已经由线程B在actor2(I_Result r)中进行了修改取值;
  所以也就会得到 r.r1 = num + num;
  而此时的num并未被重新赋值也就是说此时的num依旧是取值为0;
  所以得到结果记录 r.r1 为0;
  那么这是之前出现的有序性问题分析;

  那么现在来进行添加synchronized来解决有序性问题;
  */
}

synchronized关键字解决有序性问题

@JCStressTest
@Outcome(id = {"1" , "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering{
  
  // 先搞一个对象作为对象锁
  private Object obj = new Object();

  int num = 0;
  boolean ready = true;

  // 线程一 执行的代码
  // 测试方法actor1(I_Result r)进行添加synchronized
  @Actor
  public void actor1(I_Result r){
    synchronized(obj){
       if(ready){
         r.r1 = num + num;
      }else{
         r.r1 = 1;
      }
    }
  }

  // 线程二 执行的代码
  // 下面的测试方法actor2(I_Result r)同样进行添加synchronized
  @Actor
  public void actor2(I_Result r){
    synchronized(obj){
     num = 2;
     ready = true;
    }
  }
}

压力测试指令以及结果查看

Terminal执行指令(进行多轮压力测试):

C:\Users\13666\IdeaProjects\XXX\Synchronized> mvn clean install
C:\Users\13666\IdeaProjects\XXX\Synchronized> java -jar target/jcstress.jar

测试结果打印:
------------------------------------------------------------------
    [OK] com.xxx.demo02_concurrent_problem.Test03Ordering
(ETA:  00:00:01) (Rate: 4.15E+07 samples/sec) (Tests: 1 of 1) (Forks: 4 of 4) (Iterations: 15 of 24; 15 passed, 0 failed, 0 soft errs, 0 hard....)

    [OK] com.xxx.demo02_concurrent_problem.Test03Ordering
(ETA:  00:00:01) (Rate: 3.97E+07 samples/sec) (Tests: 1 of 1) (Forks: 4 of 4) (Iterations: 15 of 24; 15 passed, 0 failed, 0 soft errs, 0 hard....)

    [OK] com.xxx.demo02_concurrent_problem.Test03Ordering
(ETA:  00:00:01) (Rate: 3.53E+07 samples/sec) (Tests: 1 of 1) (Forks: 4 of 4) (Iterations: 15 of 24; 15 passed, 0 failed, 0 soft errs, 0 hard....)

    [OK] com.xxx.demo02_concurrent_problem.Test03Ordering
(ETA:  00:00:01) (Rate: 3.83E+07 samples/sec) (Tests: 1 of 1) (Forks: 4 of 4) (Iterations: 15 of 24; 15 passed, 0 failed, 0 soft errs, 0 hard....)

.....
------------------------------------------------------------------

结果分析:
  发现并没有出现那几种结果出现的次数;
  说明这个代码就没有问题;即没有出现这种0的情况;
  如果真的想看的话;可以这么做;
  假设将取值为4也变为感兴趣的则重新来进行压力测试

@JCStressTest
@Outcome(id = "1", expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "4", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger2")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
.....

Terminal执行指令(进行多轮压力测试):

C:\Users\13666\IdeaProjects\XXX\Synchronized> mvn clean install
C:\Users\13666\IdeaProjects\XXX\Synchronized> java -jar target/jcstress.jar

测试结果打印:
      [OK] com.xxx.demo02_concurrent_problem.Test03Ordering
    (JVM args: [-XX:TieredStopAtLevel=1])
Observed state Occurrences         Expectation     Interpretation
             0       0      ACCEPTABLE_INTERESTING  danger
             1   5,427,079          ACCEPTABLE      ok
             4   5,672,602  ACCEPTABLE_INTERESTING  danger2


    [OK] com.xxx.demo02_concurrent_probleam.Test03Ordering
  (JVM args: [])
Observed state Occurrences         Expectation     Interpretation
             0       0      ACCEPTABLE_INTERESTING  danger
             1  42,176,771          ACCEPTABLE      ok
             4  26,991,710  ACCEPTABLE_INTERESTING  danger2
....
------------------------------------------------------------------

结果分析:
  此时可以看到的是出现0的此时已经为0了;
  即都不会再出现0了也就表示有序性问题得到了解决;

分析synchronized是如何进行解决有序性问题的?
当前假设actor2(I_Result r)当中的代码已经出现了重排序,
即
num = 2;
ready = true;
变为了
ready = true;
num = 2;
这样一个顺序;

其实会产生很多的线程来执行actor1(I_Result r)与actor2(I_Result r)方法;
那么这个时候举例各有一个线程A、B来分别进行执行actor1(I_Result r)与actor2(I_Result r);
那么还是一样假设下面的线程先走,即线程B先来执行代码块actor2(I_Result r)当中的同步代码块;
那么线程B就会拿着对象锁Object obj进入actor2(I_Result r)的同步代码块当中执行已经重排序过了的代码即;
ready = true;
num = 2;
那么拿着对象锁obj的线程B就先会去执行ready=true该句代码;
那么假设此时CPU又切换到另外一个线程上面去了,
注意是在线程B执行ready=true;之后但是并没有执行num=2该句代码之前进行了CPU切换;
也就是说当前的共享变量当中num取值仍然为0;但是ready的取值已经变为了true;
即切换到线程A上去执行actor1(I_Result r)方法当中的同步代码块了;
那么到actor1(I_Result r)当中来看,
线程A如果想要进入同步代码块则首先需要去获取拿到对象锁Object obj;
但是该对象锁Object obj现在仍然在线程B当中,
即线程B获取拿到对象锁obj之后还没有释放锁没有执行完成,
CPU就切换到了线程A上了,
导致线程A没有办法拿到获取对象锁Object obj,
从而只能够在同步代码块外侧进行等待;

等待CPU又切换到线程B上,让线程B执行完流程后释放锁;
这个时候CPU再切换到线程B上时,没有其它线程竞争的话,
那么这个时候线程B就能够获取得到对象锁从而进入同步代码块当中;

所以来看,即使actor2(I_Result r)当中的代码发生了重排序,也没有问题了;
接着CPU又切换到线程B上来执行actor2(I_Result r)没有执行完成的内容;
即执行num = 2; 那么这个时候此时的共享变量 num被更新赋值为2,以及ready取值为true;
最后线程B出同步代码块,出了同步代码块之后线程B才会将对象锁Object obj还回去。

假设线程B当前已经执行完成出了同步代码块也已经释放了锁,
此时CPU再次切换到线程A上,
那么这个时候线程A就可以能够获取得到对象锁Object obj了;
那么得到对象锁之后就可以进入到同步代码块中了;
通过判断ready取值进入if/else块;
由于共享变量ready以及num在线程B的actor2(I_Result r)中被进行了修改;
当前线程A读取到变量ready取值为true;num取值为2;
所以此时线程A就将会拿着对象锁Object obj 进入到if(ready)块当中去;
I_Result r.r1将会被重新赋值为 2+2=4;
所以此时的一个记录结果为4;
现在可以观察到的是:
尽管加了同步代码块synchronized,
但是actor2(I_Result r)当中依然会发生重排序;
但是发生了重排序也没有问题;
因为actor1(I_Result r)与actor2(I_Result r)都添加了同步代码块synchronized,
保证只有一个线程来进行执行;
就算actor2(I_Result r)中线程B执行到ready=true之后CPU又切换到线程A上面去了;那么线程A将由于无法获取得到对象锁Object obj也无法进入同步代码块当中去;由于线程B没有执行完成,即没有释放锁;所以线程A只能够进行等待;
那么也就相当于是单线程在执行这些代码;
那么重排序是能够保证单线程的执行效果没有问题;

synchronized 保证有序性的原理

synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

小结

synchronized保证有序性的原理:加synchronized,依然会发生重排序,只不过,存在有同步代码块,可以保证只有一个线程执行同步代码块当中的代码。
也就能保证有序性。

有序性除了可以使用synchronized来进行解决,还能够进行给 共享变量num以及ready变量进行添加volatile关键字来进行解决有序性问题。

/**
  添加了volatile关键字之后,
  可以保证共享变量num以及ready变量不会发生重排序;
  也就不会发生有序性问题了;
*/
public class Test03Ordering{
  private Object obj =  new Object();//对象锁

  volatile int num = 0;
  volatile boolean ready = false;

  // 线程一 执行的代码
  @Actor
  public void actor1(I_Result r){
    if(ready){
      r.r1 = num + num;
    }else{
      r.r1 = 1;
    }
  }


  // 线程二 执行的代码
  @Actor
  public void actor2(I_Result r){
    num = 2;
    ready = true;
  }
}

synchronized的可重入特性

第四章:synchronized的特性

synchronized属于同步锁机制,
第四章介绍synchronized作为锁的两个特性:
1、 可重入性(当一个线程执行到同步代码块,获取了某一个锁之后,还能够再次进入同步代码块当中,获取同样的一把锁,这个是可以的,可以重新再次进入,将分析其原理以及其可重入性的好处);
2、不可中断性(synchronized是不可中断的:当一个线程进入了同步代码块,那么另外一个线程只能够在外面进行等待,这个处于等待的线程会一直处于等待状态,不会中断,所以也就叫做不可中断;
另外还会通过ReentrantLock的代码来进行演示ReentrantLock是可以进行中断的)

可重入特性

synchronized作为锁,具有两个特性;
一个是 可重入性;一个是不可中断性;

目标

了解什么是可重入
了解可重入的原理

什么是可重入

指的是 同一个线程的 可以多次获得 同一把锁。
(一个线程可以多次执行synchronized,重复获取同一把锁;)

(当一个线程执行到同步代码块获取到某一个锁之后,还能再次进入同步代码块,获取同样的一把锁,这是可以的,可以重新再进入)

演示可重入特性

package com.xxx.demo03_synchronized_nature;

/*
  可重入特性
    指的是 同一个线程获得锁之后,可以直接再次获取该锁。
*/
  public class Demo01{
    public static void main(String[] args){
      Runnable sellTicket =  new Runnable(){
        @Override
        public void run(){
          synchronized(Demo01.class){
            System.out.println("我是run");
            test01();
          }
        }

        public void test01(){
          synchronized(Demo01.class){
            System.out.println("我是test01");
          }
        }
      };

      new Thread(sellTicket).start();
      new Thread(sellTicket).start();
    }
  }

演示

package com.xxx.demo03_synchronized_nature;

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

// 1. 自定义一个线程类
/**
  如果一个线程能够获取得到MyThread.class这把锁从而进入同步代码块1并打印;
  还依然能够拿着相同的一把锁再次进入同步代码块2,
  这就说明synchronized是可重入的;
*/
class MyThread extends Thread{
  @Override
  public void run(){
    synchronized(MyThread.class){
      System.out.println(getName() + "进入了同步代码块1");

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

结果并分析synchronized可重入性执行流程

结果:
Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2
-------------------------

先画一个红色的箭头代表主线程;
主线程从main()方法开始执行;
那么就会启动两个线程即new MyThread().start() * 2;
那么画两个箭头,一个绿色的箭头一个紫色的箭头即分别代表A线程与B线程;
那么这两个线程A、B会来进行执行MyThread当中实现的run()方法;
假设线程A先执行;
那么线程A会获取得到锁MyThread.class从而能够进入代码块当中;
那么这个锁对象MyThread.class当中存在一个计数器,
该锁对象MyThread.class当中的计数器会记录自己被获取了几次,
那么当前是线程A第一次获取得到该锁MyThread.class;即计数器取值加1;
那么线程A进入到同步代码块1当中即会进行打印 线程名+"进入了同步代码块1"信息;
接着该线程A继续往下进行执行发现又是一个同步代码块,且还是获取的一个相同的锁;那么这个时候;会将锁对象MyThread.class当中计数器的取值再次++,设置为2;
假设CPU此时依然在该线程A上,而线程A继续往下进行执行,那么此时就会进行打印 线程名++"进入了同步代码块2"信息;
那么也就可以观察得到同一个线程多次执行synchronized拿到同一把锁;
那么这个锁当中存在有一个计数器,这个计数器当中会进行记录自己被拿到了几次。

那么假设此时CPU又切换到了另外一个线程;即线程B;
那么此时线程B也会来进行执行MyThread当中实现的run()方法;
但是会发现MyThread.class该对象锁依然在线程A那里,即线程A还并没有进行释放锁CPU就切换到了线程B上;
所以也就导致了线程B无法获取得到锁从而也就无法进入同步代码块1中,只能够在同步代码块1的外侧进行等待;

那么这个时候CPU又切换回来到了线程A上;
那么线程A继续接着CPU切换之前的的那个地方往下进行执行;
即当时已经打印完成 线程名称+“进入了同步代码块2”信息了;
即线程A走到了同步代码块2的大括号处说明同步代码块2要结束了;
那么此时同步代码块2结束的时候就会将该同步代码块2的锁给释放掉;
也就意味着对象锁MyThread.class当中的计数器会进行减1操作;
即此时计数器取值由2变成了1;
那么接着继续往下走;那么线程A就又走到了同步代码块1的结束大括号上,也就意味着此时线程A要出同步代码块1,即又释放一次,即锁对象MyThread.class当中的计数器又会发生一次减1操作;即此时计数器的取值由1变为0;
计数器取值为0也就意味着当前没有线程来进行获取这把锁了;
也就是相当于线程A将锁还回去了;
那么此时其他线程就可以去进行竞争获取得到该锁进入同步代码块当中来;

其实也可以不用两个synchronized同步代码块嵌套;
可以放到两个不同的方法也可以完成;

class MyThread extends Thread{
  @Override
  public void run(){
    synchronized(MyThread.class){
      System.out.println(getName() + "进入了同步代码块1");

      test01();
    }
  }

  public void test01(){
    synchronized(MyThread.class){
         System.out.println(getName() + "进入了同步代码块2");
    }
  }
}
-----------------------------------------
// 再次运行,线程依旧可以重入拿到锁两次;
结果:
Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2
-----------------------------------------

另外不仅是放到同一个类的方法可以实现,放到不同类的方法也可以实现;

package com.xxx.demo03_synchronized_nature;

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

    public static void test01(){
      synchronized(MyThread.class){
          String name = Thread.currentThread.getName();
          System.out.println(name + "进入了同步代码块2");
      }
  }
}

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

      Demo01.test01();
    }
  }
}
-----------------------------------------
// 再次运行,线程依旧可以重入拿到锁两次;
结果:
Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2
-----------------------------------------

由此说明了synchronized的可重入特性与调用哪一个对象的哪一个方法无关;
主要是看线程,看锁;

可重入原理

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

可重入的好处

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

依然是上述存在有两个同步代码块嵌套的代码;
即如果一个线程A进入了一个同步代码块当中去了;
那么假设synchronized没有可重入特性;
那么就会导致无法再次进入同步代码块当中;
那么这个时候就会卡在一个地方;
这个地方即为进入到synchronized同步代码块1之后,
打印执行完成 线程名+“进入了同步代码块1”之后;
没有办法进入下一句代码,即下一个同步代码块2synchronized中;
从而无法结束该流程也没有办法释放锁;
而其他线程也由于无法获取得到锁从而无法进入同步代码块1只能够在同步代码块1外层进行等待;
也就造成了死锁的状态;就相当于是线程A被困在同步代码块1当中了;
那么有了synchronized的可重入特性就可以避免死锁;

因为可以在同步代码块1当中调用其他方法,即比如说上述代码中的Demo01.test01();而Demo01.test01()中也存在有同步代码块,那么也就方便了使用方法来进行封装代码;

小结

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

什么是可重入

指的是 同一个线程的 可以 多次 获得 同一把锁

可重入的原理

加锁次数计数器。(recursions变量)

synchronized的不可中断性

目标

了解什么是不可中断
学习synchronized不可中断特性
学习Lock可中断特性

什么是不可中断

一旦这个锁 被别人 获得了,如果里另一个锁想获得锁,只能等待或者阻塞,直到别的线程释放这个锁,如果别人永远不释放锁,这个线程只能永远等下去,很执着!
(一个线程获得锁后,另一个线程想要锁,必须处于阻塞或等待状态;如果第一个线程不释放锁,第二个线程会一直处于阻塞或等待状态,在阻塞或者等待过程中,不可被中断,将一直等待或阻塞;)

synchronized 不可中断演示

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

(当一个线程进入一个同步代码块,那么另外一个线程只能在外面等待,这个处于等待的线程将会一直等待,不会中断,所以就叫做不可中断)

public class Demo02_Uninterruptible{
  private static final Object o1 = new Object();

  public static void main(String[] args) throws InterruptedException{
    Runnable runnable =  () - {
      synchronized( 01 ){
        String name = Thread.currentThread().getName();
        try{
            System.out.println(name + "start");
        }catch(InterruptedException e){
            System.out.println(name + "interrupted");
            e.printStackTrace();
        }
      }
    };

    Thread t1 = new Thread(runnable);
    Thread t2 = new Thread(runnable);
  }
}

演示synchronized的不可中断特性

--------------------------------------------------
package com.xxx.demo03_synchronized_nature;

/**
  目标:演示synchronized不可中断
    1. 定义一个Runnable
    2. 在Runnable定义同步代码块
    3. 先开启一个线程来执行同步代码块,保证不退出同步代码块
    4. 后开启一个线程来执行同步代码块(阻塞状态)
    5. 停止第二个线程
*/
public class Demo02_Uninterruptible{

  private static Object obj = new Object();//定义锁对象

  public static void main(String[] args){
      // 1. 定义一个Runnable
      Runnable run = ()->{
        // 2. 在Runnable定义同步代码块;
        // 同步代码块需要一个锁对象;
        synchronized(obj){

          // 进行打印是哪一个线程进入的同步代码块
          String name = Thread.currentThread().getName();
          System.out.println(name + "进入同步代码块");

          // 需要进行保证不进行退出同步代码块;
          // 所以让此线程进行沉睡sleep
          Thread.sleep(888888);
        }
      };


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

      // 沉睡一秒钟;
      // 保证第一个线程先去执行同步代码块之后再来创建第二个线程;
      Thread.sleep(1000);

      /**
      4. 后开启一个线程来执行同步代码块(阻塞状态)
      到时候第二个线程去执行同步代码块的时候,
      锁已经被t1线程锁获取得到了;
      所以线程t2是无法获取得到Object obj对象锁的;
      那么也就将会在同步代码块外处于阻塞状态。
      */
      Thread t2 = new Thread(run);
      t2.start();


      /** 5. 停止第二个线程;
      观察此线程t2能够被中断;
      */
      System.out.println("停止线程前");
      t2.interrupt();
      System.out.println("停止线程后");

      // 最后得到两个线程的执行状态
      System.out.println(t1.getState());
      System.out.println(t2.getState());
  }
}
--------------------------------------------------
运行效果:
(Run显示红灯即没有停止运行依然在继续)
Thread-0进入同步代码块
停止线程前
停止线程后
TIMED_WAITING
BLOCKED
--------------------------------------------------

通过interrupt()方法给t2线程进行强行中断;
最后进行打印t2的状态及State发现状态依然为BLOCKED;
即线程不可中断;

--------------------------------------------------

synchronized的不可中断性具体分析流程
红色的箭头表示主线程;主线程从main()方法开始执行;
然后执行到步骤3(3. 先开启一个线程来执行同步代码块)启动第一个线程t1;
那么再画一个箭头代表启动的这第一个线程t1,用线程A表示;
那么线程A启动之后就会去执行Runnable run中的所重写的run方法;
那么线程A第一次执行同步代码块;获取得到对象锁Object obj;
然后进入同步代码块中打印 线程名称+“进入同步代码块”;
继续往下进行执行就会使得线程A进行入沉睡状态;
那么此时CPU再切换到另外一个线程;即主线程继续往下执行;
当主线程沉睡一秒之后,接下来又会启动一个线程即t2,用线程B表示;
线程B启动之后也会来进行执行Runnable当中重写的run()方法;
但是由于在CPU切换之前线程A并没有释放锁;
即第一个线程,即线程A由于沉睡时间过长;没有办法释放锁;
所以导致线程B无法获取得到锁;
从而只能在同步代码块外侧进行等待锁,处于阻塞状态;
那么此时CPU再次切换到主线程,主线程继续往下进行执行;
进行打印输出“停止线程前”;后继续往下执行;
准备强行将等待锁对象的线程B进行停止掉,即使用了interrupt()方法(但是停不掉;就是因为synchronized是不可中断的;这个处于阻塞等待的线程是无法被中断的;这个线程会一直处于等待锁状态即不可被中断);
主线程继续往下执行输出打印“停止线程后”;
最后打印线程A与线程B这两个线程的状态;
线程A由于执行了Thread.sleep(888888);所以导致线程A的状态处于TIMED_WAITING;
线程B由于synchronized的不可中断性所以一直在同步代码块外侧进行等待获取锁处于等待阻塞状态,所以其状态为BLOCKED;不可被中断;

至此,可以观察到的是synchronized是不可被中断的;
它会导致没有获取得到锁的线程一直在同步代码块外侧一直处于一个等待阻塞获取锁的一个状态;

ReentrantLock可中断演示

ReentrantLock可中断

package com.xxx.demo03_synchronized_nature;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo03_INterruptible{
    private static final Lock o1 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException{
      Runnable runnable = () -> {
        String name =  Thread.currentThread().getName();
        boolean isLock = false;
        try{
            isLock = o1.tryLock( 3, TimeUnit.SECONDS);
            if(isLock){
              System.out.println(name + "lock");
              Thread.sleep(1000000);
            }
        }catch(InterruptedException e){
          System.out.println(name + "interrupted");
        }finally{
            if(isLock){
              o1.unlock();
              System.out.println(name + "unlock");
            }else{
              System.out.println(name + "指定时间内没有得到锁,中断,不等了,可以接着做其他事情。");
            }
        }
      };
    }
}

演示 ReentrantLock 的不可中断特性

package com.xxx.demo03_synchronized_nature;

/**
  目标:
    演示 Lock不可中断特性
    Lock具有两种特性(一种是可中断;另外一种是不可中断;)
*/
public class Demo03_Interruptible{

  // 在类的成员变量位置来进行创建一个Lock对象
  private static Lock lock = new ReentrantLock();

  public static void main(String[] args)throws InterruptedException{
    test01();
  }

  // 演示 Lock 不可中断
  public static void test01(){
    Runnable run = ()->{
      /**
        这种方式是属于不可中断的;
        可以看到的是:lock() void
        即该方法是没有返回值的;
        那么这种情况就属于不可中断;
        它也会一直等待锁;
        另外获取锁之后一定要记得unlock();
        那么将unlock()操作放入到finally块当中进行执行;
      */
      String name =Thread.currentThread().getName();

      try{
        lock.lock();
        //获取线程的名字
        //打印当前进入run()中的线程名称
        System.out.println(name + "获得锁,进入锁执行");
        //加一个睡眠保证该线程一直在里面进行执行不退出
        Thread.sleep(888888);

      }catch(InterruptedException e){
          e.printStackTrace();
      }finally{
        //同样unlock也是没有返回值的;void
        lock.unlock();
        System.out.println(name + "释放锁");
      }
    };

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

    //让主线程沉睡一秒;
    //让第一个线程t1启动之后去进行执行Runnable当中的所实现的run()
    Thread.sleep(1000);

    //继续创建第二个线程
    Thread t2 = new Thread(run);
    t2.start();

    //终止一下t2线程看是否能够进行终止;
    System.out.println("终止t2线程前");
    t2.interrupt();
    System.out.println("终止t2线程后");
  
    //让主线程沉睡一秒
    Thread.sleep(1000);

    //获取两个线程的状态
    //第二个线程t2是后来的即在第一个线程t1创建并启动之后以及主线程沉睡了1秒之后才创建的第二个线程t2;
    //即第一个线程t1已经获取得到锁了;
    //由于第一个线程t1在Runnable所实现的run()方法当中进行了长时间的沉睡又没有办法释放锁;
    //所导致第二个线程,即线程t2只能够在同步代码块外进行阻塞等待获取锁;
    //看第二个线程t2是否能够被中断以及观察这两个线程的状态;
    System.out.println(t1.getState());
    System.out.println(t2.getState());
  }
}
--------------------------------------------------
运行效果:
(Run显示红灯即没有停止运行依然在继续)
Thread-0进入同步代码块
停止t2线程前
停止t2线程后
TIMED_WAITING
WAITING
--------------------------------------------------
那么通过运行结果可以看出t2.interrupt()是没有执行成功的;
因为t2进行打印其线程状态发现依旧处于等待状态即WAITING;
也就是说t2线程没有抢到锁所以导致一直处于等待状态;

分析:
主线程从main()方法开始进行执行;
即执行test01()方法;
创建一个Runnable,Runnable当中实现run()方法;
创建并启动一个线程t1;去进行执行Runnable run当中的run()方法;
即线程t1执行run()当中的内容;
此时还没有其他线程获取得到lock.lock()当中的锁;
所以线程t1此时可以获取得到锁;
即进入同步代码块中进行打印 "Thread-0获得锁,进入锁执行"后进入线程t1长时间睡眠且并未释放锁;
那么此时CPU切换到主线程,主线程继续向下执行;
主线程沉睡一秒之后;又进行创建并启动第二个线程t2;
那么第二个线程t2也会去进行执行Runnable run当中的run()方法;
但是此时第二个线程t2是无法得到锁的;
由于线程t1并未释放锁而是出于长时间的睡眠状态;
所以导致线程t2无法获取得到锁从而导致在同步代码块外侧进行等待获取锁;
所以线程t2会处于一种叫做WAITING的状态;
那么这个时候CPU又切换到主线程当中来;
则主线程继续往下进行执行准备去强行停止线程t2但是停不掉
(lock.lock()也是属于不可中断的;);
打印“停止t2线程前”以及继续往下执行对线程t2进行中断以及打印“停止t2线程后”;

继续向下执行主线程又一次进行沉睡一秒后;
打印线程t1与线程t2的状态;
那么这个时候就会发现线程t2处于一种WAITING的状态;
即也说明了ReentrantLock.lock()是不可被中断的;
即lock.lock()方法则导致在同步代码块外侧的线程状态是不可中断的;
但是ReentrantLock还有一个方法叫做tryLock();

演示 ReentrantLock的可中断特性

package com.xxx.demo03_synchronized_nature;

import java.util.concurrent.locks.ReentrantLock;

/**
  目标:演示Lock的可中断特性
*/
public class Demo03_Interruptible{
  private static Lock lock = new ReentrantLock();

  public static void main(String[] args)throws InterruptedException{
    test02();
  }

  // 演示 Lock 可中断
  public static void test02() throws InterruptedException{
    Runnable run = ()->{
      String name = Thread.currentThread().getName();
      boolean b = false;
      try{
        /**
        tryLock(long time, TimeUnit unit) 返回boolean类型
        tryLock会进行尝试获取锁,
        如果能够获得锁则返回true;
        反之不能获得锁则返回false;

        到时候如果是第二个线程要来进行获取锁来进行抢锁;
        那么这个时候如果三秒内没有获取得到锁;
        那么就会进行中断;从而进入else块中做其他操作;
        */
        //尝试3秒,时间单位为秒;
        //由于单位是秒所以直接输入3即可而不是3000
        //b = lock.tryLock(3000, TimeUnit.SECONDS);
        b = lock.tryLock(3, TimeUnit.SECONDS);

        //说明尝试获取得到了锁;则进入if块当中
        if(b){
          System.out.println(name + "获得锁,进入锁执行");
          Thread.sleep(888888);
        }else{
          //那么else则说明没有获取得到锁;
          //则可以让其做其他操作从而也就证明了Lock.tryLock()是可中断的;
          System.out.println(name + "在指定时间内没有获取得到锁则做其他操作");
        }
      }catch(InterruptedException e){
        e.printStackTrace();
      }finally{

        /**
        此时的unlock()需要进行注意;
        如果说CPU当前切换到的当前线程没有获取得到锁则并不需要进行unlock()操作;
        如果CPU当前切换到的当前线程获取得到了锁并执行完成同步代码块当中的内容所以此时finally当中是需要进行执行unlock()操作的;
        */
        if(b){//得到了锁才进行释放锁操作
            lock.unlock();
            System.out.println(name + "释放锁");
        }
      }
    };

    //在后续的代码中让两个线程进行启动起来即可其余代码可以不要;
    Thread t1 = new Thread(run);
    t1.start();
    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());
*/
  }
}
--------------------------------------------------------
代码执行效果:
Thread-0获得锁,进入锁执行
Thread-1在指定时间没有得到锁做其他操作
--------------------------------------------------------
详细分析一下ReentrantLock可中断性流程:
先画一个红色的箭头来表示主线程;然后来分析整个流程;
程序由main()方法开始进行执行;然后执行test02()方法;
创建一个Runnable并实现其run()方法;
创建Runnable完成之后;创建第一个线程t1并启动去执行Runnable当中的run()方法即执行run()方法内部的内容;
而第一个线程t1是可以进行获取得到锁的;即lock.tryLock(3,TimeUnit.SECONDS)时;则进入代码块if(b)当中并打印第一句话“Thread-0获得锁,进入锁执行”;那么接着线程t1就一直睡眠了Thread.sleep(888888);
那么此时CPU又切换会主线程;接着往下走沉睡一秒之后进行创建t2线程并启动t2线程;
那么线程t2此时也会来进行执行Runnable当中的run()方法;
那么第二个线程t2进来时执行lock.tryLock时会发现锁已经被t1线程获取得到了;且t1线程并没有释放锁而且处于长时间的睡眠状态;
那么这个时候t2线程会进行尝试获取锁3秒;
3秒过后如果依旧无法获取得到锁(即线程t1三秒后依旧没有进行释放锁)那么线程t2则将中断且不会继续等待获取锁,
即tryLock则返回false;
返回false则将会执行else块当中的代码即打印输出“Thread-1在指定时间没有得到锁做其他操作”做其他操作去了;
那么此时可以看到的是 tryLock的话则会等待指定的时间,
在指定的时间内去尝试获取锁;
也就是说在指定的时间内等待上一个获取锁的线程进行释放锁;
如果说在指定的时间内上一个获取得到锁的线程执行完成并释放了锁那么则当前该线程就在指定的时间结束后会返回true表示获取得到锁;
如果上一个获取得到锁的线程在指定的时间内并没有释放锁那么则当前等待的线程在指定等待的时间结束后;则进行中断不会再去等待获取锁;
直接返回false表示没有获取得到锁,该为去做其他的操作;

小结

不可中断是指:当一个线程获得锁之后,另一个线程一致处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或者等待,并且在阻塞或等待的过程中它是不可被中断的,它会一直等待或者被阻塞;

synchronized是属于不可被中断的;
Lock(ReentrantLock)lock方法是不可被中断的;
Lock(ReentrantLock)tryLock方法是可中断的;

javap反汇编学习synchronized的原理

第五章:synchronized原理

(较难的一块,由浅入深,涉及synchronized的原理讲解;)

  • 首先会通过javap反汇编的方式,
    synchronized其实被转换成了两条字节码指令
    分别是monitorenter以及monitorexit
    然后会通过Oracle的官方文档即JVM规范来对字节码指令 monitorenter以及monitorexit的一个简单的介绍,
    介绍这两个指令时如何来进行加锁和解锁的,
    通过字节码指令来介绍synchronized属于比较深入,
    但是有并不是特别的深入,
    那么为了更加深层次的理解synchronized的底层机制
    将会深入JVM的源码来进行源码分析。
  • JVM底层是使用c、c++所编写的;
    synchronized是一个
    关键字

    底层c、c++来编写。
    会进行分析这一块的代码;
    到时候就可以清晰的知道
    synchronized
    的一个清晰的底层结构以及如何获得锁等待锁以及如何释放锁的;
  • 另外还会去介绍synchronized为什么是一个重量级的锁
    以及synchronized为什么会开销比较大
    因为synchronized是一个重量级的锁
    所以其效率不高
    那么在第六章就会来进行介绍JDK6当中对synchronized的一个优化措施synchronized的涉及到一个叫做CAS操作
    那么就会先去进行介绍CAS的一个原理
    那么其实CAS也属于一个原子操作
    可以将CAS操作看做是一个轻量级的synchronized
    它能够保证变量修改的这样一个原子操作
    介绍了CAS之后就将会介绍锁升级的一个过程
    锁升级是由 无锁-→偏向锁-→轻量级锁-→重量级锁
  • 由浅入深的来进行学习,那么存在这么多锁就会存在一个问题;
    如何来得知是属于哪一种锁
    那么这个时候会先进行了解JAVA对象的布局
    以前对java对象的理解是这样的-→java对象是存在在堆中,然后有一块空间可以来进行存放其对象当中的成员变量;
    那么介绍的java对象布局,其当中不仅会有java对象的实例数据还会有对象头以及一些对齐数据
    那么锁升级过程中的这些存在于对象头当中的Mark Word当中的,
    到时候通过c++的源码来进行详细分析;
  • 另外JDK6还对synchronized做了一些锁消除和锁粗化的优化操作,
    经过对synchronized的原理的学习之后就可以总结出写代码应该如何对synchronized优化,写出更高效的代码;
    那么会从以下几个方面进行来介绍;
    第一个:减少synchronized的范围
    第二个:降低synchronized锁的粒度
    (这当中有一个经典的例子就是HashTableHashTable住所有的数据;而后又推出了一个叫做ConcurrentHashMap,那么ConcurrentHashMap 一个桶当中的数据);
    第三个:通过读写分离的方式来进行提高效率
    以上就是synchronized课程的主要内容;

javap 反汇编

synchronized原理的学习;
首先通过javap反汇编的方式来进行学习synchronized原理;
接着再通过JVM源码再来深入的学习synchronized原理;

目标

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

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

package com.xxx.demo04_synchronized_monitor;

public class Demo01{
  //依赖的锁对象
  private static Object obj = new Object();

  @Override
  public void run(){
    for(int i = 0; i < 1000; i++){

      //synchronized同步代码块;且在代码块当中做了简单的打印操作;
      //重点是看synchronized在反汇编之后形成的字节码指令
      synchronized( obj ){
        System.out.println("1");
      }
    }
  }

  //编写了一个synchronized修饰的方法
  //synchronized修饰代码块与synchronized修饰方法反汇编之后的结果是不太一样的;
  public synchronized void test(){
    System.out.println("a");
  }
};
  //代码写好之后让idea进行一个编译得到字节码文件;
  //编译好的字节码文件目录:工程名/target/classes/xxx/demo04_synchronized_monitor/Demo01.class

要看 synchronized的原理,但是 synchronized是一个关键字,看不到源码。
可以将class文件进行反汇编。
JDK自带的一个工具: javap,对字节码进行反汇编,查看字节码指令。
在DOS命令行或者是Windows PowerShell等其他命令行输入(对字节码文件进行反汇编):

PS C:\Users\13666\IdeaProjects\Xxx\Synchronized\target\classes\com\xxx\demo04_synchronized_monitor\javap -p -v Demo01.class
# -p 是显示包括所有的私有的
# -v 就是详细的来进行显示

反汇编之后得到的字节码指令如下所示:

      LineNumberTable:
          line 3: 0
      LocalVariableTable:
          Start     Length      Slot     Name     Signature
              0          5         0     this      Lcom/xxx/demo04_synchronized_monitor/Demo01;
  
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2,  locals=3;   args_size=1
          0: getstatic           #2        // Field obj:Ljava/lang/Object;
          3: dup
          4: astore_1
          5: monitorenter
          6: getstatic           #3        // Field java/lang/System.out:Ljava/io/PrintStream;
          9: 1dc                 #4        // String 1
         11: invokevirtual       #5        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         14: aload_1
         15: monitorexit
         16: goto                24
         19: astore_2
         20: aload_1
         21: monitorexit
         22: aload_2
         23: athrow
         24: return
      Exception table:
          from     to    target   type
             6      16       19    any
            19      22       19    any
      LineNumberTable:
        line 7: 0
        line 8: 6
        line 9: 14
        line 10: 24
      LocalVariableTable:
        Start      Length     Slot   Name    Signature
            0          25        0   args    [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
......

反汇编后的效果如下:

class com.xxx.demo04_synchronized_monitor.Increment implements java.lang.Runnable{
  public static int number;

  private static java.lang.Object obj;

  com.xxx.demo04_synchronized_monitor.Increment();
    Code:
      0 : aload_0
      1 : invokespecial   #1       // Method java/lang/Object."<init>":()v
      4 : return

  public void run();
    Code:
      0 : iconst_0
      1 : istore_1
      2 : iload_1
      3 : sipush    1000
      6 : if_icmpge 39
      9 : getstatic #2           // Field obj:Ljava/lang/Object
      12: dup
      13: astore_2
      14: monitorenter
      15: getstatic #3           // Field number:I
      18: iconst_1
      19: iadd
      20: putstatic #3           // Field number:I
      23: aload_2
      24: monitorexit
      25: goto      33
      28: astore_3
      29: aload_2
      30: monitorexit
      31: aload_3
      32: athrow
      33: iinc      1,  1
      36: goto      2
      39: return
   Exception table:
        from   to  target type
          15   25    29    any
          28   31    28    any
  

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
        stack=2,  locals=1,  args_size=1
            0: getstatic     #4              // Field
java/lang/System.out:Ljava/io/PrintStream:
            3: ldc           #5              // String a
            5: invokevirtual #6              // Method java/io/PrintStream.println:(Ljava/lang.String;)V
            8: return
        LineNumberTable:
          line 16: 0
          line 17: 8
        LocalVariableTable:
          Start     Length    Slot  Name  Signature
            0          9       0     this   Lcom/xxx/demo04_synchronized_monitor/Increment;

  static {};
    Code:
       0: iconst_0
       1: putstatic       #3       // Field number:I
       4: new             #4       // class java/lang/Object
       7: dup
       8: invokespecial   #1       // Method java/lang/Object."<init>":()V
      11: putstatic       #2       // Field obj:Ljava/lang/Object
      14: return
}

主要是看观看这一块的字节码指令与Demo01.java当中的代码做对比分析:

-----------------------------------------------------------------
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2,  locals=3;   args_size=1
          0: getstatic           #2        // Field obj:Ljava/lang/Object;
          3: dup
          4: astore_1
          5: monitorenter
          6: getstatic           #3        // Field java/lang/System.out:Ljava/io/PrintStream;
          9: 1dc                 #4        // String 1
         11: invokevirtual       #5        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         14: aload_1
         15: monitorexit
         16: goto                24
         19: astore_2
         20: aload_1
         21: monitorexit
         22: aload_2
         23: athrow
         24: return
      Exception table:
          from     to    target   type
             6      16       19    any
            19      22       19    any
-----------------------------------------------------------------
首先0: getstatic 代表的是获取得到静态的成员变量Object obj的值;
即private static Object obj = new Object();

同步代码块开始的地方即synchronized(obj){ 对应于字节码指令当中的5: monitorenter指令;
那么接着往下字节码指令当中的6: getstatic 其实对应着java代码层中的System.out.println("1");该句中的out变量;到时候进行执行其实是进行执行的println()方法;
那么结束的时候就需要注意了,同步代码块synchronized结束的地方也就是synchronized(obj){ 的返回花括号}处,即对应着字节码指令当中的15: monitorexit
那么字节码指令monitorenter与monitorexit这两个字节码指令分别有什么含义呢?

monitorenter

首先来看一下JVM规范中对于 monitorenter 和 monitorexit 的描述:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

(Oracle官方 java虚拟机规范文档;Java虚拟机有好多种Oracle公司有,IBM公司也有,淘宝也有;java虚拟机是一套规范;该文档指的就是java虚拟机的规范)

Each object is associlated 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关联。
监视器被占用时会被锁住,其他线程无法来获取该monitor。
(其实可以理解为这个monitor才是真正的锁)
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。
(即尝试去获取这把锁;有可能获取得到有可能获取不到)
其过程如下:

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

monitorenter线程获取锁,宁进入同步代码块。
同时只能有一个线程获取锁。
monitorenter 插入在同步代码块的开始位置,当底代码执行到该指令时,将会尝试获取该对象monitor的所有权,即尝试获得该对象的锁。

monitorenter流程分析

分析monitor流程画图说明:
通过刚才的描述可以知道synchronized是需要传一个对象进来也就是对象锁;
即synchronized(obj){;
而真正的锁并非是这个传入进来的该对象obj;
而是该对象obj会去关联一个叫做monitor的东西,而这个叫做monitor的东西才是真正的锁;
而这个对象monitor并非是手动式使用代码进行创建的;
当拿到一个对象放到同步代码块的参数当中来;
即synchronized(obj){中来的时候,JVM会去进行检查该对象obj是否有进行关联monitor对象;
如果JVM进行检测得到该对象obj没有进行关联monitor对象的话那么就会去创建一个与之关联的monitor对象;
而且该monitor对象还需要进行注意的是monitor并不是一个java对象;
monitor而是一个C++对象;
在monitor对象当中又这么几个内容是需要注意的;
monitor当中有两个比较重要的成员变量;
一个是owner:指的是用有锁的线程;
另一个是recursions:指的是记录获取锁的次数;
当JVM执行到monitorenter的时候;那么会找到该对象Object obj上的monitor对象看这个Object obj所关联的monitor锁对象是否被别的线程所拿走了;如果别的线程没有拿走即没有竞争走;那么当前该线程就来获取该monitor锁对象;
首先先会将monitor锁对象中的成员变量owner变成当前线程;
现在假设t1线程来执行到同步代码块synchronized(obj){;
那么当t1线程来执行同步代码块的时候就会找到该Object obj对象所关联的monitor对象;看这个所关联的monitor有没有被其他的锁给获取得到;
如何查看该monitor是否有被其他线程获取得到?
如果别的线程没有竞争得到该monitor锁对象,那么该锁对象monitor的owner成员变量属性就会变成当前线程t1;
那么这也是t1线程第一次进入同步代码块当中来;
所以monitor其成员变量recursions(记录获取锁的次数,计数器)也会进行改变取值进行++操作;即当前由0变为1;
接着t1线程就拥有了这把锁,即Object obj所关联的锁对象monitor从而进入了同步代码块synchronized(obj){}当中;
在t1进入到同步代码块中时如果同步代码块当中依然存在有同步代码块,即嵌套的同步代码块时并且锁对象依然还是Object obj的话则;synchronized具有可重入特性;那么这个时候t1线程就会重入到嵌套同步代码块中去;重入的话就会将Object obj所关联的monitor锁对象的成员变量属性取值recursions计数器的取值进行++操作;即由次数1变为次数2;那么也就是说出一次同步代码块计数器recursions进行--操作即次数减一操作,由2变为1;这种类似;
另外当t1线程进入同步代码块时,并执行到输出打印语句“1”时;
此时CPU切换到了t2线程上;那么t2线程同时会来进行执行Runnable当中所实现的run()方法内容;
那么t2线程也会来进行竞争获取得到这把锁;即Object obj所关联的锁对象monitor;
那么此时由于线程t1并没有进行释放锁操作,CPU就开始进行切换到了线程t2上来了;
那么这个时候t2线程就会发现Object obj所关联的monitor锁对象当中的成员变量属性取值为并不是当前线程t2而是线程t1;
那么通过这个观察也就知道当前获取锁的线程是哪一个线程了;
那么此时t2线程就会进入阻塞状态;
那么这个就是monitorenter的原理;
monitorenter小结

synchronized的锁对象会关联一个monitor,
这个monitor不是主动进行创建的,
而是JVM的线程执行到这个同步代码块时,
会检查发现到对象没有monitor,那么此时就会创建monitor;
monitor内部有两个重要的成员变量;
owner: 拥有这把锁的线程;
recursions: 会记录线程拥有锁的次数;
当一个线程拥有monitor之后其他的线程只能够进行等待;

monitorexit

首先来看一下 JVM规范 中对于 monitorenter和monitorexit的描述:

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 ny objectref.
The thread decrements the entry count of the monitor associated with objectref.
iIf 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插入在方法结束处和异常处;
JVM保证每个 monitorenter必须有对应的monitorexit;

monitorexit流程分析

假设线程t1获取拿到Object obj所关联的对象锁monitor之后从而进入到同步代码块当中走到要结束同步代码块的位置,即synchronized(obj){所对应返回}花括号的位置处,即将要释放锁的位置;
t1线程执行完同步代码块之后就会执行字节码指令15: monitorexit 指令;
遇到这个指令之后,就会去找到这个锁对象Object obj真正的锁对象monitor;
找到之后会进行对monitor的成员变量的属性取值owner以及recursions进行更改赋值;
那么此时线程t1仍然在拥有着这把锁;所以此时monitor的owner依然是当前线程t1,不需要改变;
而线程t2即将要出同步代码块,则recursions计数器就需要进行减一操作;即赋值为0,由1变为0;
当monitor锁对象的成员变量recursions计数器取值变为0时也就代表着当前线程t1释放了当前其所拥有的这一把锁;
那么同时monitor锁对象的成员变量owner属性取值即锁的拥有者也不存在了;

另外还需要注意一个问题;
进入同步代码块时:synchronized(obj){ ------→ 字节码指令5: monitorenter
出同步代码块时:}  ------→ 字节码指令15: monitorexit
除了这两处地方存在有monitor相关的指令之外其余的地方也存在有monitor相关字节码指令操作;
即 21: monitorexit(为什么此处也存在有monitor相关字节码指令操作呢?)

字节码指令最下面的Exception table称作为异常表;即:
-----------------------------------------------------------
      Exception table:
          from     to    target   type
             6      16       19    any
            19      22       19    any
-----------------------------------------------------------
from ... to : 指的是从哪一行到哪一行;即指的是6~16行或者19~22行之间的字节码指令;
-----------------------------------------------------------
          6: getstatic           #3        // Field java/lang/System.out:Ljava/io/PrintStream;
          9: 1dc                 #4        // String 1
         11: invokevirtual       #5        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         14: aload_1
         15: monitorexit
         16: goto                24
-----------------------------------------------------------
也就是所对应在java代码当中的:
-----------------------------------------------------------
synchronized(obj){
  System.out.println("1");
}
-----------------------------------------------------------
也就意味着如果同步代码块当中的代码当中出现了异常;
那么也就是指的字节码指令6~16行出现了异常则target为19;
那么也就会走字节码指令编号为19的字节码指令代码:即19: astore_2
那么执行完字节码指令19之后继续向下执行字节码指令20: aload_1、21: monitorexit;
也就说明了其最终也会去进行释放锁;
这也就是在告知:如果在同步代码块当中出现了异常;monitor会自动帮助释放锁即monitorexit字节码指令;

面试题:synchronized出现异常会释放锁吗?

答:会释放锁;

同步方法

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

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

查看反汇编之后的字节码指令-同步方法

----------------------------------------------------------------------
public synchronized void test();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2,    locals=1,     args_size=1
        0: getstatic         #3        // Field java/lang/System.out:Ljava/io/PrintStream;
        3: 1dc               #6        // String a
        5: invokevirtual     #5        // Method java/io/PrintStream.println(Ljava/lang/String;)V
    LineNUmberTable:
      line 13: 0
      line 14: 0
    LocalVariableTalbe:
      Start       Length       Slot     Name     Signature
          0            9          0     this     Lcom/xxx/demo04_synchronized_monitor/Demo01;
......
----------------------------------------------------------------------
对应的代码:
public synchronized void test(){
  System.out.println("a");
}
----------------------------------------------------------------------
同步方法test()内部并没有形成monitorenter以及monitorexit;
只是对该方法做了一个标识;即观察flags:标识;
ACC_SYNCHRONIZED:那么该标识的作用即在于(在JVM规范当中也有相关说明)同步方法会被JVM隐式地调用monitorenter和monitorexit;
也就是说java虚拟机的线程执行到同步方法的时候它会自动的去调用字节码指令monitorenter;
当执行完同步方法之后也会自动的去调用字节码指令monitorexit;
即所谓隐式调用;

小结

通过 javap反汇编,看到synchronized使用变成了monitorenter和monitorexit两个字节码指令;真正的锁是monitor;每个锁对象都会关联一个monitor(监视器,monitor才是真正的锁对象),monitor内部有两个重要的成员变量owner(owner:会保存获得锁的线程)和recursions(会保存线程获得锁的次数);
执行monitorenter那么线程就会来进行竞争monitor这把锁;
抢到monitor这把锁之后;
就会将monitor当中的成员变量owner的取值改为当前抢到锁的该线程;
以及拥有锁的次数recursions变为1;
如果再次进入同步代码块即嵌套同步代码块也是同样的这一把锁;
那么(可重入特性)重入即monitor的成员变量recursions的取值就会进行加一操作;
当执行到monitorexit时,那么monitor的成员变量recursions计数器就会进行减一操作;
当monitor的计数器recursions减到0时,那么当前拥有该锁monitor的现场称就会去进行释放锁;

面试题:synchronized与Lock的区别

1、 synchronized是关键字,而Lock是一个接口(ReentrantLock为其实现类)。

synchronized是JDK提供的一个关键字;无法查看得到其源码;
synchronized由JVM直接来支持的;
而前面所使用到的Lock实际上是一个接口;

/**
  @since 1.5            //从jdk1.5之后有的
  @author Doug Lea
*/
public interface Lock{  

/*并且该接口Lock存在相应的实现类;
  ReadLockView in StampedLock (java.util.concurrent.....)
  WriteLock in ReetrantReadWriteLock (java.....)
  ReentrantLock (java.util.concurrent.locks)
  WriteLockView in StampedLock (java.util.concurrent....)
  ReadLock in ReentrantReadWriteLock (java....)
  这些实现类当中大部分都在(WriteLock in )ReetrantReadWriteLock类当中
*/
......

2、 synchronized会自动释放锁,而Lock必须手动释放锁。

通过javap 反汇编的形式就可以进行查看得到;
synchronized并没有手动的去获取锁以及释放锁;
它在反汇编之后会形成相应的字节码指令;
当执行到synchronized(obj){ 的时候会执行到5: monitorenter字节码指令;
当执行到synchronized(obj){所对应的回括号}(同步代码块执行完时)时会执行到15: monitorexit字节码指令;
并且就算同步代码块当中出现了异常;synchronized也会将锁给释放掉;
即Exception table中from…to的6~16行字节码指令如果出现异常则target跳转至字节码指令19行执行 19: astore_2以及接着19行字节码指令继续往下进行执行;20: aload_1、21: monitorexit即释放锁;

而Lock就需要手动的来进行释放锁;
当去尝试获取锁之后获取返回得到boolean类型变量;
根据该布尔类型变量判断是否获取得到了锁;
并且在finally块当中需要进行保证锁的释放
(前提是如果tryLock返回为true则需要进行执行unlock()操作;如果tryLock返回为true但是没有执行unlock操作那么就将会导致锁一直不释放;)

3、 synchronized是不可中断的,而Lock可以是不可中断的也可以是可中断的。

synchronized:当有一个线程执行到synchronized同步代码块当中执行代码的时候;另外一个线程由于没有锁只能够在同步代码块外侧进行等待操作;
这个等待的线程是不能够被中断的,它会一直等待获取锁;

而Lock有两种处理方式:可以中断也可以不中断;
一种是可中断式的采用tryLock()的方式;
(尝试去获取锁,并且可以定义等待获取锁的时间以及时间单位;指定等待获取锁的时间到了如果仍然没有获取得到锁的话那么这个时候是可以中断的,即一直处于等待状态的线程不必再进行等待获取锁而是可以去执行其他的任务)
另外一种是lock()方式的而这种方式是不可中断的;

4、 通过Lock可以知道线程有没有拿到锁(tryLock()),而synchronized不能。

比如说Lock定义使用了其tryLock(3, TimeUnit.SECONDS);方法;
则将返回有布尔类型的取值;
如果其返回值为true则说明当前该等待的线程拿到了锁;
如果其返回值为false则说明当前等待的该线程并没有拿到锁;则进行中断完成其他的操作;
而synchronized是不能够去进行判断有没有拿到锁的;synchronized就是一个代码块;
比如说这段代码当中拿到锁则自动进入同步代码块当中执行相应的打印操作;没有拿到锁则只能够在同步代码块外侧进行等待拿到锁的线程进行释放锁;如果没有释放则将一直等待;

synchronized(obj){
  System.out.println("1");
}

5、 synchronized能锁住方法和代码块,而Lock只能锁住代码块。

通过语法层面可以看到synchronized可以用来锁住方法以及代码块;
而Lock只能够锁住代码块而不能锁住方法;
也就是该Lock只能够在方法内部进行调用;
而不能将Lock放到方法上进行修饰方法;

6、 Lock可以使用读锁提高多线程效率。

Lock当中有一个实现类叫做ReentrantReadWriteLock;
这种锁可以进行提高读的效率的;

ReentrantReadWriteLock.java
这个锁的机制是这样的;
如果在读的时候它允许多个线程来进行读操作;
那么如果写的时候那就只能一个线程来进行写也不能进行读取操作;
所以它是可以提高多个线程来进行读的效率。

7、 synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

synchronized是非公平锁;
即其进行唤醒的时候并不是公平的先来后到的方式来进行唤醒;
举个例子:

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

假设一个线程A先进行获取得到锁从而进入同步代码块当中进行执行相关内容如打印“1”;
那么后面还有其他的线程B、C、D、E、F…进来;那么后面的线程B、C、D、E、F…在抢不到锁,即第一个线程A没有释放锁的情况下,只能在同步块外侧进行等待;
那么此时假设线程A执行完同步代码块之后;那么肯定就需要去唤醒等待在同步代码块外侧的线程B、C、D、E、F…中的某一个来获取锁进入同步代码块中;
那么唤醒的时候则不是按照先来后到的方式进行唤醒;
而是属于随机的唤醒一个线程;
所以;synchronized是非公平的;
而ReentrantLock是可以去进行控制它是否为公平锁的;

// 此时使用的是ReentrantLock当中的无参构造器;
//而无参构造器默认是非公平的;
private static Lock lock = new ReentrantLock();

------------------------------------------------------

ReentrantLock.java
/**
  Creates an instance of {@code ReentantLock}
  This is equivalent to using {@Code ReentrantLock(false)}
  无参构造器
*/
public ReentrantLock(){
  sync = new NonfairSync();
}

/**
  Creates an instance of {@code ReentrantLock} with the
  given fairness policy.
  有参构造器;boolean类型操作来指定ReentrantLock是否公平;
  也就是唤醒等待线程的时候是否采用先来后到的方式进行唤醒;
  如果是先来后到的方式那么就是采用的是公平锁;
  如果不是先来后到的方式那么就是非公平锁;

  @param fair{@code true} if this lock should use a fair ordering policy.
*/
public ReentrantLock(boolean fair){
  sync = fair ? new FairSync() : new NonfairSync();
}

深入JVM源码-monitor监视器锁

synchronized是java当中的一个关键字;
通过java是看不到synchronized关键字源码的;
synchronized是由JVM直接来进行支持的;
现在通过JVM源码的方式来进行分析synchronized原理;
JVM的源代码是使用C++编写的;那么介绍IDE工具来方便查看JVM的源代码;
synchronized本质是通过monitor然后来进行同步操作的;
那么会详细介绍monitor的结构;
另外当存在有多个线程来执行synchronized的时候,只存在有一个线程竞争得到锁,那么这个时候就会来进行介绍monitor的竞争;
以及线程没有竞争得到锁是如何处理的也就是monitor等待;
以及synchronized执行完成之后线程要释放锁;那么线程是如何来进行释放的呢?
这里就牵扯到了monitor释放;
最后还会介绍到monitor是一个重量级锁,monitor其性能开销比较大;

学习部分分为:

  • 深入JVM源码
    • 目标
    • JVM源码下载
    • IDE(Clion)下载
    • monitor监视器锁
    • monitor竞争
    • monitor等待
    • monitor释放
    • monitor是重量级锁

目标

通过JVM源码 分析 synchronized的原理

JVM源码下载

http://openjdk.java.net/
选中左边菜单栏中[Source Code]下的子菜单栏[Mercurial]
点击完之后得到的就是jdk所有源代码的一个托管结构;
那么在这当中找到jdk8;
找到的页面:http://hg.openjdk.java.net/jdk8
页面中就是jdk8的源码;
选择hotspot进行下载,Hotspot是jdk自带的虚拟机;
进行跳转页面:http://hg.openjdk.java.net/jdk8/hotspot
看到左侧菜单栏选择zip格式进行下载源代码;
Source Code ---> Mercurial ---> jdk8 ---> hotspot ---> zip

java 是开源的;
那么java开源部分的代码就属于在openjdk这个项目当中;
那么在该网站当中就能够下载得到其源代码;

jvm所有的源码都在其src目录下;
.ideea
.jcheck
agent
cmake-build-debug
make
src
.hg_archival.txt
.hgignore
.hgtags
ASSEMBLY_EXCEPTION
CMakeLists.txt
LICENSE
README
THIRD_PARTY_README
--------------------------------------
src下的目录又分成几个部分:
cpu
os
os_cpu
share

----------------------------------------
----------------------------------------

第一部分cpu;cpu跟虚拟机相关的一些代码;
其下目录有
sparc
x86
zero
----------------------------------------
第二部分os: 操作系统(也就是虚拟机在不同的操作系统当中存在有一些特别的代码)
其下目录有
bsd
linux
posix
solaris
windows
-------------------------------------------
第三部分os_cpu: 与cpu相关也与操作系统相关的一些特殊代码
其下目录有
bsd_x86
bxd_zero
linux_sparc
linux_x86
linux_zero
solaris_sparc
solaris_x86
windows_x86
-------------------------------------------------------
第四部分share: 即公共的JVM源码
其下目录有
tools //工具类
vm    //所有JVM公共的源码都在vm当中
-------------------------------------------------------
-------------------------------------------------------
vm目录下又分有一些子目录:

adlc
asm
c1
ci
classfile
code
compiler
gc_implementation
gc_interface
interpreter
libadt
memory
oops
opto
precompiled
prims
runtime
Xusage.txt

-----------------------------------------------------
-----------------------------------------------------
主要关注vm文件夹下的
oops:即面向对象即一些类的描述类的结构都存储放在该文件夹下;
runtime文件夹:主要是一些线程还有一些monitor锁都在该文件夹下。
---------------------------------------------------------

IDE(Clion)下载

-----------------------------------
下载c++的ide:
https://www.jetbrains.com/
          |
          ↓
https://jetbrains.com/clion/
-----------------------------------

导入java虚拟机的源码:
File-->new Project;
导入文件夹当中的源码;
该文件夹即为当前该目录的文件夹;
.ideea
.jcheck
agent
cmake-build-debug
make
src
.hg_archival.txt
.hgignore
.hgtags
ASSEMBLY_EXCEPTION
CMakeLists.txt
LICENSE
README
THIRD_PARTY_README

monitor监视器锁

(JVM底层由C++实现)

可以看出 无论是 synchronized代码块 还是 synchronized方法,
(最终需要一个java对象;而java对象又会关联到一个monitor监视器锁的东西;真正的同步是靠monitor监视器锁来实现的;那么monitor监视器锁的结果是什么样的?)
其线程安全的语义实现 最终依赖一个叫 monitor的东西,那么这个神秘的东西是什么呢?
下面来详细介绍一下。

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。
其源码是用c、c++来实现的,位于HotSpot虚拟机源码 ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。
ObjectMonitor主要数据结构如下:

-------------------------------------------------------------------
# 构造器,给很多的成员变量赋值(让其与java源代码组合起来进行分析比较方便)
ObjectMonitor() {
  _header              = NULL;
  _count               = 0;
  _waiters             = 0;
  _recursions          = 0; // 线程的重入次数
  _object              = NULL; //存储该monitor的对象
  _owner               = NULL; //标识拥有该monitor的线程
  _WaitSet             = NULL; //处于wait状态的线程,会被加入到_WaitSet
  _WaitSetLock         = 0;
  _Responsible         = NULL;
  _succ                = NULL;
  _cxq                 = NULL; // 多线程竞争锁时的单项列表
  FreeNext             = NULL;
  _EntryList           = NUll; //处于等待锁block状态的线程,会被加入到该列表
  _SpinFreq            = 0;
  _SpinClock           = 0;
  OwnerIsThread        = 0;
}
----------------------------------------------------------------------
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");

    obj.wait();
  }
}
----------------------------------------------------------------------
分析:
Java代码:synchronized(obj){ 同步代码块需要一个java对象,类对象都可以;
那么这个java对象new出来是肯定存放在java内存结构当中的堆中的;
那么对象当中存在一些什么呢?
以前知道对象当中存在有成员变量;
堆中会存放对象中的成员变量;也叫示例数据;
那么其实;除了示例数据以外还会有一个叫做对象头的内容;
那么这个对象头的作用就在于保存对应的monitor对象;
这个对象会关联一个monitor对象;那么这个monitor对象如何来的呢?
monitor时候由C++的类ObjectMonitor.hpp所造的这样一个对象;
首先在ObjectMonitor.hpp当中进行查看一些比较重要的成员变量;

_recursions;_recursions记录线程拿了几次锁了;即线程锁重入的次数;
举个例子:
假设线程第一次进入同步代码块中;获取得到锁对象;即synchronized(obj){;
那么此时锁对象所关联的monitor对象当中的成员属性中的_recursions就会记录拿了一次;
那么假设该同步代码块当中还存在了一个同步代码块即嵌套同步代码块以及同样的一把锁;
那么这个_recursions计数器就会执行加1操作;
假设该嵌套同步代码块当中还存在有一个同步代码块且依旧是同样的一把锁,
那么这个_recursions计数器就又会执行加1操作;
接着如果当前线程出一个同步代码块那么计数器_recursions就会减1;
再出一个同步代码块_recursions减1;
直到_recursions减到0为止就说明该线程就把该锁给释放掉了;

_object: 存储该monitor的对象;
即该_object会进行存储java对象即 synchronized(obj){}中的obj对象;
也就是说是相互引用的;
即java对象当中的obj会引用monitor对象;而monitor对象中的成员变量属性_object取值也会引用着java对象;

_owner: 指的是标识拥有该monitor的线程(即指的就是线程的拥有者)
假设线程A抢到锁进入到了同步代码块当中来了;
那么到时候ObjectMonitor当中的_owner所指的就是当前该线程A;

_WaitSet: 处于wait状态的线程,会被加入到_WaitSet
Wait是等待的意思;Set代表集合;
WaitSet是用来存放处于wait状态的线程的集合
比如说,当前线程A获取得到锁之后从而进入同步代码块执行同步代码块中的代码;
执行obj.wait()方法;那么当前该锁住就将进入无限的等待中;
那么无限等待的线程就会被放置在_WaitSet集合当中去;

_cxq: 多个线程在竞争锁时的单向列表;
假设当前有一个t1线程进入到同步代码块当中;
由于t1线程是第一次先进入同步代码块当中来的;
所以抢得到锁从而进入同步代码块;
假设又来了另外一个线程即t2线程;
那么t2线程是没有抢到锁的;
那么没有抢到锁的线程依旧要等待;
那么这些没有抢到锁的线程去哪里进行等待;
或者说用什么来进行保存这些正在等待的线程呢?
准备要等待的线程会进入到_cxq该变量当中去,_cxq是一个单向列表;
那么此时假如还有一个线程进来叫做t3线程;
那么这个t3线程执行同步代码块也抢不到锁,也会先进行进入这个_cxq单向列表当中;
假设线程t1往下继续进行执行;执行完同步代码块那么就会将锁进行给释放掉了;
那么此时就有可能是由线程t1、t2、t3都有可能获取抢到锁;
那么还是假设线程t1进行抢到了锁;
那么这就是线程t1第二次抢到了锁;也就是t1线程第二次进入同步代码块了;那么这个时候t1线程拿着锁进入同步代码块当中;
那么上一次放在在等待中的_cxq单向列表中的等待中的线程t2、t3此时就会进入到_EntryList变量当中去;

_EntryList: 处于等待锁block状态的线程,会被加入到该列表
即就处于BLOCK状态的线程就会被添加入到_EntryList变量当中来;
假设t1在执行的时候,这个时候又来了一个线程叫做t4;
那么t4线程进入同步代码块由于没有抢到锁会先进入_cxq单向列表当中去;
假设再来了一个线程叫做t5;那么t5也要执行同步代码块也没有获取得到锁从而也会被放置到_cxq单向列表中去;
假设t1线程执行完成出了同步代码块了;假设t1又再次争抢获取得到锁进入了同步代码块当中;那么上一次在_cxq单向列表中进行等待的两个线程t4、t5就会进入到_EntryList当中去;即变为BLOCK状态;
-----------------------------------------------------------------
分析Monitor结构图
Monitor主要由三个组件进行构成: _owner、_EntryList、_WaitSet
等待一轮之后依旧没有抢到锁的线程被放置到_EntryList当中来了;
那么还有就是执行obj.wait()方法的被放置放到_WaitSet单项列表中去;
当线程拥有者在执行那么执行完就会出同步代码块;
那么当线程拥有者在执行完成出同步代码块的时候
有可能会是_EntryList当中正在阻塞的线程竞争获取拿到锁变成monitor当中的_owner;
也有可能会是_WaitSet当中处于Wait的线程被别的操作所唤醒了;
那么它也有可能会获得锁变成线程的user即monitor结构中的_owner;

竞争到锁的线程、处于阻塞状态的线程、处于等待状态的线程;
  1. _owner: 初始化为NUll,当有线程占有该monitor时,owner标记为该线程的唯一表示。当线程释放monitor时,owner又恢复到NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  2. _cxq: 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。 修改前 _cxq的旧值 填入了 node的next字段, _cxq指向新值(新线程)。因此 _cxq是一个后进先出的stack(栈)。
  3. _EntryList: _cxq队列中 有资格成为 候选资源的 线程 会被移动到该队列中。
  4. _WaitSet: 因为调用wait方法而被阻塞的 线程会被放在该队列中。

每一个java对象都可以与一个监视器 monitor关联,
可以把它理解成为一把锁,
当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,
该线程得 先获取到 synchronized修饰的对象 对应的monitor。

java代码里不会 显式地去创造这么一个 monitor对象,
也无需创建,
事实上可以这么理解:
monitor并不是随着对象创建而创建的。
是通过 synchronized 修饰符 告诉 JVM 需要为 某个对象创建关联的 monitor对象。
每个线程都存在两个ObjectMonitor对象列表,分别为 free和 used列表。
同时JVM中也维护着 global locklist。
当线程需要 ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从 globallist 中申请。

ObjectMonitor 的数据结构包含三种队列: _cxq、_WaitSet和 _EntryList,他们之间的关系转换可以用下图表示:

深入JVM源码-monitor竞争

monitor竞争
monitor对象监视器锁竞争的过程;

synchronized为什么是重量级锁,为什么开销比较大?
synchronized是重量级的锁,效率不高。
synchronized的优化涉及到一个叫做CAS的操作。
CAS也属于一个原子操作,可以将其看做是一个轻量级的synchronized。

目标

分两步
第一步;了解何时会出现monitor竞争?
第二步:monitor是如何竞争的?

java代码举例:
----------------------------------------
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
  }
}
----------------------------------------
假设有一个t1线程;再来一个t2线程;
可能这两个线程t1、t2都会来进行执行同步代码块synchronized(obj){;
那么这个时候就会处于monitor竞争状态;
线程执行同步代码块就会出现竞争的现象;
之前反汇编javap看到synchronized会变成两条字节码指令monitorenter以及monitorexit;
当线程进入同步代码块synchronized时会执行monitorenter字节码指令;
当线程执行完同步代码块中的内容即退出同步代码块时会执行monitorexit字节码指令;
这个monitorenter字节码指令最终会调用到InterpreterRuntime.cpp代码中的一个方法;
  1. 执行 monitorenter时,会调用 InterpreterRuntime.cpp
    (位于:src/share/vm/interpreterRuntime.cpp)的InterpreterRuntime::monitorenter 函数。

具体代码可参见 HotSpot源码。

# 截取的部分代码
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(){}else{}块
  UseBiasedLocking是不是使用了偏向锁;
  这个UseBiasedLocking条件其实是JVM可以进行设置的一个启动参数;
  如果进行设置了启用偏向锁那么就会走if(){}块当中的代码;
  如果没有设置启用偏向锁那么则会走else{}块中的代码;
  也就是所要分析的monitor重量级锁的过程叫做slow_enter慢进入;
  */
  if(UseBiasedLocking){
    //Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_entry(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");
  1. 对于重量级锁,monitorenter的函数中(slow_enter最终)会调用ObjectSynchronizer::slow_enter
  2. 最终调用 ObjectMonitor::enter(对象监视器锁monitor的enter方法,说明还是要回到ObjectMonitor.cpp)
    (位于:src/share/vm/runtime/objectMonitor.cpp),源码如下:
void ATTR 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;
  void * cur ;

  /**
  通过 CAS 操作尝试把 monitor 的 _owner 字段设置为当前线程
  在ObjectMonitor::enter进入的时候会调用Actomic当中的cmpxchg_ptr;
  Atomic::cmpxchg_ptr(Self, &_owner, NULL)
  该函数属于linux系统内存当中的一个函数最终会依赖CPU去做原子赋值操作;
  CAS是一个原子的赋值操作;
  作用就是将monitor对象当中的_owner设置成这个当前线程Self;
  看其是否能够设置成功
  */
  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") ;
    // CONSIDER: set or assert OwnerIsThread == 1
    return ;
  }


  //线程重入;recursions++
  /**
    如果上一步骤当中设置monitor对象中的_owner设置成当前这个线程Self成功;
    并且当前线程Self的线程名字,即之前的_owner保存的线程就是当前线程Self的线程的名字,即名称一致的话;
    那么这样就意味着锁重入;即重新又进入了一个代码块有获取得到了同一把锁;
    那么这个时候进行monitor对象当中的_recursions变量进行++操作,即该线程的重入次数;并且return;
    说明当前线程竞争到该锁;
  */
  if(cur == Self){
    // TODO-FIXME: : check for integer overflow! BUGID 6557169
    _recursions ++;
    return ;
  }

/**
如果当前线程第一次来抢monitor该锁;
如果当前线程是第一次进入该monitor,如果抢到锁了;
设置_recursions为1,并且将_owner设置为当前线程;
最后返回即表示当前线程竞争到该锁;
*/
if(Self -> is_lock_owned((address)cur)){
  assert (_recursions == 0, "internal state error");
  _recursions = 1;
  // Commute error from a thread-specific on-stack BasicLockObject address to
  // a full-fledged "Thread *".
  _owner = Self;
  OwnerIsThread = 1;
  return ;
}

  // 省略一些代码
  /**
  那么如果经过以上操作当前线程都没有抢到锁的话;
  则就将进入到该for循环当中;
  假设第一个线程t1抢到锁进入到了同步代码块当中;
  那么第二个线程t2就由于t1线程已经抢到锁且当前时间内没有进行释放锁的缘故第二个线程没有抢到锁;
  那么抢不到锁的第二个线程t2就会执行方法EnterI(THREAD);
  最终进入到monitor对象的成员变量_cxq单向列表当中进行等待获取锁;
  */
  for (;;){
    jt->set_suspend_equivalent();
    // cleared by handle_special_suspend_equivalent_condition()
    // or java_suspend_self()

    // 如果获取锁失败,则等待锁的释放
    EnterI(THREAD);

    if(!ExitSuspendEquivalent(jt)) break;


    //
    // we have acquired the contended monitor, but while we were
    // waiting another thread suspended us. We don't want to enter
    // the monitor while suspend because that would surprise the
    // thread that suspended us.
    //

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

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

此处省略锁的自旋优化等操作,统一放在后面synchronized优化中说。
以上代码的具体流程概括如下:

1、 通过 CAS 尝试把monitor的 _owner字段设置为当前线程(即把monitor的_owner成员变量的属性取值设置为竞争的该线程;如果设置成功则说明该线程竞争到了锁)
2、 如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入的次数;(如果在这之前的上一次竞争当前线程获取得到了该锁,那么现在当次竞争当前线程又竞争到了该锁;两把锁一样;那么说明是锁重入;)
3、 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
4、 如果获取锁失败,则等待锁的释放(进入阻塞等待状态,即进入到monitor对象的成员变量_cxq单向列表队列);

深入JVM源码-monitor等待

回顾并引入

当前在竞争monitor对象锁的时候会发现;
有一个线程会竞争到monitor并且让线程接着往下执行;
但是有一些线程竞争不到monitor那么这个时候它会执行EnterI(THREAD)这个函数;
也就是说没有抢到锁的线程会进入等待处于阻塞状态;
那么接下来也就是学习monitor等待过程;也就是EnterI(THREAD)方法;

monitor等待

竞争失败等待调用的是 ObjectMonitor对象的EnterI(THREAD)方法
(位于:/src/share/vm/runtime/ObjectMonitor.cpp),
源码如下所示:

# 截取部分代码分析
void ATTR ObjectMonitor::EnterI(THREAD){
  Thread * 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 (_succ        == Self              , "invariant");
    assert (_owner       != 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 = next = _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){
        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, "inveriant");

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

        // park self
        if(_Responsible == Self || (SynchFlags & 1)){
          TEVENT (Inflated enter - park TIMED);
          Self->_ParkEvent->park((jlong)RecheckInterval);
          // Increase the RecheckInterval, but clamp the value.
          RecheckInterval *= 8;
          if(RecheckInterval > 1000) RecheckInterval = 1000;
        }else{
          TEVENT (Inflated enter - park UNTIMED);
          // 通过park将当前线程挂起,等待被唤醒
          Self->_ParkEvent->park();
        }

        if(TryLock(Self) > 0) break;
        // 省略部分代码
      }

      // 省略部分代码
  }



---------------------------------------------------------------
java代码:
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
    obj.wait();
  }
}
---------------------------------------------------------------
看图说话:

假设t1线程没有竞争得到锁;则t1线程就将要去处于等待状态;
那么这个处于等待这个动作是如何做的呢?

首先这个t1线程会进入EnterI(THREAD)方法当中;
进入EnterI(THREAD)方法之后;
那么会先去进行执行TryLock(Self)尝试获取锁操作;
即t1线程虽然没有抢到锁但是它还会继续TryLock(THREAD)进行尝试一下,最后做一下挣扎;
如果在这个挣扎过程中抢到了锁则后面接着执行;
如果在这个挣扎过程中没有抢到锁那么就会继续往下走会执行函数TrySpin(Self)函数;
那么函数TrySpin(Self)函数的作用就在于自旋;
自旋即意味着进入一个循环当中多次进行抢一抢锁看能不能够抢到该锁;
即再次挣扎一下看能不能再次抢救一下;
如果经过TryLock尝试获取锁以及TrySpin自旋之后还是没有办法获取得到锁;
那么这个时候就会走到下面来;
就会将当前这个没有抢到锁且经过TryLock以及TrySpin后依然没有抢到锁的t1线程放到ObjectWaiter中来并进行封装起来;
ObjectWaiter即一个等待的线程;
并且会将当前这个没有抢到锁的线程状态设置为ObjectWaiter当中的TS_CXQ状态取值;
接着通过CAS把即将要等待的线程t1线程push到_cxq单向列表当中去;
但是可能有一个线程抢到也有其他很多线程没有抢到锁;
那么这些很多没有抢到锁的线程都要被push到这个_cxq节点上面来;
那么因此这个没有抢到锁的线程要被push到_cxq结点上去的这一操作也可能会成功也有可能会失败;
所以这一操作即没有抢到锁的线程都要被push到_cxq结点上去也是使用到了一个for循环加上Atomic::cmpxchg_ptr该内核函数;即CAS一次不行就再重试再重试;直到几个没有成功获取得到锁的线程都被挂在结点_cxq单向列表上;
另外在每次重试的时候,都会再去进行重试挣扎一下即TryLock看能不能抢到锁;
那么经过for(;;)循环之后这几个没有获取得到锁的线程都会被挂在_cxq结点单向列表上;
当没有获取得到锁的线程放到结点_cxq单向列表当中之后;那么还要将该线程进行挂起操作;即下面的代码;
在挂起的时候首先它也还是会去进行TryLock尝试挣扎抢救一下去获取锁,看能不能获取得到锁;得不到锁则继续往下走;继续往下走存在if(){}else{}块但是无论走哪一块最终都会导致当前线程执行park(),park就是把当前线程进行挂起;
那么在当前该线程被挂起之后那么该线程就不会再进行执行了,那么这个时候就只有等待别的线程来进行唤醒的时候才会进行继续执行;
那么不管是从if(){}或者是else{}块当中的点开始进行被唤醒,当该线程被唤醒之后都会去进行尝试抢锁即TryLock(Self);看是否能够获取得到锁;

当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁,TryLock方法实现如下:

# 截取部分代码
int ObjectMonitor::TryLock(Thread * Self){
  for(;;){
    void * own = _owner;
    if(own != null) return 0;
    if(Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL){
      // Either guarantee _recursions == 0 or set _recursions = 0.
      assert (_recursions == 0,    "invariant");
      assert (_owner      == Self, "invariant");
      // CONSIDER: set or assert that OwnerIsThread == 1
      return 1;
    }
    // The lock had been freen momentarily, but we lost the race to the lock.
    // Interference -- the CAS faild.
    // we can either return -1 or retry.
    // Retry doesn't make as much sense because the lock was just acquired.
    if(true) return -1;
  }
}
-----------------------------------------------------------------
尝试锁也是使用的CAS操作去进行做一个判断;
如果尝试获取得到锁那么就会返回1;
如果尝试没有获取得到锁那么就会返回-1;

小结

首先第一个;当线程没有抢到锁那么就将会被放到_cxq单向列表当中去;
那么该流程的第一步骤:
以上代码的具体流程如下:

1、 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
2、 在for循环中(用CAS尝试把当前该线程放到_cxq的一个节点上去;因为同时有多个线程往单向列表_cxq当中放,所以使用了for循环,CAS多次尝试),通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
3、 (没有抢到锁的线程在放到_cxq节点上之前)node结点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取得到锁,则通过park将当前线程挂起(park内核函数,让当前线程进行挂起那么其实也就相当于阻塞状态需要别的线程进行唤醒才能够继续往下执行),等待被唤醒。
4、 当该线程被唤醒时,会从挂起的点继续执行,通过ObjectWaiter::TryLock尝试获取锁。

深入JVM源码-monitor释放

目标

分成两个部分来进行学习;
首先进行介绍什么时候monitor会进行释放;
接着进行介绍monitor的释放过程是什么样的;

monitor释放分析

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

-------------------------------------------------------------------
# 截取部分代码
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS){
  Thread * Self = THREAD;

  // 省略部分代码
  if(_recursions != 0){
    _recursions--;    // this is simple recursive enter
    TEVENT (Inflated exit - recursive);
    return ;
  }

  // 省略部分代码

  ObjectWaiter * w = NULL;
  int QMode = Knob_QMode;

  // qmode = 2:直接绕过EntryList队列,从_cxq队列中获取线程用于竞争锁
  if(QMode == 2 && _cxq != NULL){
    w = _cxq;
  assert ( w != NULL, "invariant");
  assert ( w->TState == ObjectWaiter::TS_CXQ, "invariant");
  ExitEpilog(Self, w);
  return ;
  }

  // qmode=3:cxq队列插入EntryList尾部;
  if(QMode == 3 && _cxq != NULL){
    w = _cxq;
    for(;;){
      assert (w != NULL, "Invariant");
      ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr (NULL, &_cxq, w);
      if( u == w ) break;
      w = u;
    }
    assert( w != NULL , "invariant");

    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for( p = w ;  p != NULL ; p = p->_next){
      guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->prev = q;
      q = p;
    }

    ObjectWaiter * Tail;
    for ( Tail = _EntryList; Tail != NULL && Tail->_next != NULL; Tail = Tail->_next);
    if(Tail == NULL){
      _EntryList = w;
    }else{
      Tail->_next = w;
      w->_prev = Tail;
    }
  }


  // qmode=4: cxq队列插入到_EntryList头部
  if(QMode == 4 && _cxq != NULL){
    w = _cxq;
    for(;;){
      assert (w != NULL, "Invariant");
      ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr(NULL, &_cxq, w);
      if(u == w) break;
      w = u;
    }
    assert (w != NULL , "invariant");

    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for( p = w; p != NULL ; p ->_next){
      guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->_prev = q;
      q = p;
    }

    if(_EntryList != NULL){
      q->_next = _EntryList;
      _EntryList->_prev = q;
    }
    _EntryList = w;
  }

  w = _EntryList;
  if(w != NULL){
    assert (w->TState == ObjectWaiter::TS_ENTER, "invariant");
    ExitEpilog(Self, w);
    return ;
  }

  w = _cxq;
  if(w == NULL) continue;

  for(;;){
    assert (w != NULL, "Invariant");
    ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr(NULL, &_cxq, w);
    if(u == w) break;
    w = u;
  }
  TEVENT(Inflated exit - drain cxq into EntryList);

  assert( w          != NULL , "invariant");
  assert( _EntryList != NULL , "invariant");

  if(QMode == 1){
    // QMode == 1: drain cxq to EntryList,reversing order
    // we also reverse the order of the list
    ObjectWaiter * s = NULL;
    ObjectWaiter * t = w;
    ObjectWaiter * u = NULL;
    while(t != NULL){
      guarantee(t->TState == ObjectWaiter::TS_CXQ, "invariant");
      t->TState = ObjectWaiter::TS_ENTER;
      u = t->_next;
      t->_prev = u;
      t->_next = s;
      s = t;
      t = u;
    }
    _EntryList = s;
    assert(s != NULL, "invariant");
  }else{
    // QMode ==0 or QMode == 2
    _EntryList = w;
    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for(p = w; p != NULL; p = p->_next){
      guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->_prev = q;
      q = p;
    }
  }

  if (_succ != NULL) continue;

  w = _EntryList;
  if(w != NULL){
    guarantee(w -> TState == ObjectWater::TS_ENTER, "invariant");
    ExitEpilog(Self, w);
    return ;
  }
}
-------------------------------------------------------------------
java代码:
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
    obj.wait();
  }
}
------------------------------------------------------------------
第一部分:什么时候释放monitor分析:
获得锁的线程t1执行完同步代码块当中的代码之后,
那么就需要出同步代码块;
在出同步代码块的时候就会进行monitor的释放操作;
-----------------------------------------------------------------
第二部分:monitor释放的过程是怎么样的分析:
在exit的释放过程当中;
如果_recursions计数器不等于0;
那么_recursions就会去做一个--即减一操作;再去return;
那么这个其实对应着的是重入锁;
_recursions当不为0的情况下会进行--即减一操作;
如果_recursions等于0的情况下那么就表示线程完全出了同步代码块,
且把锁释放返回了;
那么这个时候除了释放锁之外还需要做一个操作;
即去唤醒之前正在等待阻塞中的线程;那么需要唤醒哪一个线程呢?
这个时候就需要注意了;有两个链表当中都存放有需要被唤醒的线程;
即一个是_cxq;另外一个是EntryList;
那么是随机唤醒某一个线程即有可能合适唤醒_cxq列表当中的线程也有可能是唤醒_EntryList当中的线程;

所以继续往下看;
ObjectWaiter是之前被阻塞进行等待的一个线程的一个封装;
需要记住的是w,找到了w即找到了需要被唤醒的线程;

在exit当中提供很多种模式;
模式一:
如果QMode等于2;那么这个时候会让w等于_cxq的首节点即链表头;
那么也就取到了这个要被唤醒的线程;
那么这一种方法绕过了_EntryList当中被阻塞的线程直接取_cxq列表当中的线程作为要被唤醒的线程对象;
另外方法ExitEpilog(Self,w)方法就是去作为唤醒线程的方法;
模式二:
如果QMode等于3;首先要被唤醒的线程w也是等于_cxq的首节点;
并且会将_cxq的这些节点放到_EntryList的尾部去;
模式三:
如果QMode等于4;则那么首先这个要被唤醒的线程也是等于_cxq的首节点;
并且会将_cxq列表当中的元素插入到_EntryList列表的头部;
etc...

那么最后就找到了w,即需要被唤醒的线程;
那么也会去调用到方法ExitEpilog(Self, w);
即唤醒w线程;
ExitEpilog即为具体唤醒的过程;

1、 退出同步代码块时 会让_recursions减1,当_recursions的值减为0时,说明(线程完全退出了同步代码块中)线程释放了锁;
2、 (释放完锁之后需要唤醒线程)根据不停的策略(策略不同唤醒不同的线程)(由QMode指定),从_cxq或_EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpack完成(将之前park的线程进行唤醒),实现如下:

-----------------------------------------------------------------
# 截取部分代码
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * wakee){
  assert( _owner == Self, "invariant");

  _succ = Knob_SuccEnabled ? wakee->_thread : NULL;
  ParkEvent * Trigger = wakee->_event;

  wakee = NULL;

  // Drop the lock
  OrderAccess::release_store_ptr(&_owner, NULL);
  OrderAccess::fence();           // ST _owner vs LD in unpark()

  if(SafepointSynchronize::do_call_back()){
    TEVENT(unpack before SAFEPOINT);
  }

  DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
  Trigger->unpark();  // 唤醒之前被park()挂起的线程

  // Maintain stats and report events to JVMTI
  if (ObjectMonitor::_synch_Parks != NULL){
    ObjectMonitor::_sync_Parks->inc();
  }
}
-----------------------------------------------------------------
分析:
最重要的代码在于Trigger-unpark();
unpark的含义即代表:
假设找到线程t1要被唤醒;
那么找到这个线程t1之后;调用unpark()方法就会将线程t1进行唤醒;
那么也就是说这个t1线程就又能够有有机会去获取竞争得到锁从而进入到同步代码块当中去;
执行完unpark之后就会将需要唤醒的线程进行唤醒;
唤醒完成之后就会进入到之前park()让该线程挂起的代码行中;
那么后面当unpark()将该需要唤醒的线程唤醒之后,就又会去进行执行TryLock尝试获取锁的代码;抢到锁则进入到同步代码块当中去;

被唤醒的线程,会回到void ATTR ObjectMonitor::EnterI(TRAPS)的第600行,继续执行monitor的竞争。

# 截取部分代码
// park self
if(_REsponsible == Self || (SynchFlags & 1)){
  TEVENT (Inflated enter - park TIMED);
  Self->_ParkEvent->park((jlong) RecheckInterval);
  // Increase the RecheckInterval , but clamp the value.
  RecheckInterval *= 8;
  if(RecheckInterval > 1000) RecheckInterval = 1000;
}else{
  TEVENT (Infalted enter - park UNTIMED);
  Self->_ParkEvent->park();
}

if(TryLock(Self) > 0) break;

深入JVM源码-monitor是重量级锁

synchronized代码块在代码执行的时候效率比较低;
因为synchronized所关联的锁对象monitor是一个重量级锁;

目标

monitor为什么属于一个重量级锁?
monitor为什么效率比较低?

可以看到 ObjectMonitor 的函数调用中 会涉及到 Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会执行调用park()被挂起,竞争到锁的线程执行完成退出同步代码块时(即当其他线程退出同步代码块时)会调用unpark()唤醒上次那些没有竞争到锁从而被park()挂起的线程;
(这个park和unpark也属于内核函数;即也就是说synchronized在执行的时候会涉及到大量的内核函数的执行,而内核函数的执行就会涉及到操作系统中用户态和内核态的一个切换;)
这个时候就会存在操作系统 用户态和内核态的转换,这种转换会消耗大量的系统资源。
所以synchronized是java语言中的一个重量级(Heavyweight)的操作。
用户态和内核态是什么东西呢?
要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

linux系统的体系架构由
内核(操作系统的内核,而内核本质上也是一种应用程序;作用是来控制计算机的硬件资源的,比如说控制硬盘、那么还有可能控制内存等控制网络等相关的一些硬件设备比如说网卡、声卡、键盘、鼠标等);
系统调用、shell、公用函数库;
应用程序(用户空间)
(自己写的程序被称为普通的应用程序;用户空间其实指的是自己所编写的应用程序所运行的那一块内存空间就成为用户空间;而应用程序在用户空间进行运行的时候就有可能会涉及到一些硬件资源的调用;那么这个时候就需要靠内核来进行去操作硬件资源;那么用户空间去调用内核的时候,就需要通过系统调用才能够进行;那么系统调用的作用即在于让在用户空间的应用程序能够去调用内核的一些内核函数;那么系统调用可以看做是提供内核的接口供外层的应用程序来进行调用;)
这几个部分来进行组成;

从上图可以看出,linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。
内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用;
所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);
(当应用程序需要用到键盘、需要去读取文件、需要通过网络去发送一些资源的时候那么说白了也就是需要用到计算机的一些硬件资源的时候;那么这个时候就需要通过系统调用到内核来帮助执行;普通的应用程序在用户空间当中运行那么就称之为用户态;当应用程序如果需要调用内核的一些功能,即通过系统调用来进行调用内核当中的一些功能;那么这个时候应用程序就会进入内核态;那么用户态与内核态的切换是需要系统调用来进行的;)
但是当它调用系统调用执行某些操作时,例如I/O调用,此时需要陷入内核中运行,就称之为 进程处于内核运行态(或简称为 内核态)。

系统调用的过程可以简单理解为:

1、 用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。
2、 用户态程序执行系统调用。
3、 CPU切换到内核态,并跳到 位于内存指定位置 的指令。
4、 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
5、 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

首先应用程序属于用户态;即将调用内核函数的时候,那么应用程序会把现在程序的一个运行状态主要是程序运行的一些运行数值进行保存可能会保存在寄存器当中也有可能使用参数创建一个堆栈来保存现在应用程序的一些运行信息运行参数;
那么接着用户态的应用程序就会来进行执行系统调用;
那么经过系统调用之后,CPU就会切换到内核态;然后到内存当中指定的位置去执行相关指令;
接下来系统调用处理器它会进行用户态当中应用程序保存在堆栈或者是保存在寄存器当中的一些运行信息运行数据,然后并且会执行相应的内核函数以及请求一些内核的服务;
那么这个时候系统就由用户态切换到了内核态;
那么接着内核函数的调用在系统调用中调用完毕之后;
操作系统会进行重置CPU又切换为用户态;并且将内核态当中调用的结果进行返回;那么应用程序就又回到了用户空间;得到了内核函数系统调用返回的结果数据;

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

monitor是重量级锁

在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,
有两点是需要特别注意的。

首先,synchronized同步块 对 同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

第12章讲过,java的线程 是 映射到 操作系统的 原生线程 之上的,
如果要 阻塞或唤醒 一个线程,都需要操作系统来帮忙完成, 这就需要从用户态转换到核心态中,因此 状态转换需要耗费很多的处理器时间。

对于代码简单的同步块(如被synchronized修饰的 getter()或setter()方法),
状态转换消耗的时间 有可能比 用户代码执行的时间还要长。

所以 synchronized 是java语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。

而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。

可以看到ObjectMonitor的函数调用中会涉及到 Atomic::cmpxchg_ptr,Atomic::inc_prt等内核函数,这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。

用户态和内核态是什么东西呢?
要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

synchronized优化_CAS_AtomicInteger使用

第六章:JDK6 synchronized优化

  • CAS
    • JDK6当中对synchronized所做的优化处理;
    • 在JDK6当中对synchronized做了大量的优化处理;
    • 以及这些优化处理很多都涉及到了CAS操作;
  • 锁升级过程
    • 锁升级依赖java对象头;
  • Java对象的布局
  • 偏向锁
  • 轻量级锁
  • 重量级锁
  • 锁消除
  • 锁优化
  • 平时写代码如何对synchronized优化

目标

学习CAS的作用
学习CAS的原理

CAS概述和作用

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

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

(可以看做是一个轻量级的synchronized,它能保证变量修改的原子操作)

CAS和volatile实现无锁并发

java当中已经提供好了一个类叫做AtomicInteger;
这个类其底层即使用的就是CAS;

回顾代码

package com.xxx.demo05_cas;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo01{
  public static void main(String[] args)throws InterruptedException{
    AtomicInteger atomicInteger = new AtomicInteger();
    Runnable mr = ()->{
      for(int i = 0; i < 1000; i++){
        atomicInteger.incrementAndGet();
      }
    };

    ArrayList<Thread> ts = new ArrayList<>();
    for( int i=0; i < 5 ; i++){
      Thread t = new Thread(mr);
      t.start();
      ts.add(t);
    }

    for(Thread t:ts){
        t.join();
    }

    System.out.println("number = " + atomicInteger.get());
  }
}
-------------------------------------------------------------
/**
  目标:演示原子性问题
    1. 定义一个共享变量
    2. 对number进行1000次的++操作
    3. 使用5个线程来进行
*/
public class Demo01{
  // 1. 定义一个共享变量
  //private static int number = 0;
  private static AtomicInteger atomicInteger = new AtomicInteger();

  public static void main(String[] args)throws InterruptedException{
    // 2. 对number进行1000的++操作
    Runnable increment = ()->{
      for( int i = 0 ; i < 1000; i++){
        //number++;
        atomicInteger.incrementAndGet();
      }
    };

    List<Thread> list = new ArrayList<Thread>();
    // 3. 使用5个线程来进行
    for ( int i = 0 ; i < 5; i++){
      Thread t = new Thread(increment);
      t.start();
      list.add(t);
    }

    for(Thread t: list){
      t.join();
    }

    //System.out.println("number = " + number);
    System.out.println("atomicInteger = " + atomicInteger.get());
  }
}
/**
由于number++并不是原子操作;
所以就会导致原子性问题的产生;
即最后会导致number输出时number的值是小于5000的;
  private static int number = 0;
那么此时就不使用int类型的共享变量number了;
而是给共享变量number换一种类型叫做AtomicInteger类型;
  private static AtomicInteger atomicInteger = new AtomicInteger();
变量名number换为atomicInteger;
并且number++操作也要换成atomicInteger.incrementAndGet();
那么atomicInteger.incrementAndGet()也会去做一个自增操作;
其该自增操作是一个原子性的操作;
最后打印atomicInteger的最新值;通过atomicInteger.get()来进行获取atomicInteger的最新值;

最后再怎么运行发现运行结果都是5000;
也就意味着 AtomicInteger的incrementAndGet()方法可以进行保证共享变量赋值的原子性;
*/
-------------------------------------------------------------
多次运行后的运行打印:
atomicInteger = 5000
atomicInteger = 5000
atomicInteger = 5000
.....
-------------------------------------------------------------

synchronized优化_CAS原理

CAS 原理

通过刚才 AtomicInteger 的源码可以看到, Unsafe类提供了原子操作。
AtomicInteger类当中其内部会包含一个叫做UnSafe的类;
UnSafe该类可以进行保证变量在赋值时的原子操作;
也就是UnSafe类当中提供了CAS操作;

Unsafe类介绍

java是无法去操作内存地址的(即也就是没有指针);

Unsafe类使java拥有了像C语言的指针一样操作内存空间的能力(操作对象的内存空间即能够操作对象里面的内容;但是这个UnSafe类不太安全;如果使用不当会出现一些比较危险的事情;所以java官方并不推荐使用;并且在jdk当中也无法找到此类;只能够通过反射的方式才能够找到该类),同时也带来了指针的问题。
过渡的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
Unsafe对象不能直接调用,只能通过反射获得。

Unsafe实现CAS

AtomicInteger.java源码

publlic class AtomicInteger extends Number implements java.io.Serializable{
  private static final long serialVersionUID = 6214790243416807050L;

  // setup to use Unsafe.compareAndSwapInt for updates
  private static final Unsafe unsafe = Unsafe.getUnsafe();
  private static final long valueOffset;
  static {
    try{
      valueOffset = unsafe.objectFieldOffset(
        AtomicInteger.class.getDeclareField("value")
      );
    }catch(Exception ex){ throw new Error(ex);}
  }

  private volatile int value;
  ......
}
-------------------------------------------------------
AtomicInteger内部具有一个成员变量unsafe,通过Unsafe.getUnsafe()得到;
另外需要注意的还是AtomicInteger是用来进行保存数字的;
该AtomicInteger数值保存在AtomicInteger类成员变量value当中;
而且该value成员变量使用了volatile关键字进行修饰;
volatile即保证了多线程操作时的可见性;
另外AtomicInteger类中还有一个成员变量valueOffset;
那么valueOffset的作用即用来找到volatile关键字修饰的value变量的内存地址;
到时候根据AtomicInteger类所生成的对象atomicInteger的内存地址再加上偏移量valueOffset就可以找到value所在的内存地址了;
也就可以找到value的取值;也就可以对value进行修改等操作;
-------------------------------------------------------------
# java测试用例代码
Demo01.java
    private static AtomicInteger atomicInteger =  new AtomicInteger();

    Runnable increment = ()->{
      for( int i = 0 ; i < 1000; i++){
        atomicInteger.incrementAndGet();  // 变量赋值的原子性
      }
    };
-------------------------------------------------------------
# AtomicInteger.incrementAndGet()方法中具体执行流程;分析源码;
-------------------
AtomicInteger.java
    private final int value;

    public final int incrementAndGet(){
      return unsafe.getAndAddInt(this, valueOffset, 1)+1;
    }
------------------
        ↓
------------------
Unsafe.class
    public final int getAndAddInt(Object var1, long var2, int var4){
      int var5;
      do{
        var5 = this.getIntVolatile(var1 , var2);
      }while(!this.compareAndSwapInt(var1, var2, var5, var5+var4));

      return var5;
    }
//compareAndSwapInt CAS操作
------------------
分析执行流程:
当刚进行创建AtomicInteger对象(AtomicInteger atomicInteger=new AtomicInteger();)时其AtomicInteger.java当中通过volatile修饰的value变量取值为0;
假设AtomicInteger当中的成员变量value取值为0之后;
那么当前来进行for循环atomicInteger进行执行incrementAndGet()方法;
即也就意味着会执行到AtomicInteger.java类当中的incrementAndGet()方法;
那么AtomicInteger.java类当中的incrementAndGet方法中又会去通过unsafe成员变量去执行方法getAndAddInt(this , valueOffset, 1)+1;
即就相当于调用到了Unsafe.class当中的getAndAddInt(Object var1, long var2, int var4)方法;
最后Unsafe.class就在getAndAddInt(Object var1, long var2, int var4)当中进行CAS的操作即this.compareAndSwapInt(var1, var2, var5, var5+var4);

当前假设有两个线程,分别是线程t1和线程t2;
那么此时这两个线程同时来进行执行Runnable当中所实现的run()方法即去执行循环,循环中执行atomicInteger.incrementAndGet()方法;
假设t2线程先走,那么t2线程就会走过
Demo01.java当中的atomicInteger.incrementAndGet()
                   ↓
AtomicInteger.java当中的incrementAndget方法中的unsafe.getAndAddInt(this, valueOffset, 1)+1
                   ↓
UnSafe.class当中的getAndAddInt(Object var1, long var2, int var4)当中的代码
---------------------
在Unsafe.class当中的getAndAddInt(Object var1, long var2, int var4)当中的var5的含义代表的是:(可以理解为是)旧的预估值;
此时t2线程即准备进入到代码层中的do-while循环;
那么先执行do{}块当中的内容进行获取var5的取值,即获取旧的预估值取值;
获取var5取值是通过 this.getIntVolatile(var1, var2)方法进行获取得到的;
通过两个参数var1以及var2就能够获取得到一个int类型的取值即var5;
这两个参数当中的var1即Object var1,
该var1即AtomicInteger.java的incrementAndGet()方法当中的this,
也就是在Demo01.java当中new好的atomicInteger对象(AtomicInteger atomicInteger = new AtomicInteger(););
那么var2即为AtomicInteger.java的incrementAndGet()方法当中的valueOffset,是一个内存偏移值;
那么也就是说 var1+var2 → this+valueOffset 其目的也就是找到private volatile value的取值;找到AtomicInteger.java当中的成员变量value的取值;
(即当前AtomicInteger要执行类似number++的操作;而AtomicInteger所存的自增数值被存放在其AtomicInteger成员变量value当中;在进行自增操作时首先就需要去获取原先该AtomicInteger当中value的取值为多少或者是找到value的内存地址等信息然后对该value进行一个重新赋值的一个操作,修改操作;)
找到AtomicInteger当中value的取值来作为一个旧的预估值;
那么当前在创建AtomicInteger atomicInteger对象的时候,AtomicInteger当中的成员变量属性value取值为0;
即现在通过var1+var2 → this+valueOffset参数调用this.getIntVolatile方法获取得到的取值为0即赋值给var5;
假设一种极端的情况;即t2线程找到旧的预估值之后即var5变量被赋值;
此时CPU切换到了另外一个线程上面去了;
切换到t1线程上来了;
那么t1线程也会来进行执行AtomicInteger类当中的incrementAndGet方法;
即t1线程也会进入到Unsafe.class当中的getAndAddInt方法中来;
那么此时t1也会来进行寻找一个旧的预估值即也会进入do{}块当中进行调用this.getIntVolatile来进行给var5进行赋值;也是通过this和valueOffset来进行找到value的取值;
那么此时就会存在有两个线程t1、t2来竞争准备给var5进行赋值操作;(极端情况)
那么此时假设仍然还是t1线程先进行执行;
那么这个时候t1线程就会进入while当中执行this.compareAndSwapInt(var1, var2, var5, var5+var4)方法(即比较如果相同的话则并且交换一个int类型的取值);
分析compareAndSwapInt当中的参数var1、var2、var5、var4;
var1: 其实还是这个this,即AtomicInteger atomicInteger对象;
var2: 即其实还是valueOffset,即内存偏移量;var1与var2的目的还是用于来进行找到内存中value的最新取值;此时找到的是value值取值为0;由于AtomicInteger对象创建value变量便会赋值为0;
var5: 即旧的预估值;
      即value旧的预估值取值,即也是0;即var1+var2所在内存中查找到的value最新值与当前var5旧的预估值是相等的此时;
      如果var1+var2在内存当中查找得到的value最新值 与 当前var5旧的预估值的取值一致,即相等;
      那么var5+var4所加获得的值就会被赋值到内存当中的value取值上来,成为内存当中value的最新值;
      即这个内存当中value的最新值就会变成var5+var4的和值;
      当前var5旧的预估值为0;
      var4即为AtomicInteger.java类当中的方法incrementAndGet方法中所传入的值1;
      所以即也就是自增一的操作;
      当var5与var4一加法运算完成之后那么得到新值1;
      那么也就是该新的值1也就会赋值给内存当中value变量的取值;
      因为当前是比较compare成功即var1+var2所查询获取得到的内存最新值0与旧的预估值var5 0处于两者相等的情况所以匹配成功了(比较成功之后进行了替换内存中value取值的一个操作,替换的值为var5+var4;即CAS需要的三个取值:内存中的值V、旧的预估值X、要修改的新值B;这个时候就属于旧的预估值+var4=要修改的新值B并替换掉了内存中的值V);
      那么这个时候this.compareAndSwapInt(var1, var2, var5, var5+var4)就会返回true;
      由于返回的是true,而while(! this.compareAndSwapInt(var1, var2, var5, var5+var4))当中的this.compareAndSwapInt(var1, var2, var5, var5+var4)加了! 表示否定;
      即while(false)则表示do{}whille()结束循环;
      即t1线程一次性设置自增1成功;
      那么此时t1线程就完成了自增1的操作,结束任务;
var4: 常量1;在AtomicInteger.java的incrementAndGet方法当中所赋值1;

假设现在切换到t2线程当中来了;
而之前是当t2线程拿到旧的预估值,
也就是this.getIntVolatile(var1, var2)拿到取值之后并且赋值给了t2线程当中的工作内存中的var5旧的预估值变量之后才进行CPU的切换的,
即CPU当时切换到了t1线程;
需要注意的是在CPU切换到t1线程之前t2线程中获取得到var5旧的预估值为0;
且当CPU切换到t1线程之后完成了自增1的操作即比较内存value取值与t1线程中的工作内存中的var5旧的预估值取值相同的时候完成了交换SwapInt的操作即var5+var4替换掉或者说重新赋值给了内存当中value的取值了,即1;
那么也就是说此时CPU切换回来线程t2,
而t2线程的工作内存中var5的取值为0;
而此时内存当中的value取值为1;
也就是两者取值不相同;
就无法进行compareAndSwapInt交换的操作即无法完成value自增的操作;

t2线程之前的var5取值为0;那么t2线程也会来进行执行while(!this.compareAndSwapInt(var1, var2. var5, var5+var4));当中的compareAndSwapInt(var1, var2. var5, var5+var4)方法;
此时分析在t2线程中执行的时候compareAndSwapInt(var1, var2. var5, var5+var4)方法当中的四个参数;
var1 与 var2: 即this+valueOffset即找到内存当中最新的预估值value,即此时内存当中value取值为1;
而旧的预估值即var5;t2线程获取得到的其取值为0;
那么此时就发现 内存value的最新值1与旧的预估值var5的取值不一样;
不一样则此时就不会进行对内存当中的value进行更新取值;并且返回一个false;
即对于compareAndSwapInt(var1, var2. var5, var5+var4)操作返回的false;
而while(!compareAndSwapInt(var1, var2. var5, var5+var4))中该方法前是!,即表示while(!false) ---> while(true) 即继续执行do-while循环;
这次虽然内存当中value的最新值与t2线程中var5旧的预估值的取值不一样;
但是false前面加了一个非即!;那么就会变成true;也就是说while(true)则将继续进行do-while循环;
则此时t2循环会接着这个do-while循环又进入到do{}块当中;进行执行通过var1与var2即this+valueOffset调用方法this.getIntVolatile()方法获取找到得到内存当中value的最新值1并赋值给t2线程的工作内存中var5变量;即此时t2线程var5变量为1;继续执行代码while(!compareAndSwapInt(var1, var2. var5, var5+var4))中的compareAndSwapInt(var1, var2. var5, var5+var4);
那么这次需要注意的是此时内存当中value的最新值为1;
而t2线程刚刚所找的旧的预估值var5也是1;
那么这一次的时候t2线程当中的旧的预估值var5与内存当中value的最新值相同都为1;
即内存最新值与旧的预估值相同那么就需要完成交换操作,
即对内存中的value取值进行重新赋值为var5+var4,
对内存当中value取值赋值一个最新值,
那么这个最新值就是var5+var4(var5即旧的预估值1;var4为常量1);
得到运算结果为2并赋值给当前内存当中的value取值
(也就是说需要进行修改的最新值为2);
那么由于内存最新值与旧的预估值一样那么就会将var5+var4,即2 最新取值赋值给当前内存value变量取值;
即当前内存当中value的取值为2;完成一次自增效果;
当赋值成功之后;
并且compareAndSwapInt(var1, var2. var5, var5+var4)方法会返回true;
那么一旦compareAndSwapInt(var1, var2. var5, var5+var4)返回true,而while(!compareAndSwapInt(var1, var2. var5, var5+var4))中存在有一个非!的操作;那么也就是while(false)至此相对于t2线程来说整个do-while循环就结束了,完成一次自增操作;
那么此时就明白了CAS的原理;
CAS主要是靠三个变量的取值;
一个是内存当中的最新值;一个是旧的预估值;还有一个即新的要修改的值;
如果当前内存当中的最新值与旧的预估值取值一样那么就把新的要修改的值赋值给内存当中的该最新值;并且返回tue;以至于while(!true),即while(false)导致该do-while循环不再继续;
如果当前内存当中的值与旧的预估值不一样;那么就会返回false,以至于while(!false),即while(true)继续执行do-while循环;重新再一次拿到旧的预估值var5;并且再一次进行compareAndSwapInt操作,拿到最新的内存取值去进行比较以及是否交换内存取值,如果成功就不再进行循环如果不成功就继续进行do-while循环再次拿到内存最新值与旧的预估值进行比较...............;
那么以上即为CAS的原理;

乐观锁和悲观锁

从思想角度进行区分;

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

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,(这样的话就只有一个线程进来别的线程没有锁无法进入,即别的线程会阻塞,那么也就保证数据操作没有问题)这样别人想拿这个数据就会阻塞。
因此synchronized也将其称之为悲观锁
JDK中的ReentrantLock也是一种悲观锁
性能较差!
(总有刁民想害朕)

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

(乐观锁在对数据进行操作的时候总是很乐观的认为:当前线程进行操作的时候没有其他的线程来进行修改即干扰到当前线程的操作;因此乐观锁的操作是不上锁;但是也不能什么也不管,以防万一;那么乐观锁虽然在设置取值的时候不加锁但是会进行一个判断,即旧值与新值的判断; 即在设置前稍微做一个判断即旧的预估值与内存当中的最新取值是否一致;如果一致则直接设置如果不一致则说明有别的线程已经修改过,那么没有关系,当前线程进行重新循环,重新去获取内存中的最新值;重新来进行设置一次或者多次即可,即也就是重新尝试重新尝试重新尝试…)

CAS这种机制也可以将其称之为乐观锁,综合性能较好
(在修改的时候不上锁;只是做一个判断,不行则再次判断一次即可)

CAS可以保证变量在设置取值的时候可以保证其操作的一个原子性;
为了使得在线程间变量取值状态变化可见,
那么需要给该变量添加volatile关键字进行修饰;
volatile可以结合CAS机制实现无锁并发;
保证变量在修改时的原子性;
需要注意的是CAS只适合在竞争并不太激烈、多核CPU的情况场景下进行使用;
CAS之所以效率高是因为在其内部没有使用synchronized关键字;
那么也就是说CAS不会让线程进入阻塞状态;
那么也就避免了synchronized当中用户态和内核态的切换所带来的的性能消耗问题也避免了线程挂起等资源的消耗问题;
如果竞争非常激烈,那么CAS就会出现线程大量重试,因为多线程来进行竞争,那么也就导致有可能很多的线程设置取值失败,那么又要进行while循环重试;即大量的线程进行重试操作;成功存的线程反而不多;那么这样的话反而会会浪费性能,即性能变低;
所以如果竞争太激烈还使用的是CAS机制那么就会导致其性能比synchronized还会低;

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

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

小结

CAS指的是Compare And Swap;会拿旧的预估值与内存当中的最新值进行比较;如果相同就进行交换并且把最新的值赋值到内存当中的这个变量;
CAS的作用?Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证(由CPU支持)。
CAS的原理?CAS需要三个值:内存地址V、旧的预期值A、要修改的新值B,如果内存地址V和旧的预期值A相等就修改内存地址的值为B(即新的值B赋值到内存当中去);

CAS的作用是什么?

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

CAS的原理是什么?

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

synchronized优化锁升级过程

在JDK1.5之前synchronized只包含有一种锁即monitor重量级锁;
所以在JDK1.5之前其效率是比较低的;
另外在JDK的源码当中大量的使用到了synchronized;
包括java开发的时候也会经常使用到synchronized;
虚拟机开发团队就意识到了这个问题;
因此在JDK1.6这个版本当中对synchronized做了重要改进;
在JDK1.6当中synchronized就不仅仅只有monitor这一种重量级的锁了;
包括偏向锁、轻量级锁、适应性自旋、锁消除、锁优化等机制;
另外到转变成重量级锁之前会有一个适应性自旋的过程进行抢救一下;
这些机制的目的就是为了能够让synchronized的效率得到提升;

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

无锁 —→ 偏向锁 —→ 轻量级锁 —→ 重量级锁
在JDK1.6之后不是说直接就会变成重量级锁了;
而是会有一个过程;
首先对象是无锁状态;然后如果需要进行加锁那么就会进行添加一个偏向锁;
如果偏向锁无法满足不行的话就会换成轻量级锁;
如果轻量级锁不行的话就有可能会进入适应性自旋的过程;
如果通过适应性自旋依然没有抢到锁则换成重量级锁;
即会有这样一个锁升级的过程;
还需要注意的是JDK1.6当中既然存在有这些不同的锁;
那么必然是这些锁有其存在的不同场景下才适用;
并不是说这个锁是万能的;

synchronized优化-对象的布局

回顾并引入

JDK1.6时对synchronized做了很多的优化;
锁升级过程为 无锁 —→ 偏向锁 —→ 轻量级锁 —→ 重量级锁
那么这个锁升级的过程当中就会遇到锁存在有很多的状态;
那么这些锁的状态存在在哪里呢?
那么这些锁的状态也就存在在java对象的布局当中;
以前认为java当中存在成员变量,也称之为实例数据;
那么其实在JVM当中对象不仅仅会存放实例数据;
对象总共由三部分组成:对象头、实例数据(成员变量等)和对齐数据;

Java对象的布局

目标

学习java对象的布局

术语参考:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

对象头

对象头由两部分组成;
synchronized锁可能有很多的状态,那么这些状态都是靠对象头来进行存储的;

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

在Hotspot虚拟机当中对象头又分为两种;
一种是普通对象的对象头即instanceOopDesc;
另外一种是描述数组的对象头即arrayOopDesc;
那么当前仅关心普通对象的对象头即instanceOopDesc;

Hotspot采用 instanceOopDesc和arrayOopDesc来描述对象头;
arrayOopDesc对象用来描述数组类型。
instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,
另外, arrayOopDesc的定义对应 arrayOop.hpp。

instanceOop.hpp记录了对象头的信息;
instanceOopDesc当中没有很多代码;
但是可以发现instanceOop继承了父类oopDesc;
那么这个时候可以去看下其父类oopDesc中的代码;

# 截取部分代码
class instanceOopDesc : public oopDesc{
  public:
    // aligned header size.
    static int header_size(){return sizeof(instanceOopDesc)/HeapWordSize; }

    // If compressed. the offset of the fields of the instance may not be aligned.
    static int base_offset_in_bytes(){
      /* offset computation code breaks if useCompressedClassPointers
         only is true
      */
      return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc);
    }

    static bool contains_field_offset(int offset, int nonstatic_field_size){
      int base_in_bytes = base_offset_in_bytes();
      return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size = heapOopSize);
    }
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,
oopDesc的定义在Hotspot源码中的 oop.hpp文件中;

  class oopDesc{
      friend class VMStructs;
    private :
      volatile markOop _mark;
      union _metadata{
        Klass*       _klass; # 没有开启指针压缩时的类型指针
        narrowKlass  _compressed_klass; # 开启了指针压缩
      } _metadata;
  
      // Fast access to barrier set. Must be initialized.
      static BarrierSet* _bs;

      // 省略其他代码
  };

在openjdk当中Klass*与narrowKlass有相关的介绍;

klass pointer
The second word of every object header.
Points to another object(a metaobject) which
describes the layout and behavior of the original object.
For java objects, the “Klass” contains a C++ style “vtable”.

mark word
The first word of every object header.
Ususlly a set of bitfields including synchronization state
and identity hash code.
May also be a poiter(with characteristic low bit encoding)
to synchronization related infomation.
During GC,may contain GC state bits.

object header
Common structure at the beginning of every GC-managed heap object.
(Every oop points to an object header.)
Includes fundamental information about the heap object’s layout, type , GC state, synchronization state, and identity hash code.
Consists of two words.
In arrays it is immediately followed by a length field.
Note that both java objects and VM-internal objects have a common object header format.

-----------------------------------------------------------------
|                          Xxx 对象                               |
|                          -------------------                    |
|                       ---|  Mark     Word  |  markOop   _mark   |
|-------------------   |   -------------------                    |
|| instanceOopDesc |-------|  Klass pointer  |  Klass*    _klass  |
|| 对象头          |       --------------------                   |
|-------------------       |  实例      数据  |                   |
|                          --------------------                   |
|                          |  对齐      数据  |                   |
|                          --------------------                   |
-----------------------------------------------------------------

在 普通示例对象 中,oopDesc的定义包含两个成员,分别是_mark_metadata

_mark表示对象标记、属于markOop类型,也就是接下来要讲解的Mark Word,它记录了对象和锁有关的信息;
_metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针;
_compressed_klass表示压缩类指针。

对象头由两部分组成;
一部分用于存储自身的运行时数据,称之为Mark Word;
另一部分是类型指针,即对象指向它的类元数据的指针。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

Mark Word对应的类型是markOop。
源码位于 markOop.hpp中。

在32位状态下以及64位状态下每一位分别代表什么;

--------------------------------------------------------------------
// Bit-format of an object header (most significant first , big endian layout below):
//
// 32 bits:
// ---------
//           hash:25 -------------->| age:4   biased_lock:1 lock:2 (normal object)
//           JavaThread*:23 epoch:2   age:4   biased_lock:1 lock:2 (biased object)
//           size;32 ------------------------------------------->| (CMS free block)
//           PromotedObject*:29 ------------> promo_bits:3 ------>| (CMS promoted object)
//
// 64 bits:
// ----------
// unused:25 hash:31 -->| unused:1    age:4   biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1    age:4   biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3----->| (CMS promoted object)
// size:64 ---------------------------------------------------->| (CMS free block)

# 偏向锁的时候其第三位是1;(0、1、2、3的顺序)
//   [JavaThread* | epoch | age | 1 | 01]   lock is biased toward given thread
//   [0           | epoch | age | 1 | 01]   lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
# 当第三位为0的时候则表示不是偏向锁;那么就看其后两位;00表示轻量级锁;01表示无锁;10即monitor表示重量级锁;
//   [ptr             | 00]  locked         ptr points to real header on stack           # 轻量级锁
//   [header      | 0 | 01]  unlocked       regular object header                        # 无锁
//   [ptr             | 10]  monitor        inflated lock (header is wapped out)         # 重量级锁
//   [ptr             | 11]  marked         used by markSweep to mark an object
//                                          not valid at any other time
--------------------------------------------------------------------

|---------------------------------------------------------------------------------|--------------------|
|                              Mark Word(64 bits)                                 |        State       |
|---------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcoder:31 | unused:1 | age:4 | biased_lock:1 | lock:2   |        Normal      |
|---------------------------------------------------------------------------------|--------------------|
| thread:54 |        epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2   |        Biased      |
|---------------------------------------------------------------------------------|--------------------|
|                   ptr_to_lock__record:62                             | lock:2   | Lightweight Locked |
|---------------------------------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:62                          | lock:2   | Heavyweight Locked |
|---------------------------------------------------------------------------------|--------------------|
|                                                                      | lock:2   | Marked  for  GC    |
|---------------------------------------------------------------------------------|--------------------|


64位虚拟机下,Mark Word64bit大小的,其存储结构如下:

(在不同的锁情况下每一位的作用不一样)

锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

锁状态25bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

一般网络上是32位居多;
现在的虚拟机绝大多数使用的是64位虚拟机;
所以现下关注的是64位虚拟机MarkWord的存储结构;
关于上述32or64位虚拟机的mark word是多少bit的存储结构等信息在markOop.hpp当中有详细介绍;

Klass pointer 类型指针;

Klass pointer

java当中的对象肯定是由某个类所产生的;
那么Klass pointer就是用来表示该对象是哪一个类所产生的;
Klass pointer会保存这个类的元信息;
Klass pointer根据虚拟机大小其自身大小也不一样;
如果是32位虚拟机则其Klass pointer的大小为32位;
如果是64位虚拟机则其Klass pointer的大小为64位;
一般来说虚拟机都会开启指针压缩;
即也就是说在64位虚拟机情况下其Klass pointer也会被压缩成32位;

这一部分用于 存储对象的类型指针,
该指针 指向它的类元数据,
JVM通过这个指针确定对象是哪个类的实例。
该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位;

如果应用的对象过多,使用64位的指针将大量浪费内存,
统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。
为了节约内存可以使用选项-XX:+UseCompressedOops开启指针压缩;
其中,oop即ordinary object pointer普通对象指针。
开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩;
一些特殊类型的指针JVM不会优化,
比如执行PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)
在32位系统中,Mark Word = 4 bytes, 类型指针 = 4bytes, 对象头 = 8bytes = 64 bits;
在64位系统中,Mark Word = 8 bytes, 类型指针 = 8bytes, 对象头 = 16bytes = 128 bits;

实例数据

就是类中定义的成员变量。

对齐填充

(可能有可能没有;如果java对象整体大小为8字节的整数倍那么这个时候就不需要对齐填充;如果java对象整体大小不为8字节的整数倍那么这个时候就需要对齐填充的一些填充数据使之对齐从而变成8字节的整数倍;那么这样是方便操作系统来进行寻址的);
对齐填充并不是必然存在的,也没有什么特别的意义,它仅仅起着占位符的作用。
由于HotSpot VM的自动内存管理系统 要求 对象起始地址必须是8字节的整数倍。
换句话说,就是对象的大小必须是8字节的整数倍。
而对象头正好是8字节的整数倍,
因此,当对象实例数据没有对齐时,就需要通过对齐填充来补全。

查看java对象布局

之前是通过观察JVM源码的方式来进行查看的接着来进行验证一下;
工具类;openjdk提供叫做jol;
java对象布局的工具;可以来查看java对象布局;

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <aratifactId>jol-core</aratifactId>
  <version>0.9</version>
</dependency>
--------------------------------------------------
package com.xxx.demo06_object_layout;

public class LockObj{
  private int x;
}
--------------------------------------------------
package com.xxx.demo06_object_layout;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    LockObj obj = new LockObj();

    //parseInstance 解析实例对象;
    //toPrintable进行打印其解析的实例对象信息
    ClassLayout.parseInstance(obj).toPrintable().;
  }
}
----------------------------------------------------
打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-----------------------------------------------------
分析:
从整个对象开始;
OFFSET偏移0;SIZE为4表示用了4个字节去进行描述DESCRIPTION对象头object header信息;
OFFSET偏移4;SIZE为4表示用了4个字节也是去进行描述DESCRIPTION与对象头object header相关的内容;
OFFSET偏移8;SIZE为4表示用了4个字节也是去进行描述DESCRIPTION与对象头object header相关的内容;
那么此时来看对象头object header所占了3 * 4 = 12个字节来进行描述与对象头object header相关信息;
与之前所说:在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128 bits;此处存在偏差;
由于也是64位操作系统但是打印得到其对象布局信息得到的对象头所占字节信息不一致;
实际上打印出来对象头object header所占字节数并不是16个字节而是12个字节;
那么为什么打印出来的对象布局信息中的object header对象头信息所占字节不是16个字节而是12个字节呢?
那么这是因为JVM默认自动开启了指针压缩的选项参数;
从而也就导致了JVM在默认情况下去进行打印对象布局当中的对象头信息所占字节为12个字节而不是16个字节;
所以此时可以尝试进行关闭指针压缩进行尝试;
选项-XX:+UseCompressedOops是用来进行开启指针压缩;
所以此时可以用-XX:+UseCompressedOops拿过来来作为JVM的一个参数;
即打开idea的Run/Debug Configurations中填入到VM options当中即可;
那么此时要注意的是-XX:+UseCompressedOops是用来开启指针压缩的;
而JVM默认就是开启指针压缩的;而此时要做的是让JVM关闭指针压缩;
所以此时VM options当中填入的即为:-XX:-UseCompressedOops;
-XX:-UseCompressedOops即 - 为关闭指针压缩了;
-------------------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     d8 34 5a 25 (11011000 00110100 01011010 00100101) (626668760)
     12      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16      4    int   LockObj.x           0
     20      4        (loss due to the next object alignment)
 Instance size: 24 bytes
-------------------------------------------------------------------
 此时就可以分析看到对象布局当中object header对象头所占字节为16个字节;
 其中;后面8个字节即第三排和第四排的object header是用来表示Klass pointer,也就是这个对象所对应的这个类的元信息;
 LockObj.x在偏移OFFSET为16的时候所占字节为4个字节;
 那么此时所累加字节数为 16个对象头信息所占字节数 + LockObject.x int类型变量所占字节数为4个字节 = 累计当前字节数为 20 个字节;
 (那么当前此时是不满足对象头信息总占字节数需要为8个字节的整数倍这一限制条件的;)
而20个字节并不是8字节的整数倍;
所以此时就有了填充数据;即最后一排的4个字节,加了4个字节的填充数据;即:
 20      4        (loss due to the next object alignment)
那么将关闭指针压缩参数进行去掉再次分析原来的打印结果:
VM options: -XX:+UseCompressedOops
------------------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-------------------------------------------------------------------
从上面的分析可得此时Klass pointer的指针只占了4个字节;即:
8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
整个对象布局当中对象头信息所占字节数为12个字节;
再加上LockObj当中的int类型变量占有4个字节;
所以此时累计总共占有字节数为16个字节;为8字节的整数倍;
所以此时并没有填充数据;
那么再做测试在LockObj类当中再写一个变量private boolean b;
那么布尔类型的变量在java当中所占字节是为1个字节;
---------------------------------------------------------
package com.xxx.demo06_object_layout;

public class LockObj{
  private int x;
  private boolean b;
}
--------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12      4    int   LockObj.x           0
     16      1  boolean LockObj.b           false
     17      7    int (loss due to the next object alignment)
 Instance size: 24 bytes
--------------------------------------------------------
此时分析到:boolean布尔类型在其中占用到了1个字节数;
那么此时累加到字节数即为:12个对象头信息所占字节数 + LockObj当中int类型变量所占字节数4个字节 + LockObj当中boolean类型所占字节数为1个字节 = 总计字节数为17个字节;
而17个字节数显然是不满足8字节的整数倍这一说法;
所以就存在有最后一排的对齐填充数据占有7个字节进行平衡;即:
17      7    int (loss due to the next object alignment)
填充了7个字节之后导致数据实例大小为24个字节满足8字节的整数倍这一说法;
另外还有一个问题,在64位虚拟机下,Mark Word是64bit大小的;
其中是存在有31位导致hashCode的;但是根据上述的打印结果来看好像是并没有看到什么有关hashCode相关的内容;
8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
除了地址之外没有看到有关hashCode相关的内容;

hashCode的取值不是一来就有的;
而是先需要在代码层当中使用到了这个hashCode那么它才会去进行保存这个hashCode取值;
也就是说生成LockObj obj对象之后,需要去代码层当中调用该obj对象的.hashCode()方法;那么在打印结果当中才能够看到有关hashCode的信息;
-------------------------------------------------------------
package com.xxx.demo06_object_layout;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    LockObj obj = new LockObj();

    //调用obj对象的hashCode()方法
    obj.hashCode();

    //parseInstance 解析实例对象;
    //toPrintable进行打印其解析的实例对象信息
    ClassLayout.parseInstance(obj).toPrintable().;
  }
}
-----------------------------------------------------------
再次打印对象布局:
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
      4      4        (object header)     6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
      8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12      4    int   LockObj.x           0
     16      1  boolean LockObj.b           false
     17      7    int (loss due to the next object alignment)
 Instance size: 24 bytes
-----------------------------------------------------------
再次分析对象布局的打印信息当中此时存在VALUE当中的一些数据;
那么这些数据就代表着hashCode;
即:
01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
那么如何判断上述是否真的为hashCode呢?那么来进行一下验证;
-----------------------------------------------------------
package com.xxx.demo06_object_layout;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    LockObj obj = new LockObj();

    //调用obj对象的hashCode()方法
    obj.hashCode();
    //该打印的为十进制的hashCode
    System.out.println(obj.hashCode());
    //换一种打印方式;使用Integer.toHexString()将十进制hashCode转换成十六进制的hashCode
    System.out.println(Integer.toHexString(obj.hashCode()));

    //parseInstance 解析实例对象;
    //toPrintable进行打印其解析的实例对象信息
    ClassLayout.parseInstance(obj).toPrintable().;
  }
}
-----------------------------------------------------------
再次输出结果并打印对象布局:
1836019240    # 10进制hashCode
6d6f6e28      # 16进制hashCode
com.xxx.demo06_object_layout.LockObj object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
      4      4        (object header)     6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
      8      4        (object header)     43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12      4    int   LockObj.x           0
     16      1  boolean LockObj.b           false
     17      7    int (loss due to the next object alignment)
 Instance size: 24 bytes
 -----------------------------------------------------------
 分析此时发现十六进制位的hashCode:6d6f6e28与
 01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
 6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
 对比起来有些茫然;顺序不一致(尽管对象布局的对象头信息打印中出现了这些字符串);
这里需要稍微注意一下;有一个大端和小端的问题;
比如说当前存在有64位;
64位有大端和小端的区别;
第一种情况
    1       2        3        4        5         6        7       8
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
第二种情况
    8       7        6        5        4         3        2       1
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
那么刚才在64位JVM当中看到的是属于哪一种呢?
即为下面第二种情况当中的;也就是说64位的Mark Word
    8       7        6        5        4         3        2       1
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
前面25位是没有进行使用的;中间的话存在31位是被用来表示hashCode的;
后面倒数第三位表示偏向锁;最后两位表示其他锁的标志位;
但是java当中通过jol工具进行打印出来的对象布局当中的hashCode信息是反过来的;
即所以需要反过来进行查看;即
 01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
 6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
 需要反过来看即
 (忽略前面的00 00 00前面的这25位;中间的6d 6f 6e 28这31位才是用来表示hashCode的;):
 01 28 6e 6f (00000001 00101000 01101110 01101111) (1869490177)
 6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
即hashCode为6d 6f 6e 28(01101101 01101111 01101110 00101000)
至此也是使用了java工具来进行验证了对象的布局;
锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

小结

java对象由三部分组成:对象头、实例数据、对齐数据;
对象头分为两部分:Mark Word + Klass pointer;

synchronized优化-偏向锁

锁升级过程

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

无锁–→偏向锁–→轻量级锁–→重量级锁

无锁并不会直接进入到重量级锁当中去;而是先进入偏向锁;

偏向锁

目标

学习偏向锁的原理和好处

什么是偏向锁

偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,
(代码)在大多数情况下,锁不仅不存在多线程竞争而且总是由同一线程多次获得(即由同一个线程反复的得到锁释放锁得到锁释放锁;如果一上来就是重量级锁的话那么得到锁就需要花费性能释放锁也需要花费性能),为了让线程获得锁的代价更低,引进了偏向锁。
减少不必要的CAS操作。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

不过一旦出现多个线程竞争时,必须撤销偏向锁,
所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,
不然就得不偿失了。

如果是偏向锁则倒数第三个字节会变成1;另外由前面56个字节当中的前54个字节会来保存偏向锁的id;另外56个字节当中其余两个字节用来保证Epoch即时间;

锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

但是需要注意的是这个偏向锁仅限用于没有竞争的状态;
也就是说反复是同一个线程获得锁释放锁;

示例:

package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
// 循环5次并且进出同步代码块就只有一个线程;
// 那么这种情况就适合使用偏向锁;
// 即反复是同一个线程进入同步代码块的情况;
// 但是如果遇到有线程来进行竞争那么即立即要撤销掉偏向锁从而升级到轻量级锁;

偏向锁原理

当线程第一次访问同步代码块并获取锁时,偏向锁处理流程如下:

  1. 检测Mark Word是否为 可偏向状态,即是否为偏向锁1,锁标识为为01。
  2. 若为 可偏向状态,则测试线程ID是否为当前线程ID,如果是,执行同步代码块,否则执行步骤(3)
  3. 如果测试线程ID不为当前线程ID,则通过CAS操作将Mark Word的线程ID替换为当前线程,执行同步代码块
  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式
  2. 同时使用 CAS操作 把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步代码块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

首先虚拟机会进行检查这个偏向锁的状态即倒数第三位取值是否为0;
是0的话那么也就代表着可以进行偏向,将倒数第三位修改成1表示偏向锁;那么虚拟机也就会将其倒数两位数改成01;即变成偏向锁;
那么还会通过CAS操作将对象的Mark Word当中的前54位修改为当前获取得到偏向锁的线程的Thread ID;那么到此时这个偏向锁就设置成功了;
那么以后的话线程退出同步代码块的时候并不需要做任何操作而下一次循环当中重新进入同步代码块时,只需要进行判定一下THREAD ID即Mark Word当中的前54位当中的取值即THREAD ID与当前该要获取锁的线程的THREAD ID是否相同;
如果是一样的话那么则进入同步代码块当中;无需其他操作;
所以偏向锁它在一个线程的情况下其效率还是很高的;

锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
/**
线程进过5次循环每一次循环都进入到同步代码块当中;
对象锁为obj;此时只有一个线程按道理应该是使用的偏向锁;
另外在同步代码块当中进行打印的对象的对象布局信息;以此用来检查是否存在偏向锁的标记;
*/
------------------------------------------------------------
打印结果:
com.lang.Object object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
分析:
0      4        (object header)     d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
当中的
d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
当中的
d8 11011000 即最终的第8个字节即
    8       7        6        5        4         3        2       1
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
其实就是
    1
00000000
显示到前面去了即
    1       2        3        4        5         6        7       8
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
这个样子;
即d8 11011000代表的就是是否是偏向锁,即锁的标志位;
那么d8其8位当中的最后三位即000;那么000是属于偏向锁吗;不是;
那么为什么不是偏向锁呢?原因在于:偏向锁虽然在jdk1.6的时候偏向锁是开启的;
但是这个默认开启的偏向锁并不是立马可以进行使用的;
所以这个时候又需要添加一个JVM参数即在VM options处填入参数:-XX:BiasedLockingStartupDelay=0;
让其原始值为0即程序一启动那么偏向锁就生效;
那么再次运行尝试:
-------------------------------------------------------------------
再次运行打印结果:
com.lang.Object object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     05 90 61 27 (00000101 10010000 01100001 00100111) (660705285)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-----------------------------------------------------------------
分析:
程序首先由主线程从main方法开始进行执行并在main方法当中创建一个新线程叫做mt并启动它;用t1表示新线程mt;
那么新线程t1就会执行到run方法当中去进行5次循环并在每次循环当中进入同步代码块且打印其对象布局中对象头等对象布局信息;
此时只有一个线程没有其他线程来进行竞争操作;
那么此时t1第一次来进行执行同步代码块synchronized的时候就会去看这个对象头obj当中它的锁标记是什么;
那么一开始的时候该对象头是属于无锁状态即倒数第二位为0(表示不为偏向锁状态;即无锁状态);
那么这个时候由于要将无锁状态改为偏向锁状态那么此时就会将倒数第三位改成1(即偏向锁状态;表示转成偏向锁状态);
另外前56位当中的前54位会被用来保存这个偏向锁的id;以及前56位当中的后两位用来进行保存Epoch即相关时间;
那么这个时候就让t1线程进入了同步代码块当中(其实也就表示设置好了偏向锁,要进入同步代码块当中进行执行;然后进行打印对象布局信息中即101;
也就是0      4        (object header)     05 90 61 27 (00000101 10010000 01100001 00100111) (660705285)中的05 00000101其8位的倒数三位101;也就表示转变成了偏向锁状态);
当执行完成打印对象布局信息即也就是完成同步代码块中的任务要出同步代码块时,退出同步代码块时这个偏向锁就并不用做什么事情;
那么当下一次循环时(总共有5次循环),该线程t1又要从同步代码块当中来进行获取锁;
它就会发现对象锁obj当中即为一个偏向锁的状态;
并且只要去进行对比一下这个线程Id即前56位当中的54位的值是否取值相等即THREAD id是否与当前这个要获取锁的线程的THREAD id相同;
如果是,那么就直接进入到同步代码块当中;
即有了偏向锁之后,那么后面的循环中当前该线程再次去获取锁就变得很简单了只需要(对比一下THREAD ID是否一致即可)两个动作
(1: 检查倒数第三位是否是偏向锁标志,如果是则执行第二个动作;如果不是则将倒数第三位的数值改为1即表示偏向锁;2: 首先检查前56位当中的前54位的THREAD ID是否有取值;如果没有取值则设置为当前的该线程ID;如果存在取值THREAD ID则将当前要获取锁的线程进行与前54位中的THREAD ID进行比较,如果相同则直接可以进入到同步代码块当中);
退出循环之后每次再次进行循环需要进入同步代码块时之前对比一下THREAD ID是否一致即可;
所以偏向锁可以看到在一个线程执行同步代码块的情况下其效率是非常高的(较之重量级锁要高很多);

偏向锁的撤销

什么时候撤销偏向锁

一旦存在有两个线程来进行锁的时候那么就会撤销偏向锁;

示例
package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();

    MyThread mt2 = new MyThread();
    mt2.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
/**
假设在这当中存在有两个线程;
也就意味着有两个线程会去进行run()当中的synchronized;
那么两个线程来进行执行的时候就需要将该偏向锁给进行撤销;
*/

偏向锁撤销过程

  1. 偏向锁的撤销动作必须等待全局安全点
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态;

TIPS: 全局安全点
假设当前需要进行统计超市在9点的时候超市当中有多少人;
那么应该如何统计?
即在9点的时候,让在超市当中的所有人都停下来;不要有人进入超市也不要有人出超市;那么这个时候才来进行统计;
那么这个线程安全点指的就是在这个点的时候所有的线程都会进行停下来;那么也就叫做全局安全点;那么只有到了全局安全点的时候才能来进行撤销偏向锁;

偏向锁 在 java1.6 之后是默认启用的;
但是在应用程序启动几秒钟才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,
如果确定应用程序所有锁通常情况下处于竞争状态,
可以通过-XX:-UseBiasedLocking=false参数关闭偏向锁

偏向锁好处

偏向锁适合使用在只有一个线程来获取锁的时候进行使用;
即没有竞争情况;那么在这种情况下一个线程反复进入同步代码块退出同步代码块的效率是很高的;只要进行判断对象头当中的线程id即THREAD ID跟现在要获取锁的线程的THREAD ID是否相同即可;如果相同则进入同步代码块;退出同步代码块时也不需要做什么事情;所以性能高;
但是这个偏向锁不一定总是好的;
如果存在有很多的线程来竞争锁;那么这个时候偏向锁就起不到什么作用了反而会影响效率;因为每次撤销一次偏向锁都必须要等待全局安全点所有线程都会停下来才能够进行撤销偏向锁,所以反而还会影响性能;
比如说使用线程池来执行代码的时候,那么这个时候知道线程池当中肯定有多个线程反复去执行同样的任务,同样的代码即反复的去竞争同一把锁;那么在这个时候偏向锁就是多余的了;
注意在JDK1.5的时候偏向锁是默认关闭的;而在JDK1.6的时候偏向锁是默认开启的;如果不需要偏向锁可以通过启动参数-XX:-UseBiasedLocking=false来进行关闭偏向锁;让其直接进入重量级锁;

偏向锁是在 只有一个线程执行同步块时 进一步提高性能,适用于一个线程反复获得同一把锁的情况。
偏向锁可以提高带有同步但无竞争的程序性能。

它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利。
如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

在JDK1.5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。
但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,
如果确定应用中所有锁通常情况下处于竞争状态,可以通过-XX:-UseBiasedLocking=false参数关闭偏向锁;

小结

偏向锁的原理是什么?

当锁对象第一次被线程获取的时候,
虚拟机将会把对象头中的锁标志位设置为“01”,即偏向模式。
同时使用CAS操作把获取得到的这个锁的线程的ID记录在对象的Mark Word之中,
如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

当线程第一次来获取锁的时候,
那么虚拟机就会把对象头设置为01,且偏向锁设置为1;
那么这个时候就记录了偏向模式;同时通过CAS操作将前面的54位设置为获取了偏向锁的线程的THREAD ID;那么如果设置成功则说明该线程获取得到了该偏向锁;
那么后续的进入同步代码块时效率就会变高了;

偏向锁的好处是什么?

偏向锁适用于一个线程反复进入同步代码块的情况;
即没有锁竞争的情况下;那么这个时候是可以提高一个线程进入同步代码块时的效率的;但是如果存在竞争那就不行了;

偏向锁是在 只有一个线程 执行同步代码块时 进一步提高性能,
适用于 一个线程反复获得同一个锁的情况。
偏向锁可以提高带有同步但是无竞争的程序性能。

synchronized优化-轻量级锁

目标

学习轻量级锁的原理和好处

什么是轻量级锁

当偏向锁出现竞争的时候,会撤下偏向锁从而升级到轻量级锁;
轻量级锁是JDK1.6当中为了优化synchronized而引入的一种新型锁机制;
需要注意的是轻量级锁不是任何情况下其开销比较的小而是在特定的情况下才开销比较小;所以轻量级锁并不能够用来代替重量级锁;轻量级锁只是在一定的情况下来进行减少消耗;

轻量级锁是 JDK1.6之中加入的 新型锁机制,
它名字中的“轻量级”是相对于使用monitor的传统锁而言的,
因此传统的锁机制就称为“重量级”锁。
首先需要强调的一点是,轻量级锁并不是用来代替重量级锁的;

引入轻量级锁的目的: 在多线程交替执行同步块的情况下,(引入轻量级锁)尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要代替重量级锁。

(也就是说在多线程交替执行同步块的时候轻量级的性能才是比较好的)
假设在餐厅当中,有多个桌椅以便于用来用户用餐;
假设现在有第一个人来进行到一号座当中用餐,用餐完毕之后离开一号座;
那么当一号座的用户离开之后现在又来了第二个用户又坐到了一号座当中进行用餐,同样用餐完毕之后离开一号座;紧接着第二个用户离开之后第三个用户又来了且也是坐在一号座的位置上进行用餐;可以看到的是这三个用户是分别不同时刻来进行用餐的;即也就是交替进行的没有竞争的状况;那么如果说一号座的风景比较好又处于下班用餐的高峰期,同一时刻三个用户同时来进行竞争一号座的用餐;那么这个时候就出现了竞争;也就不适合使用轻量级锁了;

轻量级锁原理

当关闭偏向锁功能 或者 多个线程竞争偏向锁导致偏向锁升级为轻量级锁,
则会尝试获取轻量级锁,其获取锁步骤如下:

  1. 判断当前对象 是否处于无锁状态(hashcode、0、01);
    如果是,则JVM首先将在当前线程的栈帧中建立一个所记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word赋值到栈帧中的Lock Record中,将Lock Record的owner指向当前对象。

  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针;如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。

  3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象

栈帧

写一个方法时那么到时候这个方法运行时就将会进入到栈当中来进行执行;
那么该方法进入到栈中的该方法就称作是栈帧;
栈帧:一个进入栈中的方法就是一个栈帧;
那么这个栈帧是用来执行方法的所以这个栈帧也具有空间,也能够存储一些变量;

锁状态25bit31bit1bit4bit1bit2bit
cms_free分代年龄偏向锁锁标志位
无锁unused(没有使用)hashCode0(是否是偏向锁;0表示非偏向锁;1表示是偏向锁)01(锁的标志位;01代表无锁;00代表轻量级锁;10代表重量级锁)
偏向锁ThreadId(54bit)Epoch(2bit)101
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

轻量级锁分析

java代码:
class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
-----------------------------------------------------------
分析:
synchronized当中的obj即为锁对象(对象锁)
当前具有两个线程,分别是A线程以及B线程
(创建这两个线程并启动他们,那么他们都会将去执行run()方法当中的代码);
可以看到的是进入synchronized同步代码块之前需要一个obj对象锁;
假设线程A来先进行执行run()方法;
即,即将执行的run()方法运行就会进入到栈中去,那么进入到栈中执行的方法即一个栈帧,即此时运行的run()方法即为一个栈帧;
假设此时线程A要进入到同步代码块之中;那么这个时候就要升级为轻量级锁;因为存在有多个锁竞争;所以未使用偏向锁;
那么此时如何进行升级为轻量级锁?
首先在栈中运行执行run()方法的该栈帧当中会创建一个叫做Lock Record的锁记录空间(这块空间内存放displaced hdr以及owner);
那么接着就会将锁对象即obj升级为轻量级锁;
那么其当前obj锁对象的状态为无锁状态;
即会将对象当中的无锁状态当中的hashCode、分代年龄以及锁标记赋值到栈帧当中Lock record中创建的displaced hdr当中;
另外还会将栈帧当中的Lock record当中还会进行创建的owner指向obj,即也就是synchronized的锁对象(对象锁);
另外还需要做的事情有升级为轻量级锁的时候会将锁标志位的数值修改成“00”;
并且存在前25bit+31bit+1bit+4bit+1bit来进行保存栈帧中Lock Record的地址;
那么这些操作都是通过CAS来进行操作的;
这就是轻量级锁的原理;
笔记小结-轻量级锁的原理:
当关闭了偏向锁或者说偏向锁出现了竞争的情况那么都会导致偏向锁升级为轻量级锁;
轻量级锁步骤如下:
首先判断对象头当中的倒数两位即标志位是否为01,01即代表无锁状态;
如果确实无锁;那么JVM就会在当前的栈帧当中建立一个Lock Record这样一个空间;
这块空间用来存储对象头;即用来存储displaced hdr(displaced hdr即Displaced Mark Word)以及owner;
displaced hdr也就是指的Displaced Mark Word(会将分代年龄、锁标志、锁标志等放到displaced hdr当中去);
并且owner会执行当前的对象即synchronized当中的obj;
接着还会使用CAS操作把对象头当中的Mark Word进行保存栈帧中创建的Lock record的地址;
最后会将对象头当中的标志位改成00即代表的是轻量级锁;
如果在这个当中升级轻量级锁失败那么就会膨胀为重量级锁;

轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁 保存在Displaced Mark Word中的数据;
  2. 用CAS操作 将取出的数据 替换当前对象的Mark Word中,如果成功,则说明释放锁成功;
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,
如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁的释放即将栈帧当中的Lock Record当中的displaced hdr当中的hashCode重新放回到对象头原有的位置上即无锁状态上31bit处;以及displaced hdr当中的分代年龄以及锁标志位等都放回原位;
撤销轻量级锁也是一个CAS操作;即如果将hashCode、分代年龄以及锁标志位都还原归位了那么也就说明轻量级锁已经被撤销了;

需要注意的是,对于轻量级锁而言:轻量级锁的性能之所以高,是因为在绝大部分情况下,这个同步代码块不存在有竞争的状况;线程之间交替执行;
如果是多线程同时来进行竞争这个锁的话,那么这个轻量级锁的开销也就会更大;

轻量级锁好处

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

小结

轻量级锁的原理是什么?

轻量级锁即会在栈帧中创建一个叫做Lock record锁记录的空间;
那么在Lock Record锁记录空间内的displaced hdr会去进行保存对象头当中的hashCode、分代年龄以及锁标志等;另外Lock record锁记录空间当中的owner即指向的是这个锁对象;并且在对象头当中会来进行保存Lock Record锁记录的空间地址;然后将对象头当中的锁标志改成00以表示轻量级锁;

将对象的Mark Word赋值到栈帧中的Lock Record中,Mark Word更新为指向Lock Record的指针。

轻量级锁的好处是什么?

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

synchronized优化-自旋锁

回顾并引入

当轻量级锁发生竞争的时候会膨胀升级为重量级锁;
但是重量级锁相对于性能开销的消耗是比较大的;
因此应该尽量避免升级为重量级锁;
所以JVM在轻量级锁在升级为重量级锁的时候的这个过程当中还会再挣扎一下;
尽量避免升级为重量级锁;
这个挣扎的过程就是自旋锁;

重量级锁回顾:重量级锁是由monitor来进行实现的;当一个线程来进行竞争monitor锁如果没有竞争到那么线程就会进入阻塞状态;当其他线程将锁释放的时候会来进行唤醒那些处于阻塞状态的线程有机会去竞争锁;

举个例子:假设第一个线程,线程A来进行执行同步代码块;
那么线程A竞争得到锁从而进入同步代码块当中进行执行;
与此同时又来了第二个线程,即线程B;第二个线程,线程B也来进行执行同步代码块;但是线程A由于没有释放锁;从而线程B就没有办法获取得到锁从而无法进入同步代码块从而进入阻塞状态;只有第一个线程即线程A退出同步代码块并且将锁释放之后才会去唤醒阻塞状态的线程B;从而第二个线程线程B才能够有机会再次获取锁从而进入同步代码块中;
线程的阻塞和唤醒是需要CPU从用户态切换至内核态的;
频繁的状态切换对于CPU的开销来说也是比较大的;
并且虚拟机开发团队也发现其实大部分情况下线程对共享资源的操作所持续的时间是比较短的;即也就是说进入同步代码块到退出同步代码块释放锁的时间也是比较短的;
那么之前都是执行执行同步代码块而让其他没有获取得到锁的线程进行等待阻塞状态;那么这样其实是不划算的;

具体来说是这样的:
假设第一个线程A获取得到锁并进入同步代码块当中去执行,很快就可以执行完成;
但是与此同时来了第二个线程线程B;而此时线程A并没有释放锁;所以线程B没有获得锁从而进入阻塞状态;可能在线程B还没有进入到阻塞状态时这个线程A就已经执行完并且将锁进行释放了;
那么对于外面陷入等待阻塞的线程B来说就开销较大并且比较耗费资源;
其实这个时候只要让外面这个线程B在同步代码块外面多循环几次多尝试一下就有可能获取得到锁;就不必要让外面这个线程B进入阻塞状态;以及后面还要去唤醒它,浪费资源;

举个例子:
在火车上的一间卫生间当中;
假设有两个人分别是叫做A用户和B用户;
那么此时A用户先进入到火车的这一间卫生间当中;并且将卫生间的门给锁上了;
后面来了的第二个人即用户B,从他的位置上走过来到卫生间,表示也想要上厕所;
但是用户B发现此时卫生间的门已经被锁上了;所以用户B又从卫生间回到了他自己的位置上去了;
那么此时就有这样一种情况发生:
当用户A进行上卫生间只需要花费10秒钟的时间;
而用户B从位置上到达卫生间可能需要花费30秒的时间;
当用户A进入到卫生间并将卫生间的门锁上的时候;10秒钟还没有执行完;
与此同时用户B从位置上走到卫生间花费了30秒的时间,发现卫生间的门被锁上了;所以又从卫生间返回到其位置上去花费了30秒钟的时间;
其实用户A也许10秒钟之后就可以从卫生间中出来了;那么即用户B就浪费了一部分的时间资源;
这个例子其实就相当于是重量级锁;
那么其实也还可以这么做,当用户A进入卫生间并将门锁上的时候;用户B从位置上走到卫生间来看到卫生间的门被锁住,第一时间并不是又返回到位置上面去这样花费的时间太长了;而是在卫生间的门口进行等待;而且可以每隔一秒敲一下门看门内的用户A是否用完了卫生间可以出来;即如果用户A没有出门则可以再隔一秒钟再次敲一下门,如果用户A仍然还没有出来那么这个时候用户B又过一秒再次敲一下门,如果用户A出来了那么这个时候卫生间外的用户B就可以进去了;
那么可以看到卫生间外的用户B只要在门口等多尝试几次就可以获取得到锁;
就避免了从卫生间又走到位置上花了30秒的时间;

要让外面的线程不进行阻塞;而是循环几次来进行抢锁;
那么就需要保证本机是一个多核CPU,能够让两个或以上的线程来进行并行执行;
这样就可以线程A在同步代码块内进行执行代码;而另外其他的线程B则在同步代码块外进行尝试的来进行获取锁;
即同步代码块外面的线程B就不会进入到阻塞状态中去;
这样的话可能性能开销就小一点;
那么让同步代码块外面的线程B进行多次尝试抢锁的这个过程就叫做自旋;
那么支持这种自旋的锁就称之为自旋锁;
另外还需要注意的是循环是很快的所以就需要让这个循环尽量慢一点;即让这个循环锁花费的时间尽量长一点;
那么也就知道了自旋锁在一定情况下是可以减小性能开销的;

目标

学习自旋锁的原理

自旋锁原理

synchronized(Demo01.class){
  ...
  System.out.println("aaa");
}

前面讨论 monitor实现锁 的时候,知道 monitor会阻塞和唤醒线程,
线程的阻塞和唤醒 需要CPU从用户态转化为核心态,
频繁的阻塞和唤醒对CPU来说 是一件负担很重的工作。
这些操作给系统的并发性能 带来了很大的压力。
同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。
如果物理机器有一个以上的处理器,能让两个或两个以上的线程同时并发执行,
就可以让后面请求锁的那个线程 “稍等一下”,
但不放弃处理器的执行时间,看看持有锁的线程 是否很快就会释放锁。
为了让线程等待,只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋锁JDK 1.4.2中就已经引入,只不过默认是关闭的,
可以使用-XX:+UseSpinning参数来开启,
JDK 6中就已经改为默认开启了。
自旋等待不能代替阻塞,且先不说处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的;
因此,如果锁被占用的时间很短,自旋等待的效果就会非常好;
反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费;
因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。
自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改;

通过前面的学习中可以知道自旋锁不一定是最好的,而是在(某些情况下比较好)特定一定的条件下;
即同步代码块执行的时间较短,从而能够很快的抢到锁;
另外还有就是硬件要能够支持两个或两个以上的线程进行并行执行;
即一个线程在同步代码块内进行执行,而另一个线程则在同步代码块外进行自旋即尝试获取锁;
但是自旋也是会消耗CPU的性能的;
因为要在同步代码块外层不断的进行循环重试获取锁;
因此这个自旋锁需要去控制这个自旋的时间或者是说自旋尝试获取锁的次数;
如果自旋的次数太多了那么对于CPU的开销也是很大的;
如果自旋的次数太少了又有可能抢不到锁导致白白自旋了;
自旋锁默认的自旋次数是10次,可以通过手动的修改JVM的启动参数来修改默认自旋的次数(改变自旋锁的自旋次数);

但是又有一个问题:这个自旋锁的自旋次数改成多少合适呢?那么这就很难判定了;
改多了浪费资源;改少了又没有抢到锁也是浪费资源;所以这个自旋的次数就很难来判断了;因此在JDK6中引入了自适应自旋锁;

适应性自旋锁

在JDK 6中引入了 自适应的自旋锁。
自适应 意味着 自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机 就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。
另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序搜的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。

(自适应自旋锁中自旋的次数或者是自旋的时间就不再固定了,而是由前一次自旋的效果来进行决定)

举个例子:
假设一个线程A在同步代码块上,自旋了10次并且获得了锁;那么就会认为这个同步代码块通过自旋是比较容易获得锁的;所以在后续的执行过程中也会进行自旋;并且还允许自旋的时间稍微长一点;因为之前得到过所以感觉现在也能得到;所以自旋的时间还允许更长一点;
再举一个例子:
假设有一个同步代码块,但是自旋从来就没有在这个同步代码块上成功获取过锁过;所以jvm就会认为这个同步代码块很难通过自旋来获取得到锁;干脆就不再进行自旋了;那么这样就可以避免性能的浪费;即所以自旋锁也是越来越"聪明"了;
monitor在进入竞争的时候会进入ObjectMonitor::enter(TRAPS)来进行竞争;
该方法中前一部分是抢到锁的情况;后一部分是没有抢到锁的情况;
当没有抢到锁的时候会进入到EnterI(THREAD)方法来进行线程的阻塞以及挂起等操作;
在EnterI(THREAD)当中就准备让线程进行阻塞,但是在阻塞之前首先还是会去进行尝试获取锁;如果还是获取不到锁则进入TrySpin(Self),即就是去进行自旋;
那么接着看TrySpin(Self)进行了什么操作;

#define TrySpin TrySpin_VaryDuration
// 恒定义对于另外一个函数即TrySpin_VaryDuration;
------------------------------------------------
//TrySpin_VaryDuration时间不固定的尝试自旋锁
int ObjectMonitor::TrySpin_VaryDuration(Thread * Self){

  // Dumb , brutal spin, Good for comparative measurements against adaptive spinning.
  /**
  Knob_FixedSpin自旋固定的次数
  如自旋固定的次数不等于0那么就会进入循环来进行自旋;
  然后每次循环一次就让数量减去一;
  每次自旋一下就要去TryLock一下尝试一下去获取锁;
  尝试获取不到锁则自旋的时候稍微花点时间等待一下即调用SpinPause()方法;
  再接着下一次自旋;
  那么这个就属于之前所说的固定的自旋次数以及固定的自旋时间;
  */
  int ctr = Knob_FixedSpin ;
  if( ctr != 0 ){
    while( --ctr >= 0){
      if(TryLock(Self) > 0) return 1 ;
      SpinPause();
    }
    return 0 ;
  }


  /**
  以下的这个循环就是所推出的适应性自旋锁;
  首先来进行看到适应性自旋锁的自旋次数是多少;
  在objectMonitor.cpp当中给Knob_PreSpin设置了默认值为10次:
  static int Knob_PreSpin = 10; // 20 - 100 likely better
  并且注释推荐20到100次的自旋次数是比较合适的;
  自旋次数是10次,每次自旋一次就会去进行尝试一次看能不能抢得到锁;
  如果抢到了锁那么就回去修改自旋的时间,它就会去允许自旋的时间比以前自旋的时间稍微长一点;
  因为这次通过自旋抢到了那么下一次也有可能抢到;
  所以它允许自旋所花的时间加长一点;
  那么如果没有在一次自旋当中即循环的一次尝试当中没有获取得到锁;
  那么这个时候就会去调用SpinPause()即进入自旋的一个等待;
  */
  for( ctr = Knob_PreSpin + 1; --ctr >= 0 ; ){
    if( TryLock(Self) > 0){
      // Increase _SpinDuration ...
      // Note that we don't clamp SpinDuration precisely at SpinLimit.
      // Raising _SpurDuration to the poverty line is key.
      int x = _SpinDuration ;
      if(x < Knob_SpinLimit){
        if( x < Knob_Poverty) x = Knob_Poverty ;
        _SpinDuration = x + Knob_BonusB ;
      }
      return 1 ;
    }
    SpinPause();
  }
}

synchronized优化-锁消除

回顾并引入

在JDK6中,对synchronized做了一个优化,即会有一个锁升级(无锁-偏向锁-轻量级锁-重量级锁)的过程;
那么在JDK6当中除了这个锁升级的优化之后还会有一个锁消除的优化;

目标

学习锁消除的原理

锁消除 是指 虚拟机 即时编译器(JIT)在 运行时,
对一些代码上要求同步,但是被检测到 不可能存在共享数据竞争的锁 进行消除。

锁消除 的主要判定依据 来源于 逃逸分析的数据支持,
如果判断在一段代码中,堆上的所有数据 都不会逃逸出去 从而被其他线程 访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,
同步加锁 自然就无需进行。

变量是否逃逸,对于虚拟机来说 需要使用 数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在 明知道不存在数据争用的情况下 要求同步呢?

实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。
下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

-----------------------------------------------------------
public class Demo01{
  public static void main(String[] args){
    concatString("aa" , "bb" , "cc");
  }

  public static String concatString(String s1, String s2, String s3){
    return new StringBuffer().append(s1).append(s2).append(s3).toString();
  }
}
-----------------------------------------------------------
代码分析concatString()该方法:
StringBuffer.append()方法是同步的;
-----------------------------------------------------------
StringBuffer.java
@Override
public synchronized StringBuffer append(String str){
  toStringCache = null;
  super.append(str);
  return this;
}
-----------------------------------------------------------
那么此时可以看到StringBuffer的append()方法使用了synchronized进行了同步处理;
而concatString当中进行调用了三次append()方法,也就是表明会进行执行三次的这个同步方法;
实际上再来仔细观察一下concatString()方法;
三个append()方法调用者是谁?
即new StringBuffer 该对象进行调用的append()方法;
也就是StringBuffer的append()方法即普通的同步方法它所使用的对象锁即为this;
而这个this对象其实也就是这个append()方法的调用者,也就是new StringBuffer(),即对象锁就是这个new StringBuffer();
而接着分析concatString()方法当中的new StringBuffer()是concatString()方法当中所new出来的局部变量;
并没有逃逸出concatString()这个方法;
那么就算现在有多线程来进行执行;即当前了存在两个线程,线程A和线程B;
假设线程A先来进行执行concatString()方法;
那么这个时候线程A就会进入到concatString()方法当中new StringBuffer()对象作为锁来进行来锁住StringBuffer类当中的append()方法;
那么假设CPU切换到线程B上来,那么此时线程B也会进入到concatString()方法当中来也进行new StringBuffer()对象作为锁来进行锁住StringBuffer类的append()方法;
锁就是另外一个对象了;
也就是说concatString()方法内部的new StringBuffer()对象没有逃逸出concatString()这个方法;
就算有不同的线程来进行执行,那么每个线程也都是获取拿到的不同的锁;
即每个线程拿到的都是不同的new StringBuffer()对象即不同的锁;
所以根本不存在有竞争;
那么既然不存在有竞争那么这个StringBuffer类当中的append()方法的同步代码块synchronized就没有必要了;
所以会自动进行消除掉这个同步代码块synchronized;知道此处没有竞争;即转变成

---------------------------------------------------
public StringBuffer append(String str){
  toStringCache = null;
  super.append(str);
  return this;
}
---------------------------------------------------

即线程A与线程B两者之间锁住StringBuffer类当中的append()方法使用的不同的锁;
很明显线程A所new StringBuffer()对象与线程B所new StringBuffer()对象显然不是同一个对象,
而这个new StringBuffer()对象也就是调用StringBuffer类当中的append()方法的this,
也就是这个同步synchronized方法append()的对象锁;
即线程A与线程B所获取的锁不是同一把锁;

那么这就是锁消除;

那么这个锁消除是谁来进行完成这一职责的呢?
锁消除是虚拟机 即时编译器JIT在对代码执行之前也会对代码进行一个编译操作;
最终即时编译器会根据对象的逃逸分析来判断,如果对象逃逸不出这个方法,那么这个锁是不存在竞争的,即那么就会取消这个同步代码块;

StringBuffer的append()是一个同步方法,
锁就是this也就是(new StringBuffer())。
虚拟机发现它的 动态作用于被限制在 concatString()方法内部。
也就是说,new StringBuffer()对象的引用永远不会“逃逸”到concatString()方法之外,其他线程无法访问到它,
因此,虽然这里有锁,但是可以被安全地消除掉;
在即时编译之后,这段代码就会忽略掉所有的同步而执行了;

synchronized优化-锁粗化

回顾并引入

锁粗化

JDK1.6在对synchronized进行优化的时候除了锁升级、锁消除;
还会做一个锁粗化的操作;

----------------------------------------------------------
package com.xxx.demo10_lock_coarsing;

public void Demo01{
  public static void main(String[] args){
    /**
    建议同步代码块当中的代码尽量的少;
    执行的时间尽量的短;
    如果同步代码块当中的代码少且时间短那么偏向锁就有可能满足要求;
    或者说轻量级锁就能够满足要求;
    或者说在自旋的时候就能够满足要求;
    就不会进入到重量级锁的状态;
    但是往往可能情况又比较特殊;
    比如说下面的情况;
    */
    //synchronized(Demo01.class){
    //  System.out.println("aaa");
    //}

    /**
     在循环外层new StringBuffer();
     在循环内存调用append()即执行了100次的append()
     而StringBuffer.append()是一个同步方法;
     那么这也就意味着在调用100次StringBuffer.append()方法时,
     那么就会进入同步代码块append()100次;
     出来同步代码块append()100次;
     那么这个性能消耗也是比较大的;
     它可能会做这样一个处理;
     将StringBuffer.append()方法当中的synchronized进行消除掉;
     然后再将synchronized加入到100次for循环的外面;
     即:
     ------------------------------------
     StringBuffer.java
     public StringBuffer append(String str){
      toStringCache = null;
      super.append(str);
      return this;
     }
     ------------------------------------
     Demo01.java
      public static void main(String[] args){
        StringBuffer sb = new StringBuffer();
        synchronized{
          for(int i = 0; i< 100 ; ++){
            sb.append("aa");
          }
        }

        System.out.println(sb.toString());
      }
     ------------------------------------
     那么此时再来看就只需要进入一次同步代码块然后再for循环100次即可;
     那么这就是锁粗化;
     由之前for循环零散的调用很多次的同步代码块到现在将其放到同步代码块里面来做一次进入同步代码块即可,把很多小锁去掉了变成一个大锁;
    */
    StringBuffer sb = new StringBuffer();
    for( int i = 0; i < 100; i++){
      sb.append("aa");
    }

    System.out.println(sb.toString());
  }
}
----------------------------------------------------------
StringBuffer.java
@Override
public synchronized StringBuffer append(String str){
  toStringCache = null;
  super.append(str);
  return this;
}

目标

学习锁粗化的原理

原则上,在编写代码的时候,总是推荐将 同步亏啊的作用范围 限制得尽量小,
只在共享数据的实际作用域中 才进行同步,
这样是为了使得需要同步的操作数量 尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,上面的原则都是正确地,但是如果一些列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能消耗。

public class Demo01{
  public static void main(String[] args){
    String str = new StringBuffer()
          .append("aa").append("bb").append("cc").toString();
    
    System.out.println("aa");
    System.out.println("bb");
    System.out.println("cc");
  }
}

如果虚拟机检测到有这样一串 零碎小的操作 都是用一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。

小结

什么是锁粗化?
JVM会探测到一连串细小的操作都是用同一个对象加锁,
将同步代码块的范围放大;
放到这串操作的外面;
那么这样只需要加一次锁即可;

平时写代码如何对synchronized优化

  • synchronized的原理;
  • JDK1.6对synchronized所做的优化;
  • 偏向锁、轻量级锁、自旋锁、锁消除、锁粗化等;

减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

尽量让synchronized同步代码块当中的代码少一点这样执行的时间也就会少一点;
那么在单位时间内所执行的线程也就多一点;等待的线程也就少一点;
另外由于执行比较短,由轻量级锁就有可能搞得定;或者通过自旋锁就可以搞得定;
避免升级到重量级锁;

synchronized(Demo01.class){
  System.out.println("aaa");
}

降低synchronized锁的粒度

将一个锁拆分为多个锁提高并发度

--------------------------------
package com.xxx.demo11;

import java.util.Hashtable;

public class Demo01{
  public static void main(String[] args){
    Hashtable hs = new Hashtable();
    hs.put("aa","bb");
    hs.put("xx","yy");
  }
}
--------------------------------
# 截取部分代码 Hashtable.java
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable{

  /**
    The hash table data/
  */
  private transient Entry<?,?>[] table;

  /**
    The total number of entries in the hash table.
  */
  private transient int count;

  //省略一些代码

  //对put整个方法进行加锁
  public synchronized V put(K key, V value){
    // Make sure the value is not null
    if(value == null) {
      throw new NUllPointerException();
    }

    // Makes sure the key is  not already in the hashtable
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    /unchecked/
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next){
      if((entry.hash==hash) && entry.key.equlas(key)){
        //省略一些代码
      }
    }
  }


  //对get整个方法进行加锁
  /unchecked/
  public synchronized V get(Object key){
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for(Entry<?,?> e = tab[index] ; e != null ; e=e.next){
      if((e.hash == hash) && e.key.equals(key)){
        return (V)e.value;
      }
    }
    return null;
  }

  //对remove整个方法进行加锁
  public synchronized V remove(Object key){
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    /unchecked/
    Entry<K,V> e = (Entry<K,V>)tab[index];
    for(Entry<K,V> prev=null ; e != null; prev = e, e=e.next){
      if((e.hash==hash) && e.key.equals(key)){
        modCount++;
        if(prev != null){
          prev.next = e.next;
        }else{
          tab[index] = e.next;
        }
        //省略一些代码
      }
    }
  }
}
----------------------------------------------
由此可以看到Hashtable对增删改查的方法全部添加了synchronized,
使之成为同步方法;
那么同步方法的锁对象是什么?是this;
也就意味着Hashtable其增删改查这几个方法使用的都是同一把锁;
那么这样会造成什么问题?
因为Hashtable的方法全部加了synchronized从而变成了同步方法;
那么意味着对于Hashtable这个对象;
如果有一个线程在往这个Hashtable对象当中的一个桶当中进行添加;
那么就没有办法存在另外一个线程往同样的这个Hashtable这个对象当中的另外一个桶中进行添加了;
肯定是不行的;因为put()方法加锁synchronized了;
且锁对象为this即所操作的Hashtable对象;
由于put()方法加锁了;
所以只允许一个线程A进行往Hashtable对象的一个桶中添加;
而另外的其他线程B则必须等待线程A添加完成,才能往相同的该Hashtable对象当中的其他桶中添加;
而事实上并没有必要这样操作;
即A线程操作Hashtable对象的第一个桶进行添加操作;
线程B操作与线程A操作的同一个Hashtable对象,其当中的另外一个桶进行添加操作;实则并不影响;
另外Hashtable还存在一个问题;
即当有一个线程A在对Hashtable对象的第一个桶进行添加的时候;
此时存在有另外一个线程B对同样的Hashtable对象的其他桶进行读取get()或者是remove()方法的时候是不可以的;只能等待线程A操作完对第一个桶进行添加的这个操作;
因为其方法都是synchronized同步方法;且加的锁都是同一把锁即this;
那么这样就导致了效率的低下;
因此jdk又推出了一个新的类叫做ConcurrentHashMap;
--------------------------------------------------
package com.xxx.demo11;

import java.util.Hashtable;

public class Demo01{
  public static void main(String[] args){
    Hashtable hs = new Hashtable();
    hs.put("aa","bb");
    hs.put("xx","yy");
    hs.get("a");
    hs.remove("b");
  }
}
--------------------------------------------------
# 截取部分代码 ConcurrentHashMap.java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable{

  private static final long serialVersionUID = 724906924676318397L;

  // 省略部分代码

  //ConcurrentHashMap对于get没有加锁
  public V get(Object key){
    Node<K,V>p[] tab; Node<K,V> e, p; int n , eh; K ek;
    int h = spread(key.hashCode());
    if((tab==table) != null && (n=tab.length) > 0 && (e=tabAt(tab, (n-1)& h)) != null){
      if((eh=e.hash) == h){
        if((ek=e.key) == key || (ek != null && key.equals(ek))){
          return e.val;
        }
        else if(ek < 0)
          return (p = e.find(h, key)) != null ? p.val:null;
        while((e=e.next) != null){
          if(e.hash == h && ((ek=e.key) == key || (ek != null && key.equlas(ek))))
          return e.val;
        }
      }
      return null;
    }

    public V put(@NotNUll K key, @NotNull V value){
      return putVal(key , value , false);
    }

    /*Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent){
        if(key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for(Node<K,V>[] tab = table;;){
          Node<K,V> f;int n,i,fh;
          if(tab == null || (n = tab.length) == 0)
            tab = initTable();
          else if((f = tabAt(tab, i = (n-1) & hash)) == null){

            //casTabAt 此处即为CAS操作去进行添加一个结点

            if(casTabAt(tab, i, null, new Node<K,V>(hash,key,value,null)))
              break;  // no lock when adding to empty bin
          }
          else if((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
          else{
            V oldVal = null;

            /**
            另外下面还有synchronized来进行添加一个结点
            而且该synchronized所用的锁并不是一把锁;
            synchronized所用的锁是桶中的第一个元素;
            也就意味着是这样:
            ConcurrentHashMap在进行添加元素的时候;
            如果该桶当中一个元素也没有,那么就是用CAS操作来进行添加第一个元素;
            如果该桶当中有存在元素往后面加元素呢?
            那么这个时候在锁的时候会使用该桶当中的第一个元素第一个节点作为锁对象,那么也就意味着只会锁住这一个桶;
            即这一个线程往这一个桶当中进行添加,只会对这一个桶进行锁;
            另外再来一个线程往同一个ConcurrentHashMap对象中的其他桶当中进行添加那么这个时候就是可以进行的;
            因为另外一个线程所要进行添加的是该同一个ConcurrentHashMap对象当中的其他桶,那么锁的对象即为其他桶当中的第一个结点对象;
            即这两个线程之间所操作的同一个ConcurrentHashMap对象的桶不一样其锁也不一样;
            由于锁不一样所以线程往其他桶当中进行添加是没有问题的;
            也就是说ConcurrentHashMap只要是不同的桶那么是可以进行同时添加的;
            除此之外还看到ConcurrentHashMap的get()方法是没有加synchronized变成同步方法的;
            这也就意味着当一个线程A在对该ConcurrentHashMap当中的一个桶进行添加的时候,另外一个线程还可以对该同一个ConcurrentHashMap对象当中的同一个桶进行读取操作;那么这样的话也不会受到锁的影响;
            其性能也就更高一点;
            */

            synchronized(f){
              if(tabAt(tab, i) == f){
                if(fh >= 0){
                  binCount = 1;
                  for(Node<K,V> e = f;; ++binCount){
                    K ek;
                    if(e.hash == hash && ((ek = e.key) == key || (ek!=null && key.equals(ek)))){
                      oldVal = e.val;
                      if(!onlyIfAbsent){
                        e.val=value;
                        //省略部分代码
                      }
                    }
                  }
                }
              }
            }
          }
        }
    }
  }
}
--------------------------------------------------
package com.xxx.demo11;

import java.util.Hashtable;

public class Demo01{
  public static void main(String[] args){
    Hashtable hs = new Hashtable();
    hs.put("aa","bb");
    hs.put("xx","yy");
    hs.get("a");
    hs.remove("b");
  }

  public void test01(){
    synchronized(Demo01.class){

    }
  }
    public void test02(){
    synchronized(Demo01.class){

    }
  }
  /**
  写代码的时候千万注意不要这么去做:
  这两个方法test01()以及test02()没有任何业务关联;
  且很多时候一般使用 类名.class来作为锁;
  觉得这样很简单有可以锁住;
  如果使用类名.class作为锁的话;
  如果一个线程A在进行执行test01()当中的同步代码块时;
  而对于另外一个线程B执行毫无业务关联的test02()方法时,
  也就意味着线程A抢了线程B的锁并且线程A还没有释放锁;
  所以线程B无法进入test02()方法中的同步代码块从而阻塞只能等待线程A释放锁;
  那么这样的话并发效率就很低了;
  所以尽量不要使用类名.class这样的锁;
  建议是降低锁的粒度;这样使得其并发效率可以更高一点;
  */
}
--------------------------------------------------

HashTable:锁定整个哈希表,一个操作正在进行时,其他操作也同时锁定,效率低下

ConcurrentHashMap:局部锁定,只锁定桶,当对当前元素锁定时,其他元素不锁定

HashTable与ConcurrentHashMap这两个容器都实现了Map接口,并且都能够保证线程安全;

-----------------------------------------
| 元素1 | 元素2 | 元素3 | 元素4 | 元素5 |
----↑------------------------------↑-----
    |            objA              |
take添加元素使用一把锁objA          put添加元素使用一把锁objA

/**
往其头部进行获取元素;往尾部进行添加元素;
为了保证队列的线程安全,就有可能加一把锁;
届时获取和添加都是用这同一把锁;
这样也就导致了在获取的时候没有办法进行添加;
所以效率就会更低;
因此就有了LinkedBlockingQueue的存在;
*/

LinkedBlockingQueue入队和出队使用不同的锁,相对于读写只有一个锁效率要高。

-----------------------------------------
| 元素1 | 元素2 | 元素3 | 元素4 | 元素5 |
----↑------------------------------↑-----
    |                              |
take添加元素使用一把锁objB          put添加元素使用一把锁objA

/**
添加元素的时候使用objA锁
获取元素的时候使用objB锁
即两把锁;
当在获取的时候;那么添加元素则不受影响;
另外在添加的时候,也可以使用另外一把锁来进行获取元素;
这就可以保证同时又可以添加又可以获取;
*/

读写分离

ConcurrentHashMap
(在写的时候为了保证线程安全上锁,但是在读的时候不会修改数据所以没有加锁,这样即保证多个线程来进行读取;写的时候加锁保证在写的过程中的线程安全;读的时候不会改变数据可以让多个线程来进行读取), CopyOnWriteArrayList和ConyOnWriteSet
(都是读的时候不加锁,写的时候才加锁;)

读取时不加锁,写入和删除时加锁

  • 16
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值