| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> Java并发机制与Java内存模型JMM -> 正文阅读 |
|
[Java知识库]Java并发机制与Java内存模型JMM |
目录 零、预知识1. CPU、缓存、内存如下图,是Intel i5-4258U的处理器和内存模型,Core表示处理器,即CPU,它包含了L1级缓存和L2级缓存,L3级缓存被两个处理器共享,CPU通过总线与内存打交道。 2. 引入缓存的原因CPU速度很快,读写内存速度相对来说太慢了,引入缓存,就是为了解决CPU和内存速度不匹配的问题,从而提高计算机整体运行效率。 3.缓存的容量和速度越靠近CPU的缓存,速度越快同时容量越小价格高,越靠近内存的缓存,速度越慢同时容量更大价格越低。 容量:L1<L2<L3 速度:L1>L2>L3 价格:L1>L2>L3 一、并发机制的底层原理1.?synchronized定义及实现原理1.1?synchronized定义synchronized是实现线程同步的关键字,可以用于修饰方法、静态方法、代码块。 先给结论:
(1)修饰方法
修饰方法锁住的是当前的对象,那如果有多个方法,多个方法都有synchronized修饰呢?它们直接可以直接调用。这叫可重入,因此synchronized也被称为可重入锁(可重入锁还有ReentrantLock)。
两个方法都是用同一把锁,一个线程拿到了一把锁,自然能打开这两个方法。 (2)修饰静态方法,锁住的是当前类的class对象,每个类只有一个class对象
(3)修饰代码块
1.2?synchronized的实现原理synchronized锁的总是一个对象,无论是class对象、还是实例对象。 Java中的对象的内存模型如下: Java对象由对象头、实例数据、填充数据组成。
synchronized的实现原理关键就在于对象头中的MarkWord。 偏向锁、轻量级锁、重量级锁介绍 偏向锁,即锁会偏向某个线程,只允许有一个线程持有该偏向锁。 线程A通过CAS获取偏向锁,之后将对象头中的Mark Word中的线程ID指向线程A,线程A执行同步代码块;之后线程A获取和释放该偏向锁时,只需要查看对象头的Mark Word中是否包含当前线程ID; 当线程B也来通过CAS获取该偏向锁时,会获取失败,线程B会撤销该偏向锁,使得线程A暂停,将对象头的线程ID设置为空。 偏向锁只能被一个线程获得,第二个线程竞争偏向锁时,会撤销偏向锁,升级为轻量级锁。 轻量级锁,线程A通过CAS获取轻量级锁后,会修改Mark Word标志,执行同步体; 这时线程B也来竞争轻量级锁,发现锁已经被占用,这时通过自旋获取锁,不断循环,直到达到自旋时间阈值(自旋时间上限),这时候,轻量级锁会升级为重量级锁。 ?1.3 总结无锁状态,偏向锁,轻量级锁,重量级锁。它们的安全性是递增的,执行性能是递减的。 偏向锁只能有一个线程获得该锁,第二个线程尝试获取该锁时,会撤销偏向锁,然后升级为轻量级锁,适用于只有单个线程访问同步块的场景。 轻量级锁可以有多个线程竞争该锁,持有轻量级锁的线程访问同步块,其它竞争锁的线程使用自旋获取锁(自旋的过程消耗CPU),直到某个自旋的线程达到自旋时间上限,这时候升级为重量级锁。 重量级锁,等待的线程不会自旋。 2. 原子操作实现原理2.1 处理器实现原子操作1. 使用总线锁保证原子性 在多个处理器对共享变量进行操作时,可能会出现不一致的情况,这时处理器会将整个总线锁住,同一时间只能有一个处理器对共享变量进行操作。 2. 使用缓存锁保证原子性 缓存锁是粒度更细的锁,锁住缓存时,其它处理器仍然可以正常工作。当CPU1对缓存行A进行锁定时,CPU2就不能访问缓存行A。 3. 有两种情况不会使用缓存锁
2.2 Java如何实现原子操作(锁、循环CAS)1. 循环CAS CAS会存储一个value值,修改时传入expect值和newValue值,只有当value=expect时,才会把value更新为newValue,否则更新失败,会继续循环尝试更新。 循环CAS虽然可以免除锁带来的trouble,但它也是有问题的,如ABA问题、循环时间开销大、只能保证一个共享变量的原子操作。 ABA问题:CAS更新操作是以存储的value值作为标准的,当value值从A->B->A时,CAS不会认为它有更改,而事实上它发生了变化。解决办法:使用一个版本号,只有当版本号和值同时相同时,才会更新成功。 循环时间开销大。 只能保证一个共享变量的原子操作,Java 1.5开始,提供了AtomicReference类也保证引用对象的原子性操作。 2. 锁机制实现原子操作(这个很好理解)3. volatile的定义和实现原理3.1 volatile介绍volatile是Java中的关键字,能够保证一定的线程安全性,属于轻量级的synchronized。 volatile主要有两个特性:
需要注意的是,volatile不能很好地支持原子性。举个例子,对volatile的变量进行读、写都是原子的,而i++这种读+修改+写的操作volatile并不能保证原子性。如下面代码:
3.2 volatile可见性原理为了缓解CPU和内存速度不匹配的问题,引入了缓存。 读数据。处理器首先去缓存读数据,若一级缓存未命中,会去二级缓存查找,若二级缓存未命中,会去三级缓存查找,若三级缓存还未命中,会去内存读取数据,读到数据后,首先将数据存放到缓存中,然后再使用数据。 写数据。对于多个处理器都需要访问的共享变量,一个线程修改共享变量后,会把修改结果刷新到内存。其它线程读共享变量时,JMM会把本地内存置为无效,线程只能重新从内存中读取共享变量。 volatile写的语义:线程A修改了共享变量,将修改后的值刷新到内存中,即线程A向其它将要读这个共享变量的线程发送了消息。 volatile读的语义:线程B从内存读取了修改后的共享变量,即线程B接收了线程A发出的消息。 这就是volatile实现线程之间的通信方式,即通过内存,线程A向线程B发送了消息。 3.3 volatile禁止重排序原理
?内存屏障就像一块隔板,两边的读写不能跨过这个隔板,有点像国界的意思。
其中StoreLoad屏障是一个全能型屏障,它同时具有其它三个屏障的效果。 顺便说下,Java的concurrent包底层都是基于volatile和CAS来实现的。 ?4. final的定义和实现原理4.1 final的定义final修饰的叫常量,只能赋值一次。Java对象的成员变量若修饰为final,则最晚在构造方法中完成初始化。 final修饰的变量,会在构造方法return之前完成写操作,这个写操作不能与对象的引用赋值给引用变量进行重排序。 4.2 final的实现原理JMM(Java内存模型)禁止编译器把final域的写重排序到构造方法之外; 编译器会在构造方法会在final域写之后、构造方法之前插入一个Store-Store屏障,这个屏障禁止final域的写操作重排序到构造方法之外。 4.3 final域为引用类型final修饰引用类型时,能够保证构造方法中的final类型初始化和赋值操作不会重排序到构造方法之外,也就是说,final引用的对象能完成初始化操作。 但是,其它地方对引用变量的修改,final不提供禁止重排序的保证。 5. 单例模式之双重检查锁和静态内部类单例模式有饿汉式,懒汉式,双检锁,静态内部类,枚举等类型。
5.1 双重检查锁
回顾一下对象创建过程:
步骤1和步骤2、3之间不会进行重排序,而步骤2和步骤3可能会进行重排序。 再加上初次访问对象这个步骤,可能会出现如下情况:
而如果两个线程访问该对象,就有下面的情况: ?线程B访问了线程A新建的还未初始化的对象,这是个bug! 解决思路:1. 不允许“对象初始化”和“引用变量指向对象地址”进行重排序;2.运行这个重排序,但不允许其它线程“看到”这个重排序。 5.2 volatile解决方案
?volatile会禁止上述的重排序。 5.3 基于类初始化的解决方案(即静态内部类的单例)
首先需要了解的是对象的初始化过程:
Java语言中,一个类或接口T在下列情况下,会Class对象立即初始化:
第一阶段:获取类初始化锁 线程A获得了Class对象的初始化锁,线程B等待,线程A查看状态state=noInitialization,将状态改为initializing,线程A释放初始化锁。 第二阶段:初始化对象 线程A执行类的初始化(步骤2和3存在重排序,但对其它线程不可见,因为有锁); 线程B获得了初始化锁,发现state=initializing,于是释放锁,进入初始化锁对应的condition。(在initializing初始化过程中,任何线程都会获得锁、读状态=initializing、释放锁、进入condition中等待)。 第三阶段:唤醒condition中等待的线程 线程A再次获取初始化锁,修改state=initialized,唤醒所有condition中等待的线程,释放锁。 第四阶段:所有condition中的线程依次获取-释放初始化锁 线程B被唤醒后(包括condition中的其它线程),再次获取初始化锁,查看状态为initialized,释放初始化锁,完成类的初始化过程。 第五阶段:线程C执行类的初始化 ?线程C获取初始化锁,读到状态为initialized,释放锁,初始化完成。 以后每个线程使用该对象前,都要执行获取锁-判断状态-释放锁的过程。 静态内部类的单例模式 允许初始化过程的步骤2(初始化实例变量)和步骤3(将对象地址赋值给引用变量)这两个步骤重排序,由于初始化的过程需要用到Class初始化锁,因此重排序对其它线程不可见。 多个线程访问该对象时,Class初始化锁的存在,使得线程的访问就像串行化了一样。每个线程访问对象前,都要获得该锁,判断状态: 若state=noInitialization,将状态改为initializing,然后释放锁,执行初始化,初始化完成后,再次获取锁,修改state=initialized,唤醒condition中等待的线程,释放锁; 若state=initializing,则进入condition中等待; 若state=initialized,则释放锁。 5.4 对比volatile+双检锁和静态内部类的单例volatile是通过禁止指令重排序,来防止其它线程读到未初始化的对象; 静态内部类利用了类加载时需要获取Class初始化锁,来使得重排序的结果对其它线程不可见。 volatile+双检锁可以实现对静态字段、实例字段的单例实现; 静态内部类只能实现对静态字段的单例实现。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/11 6:55:51- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |