IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: 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、缓存、内存

2. 引入缓存的原因

3.缓存的容量和速度

一、并发机制的底层原理

1.?synchronized定义及实现原理

1.1?synchronized定义

1.2?synchronized的实现原理

?1.3 总结

2. 原子操作实现原理

2.1 处理器实现原子操作

2.2 Java如何实现原子操作(锁、循环CAS)

2. 锁机制实现原子操作(这个很好理解)

3. volatile的定义和实现原理

3.1 volatile介绍

3.2 volatile可见性原理

3.3 volatile禁止重排序原理

?4. final的定义和实现原理

4.1 final的定义

4.2 final的实现原理

4.3 final域为引用类型

5. 单例模式之双重检查锁和静态内部类

5.1 双重检查锁

5.2 volatile解决方案

5.3 基于类初始化的解决方案(即静态内部类的单例)


零、预知识

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修饰方法(对象的成员方法),锁的是当前对象;
  2. synchronized修饰静态方法时,锁住的是当前类的Class对象;
  3. synchronized修饰代码块时,锁住的是指定的对象。

(1)修饰方法

public class TestSynchronized {
    public synchronized void method1() {
        System.out.println("修饰方法");
    }
}

修饰方法锁住的是当前的对象,那如果有多个方法,多个方法都有synchronized修饰呢?它们直接可以直接调用。这叫可重入,因此synchronized也被称为可重入锁(可重入锁还有ReentrantLock)。

public class TestSynchronized {
    public synchronized void method1() {
        System.out.println("修饰方法1");
        method1();
    }

    public synchronized void method2() {
        System.out.println("修饰方法2");
    }
}

两个方法都是用同一把锁,一个线程拿到了一把锁,自然能打开这两个方法。

(2)修饰静态方法,锁住的是当前类的class对象,每个类只有一个class对象

public class TestSynchronized {
    public static void staticMethod(){
        System.out.println("静态方法");
    }
}

(3)修饰代码块

public static void test( ) {
        synchronized (TestSynchronized.class) {
            
        }
    }

1.2?synchronized的实现原理

synchronized锁的总是一个对象,无论是class对象、还是实例对象。

Java中的对象的内存模型如下:

Java对象由对象头、实例数据、填充数据组成。

  1. 对象头包含Mark Word(存储锁信息)和Class Pointer(存储类信息的指针),如果对象是数组类型,对象头将还有一个字段length用来存储数组长度;
  2. 实例数据即对象的成员变量信息;
  3. 对齐填充,?由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对于不够8字节的,填充到8字节。

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. 有两种情况不会使用缓存锁

  • 当操作的数据横跨多个缓存行时,处理器会使用总线锁。
  • 有些处理器不支持缓存锁,如Intel 486和Pentium。

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主要有两个特性:

  1. 可见性。线程A对共享变量的修改,立刻对所有线程可见。
  2. 禁止指令重排序。重排序是编译器和处理器为了优化程序以提高效率而进行的操作,volatile关键字通过内存屏障(一会介绍)来禁止重排序。

需要注意的是,volatile不能很好地支持原子性。举个例子,对volatile的变量进行读、写都是原子的,而i++这种读+修改+写的操作volatile并不能保证原子性。如下面代码:

public class TestVol {
    volatile int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
    
    public void addAndGet(){
        int v=getValue();//原子操作
        v=v+1;
        setValue(v);//原子操作
    }
    //但这个方法整体不是原子操作
}

//上面与下面代码等价
public class TestVol {
    int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized void setValue(int value) {
        this.value = value;
    }

    public synchronized void addAndGet(){
        int v=getValue();//原子操作
        v=v+1;
        setValue(v);//原子操作
    }
    //这个方法整体是原子操作
}

3.2 volatile可见性原理

为了缓解CPU和内存速度不匹配的问题,引入了缓存。

读数据。处理器首先去缓存读数据,若一级缓存未命中,会去二级缓存查找,若二级缓存未命中,会去三级缓存查找,若三级缓存还未命中,会去内存读取数据,读到数据后,首先将数据存放到缓存中,然后再使用数据。

写数据。对于多个处理器都需要访问的共享变量,一个线程修改共享变量后,会把修改结果刷新到内存。其它线程读共享变量时,JMM会把本地内存置为无效,线程只能重新从内存中读取共享变量。

volatile写的语义:线程A修改了共享变量,将修改后的值刷新到内存中,即线程A向其它将要读这个共享变量的线程发送了消息

volatile读的语义:线程B从内存读取了修改后的共享变量,即线程B接收了线程A发出的消息

这就是volatile实现线程之间的通信方式,即通过内存,线程A向线程B发送了消息。

3.3 volatile禁止重排序原理

  1. ?第一个操作是volatile读时,第二个操作无论是普通读/写、还是volatile读写,都不允许重排序;
  2. 第二个操作是volatile写时,第一个操作无论是普通读写,还是volatile读写,都不允许重排序;
  3. 第一个操作和第二个操作的volatile读写一律不能重排序。

?内存屏障就像一块隔板,两边的读写不能跨过这个隔板,有点像国界的意思。

  1. LoadLoad就是禁止两个读操作重排序;
  2. StoreStore就是禁止两个写操作重排序;
  3. LoadStore就是禁止读操作和写操作重排序;
  4. StoreLoad就是禁止写操作和读操作重排序。

其中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. 单例模式之双重检查锁和静态内部类

单例模式有饿汉式,懒汉式,双检锁,静态内部类,枚举等类型。

public class Singleton1 {
    private Singleton1(){}
    //饿汉式, 线程安全,但不支持延迟加载,占用系统开销
    private static Singleton1 singleton=new Singleton1();
    public static Singleton1 getInstance() {
        return singleton;
    }
}

class Singleton2 {
    private Singleton2(){}
    //懒汉式, 线程不安全
    private static Singleton2 singleton;
    public static Singleton2 getInstance() {
        if(singleton==null) {
            singleton=new Singleton2();
        }
        return singleton;
    }
}

class Singleton22 {
    private Singleton22(){}
    //双重检查锁- 懒汉式  延迟加载+线程安全
    private static Singleton22 singleton;
    public static Singleton22 getInstance() {
        if(singleton==null) {
            synchronized (Singleton22.class) {
                if(singleton==null)
                    singleton = new Singleton22();
            }
        }
        return singleton;
    }
}

class Singleton3  {
    private Singleton3(){}
    //静态内部类初始化,延迟加载+线程安全
    private static class InnerClass {
        public static Singleton3 instance=new Singleton3();
    }
    
    public static Singleton3 getInstance() {
        return InnerClass.instance;
    }
    
}

enum Singleton4 {//线程安全
    INSTANCE
}

5.1 双重检查锁

class Singleton22 {
    private Singleton22(){}
    //双重检查锁- 懒汉式  延迟加载+线程安全
    private static Singleton22 singleton;
    public static Singleton22 getInstance() {
        if(singleton==null) {
            synchronized (Singleton22.class) {
                if(singleton==null)
                    singleton = new Singleton22();//这里有问题!!!
            }
        }
        return singleton;
    }
}

回顾一下对象创建过程:

  1. 分配内存:memory=allocate();
  2. 初始化对象:initialization;
  3. 将对象地址赋给引用变量。

步骤1和步骤2、3之间不会进行重排序,而步骤2和步骤3可能会进行重排序。

再加上初次访问对象这个步骤,可能会出现如下情况:

  1. 分配内存:memory=allocate();
  2. 将对象地址赋给引用变量;
  3. 初始化对象:initialization;(步骤2和3已经重排序)
  4. 初次访问对象。(始终保持“初次访问对象”在“初始化对象”之后)

而如果两个线程访问该对象,就有下面的情况:

?线程B访问了线程A新建的还未初始化的对象,这是个bug!

解决思路:1. 不允许“对象初始化”和“引用变量指向对象地址”进行重排序;2.运行这个重排序,但不允许其它线程“看到”这个重排序。

5.2 volatile解决方案

class Singleton22 {
    private Singleton22(){}
    //双重检查锁- 懒汉式  延迟加载+线程安全
    private volatile static Singleton22 singleton;
    public static Singleton22 getInstance() {
        if(singleton==null) {
            synchronized (Singleton22.class) {
                if(singleton==null)
                    singleton = new Singleton22();
            }
        }
        return singleton;
    }
}

?volatile会禁止上述的重排序。

5.3 基于类初始化的解决方案(即静态内部类的单例)

class Singleton3  {
    private Singleton3(){}
    //静态内部类初始化
    private static class InnerClass {
        public static Singleton3 instance=new Singleton3();
    }

    public static Singleton3 getInstance() {
        return InnerClass.instance;
    }

}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

首先需要了解的是对象的初始化过程:

  1. 线程获得Class对象的初始化锁;
  2. 释放锁;
  3. 线程执行分配内存空间、设置引用指向对象地址、初始化对象(这里步骤2和3发生了重排序,但其它线程无法看到,因为有Class对象锁)

Java语言中,一个类或接口T在下列情况下,会Class对象立即初始化:

  1. T是一个类,T的实例被创建;
  2. T是一个类,T的静态方法被调用;
  3. T的静态变量被赋值或使用(这就是静态内部类的加载时机,即访问InnerClass.instance时)

第一阶段:获取类初始化锁

线程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+双检锁可以实现对静态字段、实例字段的单例实现;

静态内部类只能实现对静态字段的单例实现。

参考资料:Java并发编程的艺术

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-31 16:29:19  更:2021-07-31 16:30:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/2 5:07:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码