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

  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;
    }
}

    懒汉式单例模式示例:

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;
    }
}

    懒汉式单例模式测试:

/**
 * 测试懒汉式单例是否线程安全
 * 如果按照最简单的写法,拿到的对象并不是相同的。
 * 解决方法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);
}

二、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: houwentao@g7.com.cn
 * @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();
    }
}

  锁的升级与对比

    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++;
    }
}

  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         //更新后的标志
)

    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和原子操作的实现原理, 了解这些原理对我们进行并发编程会更有帮助.

原文地址:https://www.cnblogs.com/wange/p/10549915.html

时间: 2024-08-24 17:12:20

[并发编程的艺术] 02-Java并发机制的底层实现原理的相关文章

Java并发编程的艺术,解读并发编程的优缺点

并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素.即时将所有的像素遍历一边都需要很长的时间, 面对如此复杂的计算量就需要充分利用多核的计算的能力.又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存, 生成订单等等这些操作,就可以进行拆分利用多线程的技术完成. 面对复

《Java并发编程的艺术》--Java中的锁

No1: Lock接口 Lock lock = new ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); } No2: 不要讲获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放 No3: No4: 队列同步器(同步器)是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者期望它能

并发编程(十)—— Java 并发队列 BlockingQueue 实现之 SynchronousQueue源码分析

BlockingQueue 实现之 SynchronousQueue SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样. 不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行

那些年读过的书《Java并发编程的艺术》一、并发编程的挑战和并发机制的底层实现原理

一.并发编程的挑战 1.上下文切换 (1)上下文切换的问题 在处理器上提供了强大的并行性就使得程序的并发成为了可能.处理器通过给不同的线程分配不同的时间片以实现线程执行的自动调度和切换,实现了程序并行的假象. 在单线程中:线程保存串行的执行,线程间的上下文切换不会造成很大的性能开销. 而在多线程中:线程之间频繁的调度需要进行上下文切换以保存当前执行线程的上下文信息和加载将要执行线程的上下文信息,而上下文切换时需要底层处理器.操作系统.Java虚拟机提供支持的会消耗很多的性能开 销.如果频繁的进行

【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile

章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译==>Java字节码 ==通过类加载器==>JVM(jvm执行字节码)==转化为汇编指令==>CPU上执行. Java中使用的并发机制依赖于JVM的实现和CPU的指令. volatile初探 volatile是是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性.可见性

Java并发编程的艺术(六)——线程间的通信

多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java并发编程的艺术(三)--volatile 1.1 如何实现通信? 这两种方式都采用了同步机制实现多条线程间的数据通信.与其说是"通信",倒不如说是"共享变量"来的恰当.当一个共享变量被volatile修饰 或 被同步块包裹后,他们的读写操作都会直接操作共享内存,从而各个

&lt;java并发编程的艺术&gt;读书笔记-第三章java内存模型(一)

一概述 本文属于<java并发编程的艺术>读书笔记系列,继续第三章java内存模型. 二重排序 2.1数据依赖性 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分下列三种类型: 名称 代码示例 说明 写后读 a = 1;b = a; 写一个变量之后,再读这个位置. 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量. 读后写 a = b;b = 1; 读一个变量之后,再写这个变量. 上面三种情况,只要重排序两个操作的执行顺序,

Java并发编程的艺术下载 &#155949;

下载地址: http://www.gqylpy.com/di/11 <Java并发编程的艺术>PDF高清完整版-下载 内容简介 并发编程领域的扛鼎之作,作者是阿里和1号店的资深Java技术专家,对并发编程有非常深入的研究,<Java并发编程的艺术>是他们多年一线开发经验的结晶.本书的部分内容在出版早期发表在Java并发编程网和InfoQ等技术社区,得到了非常高的评价.它选取了Java并发编程中核心的技术进行讲解,从JDK源码.JVM.CPU等多角度全面剖析和讲解了Java并发编程的

读《Java并发编程的艺术》(一)

离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督促自己要不断的学习,不断的进步. 最近在进一步学习Java并发编程,不言而喻,这部分内容是很重要的.现在就以<并发编程的艺术>一书为主导线,开始新一轮的学习. 进程和线程 进程是一个应用程序在处理机上的一次执行过程,线程是进程的最小基本单位(个人理解).一个进程可以包含多个线程. 上下文切换 我们

《java并发编程的艺术》读书笔记-第三章Java内存模型(二)

一概述 本文属于<java并发编程的艺术>读书笔记系列,第三章java内存模型第二部分. 二final的内存语义 final在Java中是一个保留的关键字,可以声明成员变量.方法.类以及本地变量.可以参照之前整理的关键字final.这里作者主要介绍final域的内存语义. 对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序. 初次读一个包含final域的对象的引用,与随后初次读这