近况

实习结束了就回到家里,准备毕设和学习多线程了,我还是自制力太差,学习效果不太好,自制力差的原因
在于没有认清自己目前所处的阶段,所以才无所事事,在沉沦很多天之后,期间断断续续学习,坦白说
没有很好的收获,人啊,一旦没有了目标,心就空了,就容易迷茫,人还是应该有所追求,有目标才有动力。

我一直认为,学习如果没有输出,是很难继续下去的,这就是为什么我写博客的原因,哪怕没人看
最近一段时间开始学习多线程,原本想搞定完毕设再学习的,但是毕设学习若依项目,继续不下去,
而且需要另写一套前端,CSS极差,又没有经验写,虽然学习过vue,但是我给鸽了,唉,
我怕再不学习多线程,等家人都回来过年了,吵吵闹闹的又没定力学习去了,怕是又跑去打麻将了~
所以趁家人没回来,赶紧把多线程学习了,这个多线程,是我众多心病之一,所以要解决掉它
现在写第一篇多线程博客,随便聊聊

本篇博客内容如下:

Java线程的6种状态

我们有时候很难分清楚线程的状态到底有几种,有人说5种,有人说6种,我们在操作系统上看到过
进程的5种状态,你可能有疑问,进程?是的,操作系统上说的是进程的5种状态,一般指的是进程只有1个线程。
所以,操作系统书上说进程的5种状态,我们可以简单理解为线程的5种状态。
这5种状态是:新建,就绪,运行,阻塞/等待,死亡

这5种状态是对于操作系统而言的,而对于JVM而言,却是有6种线程状态,Java线程的6种状态在Thread类
中的State枚举类中,它们分别是:New,Runnable,Blocked,Waiting,Timed_Waiting,Terminated

注意:这6种状态与操作系统状态并不是一一对应的,例如Java中的Runnable状态相当于操作系统中的
就绪,运行,一部分阻塞状态(IO阻塞),而Blocked,Waiting,Timed_Waiting都属于操作系统中阻塞状态的一部分。

具体聊聊Java的线程状态

  • 新建和死亡状态与操作系统中一致,就没什么好讲的
  • Runnable状态这是对jvm而言,jvm才不需要管操作系统如何调度线程,不管这个线程在操作系统中
    是就绪状态,还是运行状态,还是在等待IO操作完成,对于jvm来说都是Runnable状态,
  • Blocked状态则是多线程环境中,使用synchronized关键字,当线程被动阻塞后,
    这个线程的状态就是Blocked,这个线程会被放进monitor对象的entry list中,当然这是后话了
    简单而言,当线程进入同步方法时,没有拿到锁时,该线程处于blocked状态
  • Waiting状态
    当线程拿到锁进入同步代码块后,主动调用wait方法,主动阻塞后,该线程释放锁,进入monitor对象
    中的wait set中,等到其他线程调用notify方法唤醒,此时该线程处于Waiting状态,当该线程被唤醒后,
    重新获取锁,获取锁失败,进入entry set中等待竞争锁,waiting状态转变成blocked状态,
    获取锁成功,则waiting状态转变成runnable状态,并从之前调用wait方法处开始执行。
  • Timed_Waiting
    当线程在执行代码时调用sleep等方法,并不一定要在同步代码块中调用,只要调用sleep等方法
    则该线程进入monitor对象的wait set中,此时的线程状态是Time_Waiting,相比于Waiting状态
    Timed_Waiting状态是有限时间内等待,而处于Waiting状态的线程则是需要其他线程唤醒的,
    Timed_Waiting状态的线程则不需要,另外,sleep方法并不会释放锁,时间一到,线程继续执行
    原来的代码,而不需要竞争,另外不只是sleep方法可以使线程处于Timed_Waiting状态,
    像wait(时间),join(时间),这些方法都可以,都是使线程有限时间内等待,当然,这种方式就释放
    锁了。

看完线程状态,你可能仍然觉得,写的什么鬼,乱七八糟的,没关系,我找到了一个大佬的文章
看完感觉神清气爽,仿佛打通任督二脉~

创建线程的3种方式

  • 通过Thread子类,或者匿名类创建线程

    public void test1(){
        //通过Thread创建线程
        Thread thread = new Thread("t1"){
            @Override
            public void run() {
                System.out.println("自定义线程正在执行");
            }
        };
        thread.start();
        System.out.println("main线程执行");
    }

    重写run方法后,当jvm调用run方法时,就走重写后的run方法。

  • 通过Runnable接口创建线程

    public void test2(){
        //通过runnable接口配合Thread创建线程,下面是不断的简化
        //方式1
        Runnable runnable  = new Runnable() {
            @Override
            public void run() {
                System.out.println("t1线程正在执行");
            }
        };
        new Thread(runnable,"t1").start();
    
        //方式2
        Runnable runnable1 = ()-> System.out.println("t2线程正在执行");
        new Thread(runnable1,"t2").start();
    
        //方式3
        new Thread(()->{
            System.out.println("t3线程正在执行");
        },"t3").start();
    
        System.out.println("main线程执行");
    }
  • 通过Callable接口创建线程
    我们从Runnable接口可以看到,实际上是将同步代码抽离成接口,但是这个run方法并不能抛出异常
    也没有返回值,而Callable接口有返回值并且可以抛出异常。

    //通过FutureTask配合Thread创建线程
    FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            System.out.println("t4线程正在执行");
            return 1024;
        }
    });
    new Thread(task,"t4").start();
    Integer result = task.get();
    System.out.println("main线程执行,拿到t4线程数据:"+result);
    //这种方式能使得main线程拿到其他线程的返回值

Thread的常用方法

Thread有很多方法,我这里就不一一介绍了,下面是一些常用的方法的含义

我这里就简单聊聊interrupt方法
当我们想停止一个线程时,我们可能会发现Thread类中有stop方法,但是这个stop方法却被废弃了
因为stop方法太暴力了,还没等线程处理完自己的事就直接关掉,所以后来使用了Interrupt方法来代替stop方法
当在主线程代码中,其他线程调用interrupt方法后,相当于主线程通知其他线程快点结束吧,并不会直接杀掉该线程
其他线程停不停止完全取决于自己的处理,interrupt方法仅仅给该线程设置一个打断标记,线程打断标记默认是false
下面是来自interrupt源码注释的三句话:

调用正常运行线程的isInterrupted方法返回false,即打断标记默认为false
调用正常运行中线程的interrupt方法会将打断标记置为true
interrupt一个被sleep,wait,join锁住的方法,会将打断标记置为true,然后清除打断标记,
然后throw interrupted exception,这里的清除打断标记,我想是恢复打断标记为false

由上可知,打断一个线程就是仅仅通知该线程应该尽快结束运行,那被打断线程应该如何停止当前线程呢?
等下讲讲多线程设计模式中的二阶段终止模式。

我们还需要了解的方法是,isInterrupted方法和interrupted方法
这两个方法都是用来判断当前线程是否被打断了,但有区别的是,isInterrupted方法判断后,不会修改线程打断标记
而interrupted方法判断后,会修改当前线程的打断标记,false改为true,true改为false。

除了使用interrupt方法可以打断线程,LockSupport的park方法也可以打断线程,下面是一个例子:

Thread thread = new Thread(() -> {
    System.out.println("进入线程run方法,此时线程打断标记:"+Thread.currentThread().isInterrupted());
    LockSupport.park(); //当线程打断标记为false时,则线程在此处暂停运行,如果当前线程打断标记为true,则不会暂停,直接向下运行
    System.out.println("线程被打断后,继续运行,此时线程打断标记:"+Thread.currentThread().isInterrupted());
    System.out.println("通过调用intercepted方法,查看线程打断标记"+Thread.interrupted());
    System.out.println("再来看此时的线程打断标记:"+Thread.currentThread().isInterrupted());
}, "t1");
thread.start();
TimeUnit.MILLISECONDS.sleep(200);
thread.interrupt();

结果输出为:

进入线程run方法,此时线程打断标记:false
线程被打断后,继续运行,此时线程打断标记:true
通过调用intercepted方法,查看线程打断标记true
再来看此时的线程打断标记:false

那么,既然interrupt方法只是通知被打断线程尽快结束线程,那么我们应该如何停止一个线程呢?
二阶段终止模式
来看看该模式下的代码

import java.util.concurrent.TimeUnit;
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        /*
            多线程的设计模式之二阶段终止模式
            该设计模式用来在一个线程T1中优雅的终止线程T2

            错误思路
                1,使用线程对象的stop方法停止线程
                    stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁
                    其他线程将永远无法获取锁
                2,使用System.exit(int)方法停止线程
                    目的仅是停止一个线程,但这种做法会让整个程序都停止

         */

        TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
        twoPhaseTermination.start();
        TimeUnit.SECONDS.sleep(5);
        twoPhaseTermination.stop();
    }
}

class TwoPhaseTermination{
    Thread thread;
    public void start(){
        System.out.println("正在开启线程");
        thread = new Thread(){
            @Override
            public void run() {
                while (true){
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("线程正在停止,处理后事");
                        break;
                    }
                    System.out.println("线程休眠之前");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("执行业务逻辑");
                    } catch (InterruptedException e) {
                        System.out.println("异常处理");
                        thread.interrupt();
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
    }

    //停止线程
    public void stop(){
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }
}

由上我们看到,我们在catch块异常处理中,再次打断了当前线程,这是因为,如果线程休眠时当前线程中断标志
为true,则会抛出打断异常,并会清除打断标志,即将打断标志true改为false,这样的话,再一次循环就出不去了
所以在异常处理中,再一次打断线程,将因异常被改为false的中断标志,重新改为true,从而停止while循环,停止线程运行。

有一点需要注意的是:只要线程的中断标志为true时调用了sleep等方法,都会抛出中断异常,并不一定要
线程在睡眠的时候中断,才会抛出异常,当然,在线程睡眠之后,哪怕仍然在try catch块中打断线程,都不会抛出
中断异常。

总之:线程停不停止运行,不在于自身被不被打断,而在于自己想不想停止!线程停止运行最好使用二阶段终止模式。

线程安全问题

什么是线程安全?在多线程环境下,因为线程上下文切换导致其执行顺序发生改变,从而影响最终结果的代码存在线程安全问题。
下面是一段存在线程安全的代码

static int count = 0;	//成员变量,被所有线程共享
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for (int i = 1;i<5000;i++){
            count++;
        }
    });
    Thread t2 =new Thread(()->{
        for (int i = 1;i<5000;i++){
            count--;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("count的值是{}",count);
}

如果了解过JMM,java内存模型的都知道,count++,count–的操作在底层都不是原子性的操作,它分为多个指令
下面是JMM,内存模型

可以看到,每个线程都有自己的工作内存,一个线程如果对一个数相加,需要先从主内存(共享内存)中拿到值
拷贝到工作内存中,再对工作内存中数据+1,再将相加后的结果写回主内存中。正常的执行顺序如下:

但是如果发生线程上下文切换的时候,就是下面这种执行顺序
出现负数的情况

出现正数的情况:

我们知道,线程上下文切换是操作系统调度的,JVM并不能控制什么时候切换,什么时候不切换,那如果解决呢?
答案当然是使用synchronized关键字,对那些多线程修改共享变量的代码进行加锁,也就是对临界区进行加锁
临界区是操作系统的名词,指的是:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区
如果任由多个线程在临界区执行对共享变量读写操作,则一定会发生线程安全问题,所以我们要控制在多线程
并发修改共享变量的时候,只能有一个线程进入临界区修改代码,这也叫加锁,锁的是临界区的代码
加锁的方式有很多种,最常用的就是使用synchronized关键字,我们知道,synchronized关键字可以使用在
静态方法上,锁的对象是该类的Class对象,可以使用在实例方法上,锁的对象是该类的实例,
可以使用在方法块上,锁的就是提供的参数对象

public static synchronized void test1(){
    //临界区
}
public synchronized void test2(){
    //临界区
}
public void test3(){
    synchronized(对象){  //这里的对象可以是任意对象
        //临界区
    }    
}

上面案例中的代码修改如下,即可解决线程安全问题:

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
     Thread t1 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
             synchronized (room) {
             counter++;
        	}
 		}
 	}, "t1");
     Thread t2 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
             synchronized (room) {
             counter--;
         }
     }
     }, "t2");
     t1.start();
     t2.start();
     t1.join();
     t2.join();
     log.debug("{}",counter);
}

synchronized实际上利用对象保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断

变量的线程安全分析

之所以存在线程安全问题,就是因为多个线程并发修改共享变量的值,如果变量不是共享的,
或者多个线程只是读取并未修改共享变量,都不会产生线程安全问题。那么关键在于变量是否是共享变量
下面是一段对变量的安全分析

  • 成员变量(实例变量+静态变量)和静态变量的线程安全分析

    • 如果没有变量没有在线程间共享,那么变量是安全的
      实例变量没有共享案例

      public class Test {
          public static void main(String[] args) {
              MyThread t1 = new MyThread("t1");
              MyThread t2 = new MyThread("t2");
              t1.start();
              t2.start();
          }
      }
      
      class MyThread extends Thread{
          private int count = 5;
      
          MyThread(String name){
              super(name);
          }
      
          @Override
          public void run() {
              while (count>0){
                  count--;
                  System.out.println(Thread.currentThread().getName()+":"+count);
              }
          }
      }
      //输出结果
      t1:4
      t2:4
      t2:3
      t2:2
      t2:1
      t2:0
      t1:3
      t1:2
      t1:1
      t1:0
    • 如果变量在线程间共享
      1)如果只有读操作,则线程安全
      2)如果有读写操作,则这段代码是临界区,需要考虑线程安全

      实例变量共享案例

      /*
          实例变量是共享变量有多种方式,例如上面的案例,如果把private int count = 5;加上static静态变量
          因为静态变量是所有类实例共享的,所以count变量是共享变量
      */
      public class Test {
          public static void main(String[] args) {
              MyRunnable myRunnable = new MyRunnable();
              new Thread(myRunnable,"t1").start();
              new Thread(myRunnable,"t2").start();
      
          }
      }
      
      class MyRunnable implements Runnable{
          private int count=5;
          @Override
          public void run() {
              while (count>0){
                  count--;
                  System.out.println(Thread.currentThread().getName()+":"+count);
              }
          }
      }
      //输出结果
      t2:3
      t1:3
      t2:1
      t1:1
      t1:0
  • 局部变量线程安全分析

    • 局部变量【局部变量被初始化为基本数据类型】是安全的

      public static void test1() {
           int i = 10;
           i++;
      }
    • 局部变量引用的对象未必是安全的
      1)如果局部变量引用的对象没有引用线程共享的对象,那么是线程安全的
      2)如果局部变量引用的对象引用了一个线程共享的对象,那么要考虑线程安全的

      public class Test15 {
          public static void main(String[] args) {
              UnsafeTest unsafeTest = new UnsafeTest();
              for (int i =0;i<100;i++){
                  new Thread(()->{
                      unsafeTest.method1();
                  },"线程"+i).start();
              }
          }
      }
      class UnsafeTest{
          ArrayList<String> arrayList = new ArrayList<>();
          public void method1(){
              for (int i = 0; i < 100; i++) {
                  method2();
                  method3();
              }
          }
          private void method2() {
              arrayList.add("1");
          }
          private void method3() {
              arrayList.remove(0);
          }
      }

      上述案例中,method2和method3引用的变量是多个线程共享的成员变量,所以上述代码是线程不安全的
      代码的内存示意图如下:

      我们只需要将共享的成员变量,修改成局部变量即可,代码如下:

      class safeTest{
          public void method1(){
              ArrayList<String> arrayList = new ArrayList<>();
              for (int i = 0; i < 100; i++) {
              method2(arrayList);
              method3(arrayList);}
          }
          private void method2(ArrayList arrayList) {
              arrayList.add("1");
          }
          private void method3(ArrayList arrayList) {
              arrayList.remove(0);
          }
      }

      修改后的内存示意图如下:

看一个类是否是线程安全,先关注变量是否共享,再关注共享变量是否被修改,
下面是一些文章,关于线程间通过共享变量通信,和变量的线程安全性分析

以上便是变量的线程安全分析

常见的线程安全类

常见的线程安全类如下:

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector,底层list
  • Hashtable,底层map
  • JUC包下的类

这里所说它们是线程安全指的是:多个线程调用它们同一个实例的某个方法时,是线程安全的
例如:

Hashtable table = new Hashtable();
new Thread(()->{
 	table.put("key", "value1");
}).start();
new Thread(()->{
 	table.put("key", "value2");
}).start();

但是,并不代表这些类方法的组合是线程安全的,例如下面代码就不是线程安全的:

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

线程调用示意图如下:

对象头与monitor

之前说过,要解决线程安全问题,最常用的是使用synchronized关键字进行加锁,那么在学习synchronized原理之前
我们先来了解一下什么是对象头和monitor

对象头
一个Java对象包括三个部分,对象头,实例数据,对齐补充,其中对象头又包括Mark Word,Klass Word
如图所示:

再看具体对象头具体结构,以 32 位虚拟机为例

  • 普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象
  • 数组对象的对象头结构如下

对象头中Mark Word有以下几种状态:

其中,Normal指的是一个对象创建出来,默认无锁的,即Normal状态
如果经过JVM配置的话,我们可以使得对象一创建出来是拥有偏向锁的,即Biased,与Normal区别在于Mark Word的倒数第三位是1
拥有偏向锁的对象,其Mark Word中保存的是线程的ID,这个ID是操作系统级别,并不是JVM
Lightweight Locked则指的是轻量级锁,它的Mark Work中保存的是栈帧中的锁记录指针,
锁记录是栈帧中一个对象,这里记录的是锁记录的地址,具体最后一节再说。
Heavyweight Locked表示的是重量级锁,如果多个线程竞争对临界区的共享变量进行操作,我们可以通过
synchronized关键字为该对象进行加锁,加锁就是改变这个对象对象头的Mark Word,换成一个monitor地址
monitor是操作系统创建的一个内存空间,具体等下介绍。
Marked for GC,当GC线程检测该对象没有被引用后,就将堆中该对象的内存回收。

Monitor
Monitor,也被叫做管程或监视器,其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象
我们可以简单理解为操作系统分配的一个内存空间。不同的编译器对monitor有不同的实现。
在HotSpot虚拟机中,Monitor是基于C++的ObjectMonitor类实现的,其主要成员包括:

_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
_EntryList:存放处于等待锁block状态的线程队列
_count:约为_WaitSet 和 _EntryList 的节点数之和
_cxq: 多个线程争抢锁,会先存入这个单向链表
_recursions: 记录重入次数

每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),
该对象头的Mark Word中就被设置为指向Monitor对象的指针

上图具体含义如下:

  • 刚开始时Monitor中的Owner为null
  • 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功
    Monitor中同一时刻只能有一个Owner
  • 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,
    就会进入EntryList中变成BLOCKED状态
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,
    这里的条件不满足,指的是线程主动阻塞调用wait方法,调用wait方法后,当前线程释放锁,
    并进入wait set等待其他线程唤醒。

注意:synchronized 必须是进入同一个对象的monitor才有上述的效果,不加 synchronized 的对象不会关联监视器(monitor)。

synchronized原理

synchronized关键字就是用来给同步代码块加锁的,加锁指的是线程去修改锁对象的对象头的mark word为其他指针
如果修改成功,则表示该线程成功上锁,或者该线程成功拿到锁,锁分为下面几种:

  • 如果是重量级锁,则mark word中就是指向一个monitor地址,或者说指向一个monitor对象
    相当于让这个对象关联了一个monitor对象
  • 如果是轻量级锁,则mark word中就是指向当前线程所在栈的栈帧的锁记录,这个锁记录可以理解为
    栈帧中的一个对象(Lock Record),说白了,mark word保存的也是指向一个对象的地址
  • 如果是偏向锁,则mark word中就是指向当前线程的ID,把当前整个线程抽象成一个内存空间,
    类似的monitor,锁记录都是一块内存空间。

在锁的级别上来看,偏向锁是对轻量级锁的优化,轻量级锁是对重量级锁的优化,都是通过synchronized关键字来实现
jvm默认创建的对象是无锁状态,随着线程竞争越来越激烈,锁会从偏向锁升级到轻量级锁,最后是重量级锁。

我一直不知道怎么描述锁到底是什么东西,synchronized需要的对象是锁吗?monitor对象是锁吗?
后来我终于感觉到,锁其实是一种机制,一种保证多线程环境下只有一个线程进入临界区的机制。
锁记录,monitor,synchronized需要的对象等等,都是锁机制的一部分,偏向锁,轻量级锁,重量级锁
也都是一种机制,锁不是某个地址空间,某个对象。

有时候你可能看到说加锁指的是对synchronized指定的对象加锁,这个对象被称为锁对象,
我觉得加锁指的是对临界区加锁,当然这个对象也可以说是锁对象,只是这些乱七八糟的含义很影响学习
比如成员变量,全局变量,实例变量,静态变量,或者javaweb中描述实体类的pojo,bean,domain。

synchronized原理进阶

这小节细讲一下synchronized中各个锁机制

  • 轻量级锁
    锁是一种机制,当锁对象(synchronized指定的对象)的mark word指向线程的栈帧中锁记录时,
    我们称这个线程获得了轻量锁,锁记录简单理解为使用synchronized就在栈帧中创建一个锁记录对象吧

    轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的
    (也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,
    即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

    static final Object obj = new Object();
    public static void method1() {
         synchronized( obj ) {
             // 同步块 A
             method2();
         }
    }
    public static void method2() {
         synchronized( obj ) {
             // 同步块 B
         }
    }

    线程获取轻量锁流程如下:

    • 每次线程运行到synchronized代码块时,都会创建锁记录(Lock Record)对象,
      锁记录内部可以储存对象的Mark Word和对象引用reference(这个对象就是synchronized指定的对象)
    • 让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换
      Object对象的Mark Word ,将Mark Word 的值存入锁记录中
    • 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示
    • 如果cas失败,有两种情况
      1)如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
      2)如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数
    • 当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,则直接释放掉,删除锁记录
    • 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象
      1)成功则解锁成功
      2)失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
  • 锁膨胀
    如果线程在cas交换Object中mark word时失败,说明之前已经有线程交换过Object信息了,
    如果是自己,则进入锁重入阶段,即当前线程创建一个新的锁记录对象,并将锁记录的地址设置为null

    static final Object obj=new Object();
    public static void method1(){
        synchronized( obj ){
          // 同步块
          A method2();
         }
    }
    public static void method2(){
         synchronized( obj ){
         // 同步块 B
          }
    }

    上面代码中,如果一个线程进入method1的同步代码块时,则栈帧中就创建了一个锁记录对象,
    当在锁记录中又调用其他同步方法时,则会创建一个值为null的锁记录,这就是锁重入。

    如果不是自己,是其他线程已经为这个对象加上了轻量级锁,那么就要进入锁膨胀阶段,即升级为重量级锁

    • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
      即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList变成BLOCKED状态
    • 当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程
  • 锁自旋

    重量级锁竞争锁资源的时候,还可以通过自旋来进行优化,
    场景发生在:当锁资源被占用的情况下,Monitor对象中的Entry List线程不用马上进入堵塞队列,
    而是进入自旋状态,简单可以理解为在做循环试探锁资源是否被释放了,目的是达到锁资源一释放就可以
    立马被下一个线程使用,不要再去进行唤醒操作。
    但是要注意的是:

    • 自旋会占用CPU的资源,如果是单核CPU就会存在很大的浪费,所以自旋使用与多核的CPU.
    • Java 7之后就不能手动控制是否开启自旋功能了,而是由JVM自动执行,并且是自适应的,
      例如如果一次自旋成功,就会被认为自旋成功的可能性大,就会多自旋几次,反之,少自旋或者不自旋,设计的比较智能。
  • 偏向锁
    偏向锁是对轻量级锁的优化,在轻量级锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,
    也需要执行CAS操作,这个操作非常耗时,那么java6开始引入了偏向锁,只有第一次使用CAS时将对象的
    Mark Word头设置为线程ID,之后这个线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了。

    如果发现线程ID不是自己的时候,就会尝试CAS替换操作:
    如果操作成功了, 此时该线程就获得了锁对象。( 此时是交替访问临界区, 撤销偏向锁, 升级为轻量级锁)
    如果操作失败了, 此时说明发生了锁竞争。( 此时是多线程访问临界区, 撤销偏向锁, 升级为重量级锁)

    • 偏向锁的状态
      1)一个java对象在被创建出来的时候,这个对象的对象头中mark word默认是偏向锁状态,
      其mark word末尾三位是101,此时其他位(thread位,epoch位,age位)都是0,这些值都是在加锁的时候才会被设置

      2)可当你测试的时候,你会发现你打印的对象的mark word信息末尾三位竟然是001,也就是无锁状态
      这是因为对象偏向锁状态默认是有延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,
      可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟

      3)处于偏向锁的对象解锁后,线程 id 仍存储于对象头中,这就体现了偏向的概念了
      等待下次同一个线程再次加锁就会发现,这个线程id与自己一致,表明需要进行锁重入了,而不需要进行CAS替换

    • 偏向锁被撤销的三种情况
      1)synchronized指定的对象,即锁对象调用了hashcode方法后,该对象就从偏向锁状态转为无锁状态

      2)当其他线程进行cas交换失败时,此时偏向锁被撤销,升级为轻量级锁,偏向锁适合一个线程执行同步代码块
      轻量级锁适合多个线程执行不同的代码块,或者交替获取锁,没有存在竞争情况
      重量级锁适合多个线程竞争执行同一个代码块,存在相互竞争的情况

      3)当锁对象调用wait,notify方法后,偏向锁会被撤销,这时偏向锁会转变为重量级锁,因为这些方法是重量级锁才有。

    • 批量重偏向
      当只有一个线程在执行同步代码块时,这是锁对象状态是偏向锁状态,但如果此时另一个线程获取锁
      执行其他同步代码块,没有产生竞争时,这时锁对象状态从偏向锁状态被撤销,升级为轻量级锁状态,
      如果线程中上述对象撤销偏向锁的次数超过20次,则对象的mark ward的线程ID会偏向另一个线程。

    • 批量撤销
      如果线程中上述对象撤销偏向锁的次数超过40次,则之后创建的所有对象都是无锁状态。

    • 锁消除
      如果jvm中的即时编译器发现你同步代码块中,根本没有对共享变量的修改,或者根本没有线程安全问题
      那么你加的synchronized并不会生效,编译后的字节码中不存在monitorenter和monitorexit原语
      相当于没写synchronized,这就是锁消除。

梳理下整个流程
假设我们关闭了偏向锁的延迟,那么创建的锁对象的mark word状态是偏向锁状态
此时一个线程准备进入同步代码块,我们叫它t1吧,t1线程碰到synchronized关键字,首先检查锁对象状态
很显然是偏向锁状态,但此时的mark word只有后三位是101,而前面位数都还是0,t1线程随即进行CAS操作
将t1线程的线程id保存到mark word中,当然,此时没其他线程跟它争抢,CAS操作成功代表t1线程拥有了锁
紧接着t1线程执行同步代码块中代码。

当t1线程在同步代码块中又调用了其他同步方法,则t1线程会检查锁对象中的markword保存的内容,
很显然,markword中保存的正是t1线程id,这时t1不需要进行CAS操作就拥有锁,这是锁重入

当t1线程执行完同步代码块后,锁对象中markword仍然保留着t1线程的线程id,等t1线程走后
t2线程执行同步代码块,t2线程会撤销锁对象的偏向锁,升级为轻量级锁,t2会在栈帧中创建锁记录对象
并进行CAS操作,交换锁对象中的mark word,使mark word保存锁记录的地址,当然锁记录中还会保存
锁对象的地址,此时锁对象的mark word中后两位是00,代表此时已经是轻量级锁的状态

当t2线程还未结束同步代码块时,又有一个线程t3准备进入和t2同一处代码块中,首先t3进行CAS交换锁对象
的mark word信息,发现CAS操作失败了,这时,t3线程会申请一个monitor对象,并将锁对象中mark word
锁记录的地址改成monitor对象的地址,然后t3修改monitor中owner指向,使之指向t2线程,并自己保存在
monitor对象的entry set中。

当t2线程结束同步代码块时,t2进行CAS操作,交换锁对象的mark word信息,发现失败了,于是通过锁对象
中保存的monitor的地址,找到monitor对象,将monitor中owner引用置为null,并唤醒entry set中的t3线程
最后t2线程退出同步代码块中

以上过程是非常不清晰的,部分重要操作有遗漏,仅仅当做大概的流程,等以后学习更加深入再更新
下面是一张锁升级步骤图,可能也有些不太正确,例如自旋并不会发生在获取轻量级锁时,而是用来获取重量级锁

文章及资料