高并发的三大特性
引言
上一篇讲了线程的基础知识,谈到线程了,就会想到多线程与高并发,本篇就讲讲高并发的三大特性。
可见性
问题由来
线程用到堆中一个变量时,会copy一份使用,对其进行修改,不会立即刷新原值,若有其他线程用到这个值,会用到旧的值,造成数据不一致的问题。
解决方法
使用volatile修饰变量,会让所有线程都去堆内存中读取变量值
小实验
该程序中主线程和新的线程都用到了running变量。
主线程会对running的值进行改变。
对该变量加volatile与不加运行该程序,查看运行结果。
public class TestVolatile {
private static boolean running = true; // 对该变量加volatile与不加运行该程序,查看结果
private static void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
public static void main(String[] args) throws InterruptedException {
new Thread(TestVolatile::m, "t1").start();
Thread.sleep(1000);
running = false;
}
}
不加volatile。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCnHeCXJ-1650283589632)(C:\Users\27901\AppData\Roaming\Typora\typora-user-images\image-20220417181600212.png)]
加volatile
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHhpIRj1-1650283589634)(C:\Users\27901\AppData\Roaming\Typora\typora-user-images\image-20220417181646752.png)]
缓存行对齐
缓存行指的是CPU每个核读取数据时,一次读取64个字节。而多个核读取操作同一块数据时,为了保证数据一致性,cpu有缓存一致性协议,这样不可避免影响cpu效率。
为了保证避免一块数据被多个核读取,其中一个核只用到其中某些数据,而造成其他核频繁的同步这种情况。将数据进行缓存行对齐,保证每个核读取块中,只有其自己用到的数据。这种编程方式叫做缓存行对齐。
java实现方式
1.将要用到的变量长度为X,在其前面申请足够的变量,总长度为X1,使得X+X1 = 64字节。同理其后也是。
private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L;
private long p9, p10, p11, p12, p13, p14, p15;
2.直接使用@Contended注解,不过只有java1.8有效,且要配置虚拟机,去掉限制参数:-XX:-RestrictContended
有序性
问题由来
cpu为最大化利用其效率,会对指令进行重排序。
解决方法
volatile关键字
as-if-serial原则,保证单线程最后执行结果不变
happens-before原则
CPU内存屏障指令 lfence sfence mfence
JVM中的内存屏障,LoadLoadBarrier LoadStore SL SS
多线程环境下的有序性问题
这里注意new 对象底层其实有很多条指令的,并且会重排序,所以不要在构造方法中new 线程并启动,可能会在对象没new成功就取对象值,出现不可预估的结果。
原子性
问题由来
在并发操作同一变量的情况下,一个线程对数据的操作,被其他打断了。比如你操作一个变量,还没回写,又有别的线程来对它进行读操作了。
解决方法
- 对并发的代码块加锁。实质就是将并发操作编程序列化。加锁后的代码块具有原子性,解锁后会从线程刷回内存,保证了数据的可见性。
- java原子性指令