近况
实习结束了就回到家里,准备毕设和学习多线程了,我还是自制力太差,学习效果不太好,自制力差的原因
在于没有认清自己目前所处的阶段,所以才无所事事,在沉沦很多天之后,期间断断续续学习,坦白说
没有很好的收获,人啊,一旦没有了目标,心就空了,就容易迷茫,人还是应该有所追求,有目标才有动力。
我一直认为,学习如果没有输出,是很难继续下去的,这就是为什么我写博客的原因,哪怕没人看
最近一段时间开始学习多线程,原本想搞定完毕设再学习的,但是毕设学习若依项目,继续不下去,
而且需要另写一套前端,CSS极差,又没有经验写,虽然学习过vue,但是我给鸽了,唉,
我怕再不学习多线程,等家人都回来过年了,吵吵闹闹的又没定力学习去了,怕是又跑去打麻将了~
所以趁家人没回来,赶紧把多线程学习了,这个多线程,是我众多心病之一,所以要解决掉它
现在写第一篇多线程博客,随便聊聊
本篇博客内容如下:
- Java线程的6种状态
- 创建线程的3种方式
- Thread的常用方法
- 线程安全问题
- 变量的线程安全分析
- 常见的线程安全类
- 对象头与monitor
- synchronized原理
- synchronized原理进阶
- 文章及资料
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)失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
- 每次线程运行到synchronized代码块时,都会创建锁记录(Lock Record)对象,
锁膨胀
如果线程在cas交换Object中mark word时失败,说明之前已经有线程交换过Object信息了,
如果是自己,则进入锁重入阶段,即当前线程创建一个新的锁记录对象,并将锁记录的地址设置为nullstatic 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线程
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
锁自旋
重量级锁竞争锁资源的时候,还可以通过自旋来进行优化,
场景发生在:当锁资源被占用的情况下,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线程退出同步代码块中
以上过程是非常不清晰的,部分重要操作有遗漏,仅仅当做大概的流程,等以后学习更加深入再更新
下面是一张锁升级步骤图,可能也有些不太正确,例如自旋并不会发生在获取轻量级锁时,而是用来获取重量级锁
文章及资料