【Java并发编程实战】——Java内存模型与线程

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

缓存一致性

在多核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;//指令1
    j = i+1;//指令2
    k = 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并发编程实战》

本项目是一个基于SSM(Spring+SpringMVC+MyBatis)框架和Vue.js前端技术的大学生第二课堂系统,旨在为大学生提供一个便捷、高效的学习和实践平台。项目包含了完整的数据库设计、后端Java代码实现以及前端Vue.js页面展示,适合计算机相关专业的毕设学生和需要进行项目实战练习的Java学习者。 在功能方面,系统主要实现了以下几个模块:用户管理、课程管理、活动管理、成绩管理和通知公告。用户管理模块支持学生和教师的注册、登录及权限管理;课程管理模块允许教师上传课程资料、设置课程时间,并由学生进行选课;活动管理模块提供了活动发布、报名和签到功能,鼓励学生参与课外实践活动;成绩管理模块则用于记录和查询学生的课程成绩和活动参与情况;通知公告模块则实时发布学校或班级的最新通知和公告。 技术实现上,后端采用SSM框架进行开发,Spring负责业务逻辑层,SpringMVC处理Web请求,MyBatis进行数据库操作,确保了系统的稳定性和扩展性。前端则使用Vue.js框架,结合Axios进行数据请求,实现了前后端分离,提升了用户体验和开发效率。 该项目不仅提供了完整的源代码和相关文档,还包括了详细的数据库设计文档和项目部署指南,为学习和实践提供了便利。对于基础较好的学习者,可以根据自己的需求在此基础上进行功能扩展和优化,进一步提升自己的技术水平和项目实战能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值