[并发编程的艺术]02-java并发机制的底层实现原理

wange      2022-05-01     630

关键词:

  Java代码在编译后会变成Java字节码,字节码被类加载起加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行, Java中所使用的并发机制依赖于JVM的实现和CPU的指令.

一、volatile的应用

  在多处理器开发中保证共享变量的 "可见性", 可见性的意思是: 当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值.  如果 volatile变量修饰符使用恰当的话, 它比synchronized的使用和执行成本更低, 因为它不会引起线程上下文的切换和调度.

  volatile的定义与使用

    Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致的更新, 线程应该确保通过排他锁单独获得这个变量. Java语言提供了volatile, 在某些情况下比锁要更加方便.  如果一个字段被声明为 volatile, Java线程内存模型确保所有线程看到这个变量的值是一致的.

    为了提高处理速度,处理器不直接和内存通信,而是先将系统内存的数据读取到内部缓存再进行操作, 但操作完不知道何时写回到内存. 如果对声明了volatile的变量进行写操作, JVM就会向处理器发送一条Lock前缀的指令, 将这个变量所在缓存行的数据写回到内存.  但是, 就算写回到内存,如果其它处理器缓存的值还是旧的,再执行计算操作就会有问题.  所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议, 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存里.

    Lock前缀的指令在多核处理器下会引发两件事情:

      a) 将当前处理器缓存行的数据写回到系统内存

      b) 这个写回内存的操作会使在其它CPU里缓存了该内存地址的数据无效.

    使用较多的场景

      a) 作为线程开关

      b) 在懒汉式单例设计模式中,修饰对象实例, 禁止指令重排

    作为线程开关示例:

技术图片
public class Test3 implements Runnable{
    private static volatile boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Test3());
        thread.start();

        TimeUnit.SECONDS.sleep(1);
        flag = false;
    }
}
View Code

    懒汉式单例模式示例:

技术图片
public class LazySingleton {
    // private static LazySingleton lazySingleton = null;
    private static volatile LazySingleton lazySingleton = null;

    private LazySingleton(){}

    public static LazySingleton getInstance(){
        /////////////////最简单的写法/////////////////
        // // 实例为空就实例化
        // if (null == lazySingleton) {
        //     lazySingleton = new LazySingleton();
        // }
        //
        // // 否则直接返回
        // return lazySingleton;
        /////////////////最简单的写法/////////////////

        // 这样去实例化,结果也不是预期的,因为第一个线程进入代码块进行实例化之后,退出代码块,随之切换到了其它线程,其它线程进入代码块
        // 也会进行实例化
        // if (null == lazySingleton) {
        //     try {
        //         TimeUnit.SECONDS.sleep(1);
        //     } catch (InterruptedException e) {
        //         e.printStackTrace();
        //     }
        //
        //     synchronized (LazySingleton.class) {
        //         lazySingleton = new LazySingleton();
        //     }
        // }

        // 使用双重检查保证单例的线程安全(此时也不是绝对的线程安全(指令重排序会导致不安全),
        // 要达到线程安全,还要给lazySingleton加上volatile关键字,禁止指令重排序 )
        //
        // 第一个线程实例化后,离开代码块,此时即使第二个线程进入代码块,经过判断会发现实例已经存在了,所以第二个线程不会去实例化对象了
        if (null == lazySingleton) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (LazySingleton.class) {
                if (null == lazySingleton) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}
View Code

    懒汉式单例模式测试:

技术图片
/**
 * 测试懒汉式单例是否线程安全
 * 如果按照最简单的写法,拿到的对象并不是相同的。
 * 解决方法1:给getInstance方法加上synchronized,但是这样会导致其它线程等待,消耗性能。
 * 解决方法2:同步代码块
 */
@Test
public void f2() throws InterruptedException {
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            System.out.println(LazySingleton.getInstance());
        }).start();
    }

    TimeUnit.SECONDS.sleep(2);
}
View Code

 

二、synchronized的应用

  在多线程并发编程中,synchronized一直是元老级角色, 它能保证原子性,很多人都会称呼它为 "重量级锁", 随着 Java SE 1.6对synchronized进行优化之后,有些情况下它就并不那么重了, Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗, 引入了偏向锁和轻量级锁以及锁的存储结构.

  Java中每一个对象都可以作为锁,具体表现为3种形式:

    1) 对于普通同步方法,锁是当前实例对象(会锁住对象实例)

    2) 对于静态同步方法, 锁是当前类的class对象(会锁住整个类)

    3) 对于同步方法块, 锁是synchronized括号里配置的对象(会锁住括号里的对象)

  当一个线程试图访问同步代码块时, 它必须得到锁, 退出或抛出异常时,必须释放锁. 对于1、2、3 这三种情况的测试:

技术图片
/**
 * 深入理解synchronized关键字
 *  保证原子性和可见性操作
 *  内置锁
 *      每个java对象都可以用作一个实现同步的锁,这些锁称为内置锁,线程进入同步代码块或方法块时会自动获得该锁,在退出代码块/方法块时会释放该锁
 *      获得内置锁的唯一途径就是进入这个锁保护的同步代码块/方法
 *  互斥锁
 *      内置锁是一个互斥锁,意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个
 *      锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
 *
 *  可修饰哪些地方
 *      1、可修饰实例方法、静态方法
 *          实例方法:锁住对象的实例 f1 两个不同对象都锁3秒,结果是几乎同时运行结束,说明锁的是各自的对象实例
 *          静态方法:锁住整个类 f2 两个对象调用m2不会同时结束,第一个线程结束3miao后第二个线程结束,说明整个类被锁类
 *              实际编程中尽量少用synchronized修饰静态方法,因为它会导致整个类被锁,所有线程串行执行
 *      2、可修饰代码块
 *          锁住括号中的对象 m3中锁的就是lock对象,因此f3中也是串行的效果, f4是并行效果
 *
 * @Auther: [email protected]
 * @Date: 2019-03-01 21:18
 */
public class Test2 {
    private Object lock = new Object();
    public void m3(){
        synchronized (lock) {
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 修饰静态方法
    public synchronized static void m2(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }

    // 修饰实例方法
    public synchronized void m1() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }

    @Test
    public void f4() throws InterruptedException {
        Test2 class1 = new Test2();
        Test2 class2 = new Test2();

        Thread thread = new Thread(() -> {
            class1.m3();
        });

        Thread thread1 = new Thread(() -> {
            class2.m3();
        });
        thread.start();
        thread1.start();

        thread.join();
        thread1.join();
    }

    @Test
    public void f3() throws InterruptedException {
        Test2 class1 = new Test2();

        Thread thread = new Thread(() -> {
            class1.m3();
        });

        Thread thread1 = new Thread(() -> {
            class1.m3();
        });
        thread.start();
        thread1.start();

        thread.join();
        thread1.join();
    }

    @Test
    public void f2() throws InterruptedException {
        Test2 class1 = new Test2();
        Test2 class2 = new Test2();

        Thread thread = new Thread(() -> {
            class1.m2();
        });

        Thread thread1 = new Thread(() -> {
            class2.m2();
        });
        thread.start();
        thread1.start();

        thread.join();
        thread1.join();
    }

    @Test
    public void f1() throws InterruptedException {
        Test2 class1 = new Test2();
        Test2 class2 = new Test2();

        Thread thread1 = new Thread(() -> {
            class1.m1();
        });

        Thread thread2 = new Thread(() -> {
            class2.m1();
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}
View Code

  锁的升级与对比

    Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了 "偏量锁" 和 "轻量级锁", 在1.6中, 锁一共有4中状态, 级别从低到高依次是: 无锁状态、偏量锁状态、轻量级锁状态和重量级锁状态, 这几个状态会随着竞争情况逐渐升级, 锁可以升级但不能降级. 锁的优缺点对比:

    技术图片

三、原子操作的实现原理

  不可被中断的一个或一系列操作称为原子操作, 在Java中可以通过锁和循环CAS的方式来实现原子操作.

  CAS(Compare and Swap)比较并交换: CAS操作需要数据两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化在不交换.

  在Java中通过锁和循环CAS的方式来实现原子操作.

  1) 使用循环CAS实现原子操作  

技术图片
public class Counter {
    private AtomicInteger atomicInteger = new AtomicInteger();
    private int i = 0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }

        for (Thread t : ts) {
            t.start();
        }
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicInteger.get());
        System.out.println(System.currentTimeMillis() - start);
    }

    // 使用cas实现线程安全的计数器
    private void safeCount(){
        for (; ; ) {
            int i = atomicInteger.get();
            boolean suc = atomicInteger.compareAndSet(i, ++i);
            if (suc)
                break;
        }
    }

    // 非线程安全计数器
    private void count(){
        i++;
    }
}
View Code

  JDK并发包里提供了一些类来支持原子操作,如 AtomicBoolean(用原子方式来更新的boolean值), AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值), 这些原子包装类还提供了有用的工具方法, 比如以原子的方式将当前值自增1或自减1.

  2) CAS实现原子操作的三大问题

    CAS虽然高效的解决了原子操作,但是CAS仍然存在三大问题,ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作.

    a) ABA问题

      因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A, 那么使用CAS进行检查时会发现它的值没有发生变化,但实际上却变化了, 解决思路就是使用版本号,在变量前面加上版本号,每次变量更新的时候把版本号加1,那么 A->B->A就会变成 1A->2B->3A, JDK1.5开始提供了AtomicStampedReference来解决ABA问题, 这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用, 并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值.

技术图片
public boolean compareAndSet(
    V   expectedReference,    //预期引用
    V   newReference,         //更新后的引用
    int expectedStamp,     //预期标志
    int newStamp         //更新后的标志
)
View Code

    b) 循环时间长开销大

      自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销.

    c) 只能保证一个共享变量的原子操作

      当对一个共享变量进行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以使用锁. 还有一个取巧的办法,就是把多个共享变量合并成一个共享变量进行操作.  比如又2个共享变量 i=2, j=a; 合并一下 ij=2a, 然后用CAS来操作ij, JDK1.5开始提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作.

  3) 使用锁机制实现原子操作

    锁机制保证了只有获得锁的线程才能够操作锁定的内存区域. JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁, 除了偏向锁,JVM实现锁的方式都使用了循环CAS, 即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当他退出同步块的时候使用循环CAS释放锁.

四、小结

  本章学习了 volatile、synchronized和原子操作的实现原理,Java中大部分容器和框架都依赖于volatile和原子操作的实现原理, 了解这些原理对我们进行并发编程会更有帮助.

《java并发编程的艺术》读后笔记-part2(代码片段)

文章目录《Java并发编程的艺术》读后笔记-part2第二章Java并发机制的底层实现原理1.volatile的应用1.1volatile的定义与实现原理2.synchronized的实现原理和应用2.1Java对象头2.2锁的升级与对比3.原子操作的实现原理3.1处理器如何实现原子... 查看详情

java并发编程艺术系列-二java并发机制底层原理

二、Java并发机制底层原理volatilesynchronized原子操作2.1volatile原理与应用2.1.1特点轻量级的synchronized共享变量的“可见性”(定义):如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的不会引... 查看详情

并发编程艺术-锁类型以及底层原理(代码片段)

Java并发编程艺术-并发机制的底层原理实现1.Volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。volatile借助Java内存模型保证所有线程能够看到... 查看详情

并发编程java并发机制的底层实现原理

volatile原理volatile是轻量级的synchronized,在多处理器开发中保证了共享变量的"可见性",volatile是一个轻量级的synchronized,在多CPU开发中保证了共享变量的“可见性”,也就是说当一个线程修改一个共享变量的时候,另一个线程能... 查看详情

java并发编程:java并发机制的底层实现原理(代码片段)

...最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。2、volatile应用2.1、简介在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile 查看详情

java并发编程:java并发机制的底层实现原理(代码片段)

...最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。2、volatile应用2.1、简介在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile 查看详情

java并发编程:底层实现机制

一、volatile的应用1.volatile的定义与实现原理2.volatile的使用优化二、synchronized的应用1.锁的实现原理2.锁的对比2.1偏向锁2.2轻量级锁2.3锁的对比三、原子操作的实现原理1.术语2.处理器如何实现原子操作3.Java如何实现原子操作四、小... 查看详情

《java并发编程的艺术》epub下载在线阅读,求百度网盘云资源

《Java并发编程的艺术》(方腾飞)电子书网盘下载免费在线阅读资源链接:链接:https://pan.baidu.com/s/19JrldXCS7yGVJadthE2VNw提取码:1dub  书名:Java并发编程的艺术作者:方腾飞豆瓣评分:7.4出版社:机械工业出版社出版年份... 查看详情

第二章并发机制的底层实现原理

...,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。volatiled的应用volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是一个线程修改一个共享... 查看详情

《java多线程编程核心技术》和《java并发编程的艺术》两本书的异同

...编程核心技术》:这本书让你入个门,整体上了介绍一些并发编程的基本API、常见场景和一些坑,推荐先看这本书,比较简单,适合新手,但是原理不够深入和《java并发编程的艺术》这本书从底层和实现原理开始讲起,深入java... 查看详情

并发编程的挑战(java并发编程的艺术)

1.上下文切换CPU通过给每个线程分配CPU时间片来实现并发,切换过程中线程的信息从保存到再加载就是一个上下文切换。由于频繁的进行上下文切换,会消耗资源,所以并发不一定比串行快。可以通过Lmbench3测量上下文切换的时... 查看详情

)(代码片段)

文章目录《Java并发编程的艺术》读后笔记-ScheduledThreadPoolExecutor详解(第十章)1.ScheduledThreadPoolExecutor简介2.ScheduledThreadPoolExecutor的运行机制3.ScheduledThreadPoolExecutor的实现《Java并发编程的艺术》读后笔记-ScheduledThread 查看详情

)(代码片段)

文章目录《Java并发编程的艺术》读后笔记-ScheduledThreadPoolExecutor详解(第十章)1.ScheduledThreadPoolExecutor简介2.ScheduledThreadPoolExecutor的运行机制3.ScheduledThreadPoolExecutor的实现《Java并发编程的艺术》读后笔记-ScheduledThread 查看详情

java并发机制的底层实现和原理

volatile的实现原理volatile修饰的变量的汇编代码volatilePersonp=newPerson();//汇编伪指令lockaddl$0x0,(%esp)  lock前缀的指令在多核处理器的作用【1】将当前处理器高速缓存行内的数据回写到内存中【2】这个回写内存的操作会将其他CPU里... 查看详情

2.java并发机制的底层实现原理

...,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。2.1volatile的应用  volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的”可见性“。可见性的意思是当一个线程修改... 查看详情

《java并发编程的艺术》读后笔记-part4(代码片段)

文章目录《Java并发编程的艺术》读后笔记-part4第四章Java并发编程基础1.线程简介1.1什么是线程?1.2为什么要使用多线程?1.3线程优先级1.4线程的状态1.5Daemon线程2.启动和终止线程2.1构造线程2.2启动线程2.3理解中断3.线程间... 查看详情

第2章java并发机制的底层实现原理

2.2synchronized的实现原理与应用  当一个线程A执行字节码时遇到monitorenter指令时,会首先检查该指令关联的Object的对象头中的MarkWord状态。2.2.1如果是偏向锁  如果2bit标志位为01代表此时处于偏向锁状态。  如果2bit标志位为... 查看详情

java并发编程:synchronized底层优化(偏向锁轻量级锁)

Java并发编程系列:Java并发编程:核心理论 Java并发编程:Synchronized及其实现原理Java并发编程:Synchronized底层优化(轻量级锁、偏向锁)Java并发编程:线程间的协作(wait/notify/sleep/yield/join)Java并发编程:volatile的使用及其原理... 查看详情