通过了解并发的理论基础和编程基础,让我们对并发有一个总体的认识,本文先了解下并发的理论基础。
并发理论基础
1. 并发的来源
CPU、内存、I/O 设备的速度从前到后存在明显的速度差异。为了合理利用 CPU 的高性能,计算机做了以下事情:
- CPU 增加了缓存,均衡与内存的速度差异;
- 操作系统增加了进程、线程,分时复用 CPU,来均衡 CPU 与 I/O 设备的速度差异;
- 编译程序优化指令执行次序,优化缓存使用。
2. 并发不安全的本质:可见性、原子性和有序性
并发虽然提高了计算机的执行效率,但是并发会引发:缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题。
2.1.(CPU缓存导致的)可见性问题
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
看一个案例:两个线程同时对count操作add10k。
/**
* 成员变量是线程共享的,当线程同时对count操作时可能会导致数据不可见的问题
*/
private static long count;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> { add10k(); });
Thread thread2 = new Thread(() -> { add10k(); });
thread1.start();
thread2.start();
//等待线程执行结束
thread1.join();
thread2.join();
System.out.println(count);
}
private static void add10k() {
int idx = 0; //局部变量是维护到各自线程里的,所以是线程安全的
while (idx++ < 10000) {
/**
* 最终的结果是两个线程的idx会=10k,
* 而count因为线程有时不可见,导致相同的结果会执行,最终会小于20000
*/
count += 1;
}
}
对于多核时代,每颗 CPU 都有自己的缓存,每个线程操作自己对应的CPU缓存。
假设线程A和B同时开始执行,第一次将内存里的count=0,加载到各自CPU的缓存中,执行count+=1之后,各自CPU的缓存都为1,然后同时写入内存,这时我们发现内存里是1,而不是2。。。
同时,由于此时各自线程的CPU缓存里都有了count值(线程基于缓存计算),虽然两个线程最终都会执行10k次,但count结果是小于20k的。
小结一下:
因为共享变量会加载到线程对应CPU的缓存中对变量进行操作,而不同CPU的缓存是相互不可见的,所以最后当共享变量写到内存中时,结果就会差强人意。
2.2. (分时复用引起的)原子性问题
原子性:把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。
时间片:操作系统允许某个线程执行一小段时间,例如 50毫秒,过了 50 毫秒会重新选择线程来执行,这个 50 毫秒称为“时间片”。
从上面的概念我们知道,时间片的切换可能会破坏原子性。
接下来看一个简单的例子:两个线程都执行一次count += 1操作。count += 1执行需要下面三条CPU指令。
- 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
- 指令 2:之后,在寄存器中执行 +1 操作;
- 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
假设线程A执行到 指令 1,然后线程B开始执行这三条指令,执行完后切换到线程A,此时A得到的结果是1,而不是2.
2.3. (重排序引起的)有序性问题
左图展示了单例创建的逻辑,程序在执行之前会进行指令重排序。具体的:
在 new 操作上,优化后的执行路径是这样的:
- 分配一块内存 M;
- 将 M 的地址赋值给 instance 变量;
- 最后在内存 M 上初始化 Singleton 对象。
假设两个线程A、B同时执行代码,当线程A执行到第二步,然后切换到B执行,此时判断对象不为空,返回未初始化的对象,导致调用报错。
3. 实现线程安全
3.1 互斥同步
互斥:同一时刻只有一个线程执行临界区。把一段需要互斥执行的代码称为临界区。
实现互斥:synchronized 和 ReentrantLock。
3.2. 非阻塞同步CAS
互斥同步会带来线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。
乐观的非阻塞同步 CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。
比较并交换(Compare-and-Swap,CAS):冲突检测的实现
CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V的值更新为 B。
具体过程看一个例子:
在内存地址V当中,存储着值为10的变量。
此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
线程1要提交更新之前,线程2抢先一步,把内存地址V中的变量值率先更新成了11。线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
线程1进行SWAP,把地址V的值替换为B,也就是12。
CAS底层如何实现?
利用unsafe提供的原子性操作方法。
CAS的缺点:
缺点 | 描述 |
---|---|
CPU开销较大 |
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
|
不能保证代码块的原子性 |
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
|
ABA问题 |
当一个值从A更新成B,又更新会A,普通CAS机制会误判通过检测。 利用版本号比较可以有效解决ABA问题,A-B-A就变成1A-2B-3A。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。
|
AtomicInteger
AtomicInteger是一个支持原子操作的 Integer 类,就是保证对AtomicInteger类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。
如果不使用 AtomicInteger,要实现一个按顺序获取的ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的ID的现象。
看下 AtomicInteger 的 incrementAndGet()
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current +1;
if(compareAndSet(current, next))
return next;
}
}
//为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令(硬件的机器指令?)来实现的。
//基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。
//并且由于 CAS 操作是 CPU 原语,所以性能比较好。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库
3.3 无同步方案
方面 | 解释 |
---|---|
局部变量栈封闭 |
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
|
线程本地存储(ThreadLocal) |
当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。
|
可重入代码(Reentrant Code) |
这种代码可以在执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。
特点:不依赖堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
|
参考:
https://pdai.tech/md/java/thread/java-thread-x-overview.html
极客时间-Java并发编程实战
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65397.html