这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
并发时的三大特性:可见性 有序性 原子性
可见性 (Visibility)
volatile
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
*/
package com.mashibing.juc.c_001_00_Visibility;
import com.mashibing.util.SleepHelper;
public class T01_HelloVolatile {
private static /*volatile*/ boolean running = true;
private static void m() {
System.out.println("m start");
while (running) {
//System.out.println("hello");
}
System.out.println("m end!");
}
public static void main(String[] args) {
new Thread(T01_HelloVolatile::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
解析 System.out.println("hello") 底层代码
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
可以看出,println()里面使用了synchronized,在一定程度上也可以实现可见性,但是缺少volatile的即时性,会执行多次循环后可能才会达到主内存和工作内存中数据的一致。
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
public class T02_VolatileReference {
private static class A {
boolean running = true;
void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
}
private volatile static A a = new A();
public static void main(String[] args) {
new Thread(a::m, "t1").start();
SleepHelper.sleepSeconds(1);
a.running = false;
}
}
缓存
从CPU的计算单元(ALU)到:
多级缓存
缓存行
按块读取,64个字节
程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力
为什么缓存一行是64个字节?
缓存行越大,局部性空间效率越高,但读取时间慢
缓存行越小,局部性空间效率越低,但读取时间快
工业实践后取一个折中值,目前多用:64字节
缓存行对齐
-
缓存行对齐
缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
Disruptor
-
认识缓存行对齐的编程技巧
package com.mashibing.juc.c_001_02_FalseSharing;
import java.util.concurrent.CountDownLatch;
public class T01_CacheLinePadding {
public static long COUNT = 10_0000_0000L;
private static class T {
//private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L;
//private long p9, p10, p11, p12, p13, p14, p15;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
});
Thread t2 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
});
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start)/100_0000);
}
}
缓存一致性协议MESI
伪共享
A、B两个变量在同一个缓存行内,A变量修改同步数据的同时B也被同步。
有序性
CPU的乱序执行
为什么会乱序?
简单来说,主要是为了提高效率。
单线程的as-if-serial
单个线程,两条语句,没有依赖关系,未必是按顺序执行
单线程的重排序,必须保证最终一致性
as-if-serial:看上去像是序列化(单线程)
举例
对象的创建过程会出现乱序 this对象逸出
public class T03_ThisEscape {
private int num = 8;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num)
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read();
}
}
this可能在num半初始化的时候指向了T03_ThisEscape对象,num输出为0。
不要在构造方法里面new完线程直接启动,可以在其它方法进行线程的启动。
原子性
线程的原子性
从一个简单的小程序谈起:
import java.util.concurrent.CountDownLatch;
public class T00_IPlusPlus {
private static long n = 0L;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
//synchronized (T00_IPlusPlus.class) {
n++;
//}
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
上述程序由于多线程之间的竞争,导致数据没有达到预期。
可以通过上锁解决问题,但注意synchronized可以保证数据的可见性、原子性,不能保证有序性,如果有许多业务需要处理,并且之间没有依赖关系,会产生重排序。
上锁的本质
上锁的本质是把并发编程序列化
注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)
一定是锁定同一把锁(抢一个坑位)
import com.mashibing.util.SleepHelper;
public class T00_01_WhatIsLock {
private static Object o = new Object();
public static void main(String[] args) {
Runnable r = () -> {
//synchronized (o) { //打开注释试试看,对比结果
System.out.println(Thread.currentThread().getName() + " start!");
SleepHelper.sleepSeconds(2);
System.out.println(Thread.currentThread().getName() + " end!");
//}
};
for (int i = 0; i < 3; i++) {
new Thread(r).start();
}
}
}
什么样的语句(指令)具备原子性?
CPU级别汇编,需要查询汇编手册!
Java中的8大原子操作:(了解即可)
- lock:主内存,标识变量为线程独占
- unlock:主内存,解锁线程独占变量
- read:主内存,读取内存到线程缓存(工作内存)
- load:工作内存,read后的值放入线程本地变量副本
- use:工作内存,传值给执行引擎
- assign:工作内存,执行引擎结果赋值给线程本地变量
- store:工作内存,存值到主内存给write备用
- write:主内存,写变量值
一些基本概念
race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争
数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果
如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好)
monitor (管程) ---> 锁
critical section -> 临界区
如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细