一、原子性
1.JMM
不同的硬件和操作系统在操作内存上是有一定差异的,java为了可以跨平台,在同一套代码下不同的硬件和操作系统出现各种不同的问题,使用JMM屏蔽掉各种操作系统和硬件操作内存的差异。
JMM规定,所有的变量都需要存储在主内存中,当一个线程需要操作变量时,需要将变量从主内存中拿到放入线程内存(CPU内存),然后再进行操作,在操作完成后再放入到主内存中(不一定)。
2.什么是原子性
一个操作是不可分割、不可中断的,一个线程在执行过程中,另一个线程不可影响。
java多线程操作临界资源时,预期的结果与最终的结果一致。
3.不满足原子性带来的问题
例:当多个线程同时操作同一个变量,输出的结果与预期的不符(两个线程对0同时增加100次,预期返回结果为200)
public class Demo01 {
private static int COUNT = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(COUNT);
}
private static void increment() {
COUNT++;
}
}
4.如何保证原子性
4.1.synchronize锁
synchronize锁可以保证多线程在同时操作临界资源时,只有一个线程正在操作临界资源。
private static void calculation() {
synchronized (COUNT){
COUNT++;
}
}
4.2.lock锁
lock锁是在java1.5时出现的,java1.5时的lock锁比synchronize性能好很多,在java1.6之后二者性能相差不大,在并发比较多时,推荐使用ReentrantLock锁,性能较好。
private static void calculation() {
reentrantLock.lock();
try {
COUNT++;
} catch (Exception e) {
} finally {
reentrantLock.unlock();
}
}
4.3.ThreadLocal
ThreadLocal保证原子性,是让多线程不去操作资源,每个线程使用自己的资源。
public class ThreadLockDemo {
public static void main(String[] args) {
ThreadLocal tl1 = new ThreadLocal<>();
ThreadLocal tl2 = new ThreadLocal<>();
tl1.set("aaa");
tl2.set("bbb");
System.out.println("main->" + tl1.get());
System.out.println("main->" + tl2.get());
Thread thread = new Thread(() -> {
System.out.println("thread->" + tl1.get());
System.out.println("thread->" + tl2.get());
});
thread.start();
}
}
4.4.cas
cas是compare and swap,也就是比较与交换的缩写,是一条CPU的并发原语。
cas在替换内存中的某个值时,会先判断该值与预期的是否一致,如果一致才进行替换,不一致则重新获取值进行操作,满足了原子性的操作。
Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。
但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要自己实现。
public class CasLockDemo {
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++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
calculation();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
calculation();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(COUNT);
}
private static void calculation() {
COUNT.incrementAndGet();
}
}
4.4.1.cas的问题
- cas只能保证一个变量的原子性,无法保证一个代码块的原子性。
- 自旋时间过长:若其他线程对该变量进行不停的修改,该线程在比较时无法获取到预期结果的值,就会一直进行判断不断的获取值。
二、可见性
1.什么是可见性
可见性的问题是基于CPU出现的,CPU的处理速度非常快,如果每次处理数据都要从主内存中拿取数据的话,效率是非常低的。因此CPU提供了L1,L2,L3的三级缓存,CPU从主内存中获取到值时,放到CPU的三级缓存中,下次直接从缓存中获取数据,效率会得到提升。
2.三级缓存带来的问题
这样也带来了可见性的问题,CPU都是多核的,每个线程的三级缓存都是独立的,在多线程操作过程中,若当前线程对数据进行操作后没有及时同步到主内存,其他线程拿到的数据就是错误的,出现了数据不一致的问题。
public class Demo1 {
private static Boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
}
System.out.println("thread线程停止了");
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.println("主线程将flag改为true");
}
}
3.如何保证可见性
3.1.volatile关键字
volatile是一个关键字,用于修饰成员变量,该成员变量被volatile修饰时,CPU操作该变量会将该值在内存中设置为无效,每次从主内存中获取最新的变量值。
- volatile属性的变量被写:JMM会将当前CPU缓存中的变量值及时同步到主内存中。
- volatile属性的变量被读:JMM会将CPU缓存中的变量设置为无效,从主内存中获取变量的值。
public class VolatileDemo {
private static volatile Boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
}
System.out.println("thread线程停止了");
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.println("主线程将flag改为true");
}
}
3.2.synchronize锁
涉及到synchronize锁的方法或代码块,在获取到锁资源后,会将内部所用到的变量从CPU缓存中移除,重新从缓存中获取一份最新的数据,在锁资源释放后,会立即将缓存中的变量同步到主内存中。
public class SynchronizedDemo {
private static Boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
synchronized (SynchronizedDemo.class){
}
}
System.out.println("thread线程停止了");
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.println("主线程将flag改为true");
}
}
3.3.Lock锁
public class LockDemo {
private static Boolean flag = true;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
lock.lock();
try {
} finally {
lock.unlock();
}
}
System.out.println("thread线程停止了");
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.println("主线程将flag改为true");
}
}
3.4.final关键字
final修饰成员变量,代表该变量的值不可进行修改,在一定程度上解决了可见性的问题。
同时需要注意fiinal和volatile不可以同时修饰。
三、有序性
1.指令重排
.java文件在执行前,会编译成CPU可以识别的指令,CPU为了提高执行效率,会在满足一定条件下,不影响最终结果的前提下,对指令进行重排。
因此可以说,java程序在CPU中是乱序执行的。
2.指令重排造成的问题
在多线程环境下,由于出现了指令重排,重排的这一部分语句还没执行完,就切换到了其他线程,导致结果与预期的结果不符,这就是编译器的编译优化带来的问题程序有序性的问题。
3.如何解决有序性
3.1.happens-before原则
-
单线程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-before原则:一个对象的初始化完成先于他的finalize方法调用。
JMM只有在不出现上述8中情况时,才不会触发指令重排效果。
不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。
3.2.volitale关键字
如果想要程序对一个属性的操作不进行指令重排,除了满足happens-before原则,还可以对该属性加volitale关键字,就不会出现指令重排的问题了。
加了volitale后,会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。
3.3.synchronize锁
synchronized为可能会重排序的部分加锁,让其在宏观上或者说执行结果上看起来没有发生重排序。