博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【Java并发编程实战】——Java内存模型与线程
阅读量:4180 次
发布时间:2019-05-26

本文共 6694 字,大约阅读时间需要 22 分钟。

为了缩短计算机的存储设备与处理器的运算速度的差距,现代计算机都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存中,这样处理器不用等待缓慢的内存读写了。这带来了缓存一致性问题。

缓存一致性

在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。这时CPU缓存中的值可能和缓存中的值不一样,这就是著名的缓存一致性问题。

内存模型

内存模型可以理解为,在特定的操作协议下,对特定的内存或者高速缓存进行多谢访问的过程抽象。

JVM为了实现跨平台,定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异。JMM的目标是定义程序中各个变量的访问规则,即在虚拟机中将变量从内存中取出和存入的底层细节。

Java中变量都存储在主内存中,每条线程都有自己的工作内存,线程不能直接读写主内存中的变量,也不能访问其他工作内存中的变量,线程间传递变量需要通过主内存来完成。

线程、主内存、工作内存的交互关系

在这里插入图片描述

volatile 的特殊规则

  • 保证变量对所有线程可见,但是不保证操作的原子性
    下面这个例子启动10个线程,每个线程给 sum 进行1000次自增,然后输出此时的 sum 值,你会发现最后的输出小于10000。
    /** * 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(); } }}
  • 禁止指令重排,普通的变量只会保证最终的结果正确,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
    指令重排指CPU允许在正确处理指令依赖情况下,以保证程序得出正确的执行结果,可以不按程序规定的顺序分开发送给各相应电路单元处理。
    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值未知。

JMM中已规定的先行发生原则

  • 同一个线程内,按照程序的控制流顺序,书写在前的操作先行发生于书写在之后的操作;
  • unlock 操作先行发生于时间上在此之后的 lock 操作;
  • 对一个 volatile 变量的写操作先行发生于时间上在此之后对这个变量的读操作;
  • Thread 对象的 start() 方法先行发生于此线程的每个动作;
  • 线程中的所有操作都先行发生于对此线程的终止检测,join()、isAlive();
  • 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 一个对象的初始化完成先行发生于它的 finalize() 方法执行;
  • 传递性:操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

操作“时间上的先发生”不能代表这个操作会是“先行发生”(考虑多线程访问),一个操作“先行发生”也不能推导这个操作必定是“时间上的先发生”,一切必须以先行发生原则为准(考虑指令重排)。

Java与多线程

Java的线程 Thread 类的关键方法都申明为 native,表明与平台相关。

线程的实现方式
  • 使用内核线程实现,内核线程(KLT),直接由操作系统内核直接支持的线。程序一般不直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,他们之间是一对一的关系。缺点:系统调用的代价较高,需要在用户态和内核态之间切换,LWP需要消耗一定的系统资源(如KLT的栈空间),系统能支持的LWP数量有限;
  • 使用用户线程实现,实现难度大;
  • 使用用户线程和轻量级进程混合实现;
  • 一条Java线程就映射到一条轻量级进程。

实现线程安全的方法

  • 互斥同步

    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需要三个操作数,分别是内存位置、旧的预期值、新值。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); }}

ABA问题

CAS存在一个逻辑漏洞:如果变量V初次读取的值为A,并在准备赋值的时候检测到它还是A,那我们能说它的值没有被其他线程改变过吗?如果其他线程将他的值改为B,后来又改为A,那CAS操作当做它没有发生过改变。ABA问题可以使用 AtomicStampedReference(增加了控制变量),不过ABA问题不会影响程序并发的正确性,使用互斥同步也可以。

参考书籍:《深入理解Java虚拟机》、《JAVA并发编程实战》

转载地址:http://qmrai.baihongyu.com/

你可能感兴趣的文章
Linux中yum工具使用教程
查看>>
C++字符串函数
查看>>
mknod详解
查看>>
linux中的run-level何解?
查看>>
Linux内核编译详解(转自linuxSir)
查看>>
实模式,保护模式与V86模式
查看>>
628. Maximum Product of Three Numbers(排序)
查看>>
Linux内核-------同步机制(二)
查看>>
面试题31-------连续子数组的最大和(数组)
查看>>
epoll 实现Chat
查看>>
21. Merge Two Sorted Lists(链表)
查看>>
2. Add Two Numbers(链表)
查看>>
637. Average of Levels in Binary Tree(Tree)
查看>>
226. Invert Binary Tree(Tree)
查看>>
328. Odd Even Linked List(链表)
查看>>
199. Binary Tree Right Side View(Tree)
查看>>
230. Kth Smallest Element in a BST(Tree)
查看>>
求字符串的最长回文串-----Manacher's Algorithm 马拉车算法
查看>>
回溯法常用的解题模板和常见题型
查看>>
深入分析Java I/O 的工作机制
查看>>