JUC-JMM

https://blog.csdn.net/qq_43842093/article/details/127200931

https://juejin.cn/post/6977323236186914852

JMM

内存模型

Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM 作用:

  • 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
  • 规定了线程和内存之间的一些关系

根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成

主内存和工作内存:

  • 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值
  • 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝

CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。

这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

JVM 和 JMM 之间的关系:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来:

  • 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
  • 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存

https://blog.csdn.net/fox_bert/article/details/88661569?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4-88661569-blog-122598335.235^v38^pc_relevant_anti_t3_base&spm=1001.2101.3001.4242.3&utm_relevant_index=5

内存交互

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,每个操作都是原子

非原子协定:没有被 volatile 修饰的 long、double 外,默认按照两次 32 位的操作

  • lock:作用于主内存,将一个变量标识为被一个线程独占状态(对应 monitorenter)
  • unclock:作用于主内存,将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定(对应 monitorexit)
  • read:作用于主内存,把一个变量的值从主内存传输到工作内存中
  • load:作用于工作内存,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use:作用于工作内存,把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令
  • assign:作用于工作内存,把从执行引擎接收到的一个值赋给工作内存的变量
  • store:作用于工作内存,把工作内存的一个变量的值传送到主内存中
  • write:作用于主内存,在 store 之后执行,把 store 得到的值放入主内存的变量中

JUC-Lock接口获取锁的4种方法

java.util.concurrent.locks

在 Lock 接口中,获取锁的方法有 4 个:lock()、tryLock()、tryLock(long,TimeUnit)、lockInterruptibly(),为什么需要这么多方法?这些方法都有什么区别?

lock 方法

lock 方法是 Lock 接口中最基础的获取锁的方法,当有可用锁时会直接得到锁并立即返回,当没有可用锁时会一直等待,直到获取到锁为止,它的基础用法如下:

1
2
3
4
5
6
7
8
9
Lock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
    // 执行业务代码...
} finally {
//释放锁
    lock.unlock();
}

lockInterruptibly 方法

lockInterruptibly 方法和 lock 方法类似,当有可用锁时会直接得到锁并立即返回,如果没有可用锁会一直等待直到获取锁,但和 lock 方法不同,lockInterruptibly 方法在等待获取时,如果遇到线程中断会放弃获取锁。它的基础用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Lock lock = new ReentrantLock();
try {
// 获取锁
lock.lockInterruptibly();
try {
// 执行业务方法...
} finally {
// 释放锁
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}

PS:该方法获取的锁支持使用 thread.interrupt() 方法中断线程对锁的等待。

tryLock 方法

与前面的两个方法不同,使用无参的 tryLock 方法会尝试获取锁,并立即返回获取锁的结果(true 或 false),如果有可用锁返回 true,并得到此锁,如果没有可用锁会立即返回 false。它的基础用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Lock lock = new ReentrantLock();
// 获取锁
boolean result = lock.tryLock();
if (result) {
try {
// 获取锁成功,执行业务代码...
} finally {
// 释放锁
lock.unlock();
}
} else {
// 执行获取锁失败的业务代码...
}

tryLock(long,TimeUnit) 方法

有参数的 tryLock(long,TimeUnit) 方法需要设置两个参数,第一个参数是 long 类型的超时时间,第二个参数是对参数一的时间类型描述(比如第一参数是 3,那么它究竟是 3 秒还是 3 分钟,是第二个参数说了算的)。在这段时间内如果获取到可用的锁了就返回 true,如果在定义的时间内,没有得到锁就会返回 false。它的基础用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Lock lock = new ReentrantLock();
try {
// 获取锁(最多等待 3s,如果获取不到锁就返回 false)
boolean result = lock.tryLock(3, TimeUnit.SECONDS);
if (result) {
try {
// 获取锁成功,执行业务代码...
} finally {
// 释放锁
lock.unlock();
}
} else {
// 执行获取锁失败的业务代码...
}
} catch (InterruptedException e) {
e.printStackTrace();
}

总结

lock()、tryLock()、tryLock(long,TimeUnit)、lockInterruptibly() 都是用来获取锁的,其中

  • lock 方法如果获取不到锁会一直阻塞等待

  • lockInterruptibly 方法虽然也会阻塞等待获取锁,但它却能中途响应线程的中断

  • 无参的 tryLock 方法会立马返回一个获取锁成功与失败的结果

  • 有参数的 tryLock(long,TimeUnit) 方法会在设定的时间内返回一个获取锁成功与失败的结果

这 4 个方法的特性各不相同,需要根据实际的业务情况选择合适获取锁的方法。


JUC-锁细化

一间大屋子有两个功能:睡觉、学习,互不相干

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

细化前

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}

执行:

1
2
3
4
5
6
7
8
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.compute();
},"小南").start();

new Thread(() -> {
bigRoom.sleep();
},"小女").start();

某次结果:

1
2
12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时

细化后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
1
2
12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时

将锁的粒度细分

  • 好处,是可以增强并发度

  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁