持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
JUC学习系列笔记(三)
JMM(Java Memory Model)——Java内存模型
参考:zhuanlan.zhihu.com/p/258393139
JMM是一个理论模型,类似于一个约定。面试时问内存模型实际上是想要询问多线程的内容
Java 内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
volatile关键字
1、保证可见性
代码示例:
public class VolatileVisibilityTest {
// 添加volatile关键字保证主线程中修改num,A线程中可见。不添加volatile关键字可能会导致A线程对num的修改不可见,陷入死循环
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
// 开启一个线程,num==0时持续循环
new Thread(() -> {
while (num == 0) {
System.out.println("循环中···");
}
}, "A").start();
// 模拟延时,主线程停2秒,让A线程先执行
TimeUnit.SECONDS.sleep(2);
// num==1时A线程循环停止
num = 1;
System.out.println(num);
}
}
2、不保证原子性
可以使用原子类来保证原子性
代码示例:
public class VolatileAutoTest {
// private volatile static int num = 0;
// 保证原子性
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
// 不是一个原子性操作
// num++;
// +1操作
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
add();
}
}, String.valueOf(i)).start();
}
// 当线程大于2时(除了main 和 gc线程),还有其他线程,那么main线程让出CPU给其他线程执行
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 理论上这里num应该是2000,但是volatile不保证原子性,这里实际上并不是2000
System.out.println(Thread.currentThread().getName() + " -> " + num);
}
}
3、禁止指令重排
参考:segmentfault.com/a/119000003…
指令重排:程序v可能并非按照代码编写顺序执行
源代码 -> 编译器优化可能导致重排 -> 指令并行也可能重排 -> 内存系统也可能重排 -> 执行
volatile关键字可以利用内存屏障禁止指令重排
单例模式
饿汉式单例(可能浪费资源) -> 懒汉式单例(线程不安全) -> 双重检验机制饿汉式(线程安全,可能存在指令重排问题) -> volatile改进双重检验机制饿汉式(可能存在反射破解问题) -> 枚举类型单例模式
CAS机制
参考:segmentfault.com/a/119000001…
Compare And Swap 比较并交换,是操作系统的并发原语。比较当前工作内存中的值与主内存中的值是否一致,一直则执行操作,不一致则一直循环。
CAS实际上就是乐观锁 的一种实现方式,乐观锁参考:www.cnblogs.com/kismetv/p/1…
缺点:
- 使用自旋锁,循环比较耗时
- 一次性只能保证一个变量的原子性
- 存在ABA问题
Unsafe类
Java无法直接操作内存,C++可以操作内存,Java可以调用C++,进而来简介操作内存。native表明方法为本地方法,是通过C/C++实现的,供Java来调用。Unsafe类中有大量的native方法,Unsafe 类相当于一个Java操作内存的手段(途径)。
原子类的CAS机制
以整形原子类为例,查看+1操作的源码,分析CAS机制
源码如下:
AtomicInteger类中的操作:
// 原子地递增当前值,具有VarHandle.getAndAdd指定的记忆效应。
// 等效于getAndAdd(1)
public final int getAndIncrement(){
return U.getAndAddInt(this,VALUE,1);
}
Unsafe类中的操作:
// 以原子方式将给定值添加到给定对象o中给定offset处的字段或数组元素的当前值。
// 参数:o – 用于更新字段/元素的对象/数组
// 偏移量 - 字段/元素偏移量
// delta – 要添加的值
// 返回:之前的值
public final int getAndAddInt(Object o,long offset,int delta){
int v;
// 这里是一个自旋锁,一直在循环
do{
// 调用本地方法通过对象和偏移量获得内存中的值
v=getIntVolatile(o,offset);
// 判断并更新值,如果现在内存地址对应的值和获得v的值相同,则更新内存地址中的值为v+data
}while(!weakCompareAndSetInt(o,offset,v,v+delta));
return v;
}
CAS存在的ABA问题
假设有两个线程A、B,和一个资源p,假设p=2021,现在作一个操作:p=2021时A把p改为2022,B线程判断p=2021,此时B线程修改p=2025,然后修改为2021,A线程进行判断时虽然p还是2021,但这时的p 已经是被修改过的了。这就是ABA问题,p从2021变为2022再变为2021
原子引用
带版本号的原子操作,AtomicStampedReference类,AtomicReference类,可以使用时间戳也可以使用自定义的版本号。这里面的CAS机制比较的是对象的引用
public class AtomicReferenceTest {
public static void main(String[] args) {
/**
* 这里有一个坑,compareAndSet方法底层是使用==比较的,初始化的值Integer类型,如果直接传入一个整数会进行装箱,如果是-128~127
* 之间,则可以正常使用,因为在内存中是指向的是同一个内存地址。如果不在这个范围内,进行自动装箱之后,再通过原子操作对该数据进行修改,则实际上修改的不是同一个对象
* 这里可以在外部定义一个Integer类型的变量,作为参数传递进去,例如Integer integer = Integer.valueOf(2020);将integer作为参数initial
*/
AtomicStampedReference<Integer> integerAtomicReference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
// 获得版本号
int stamp = integerAtomicReference.getStamp();
System.out.println("A1->" + stamp);
try {
// 模拟延时
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新值,修改为2,里面的CAS机制比较的是对象的引用
integerAtomicReference.compareAndSet(1, 2, integerAtomicReference.getStamp(),
integerAtomicReference.getStamp() + 1);
// 输出新的版本号
System.out.println("A2->" + integerAtomicReference.getStamp());
// 再修改为1,这里虽然expectedReference(期望值)再次被修改回来了,但是版本号已经改变了
System.out.println("A->" + integerAtomicReference.compareAndSet(2, 1, integerAtomicReference.getStamp(),
integerAtomicReference.getStamp() + 1));
System.out.println("A3->" + integerAtomicReference.getStamp());
}, "A").start();
new Thread(() -> {
// 获得版本号
int stamp = integerAtomicReference.getStamp();
System.out.println("B1->" + stamp);
try {
// 模拟延时
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新值,修改为5,这里更新失败,因为之前(延时前)获得的stamp和现在再获得的integerAtomicReference.getStamp()
// 已经是不同了,A中进行了一次修改,虽然expectedReference(期望值)是相同的,但是仍旧不能修改成功
System.out.println("B->" + integerAtomicReference.compareAndSet(1, 5, integerAtomicReference.getStamp(),
integerAtomicReference.getStamp() + 1));
// 输出新的版本号
System.out.println("B2->" + integerAtomicReference.getStamp());
}, "B").start();
}
}
各种锁的再理解
公平锁、非公平锁
公平锁: 排队获取资源,不可抢占,申请锁时会直接进入等待队列,等待队列的第一个线程才能获得锁
非公平锁: 可以抢占,一般默认是非公平的,申请锁时会直接尝试获得锁,如果获取失败则进入队列
可重入锁
可重入锁(递归锁):获得最外层的锁之后,就可以获得内部所有的锁,这就是可重入锁。所有的锁都是可重入锁
代码示例:
资源类:
public class Phone {
// sendMail方法上了锁,在里面又调用了加了锁的call方法,如果获得sendMail方法的锁,同时会获得call方法的锁,call方法执行完毕才会释放锁。
// 这里实际上获得就是Phone对象实例的锁,对应方法执行完毕之后才会释放锁。这里对应call执行完毕之后,sendMail才执行结束。
public synchronized void sendMail() {
System.out.println(Thread.currentThread().getName() + "发邮件");
call();
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + "打电话");
}
}
测试类:
public class ReentrantLockTest {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(phone::sendMail, "A").start();
new Thread(phone::sendMail, "B").start();
}
}
自旋锁
一直进行循环,直到获得想要的资源之后才结束循环。
自定义自旋锁:
public class SpinLockDiy {
private AtomicReference atomicReference = new AtomicReference<>();
// 使用CAS机制,加锁
public void lock() {
Thread thread = Thread.currentThread();
// 使用CAS机制实现自旋锁,如果当前值不为空则一直循环,直到当前值为空,没有其他线程获得锁,则当前循环结束,停止自旋
while (!atomicReference.compareAndSet(null, thread)) {
System.out.println(thread.getName() + "自旋中");
}
System.out.println(thread.getName() + " -> lock");
}
// 解锁
public void unlock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " -> unlock");
// 解锁,当前值设为null
atomicReference.compareAndSet(thread, null);
}
}
自定义自旋锁测试:
public class SpinLockDiyTest {
public static void main(String[] args) throws InterruptedException {
SpinLockDiy spinLockDiy = new SpinLockDiy();
new Thread(()->{
// 添加锁
spinLockDiy.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
spinLockDiy.unlock();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
// 添加锁
spinLockDiy.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
spinLockDiy.unlock();
}
},"B").start();
}
}
死锁分析
死锁排查方法参考:www.cnblogs.com/itsoku123/p…
死锁模拟:
资源类:
public class Resource {
// 这里s1和s2都是再常量池中获取的,每次实例化resource使用的都是常量池中同一个内存地址的s1和s2;new 方式产生的字符串,每次实例化Resource都会在堆中构建一个新的对象。
// String s1 = new String("s1");这里会产生两个对象,先在常量池中查找是否存在s1,如果不存在则在常量池中创建一个,存在则不管。因为new关键字需要在堆中创建一个对象。
private String s1 = "s1";
private String s2 = "s2";
// 如果是这种方式创建对象则不会发生死锁,因为每次实例化Resource都会创建新的s1和s2,锁的不是同一个对象,所以不会发生死锁
// private String s1 = new String("s1");
// private String s2 = new String("s2");
// 先获得s1,再获得s2
public void get1() {
synchronized (s1) {
System.out.println(Thread.currentThread().getName() + "获得s1,想要获得s2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
System.out.println(Thread.currentThread().getName() + "获得s2");
}
}
}
// 先获得s2,再获得s1
public void get2() {
synchronized (s2) {
System.out.println(Thread.currentThread().getName() + "获得s2,想要获得s1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
System.out.println(Thread.currentThread().getName() + "获得s1");
}
}
}
}
测试类:
public class DeadLockTest {
public static void main(String[] args) {
// 定义一个资源类
Resource resource = new Resource();
// 两个线程互相抢夺互斥资源,发生死锁
new Thread(() -> {
resource.get1();
}, "A").start();
new Thread(() -> {
resource.get2();
}, "B").start();
}
}
死锁排查方法:
- 查看日志
- 查看堆栈信息
查看堆栈信息方式:
# 首先查看Java线程状态
PS C:\IdeaProject\WorkPlace\JavaBase> jps -l
16420 org.jetbrains.jps.cmdline.Launcher
17396 com.zhang.Java10JUC.JUC15lock.deadLock.DeadLockTest
13016
16092 jdk.jcmd/sun.tools.jps.Jps
# 查看指定线程的堆栈信息
PS C:\IdeaProject\WorkPlace\JavaBase> jstack 17396
2021-09-15 09:23:29
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.8+10-LTS mixed mode):
Threads class SMR info:
_java_thread_list=0x00000207d4c23d00, length=13, elements={
0x00000207d48a0800, 0x00000207d48ab000, 0x00000207d4906000, 0x00000207d4908000,
0x00000207d490a000, 0x00000207d490d000, 0x00000207d4098000, 0x00000207d4027800,
0x00000207d4ca0800, 0x00000207d4ca1000, 0x00000207d4be7800, 0x00000207d4be8800,
0x00000207b5367800
}