第一章 并发编程中的三个问题
1.1 可见性
1.1.1 目标
学习什么是可见性问题
1.1.2 可见性概念
可见性(Visibility):是指当一个线程对共享变量进行修改,另一个线程要立即得到修改后的最新值。
1.1.3 案例演示
/*
目标:演示可见性问题
1.创建一个共享变量
2.创建一条线程不断读取共享变量
3.创建一条线程修改共享变量
*/
public class Test01Visibility {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(flag){
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag = false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
1.1.4小结
并发编程中,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
1.2 原子性
1.2.1 目标
学习什么是原子性问题
1.2.2 原子性概念
原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素的干扰而中断,要么所有的操作都不执行。
1.2.3 案例演示
/*
目标:演示原子性问题
1.定义一个共享变量number
2.对number进行1000次++操作
3.使用5个线程来进行
*/
public class Test02Atomicity {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = ()-> {
for(int i=0;i<1000;i++){
number++;
}
};
List<Thread> list = new ArrayList<>();
for(int i=0;i<5;i++){
Thread t = new Thread(increment);
t.start();
// t.join();
list.add(t);
}
for(Thread t : list){
t.join();
}
System.out.println(number);
}
}
使用javap反汇编class文件(javap -p -v .\Test02Atomicity.class )
得到如下字节码指令:
9: getstatic #51 // Field number:I
12: iconst_1
13: iadd
14: putstatic #51 // Field number:I
由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出问题。比如一个线程在执行13: iadd时,另一个线程又执行了getstatic。会导致两次number++,实际上只加了1。
1.2.4 小结
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。
1.3 有序性
1.3.1 目标
学习什么是有序性问题
1.3.2 有序性概念
有序性(Ordering):是指程序中代码的执行顺序,为了提高程序执行效率,java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
1.3.3 有序性演示
.....
1.3.4 小结
程序代码在执行过程中的先后顺序,由于java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序
第二章 Java内存模型(JMM)
2.1 计算机结构
2.1.1 目标
学习计算机的主要狗组成
学习缓存的作用
2.2.2 计算机结构简介
冯诺依曼,五大组成部分
2.2 java内存模型
2.2.1 目标
学习java内存模型的概念和作用
2.2.2 Java内存模型的概念
Java Memory Molde(Java内存模型/JMM),不要与java内存结构混淆
关于“Java内存模型”的权威解释,参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf
Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。
● 主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
● 工作内存
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
2.2.3 Java内存模型的作用
Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性和原子性的规则和保障。
2.2.4 CPU缓存,内存与Java内存模型的关系
通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行。
但ava内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
JMM内存模型与CPU硬件内存架构的关系:
2.2.5 小结
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。
2.3 主内存与工作内存之间的交互
2.3.1 目标
了解主内存与工作内存之间的数据交互过程
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
对应如下的流程图:
注意:
1.如果对一个变量执行lock操作,将会清空工作内存中此变量的值
2.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中
2.3.2 小结
主内存与工作内存之间的数据交互过程
lock -> read -> load -> use -> assign -> store -> write -> unlock
第三章 synchronized保证三大特性
synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized (锁对象) {
//受保护资源;
}
3.1 synchronized与原子性
3.1.1 目标
学习使用synchronized保证原子性的原理
3.1.2 使用synchronized保证原子性
原理:synchronized保证只有一个线程拿到锁,能够进入同步代码块。
3.2 synchronized与可见性
3.2.1 目标
学习使用synchronized保证可见新给的原理
3.2.2 使用synchronized保证可见性
public class Test01Visibility {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(()->{
while(flag){
synchronized (obj){
}
//System.out.println(flag);
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag = false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
//使用print也会达到相同效果,因为print的原码中含有synchronized语句
3.2.3 小结
synchronized保证可见性的原理:执行synchronized时,对应lock原子操作会刷新工作内存中共享变量的值
3.3 synchronized与有序性
3.3.1 目标
学习使用synchronized保证有序性的原理
3.3.2 为什么要重排序
为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
3.3.3 as-if-serial语义
as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
由于还不会使用jcstress并发压力测试,所以使用while循环外加一个if判断来验证是否会出现result==0的情况
public class Test03Ordering {
private static int result;
public static int num;
public static boolean ready;
public void actor2(){
num = 2;
ready = true;
}
public static void main(String[] args) throws InterruptedException {
while(true){
num = 0;
ready = false;
result = 0;
Object obj = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
if (ready) {
result = num + num;
} else {
result = 1;
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
ready = true;
num = 2;
}
}
});
t1.start();t2.start();
t1.join();t2.join();
System.out.println("num = " + num + " ,ready = " + ready + " ,result = "+ result);
if(result == 0){
break;
}
}
}
}
第四章 synchronized的特性
4.1 可重入特性
4.1.1 目标
了解什么是可重入
了解可重入的原理
4.1.2 什么是可重入
一个线程可以多次执行synchronized,重复获取同一把锁
4.1.3 代码演示
/*
目标:演示synchronized可重入
1.自定义一个线程类
2.在线程类的run方法中使用秦涛的同步代码块
3.使用两个线程来执行
*/
public class Demo01 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
//1.定义一个线程类
class MyThread extends Thread{
@Override
public void run() {
synchronized (MyThread.class){
System.out.println(getName() + "进入了同步代码块1");
synchronized (MyThread.class){
System.out.println(getName() + "进入了同步代码块2");
}
}
}
}
4.1.4 可重入原理
synchronized的锁对象中有一个计数器(recursions变量) 会记录线程获得几次锁
4.1.5 可重入的好处
1.可以避免死锁
2.可以让我们更好的来封装代码
4.1.6 小结
synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁
4.2 不可中断特性
4.2.1 目标
学习synchronized 不可中断特性
学习Lock的可中断特性
4.2.2 什么是不可中断
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,在等待或阻塞中不可被中断。
4.2.3 synchronized不可中断代码演示
synchronized是不可中断,处于阻塞状态的线程会一直等待锁。
/*
目标:演示synchronized不可中断
1.定义一个Runnable
2.在Runnable定义同步代码块
3.先开启一个线程来执行同步代码块,保证不退出同步代码块
4.后开启一个线程来执行同步代码块(阻塞状态)
5.强行停止第二个线程
*/
public class Uninterruptible {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//1.定义一个Runnable
Runnable run = () ->{
//2.在Runnable定义同步代码块
synchronized (obj){
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
//保证不退出同步代码块
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//3.先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
//保证第一个线程先去执行
Thread.sleep(1000);
//4.后开启一个线程来执行同步代码块
Thread t2 = new Thread(run);
t2.start();
//5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
4.2.4 ReentreantLock可中断演示
Lock有两种方式,一种是可中断一种是不可中断
/*
目标:演示Lock不可中断和可中断
*/
public class Interruptible {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//test01();
test02();
}
//演示Lock不可中断
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock();
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
b = lock.tryLock(3, TimeUnit.SECONDS);
if (b) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定的时间内没有得到锁做其他操作");
}
}catch(InterruptedException e){
e.printStackTrace();
} finally{
if (b) {
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
/* System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());*/
}
}
执行结果:
test01:
test02:
4.2.5 小结
不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放,锁后一个线程会一直阻塞或等待,在阻塞和等待过程中是不可被中断的。
synchronized属于不可被中断
Lock的lock方法是不可被中断的
Lock的tryLock方法是可中断的
第五章 synchronized原理
5.1 javap反汇编
5.1.1 通过javap反汇编学习synchronized的原理
我们编写一个简单的synchronized代码,如下
public class Demo01 {
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj){
System.out.println("1");
}
}
public synchronized void test(){
System.out.println("a");
}
}
通过cmd对class文件进行反汇编
得到
main方法的
test方法的
5.1.2 monitorenter
首先看一下JVM规范中对于monitorenter的描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
● If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
● If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
● If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
翻译:
每一个对象都会和一个监视器monitor关联。(这个对象也就是被我们当作锁的Object obj)
监视器被占用时会被锁住,其他线程无法来获取该monitor。
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
1.若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2.若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3.若其他线程已占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
monitorenter小结
synchronized的锁对象会关联一个monitor(C++ 对象),这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner(拥有这把锁的线程),recursions(记录线程拥有锁的次数),当一个线程拥有monitor后其他线程只能等待
5.1.3 monitorexit
首先看一下JVM规范中对于monitorenter的描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻译:
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权
我们发现在这里有两个monitorexit,第一个monitorexit是 6、9、11、14执行都不出错时执行的,第二个monitorexit,我们观察最下面的Exception table,这个意思时当代码出错时,就执行到19,这时第二个monitorexit就保证了锁一定会被释放。
面试题:synchronized出现异常还会释放锁吗?
会释放锁,原因如上。
5.1.4 同步方法
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
接下来观察 test的反汇编代码
发现没有见到monitorenter与monitorexit
可以看到同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit
5.1.5 小结
通过javap反汇编我们看到synchronized使用编程了monitorenter和monitorexit两个指令。每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁
5.1.6 面试题:synchronized与Lock的区别?
synchronized是关键字,而Lock是一个接口。 synchronized没有源码,是由JVM直接支持。
synchronized会自动释放锁,而Lock必须手动释放锁。
synchronized是不可中断的,Lock可以中断也可以不中断。
通过Lock可以知道线程有没有拿到锁,而synchronized不能。 tryLock方法会返回一个boolean值,来告知是否获得锁
synchronized能锁住方法和代码块,而Lock只能锁住代码块。
Lock可以使用读锁提高多线程读效率。Lock 的一个实现类 ReentrantReadWriteLock,允许多个线程来读,允许一个线程来写,可以提高多个线程来读的效率。
synchronized是非公平锁,ReentreantLock可以控制是否是公平锁。当有多个线程在等待锁的时候,不是按照先来后到来给锁的,而是随机的在构造ReentreantLock锁时使用带参构造方法,传入一个boolean类型变量 fair来选择锁是公平锁。
5.2 深入JVM源码
5.2.1 目标
通过JVM源码分析synchronized的原理
5.2.2 JVM源码下载
5.2.3 IDE(Clion)下载
5.2.4 monitor监视器锁
可以看出无论是synchronized代码块还是synchronized方法,其线程安全的语义实现最终依赖一个叫monitor的东西,那么这个东西是什么呢?下面让我们来详细介绍一下。
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。
_recursions //现成的重入次数
_object //存储该monitor的对象
_owner //标识拥有该monitor的线程
waitSet //处于wait状态的线程,会被加入到waitSet
_cxq //多线程竞争锁时的单向列表
_EntryList //处于等待锁block状态的线程,会被加入到该列表
5.2.5 monitor竞争
1.执行monitorenter时,会调用InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
这段代码的学习重点在最下面的if else语句中
if判断是不是用偏向锁,如果我们设置了偏向锁就会走fast_enter
如果没有设置就执行else中的语句
2.执行else语句,对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter
3.最终调用ObjectMonitor::enter (位于src/share/vm/runtime/objectMonitor.cpp)
void ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD;
//通过CAS操作尝试把monitor的_owner字段设置为当前线程
void * cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL);
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert(_recursions == 0, "invariant");
assert(_owner == Self, "invariant");
return;
}
// 线程重入,recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions++;
return;
}
//如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
if (Self->is_lock_owned ((address)cur)) {
assert(_recursions == 0, "internal state error");
_recursions = 1;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self;
return;
}
省略一些代码
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
//如果获取锁失败,则等待锁的释放
EnterI(THREAD);
if (!ExitSuspendEquivalent(jt)) break;
_recursions = 0;
_succ = NULL;
exit(false, Self);
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
此处省略锁的自旋优化等操作,统一放在后面synchronized优化中说。
以上代码的具体流程概括如下:
1.通过CAS尝试把monitor的owner字段设置为当前线程。
2.如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入的次数。
3.如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
4.如果获取锁失败,则等待锁的释放
5.2.6 monitor等待
void ObjectMonitor::EnterI(TRAPS) {
Thread * const Self = THREAD;
// Try the lock - TATAS
//尝试抢救一下,看还能不能抢到锁
if (TryLock (Self) > 0) {
assert(_succ != Self, "invariant");
assert(_owner == Self, "invariant");
assert(_Responsible != Self, "invariant");
return;
}
//自旋,尝试抢救一下,看还能不能抢到锁
if (TrySpin (Self) > 0) {
assert(_owner == Self, "invariant");
assert(_succ != Self, "invariant");
assert(_Responsible != Self, "invariant");
return;
}
//当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev = (ObjectWaiter *) 0xBAD;
node.TState = ObjectWaiter::TS_CXQ;
//通过CAS把node节点push到_cxq列表中
ObjectWaiter * nxt;
for (;;) {
node._next = nxt = _cxq;
if (Atomic::cmpxchg_ptr(&node, &_cxq, nxt) == nxt) break;
// Interference - the CAS failed because _cxq changed. Just retry.
// As an optional optimization we retry the lock.
//尝试抢救一下,看还能不能抢到锁
if (TryLock (Self) > 0) {
assert(_succ != Self, "invariant");
assert(_owner == Self, "invariant");
assert(_Responsible != Self, "invariant");
return;
}
}
for (;;) {
//线程在被挂起前做一下挣扎,看还能不能抢到锁
if (TryLock(Self) > 0) break;
assert(_owner != Self, "invariant");
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr(Self, &_Responsible, NULL);
}
// park self
//park就是挂起,park就不会执行了,就等被唤醒了
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT(Inflated enter - park TIMED);
Self->_ParkEvent->park((jlong) recheckInterval);
// Increase the recheckInterval, but clamp the value.
recheckInterval *= 8;
if (recheckInterval > MAX_RECHECK_INTERVAL) {
recheckInterval = MAX_RECHECK_INTERVAL;
}
} else {
TEVENT(Inflated enter - park UNTIMED);
Self->_ParkEvent->park();
}
//尝试抢救一下,看还能不能抢到锁
if (TryLock(Self) > 0) break;
}
}
以上代码的具体流程概括如下:
1.当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
2.在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒
4.当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁
5.2.7 monitor释放
当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其他线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp)
退出同步代码块会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。
5.2.8 monitor是重量级锁
可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。
用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:
从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。
内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、l/O资源等。
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。
所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如I/0调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。系统调用的过程可以简单理解为:
1.用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。
2.用户态程序执行系统调用。
3.CPU切换到内核态,并跳到位于内存指定位置的指令。
4.系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
5.系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。
由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。
第六章 JDK6 synchronized优化
6.1 CAS
6.1.1 目标
学习CAS的使用
学习CAS的原理
6.1.2 CAS概述和作用
CAS的全称是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。
CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中
CAS和volatile实现无锁并发
java提供了一个类 AtomicInteger 可以实现原子操作,这是因为它底层就是由CAS实现的
private static AtomicInteger number = new AtomicInteger();
number.getAndIncrement();
6.1.3 CAS原理
通过刚才AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。
Unsafe类介绍
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,
官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。
Unsafe实现CAS
CAS原理分析
乐观锁和悲观锁
悲观锁从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。
因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁。性能较差!
乐观锁从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。
所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。
CAS这种机制我们也可以将其称之为乐观锁。
综合性能较好!
CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
6.1.4 小结
CAS的作用?Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证。
CAS的原理?CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果 内存地址值V 和 旧的预期值A 相等就修改 内存地址值为B
6.2 synchronized锁升级过程
高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,
包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等等,
这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
无锁--》偏向锁--》轻量级锁–》重量级锁
6.3 Java对象的布局
6.3.1 目标
学习对象头布局
6.3.2 对象头
术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html、Java对象的布局
在JVM中,对象在内存中的布局分为三块区域:**对象头、实例数据和对齐填充**。如下图所示:
在普通实例对象中,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 中。
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:
klass pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。
该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。
为了节约内存可以使用选项-XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。
开启该选项后,下列指针将压缩至32位:
每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,
比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)
在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;
在64位系统中,Mark Word = 8 bytes,类型指针 = 4bytes,对象头 = 16 bytes = 96bits;开启指针压缩
实例变量
就是类中定义的成员变量。
对齐填充
对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,
换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
查看Java对象布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
7. 偏向锁
什么是偏向锁
单线程竞争,当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,
以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。
偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 , 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
偏向锁的撤销
偏向锁的撤销动作必须等待全局安全点
暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟(4s)之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,
如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false 参数关闭偏向锁。
偏向锁好处
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。
偏向锁可以提高带有同步但无竞争的程序性能。
它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。
在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。
但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false 参数关闭偏向锁。
总结
1.偏向锁的原理是什么?
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,
如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
2.偏向锁的好处是什么?
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。
偏向锁可以提高带有同步但无竞争的程序性能。
8. 轻量级锁
是什么
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寨
主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短
本质就是自选锁CAS
64位标记图再看
轻量级锁的获取
轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗.说白了先自旋,不行才升级阻寨。
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A己经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁己经被线程A拿到,当前该锁己是偏向锁了。
而线程B在争抢时发现对象头Mark Ward中的线程ID不是线程B自己的线程1D(而是线程A),那线程B就会进行CAS操作希望能获得锁。此吋线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程1D为B自己的1D(A—B).重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位:
如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
补充说明
轻量级锁的加锁
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋CAS:不断尝试去获取锁,能不升级就不往上捅,尽量不要阻寨
轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻寨的线程。
Code演示
如果关闭偏向锁,就可以直接进入轻量级锁
-XX:-UseBiasedLocking
步骤流程图示
轻量级锁状态下,CAS自旋达到一定次数也会升级为重量级锁
自旋达到一定次数和程度
java6之前
了解即可
java6之后
【自适应自选锁】的大致原理
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之
如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
总之,自适应意味着自选的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。
轻量锁和偏向锁的区别和不同
争夺轻量级锁失败时,自旋尝试抢占锁
轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
9. 重量级锁
有大量的线程参与锁的竞争,冲突性很高
锁标志位
重量级锁原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
Code演示
小总结-面试中的高频考点
锁升级发生后,hashcode去哪啦
说明
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,己经没有位置再保存哈希码,GC年龄了,那么这些信息被移动到哪里去了呢?
用更加通俗的话解释(四种锁的不同情况)
在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
对于偏向锁,在线程获取偏向锁时,会用Thread |D和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code****共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
code01
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态,跳过偏向锁,直接升级轻量级锁
code02
在偏向锁的状态中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁
各种锁优缺点、synchronized锁升级和实现原理
synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。
实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。
偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果
同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁
更严重,这时候就需要升级为重量级锁。
JIT编译器对锁的优化
JIT
Just In Time Compiler,一般翻译为即时编译器
锁消除
从JIT角度看相当于无视它,synchronized(o)不存在了,
这个锁对象并没有被共用扩散到其它线程使用,
极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
锁粗化
假如方法中首位相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
加粗加大范围,一次申请使用即可,避免次次都申请和释放锁,提升了性能
小总结
没有锁:自由自在
偏向锁:唯我独尊
轻量锁:楚汉争霸
重量锁:群雄逐鹿
评论