一、透彻掌握高并发-从理解JVM开始
二、从线程的开闭看JVM的作用
1.run方法
启动start方法,会调用底层C++方法,告诉操作系统当前线程处于可运行状态,而如果直接调用run方法,则就不是以线程的方式来运行了,只是当做一个普通的方法来执行。
2.stop方法 -不推荐使用,stop是强制执行,不管是否正在运行,在做什么,直接停止。
3.如何中断一个阻塞线程
关闭处于阻塞中的线程:
按照上面关闭普通线程的方式,来关闭阻塞中的线程,会发现报了一个异常,首先需要明确的是,
这个异常不是错误。
继续修改下代码
当线程即使处于阻塞的时候,线程不再收到信号,线程也是可以收到一个异常,可以这个异常理解为一个信号,就像闹钟一样就会响,强制这个线程做出一定的响应,而这个异常就是这个子线程的那种,当调用线程终止方法,就会触发这个异常。
这里为什么没有停止呢,这是因为当阻塞的时候,父线程只能给子线程发停止的信号,要不要停止子线程说了算。
再修改下代码
重新执行,子线程自己停止了线程(即Main方法的thread.interrupt()只是发了一个停止的信号,实际子线程停止是子线程自己负责执行)。
这也就是为什么一般写代码遇见wait、sleep要加这个异常。
大白话:
? ? ? ? Java线程调用start(),JVM通过C++调用操作系统的线程接口,由操作系统创建一个线程,再由CPUrun(执行)这个线程;
? ? ? ? 同理Java调通stop或interrupt(),JVM通过C++调用操作系统的线程停止接口,再由CPU收到stop命令停止线程。
三、原子性问题的产生原因与解决方案
package ch12_thread.class2;
/**
* 测试线程的原子性
*/
public class CasExampleTest1 {
private int i;
public void incr(){
i++;
}
}
mac@MacdeMBP class2 % javap -v CasExampleTest1.class
Classfile /Users/mac/IdeaProjects/OOM/JVMSample/src/main/java/ch12_thread/class2/CasExampleTest1.class
Last modified 2024-1-11; size 309 bytes
MD5 checksum 7baf13265d8f13f9acf4aacb6a6ef3b4
Compiled from "CasExampleTest1.java"
public class ch12_thread.class2.CasExampleTest1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // ch12_thread/class2/CasExampleTest1.i:I
#3 = Class #16 // ch12_thread/class2/CasExampleTest1
#4 = Class #17 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 incr
#12 = Utf8 SourceFile
#13 = Utf8 CasExampleTest1.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = NameAndType #5:#6 // i:I
#16 = Utf8 ch12_thread/class2/CasExampleTest1
#17 = Utf8 java/lang/Object
{
public ch12_thread.class2.CasExampleTest1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
public void incr();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 9: 0
line 10: 10
}
SourceFile: "CasExampleTest1.java"
改造成多线程代码
package ch12_thread.class2;
public class AtomicExample {
private int i = 0;
public void incr() {
i++;
}
public static void main(String[] args) throws InterruptedException {
final AtomicExample atomicExample = new AtomicExample();
Thread[] threads = new Thread[2];
for(int j = 0; j < 2; j++){
threads[j] = new Thread(() -> {
for (int k = 0; k < 10000; k++){
atomicExample.incr();
}
});
threads[j].start();
}
threads[0].join();
threads[1].join();
// 预期结果 20000
System.out.println(atomicExample.i);
}
}
运行结果:
结果13644与预期20000不符,CPU切换导致原子性问题。
修改代码, incr方法加同步锁
再次执行
执行结果与预期结果一致。
大白话:
? ? ? ? (1)在早期32位,这时一个Long型数据非常大62位,会将数据分为低32位、高32位,最后合在一起,这时中间被打断,这个数据可能就不准确了,而CPU层保证内存操作原子性就是说CPU去读取数据保证内存操作的原子性,中间不会发生中断,保证数据读取准确。
? ? ? ? (2)CPU和数据之间通过公共总线通信,当CPU-1读取变量时,给总线加锁,保证这个变量不能被其他CPU读取,当CPU-1操作完,将数据存回内存后,在放开总线锁,其他CPU继续访问操作。
? ? ? ? (3)对Cache加锁,后续课程解释。
大白话:
? ? ? ? 临界区加锁,就是对临界区资源(数据)加锁,保证操作安全,操作完后解锁。缺点,耗时,不需要加锁的也加锁了,影响性能。
四、CAS与乐观锁原理
乐观锁 - 将数据比较判断提交到汇编层面执行,保证数据一致性,没有发生冲突继续执行,如果发生冲突再想办法解决。
前面的代码是通过加synchronized同步锁完成的,现在通过在不加锁使用原子类完成
五、可见性问题的本质
package ch12_thread.class5;
public class VolatileExample {
public static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
int i = 0;
while (!stop) {
i++;
}
System.out.println("finish while ...");
});
t1.start();
System.out.println("t1 start ...");
Thread.sleep(1000);
stop = true;
}
}
上面的子线程是否收到stop变量的变化,并最终终止循环输入"finish while ..."
执行结果:
发现在主线程改变静态变量的值,子线程是看不到的变化的。
继续修改代码,给stop变量加上voliatile参数
运行结果:
造成这种情况的原因:
????????线程内部、CPU有缓存,当变量改变时,线程之间、CPU之间感知不到。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值
这里涉及到MSEI缓存一致性协议,具体讲解见
六、顺序性问题的本质和valatile的源码实现原理screenflow
package class6;
/**
* 顺序性问题演示
*/
public class MemoryReorderingExample {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
x = 0;
y = 0;
a = 0;
b = 0;
count++;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = b;
});
// 情况1.t1先执行,y=1, x=0
// 情况2.t2先执行,x=1, y=0
// 情况3.t1和t2同时执行,x=1, y=1
t1.start();
t2.start();
t1.join();
t2.join();
if(x == 0- && y == 0){
System.out.println("第" + count + "次 x=" + x + "y=" + y);
break;
}
}
}
}
运行结果:
正常情况下,不应该出现x==0&&y==0,但是实际测试结果,出现了x==0&&y==0。
为什么会出现这种情况呢,其实是因为出现了指令重排序问题。
当t1线程的x=b,从a=1代码的下面,移动到a=1代码的上面,且t2线程的y=a,从b=1代码的下面移动到b=1代码的上面,就会计算出x==0&&y==0,这种问题就是顺序性问题。
编译器优化场景举例:
????????上图这种情况,编译器认为左边代码太消耗资源,会自动优化成右边代码。
那么针对前面的这种重排序问题,怎么解决呢?
最简单的解决方法,是加锁!
多运行一些时间,没有发现问题
刚才的案例里,使用synchronized关键字,主要作用是加锁,最大缺点是性能低。
大白话:
? ? ? ? java层面设置volatile,JVM转换成C++程序,C++通过操作系统操作一系列硬件指令,通过操作内存屏障保证顺序性问题,通过lock锁操作缓存行,保证不同的缓存之间是一致的。
七、Java里的对象到底是什么
大白话:
? ? ? ? 这里的加锁即synchronized,加锁后具体是哪种锁(偏向锁、轻量级锁、重量级锁) ,都有可能。
? ? ? ? 堆中对象能否使用,基于以下几种状态判断:
? ? ? ? 1.无锁 - 堆中的对象无锁,可以直接使用;
? ? ? ? 2.偏向锁 - 堆中对象被线程占用,对象的对象头会存在偏向锁,保存线程ID,别的线程发现这个对象中有线程信息了,被标记偏向锁,就不能使用了;
? ? ? ? 3.轻量级锁 -?对象竞争比较弱,就会采用轻量级锁;
? ? ? ? 4.重量级锁 - 好多线程都来访问同一个对象,对象竞争比较强,这个对象加重量级锁,第一个线程处理完了,第二个线程再使用,依次使用。
通过JOL查看Java对象信息
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>provided</scope>
</dependency>
</dependencies>
package class6;
public class MyObject {
}
package class6;
import org.openjdk.jol.info.ClassLayout;
// -XX:+UseCompressedOops 默认开启的压缩所有指针
// -XX:+UseCompressedClassPointers 默认开启的压缩对象头的类型执行Klass Pointer
// Oops : Ordinary Object Pointers
public class JOLSample {
public static void main(String[] args) {
ClassLayout layout = ClassLayout.parseInstance(new MyObject());
System.out.println(layout.toPrintable());
}
}
开启压缩后
占了12个字节,不够8的整数倍,还差4个字节,这四个字节,也就是说还可以放其他信息。
八、synchronized锁的状态与实现原理
1.无锁
对应
说明是无锁状态。
2.偏向锁
在同一时刻,有且只有一个线程执行了这个同步锁方法,而且并没有发生竞争的情况,这个时候锁的状态就是偏向锁。
对应
3.轻量级锁
已经发生多线程冲突了,但是不太严重,具体实现CAS(乐观锁)。
当没有多线程访问的状态,就是轻量级锁。
对应锁标志位00
4.重量级锁
很多线程访问对象时,对象已经被锁住,这时,其他线程也来抢占对象,则升级为重量级锁。?
演示代码:
package class6;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class HeavyLockExample {
public static void main(String[] args) throws InterruptedException {
final HeavyLockExample heavy = new HeavyLockExample();
System.out.println("加锁之前");
System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
Thread t1 = new Thread(() -> {
synchronized (heavy){
try{
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
//确保t1线程已经运行
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("t1线程抢占了锁");
System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
synchronized (heavy) {
System.out.printf("main线程来抢占锁");
System.out.printf(ClassLayout.parseInstance(heavy).toPrintable());
}
// System.gc();
// System.out.printf(ClassLayout.parseInstance(heavy).toPrintable());
}
}
一开始,heavy无锁
t1抢占后,heavy变为轻量级锁
main线程,再去抢占对象,变为重量级锁
注:偏向锁不太好演示出来。