本文共 6694 字,大约阅读时间需要 22 分钟。
为了缩短计算机的存储设备与处理器的运算速度的差距,现代计算机都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存中,这样处理器不用等待缓慢的内存读写了。这带来了缓存一致性问题。
在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。这时CPU缓存中的值可能和缓存中的值不一样,这就是著名的缓存一致性问题。
内存模型可以理解为,在特定的操作协议下,对特定的内存或者高速缓存进行多谢访问的过程抽象。
JVM为了实现跨平台,定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异。JMM的目标是定义程序中各个变量的访问规则,即在虚拟机中将变量从内存中取出和存入的底层细节。Java中变量都存储在主内存中,每条线程都有自己的工作内存,线程不能直接读写主内存中的变量,也不能访问其他工作内存中的变量,线程间传递变量需要通过主内存来完成。
/** * Created by Tangwz on 2019/6/23 */public class VolatileTest { private static volatile int sum = 0; public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { sum += 1; } System.out.println(sum); } }; for (int i = 0; i < 10; i++) { Thread thread = new Thread(runnable); thread.start(); } }}
int i,j,k;i = 2;//指令1j = i+1;//指令2k = 3;//指令3指令3可以排在指令1、指令2的前面中间或者后面,但是指令1和指令2的顺序不能变化。
下面这个例子如果没有包含足够的同步,那么可能产生奇怪的结果(不要这么做)
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join(); other.join(); System.out.println("( " + x + "," + y + ")"); }}
按照下图的交替执行方式,会输出(0,0)。
上面的程序很简单,但是要列举出所有的可能却很困难。内存排序使程序的行为变得不可预测。先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了共享内存中变量的值、发送了消息、调用了方法等。
//这个操作在线程A中执行i=1;//这个操作在线程B中执行j=i;//这个操作在线程C中执行i=2;
假如操作A先行发生于操作B,在操作C没执行的情况下,操作B一定能看到操作A之后后的结果 i=1;
假如操作A先行发生于操作B,在操作C与线程A和线程C之间,且操作C与线程B没有先行发生关系,那么j值未知。操作“时间上的先发生”不能代表这个操作会是“先行发生”(考虑多线程访问),一个操作“先行发生”也不能推导这个操作必定是“时间上的先发生”,一切必须以先行发生原则为准(考虑指令重排)。
Java的线程 Thread 类的关键方法都申明为 native,表明与平台相关。
互斥同步
synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。在执行monitorenter 指令之前,首先要获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加一,对应的执行 monitorexit 指令时会将锁的计数器减一,当计数器的值为0时,锁就被释放了。如果获取对象锁失败,那么当前线程就要阻塞等待,之后对象锁被其他的线程释放为止。
ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。一个表现在API层面,另一个是原生语法。再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。ReentrantLock 增加了一些高级功能:等待可中断、公平锁、锁可绑定多个条件。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。带超时的获取锁尝试。可以判断是否有线程,或者某个特定线程,在排队等待获取锁。可以响应中断请求。
/** * Created by Tangwz on 2019/6/23 */public class VolatileTest { private static ReentrantLock lock = new ReentrantLock(); private static volatile int sum = 0; public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { lock.lock(); try { for (int i = 0; i < 1000; i++) { sum += 1; } } finally { lock.unlock(); } countDownLatch.countDown(); } }); thread.start(); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(sum); }}
非阻塞同步
基于冲突检测的乐观并发策略,这依赖硬件指令集。 测试并设置、获取并增加、交换、比较并交换(CAS)、加载链接/条件存储。无同步方案
ThreadLocal 提供线程本地存储功能,每一个线程都使用一个单独的TheadLocalMap对象来存储数据,互不干扰。CAS需要三个操作数,分别是内存位置、旧的预期值、新值。CAS执行时,当且仅当内存位置符合旧的预期值时,处理器才用新值更新内存位置的值,否则就不更新,不论成功失败都会返回旧值,上述操作过程是一个原子操作。
AQS 更新内部状态 waitStatus 就使用了CAS。/** * CAS waitStatus field of a node. */ private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) { return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); } public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
还是上面的累加例子,使用CAS操作解决
/** * Created by Tangwz on 2019/6/23 */public class VolatileTest { // private static volatile int sum = 0; private static volatile AtomicInteger sum = new AtomicInteger(0); public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { //返回先前的值,在当前值上加一 sum.getAndIncrement(); } countDownLatch.countDown(); } }); thread.start(); } try { //等待上面的10个线程执行完毕 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(sum); }}
CAS存在一个逻辑漏洞:如果变量V初次读取的值为A,并在准备赋值的时候检测到它还是A,那我们能说它的值没有被其他线程改变过吗?如果其他线程将他的值改为B,后来又改为A,那CAS操作当做它没有发生过改变。ABA问题可以使用 AtomicStampedReference(增加了控制变量),不过ABA问题不会影响程序并发的正确性,使用互斥同步也可以。
参考书籍:《深入理解Java虚拟机》、《JAVA并发编程实战》
转载地址:http://qmrai.baihongyu.com/