JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。
Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
让Java的并发编程可以做到跨平台。JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定!)。
原子性的定义:原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
并发编程的原子性用代码阐述:
package cn.pottercoding.juc;
/**
* <p>
* 原子性
* </p>
*
* @author potter
* @since 2024-03-30
*/
public class AtomicDemo {
private static int count;
public static void increment(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
当前程序:多线程操作共享数据时,预期的结果,与最终的结果不符。
原子性:多线程操作临界资源,预期的结果与最终结果一致。
通过对这个程序的分析,可以查看出,++的操作,一共分为了三部,首先是线程从主内存拿到数据保存到CPU的寄存器中,然后在寄存器中进行+1操作,最终将结果写回到主内存当中。
因为++操作可以从指令中查看到
可以在方法上追加synchronized
关键字或者采用同步代码块的形式来保证原子性
synchronized
可以避免多线程同时操作临界资源,同一时间点,只会有一个线程正在操作临界资源
到底什么是CAS?compare and swap
也就是比较并交换,它是一条CPU的并发原语。
它在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
Java中基于Unsafe
的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。
但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
Doug Lea在CAS的基础上帮助我们实现了一些原子类,其中就包括现在看到的AtomicInteger
,还 有其他很多原子类…
CAS的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。 CAS的问题
AtomicStampeReference
AtomicStampedReference
在CAS时,不但会判断原值,还会比较版本信息.public static void main(String[] args) {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
System.out.println("修改1版本的:" + b);
boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
System.out.println("修改2版本的:" + c);
}
Lock
锁是在JDK1.5
由Doug Lea研发的,他的性能相比synchronized
在JDK1.5
的时期,性能好了很多,但是在JDK1.6
对synchronized
优化之后,性能相差不大,但是如果涉及并发比较多时,推荐 ReentrantLock
锁,性能会更好。
实现方式:
private static int count;
private static ReentrantLock lock = new ReentrantLock();
public static void increment() {
lock.lock();
try {
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
ReentrantLock
可以直接对比synchronized
,在功能上来说,都是锁。但是ReentrantLock
的功能性相比synchronized
更丰富。
ReentrantLock
底层是基于AQS
实现的,有一个基于CAS
维护的state
变量来实现锁的操作。
Java中的四种引用类型
Java中的使用引用类型分别是 。强,软,弱,虚
User user = new User();
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
SoftReference:其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。
ThreadLocal保证原子性的方式,是不让多线程去操作 ,让每个线程去操作属于自己的数据临界资源。
代码实现
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get());
System.out.println("t1:" + tl2.get());
});
t1.start();
System.out.println("main:" + tl1.get());
System.out.println("main:" + tl2.get());
}
ThreadLocal实现原理:
Thread
中都存储着一个成员变量,ThreadLocalMap
ThreadLocal
本身不存储数据,像是一个工具类,基于ThreadLocal
去操作ThreadLocalMap
ThreadLocalMap
本身就是基于Entry[]
实现的,因为一个线程可以绑定多个ThreadLocal
,这样一来,可能需要存储多个数据,所以采用Entry[]
的形式实现。ThreadLocalMap
,再基于ThreadLocal
对象本身作为key
,对value进行存取ThreadLocalMap
的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal
对象失去引用后,如果key的引用是强引用,会导致ThreadLocal
对象无法被回收ThreadLocal内存泄漏问题:
ThreadLocal
引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。ThreadLocal
对象之后,及时的调用remove方法,移除Entry
即可可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
可见性问题的代码逻辑
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
// ....
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
volatile
是一个关键字,用来修饰成员变量。
如果属性被volatile
修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去主内存操作
volatile
的内存语义:
volatile
属性被写:当写一个volatile
变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中volatile
属性被读:当读一个volatile
变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量其实加了volatile
就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile
修饰的属性,会在转为汇编之后后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据。
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
// ....
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
synchronized
也是可以解决可见性问题的,synchronized
的内存语义。
如果涉及到了synchronized
的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
synchronized(MyTest.class){
//...
}
System.out.println(111);
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
Lock
锁保证可见性的方式和synchronized
完全不同,synchronized
基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock
锁是基于volatile
实现的。Lock
锁内部再进行加锁和释放锁时,会对一个由volatil
e修饰的state
属性进行加减操作。如果对volatile
修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。
private static boolean flag = true;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
lock.lock();
try{
//...
}finally {
lock.unlock();
}
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。
final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的
final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。
在Java中,.java
文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。指令乱序执行的原因,是为了尽可能的发挥CPU的性能。Java中的程序是乱序执行的。
Java程序验证乱序执行效果:
static int a,b,x,y;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
a = 0;
b = 0;
x = 0;
y = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
if(x == 0 && y == 0){
System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
}
}
}
单例模式由于指令重排序可能会出现问题:线程可能会拿到没有初始化的对象,导致在使用时,可能由于内部属性为默认值,导致出现一些不必要的问题
private static volatile MyTest test;
private MyTest(){
}
public static MyTest getInstance(){
// B
if(test == null){
synchronized (MyTest.class){
if(test == null){
// A , 开辟空间,test指向地址,初始化
test = new MyTest();
}
}
}
return test;
}
as-if-serial
语义:不论指定如何重排序,需要保证单线程的程序执行结果是不变的。而且如果存在依赖的关系,那么也不可以做指令重排。
// 这种情况肯定不能做指令重排序
int i = 0;
i++;
// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
具体规则:
happen-before
原则:在同一个线程中,书写在前面的操作happen-before
后面的操作。happen-before
原则:同一个锁的unlock
操作happen-before
此锁的lock操作。volatile
的happen-before
原则: 对一个volatile
变量的写操作happen-before
对此变量的任意操作。happen-before
的传递性原则: 如果A操作 happen-before
B操作,B操作happen-before
C操作,那么A操作happen-before
C操作。happen-before
原则:同一个线程的start方法happen-before
此线程的其它方法。happen-before
原则:对线程interrupt方法的调用happen-before
被中断线程的检测到中断发送的代码。happen-before
原则:线程中的所有操作都happen-before线程的终止检测。happen-befor
e原则:一个对象的初始化完成先于他的finalize
方法调用。JMM只有在不出现上述8中情况时,才不会触发指令重排效果。不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。
volatile如何实现的禁止指令重排?内存屏障概念。将内存屏障看成一条指令。会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。