美文网首页
并发基本原理以及常用工具类介绍

并发基本原理以及常用工具类介绍

作者: 胖嘟嘟洒酒疯 | 来源:发表于2019-05-21 21:34 被阅读0次

    并发编程的挑战:

    • 频繁的上下文切换
    • 死锁(线程循环依赖对方释放锁)
    • 资源限制的挑战
    如何减少上下文切换:

    无锁并发编程、CAS算法、使用最少线程

    • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
    • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
    • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
      样会造成大量线程都处于等待状态。

    分析线程状态使用jstack命令dump线程信息 ex:jstack 8444

    避免死锁的几个常见方法:
    • 避免一个线程同时获取多个锁。
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
    • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

    Java并发机制的底层实现原理:

    volatile的两条实现原则:
    • Lock前缀指令会引起处理器缓存回写到内存
    • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效
    synchronized的实现原理与应用:

    Java中的每一个对象都可以作为锁。

    • 对于普通同步方法,锁是当前实例对象。
    • 对于静态同步方法,锁是当前类的Class对象。
    • 对于同步方法块,锁是Synchonized括号里配置的对象。

    JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

    对象头:

    在虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据、对齐填充

    对象头一般由三部分组成: mark word、指向类的指针、数组长度(只有数组对象才有)
    32位虚拟机中对象头存储结构如图所示:(Epoch 偏向锁时间戳)


    image

    锁介绍:

    偏向锁:

    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否
    存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    轻量级锁:

    1)轻量级锁加锁
    ​ 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并
    将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用
    CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失
    败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
    ​ 2)轻量级锁解锁
    ​ 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成
    功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是
    两个线程同时争夺锁

    如何实现原子操作:

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

    • cas操作时会面临ABA问题,可以通过在变量前加版本号解决ABA问题
    • 循环时间长开销大,当自旋cas长时间不成功时,会给cpu带来非常大的执行开销
    • 只能保证一个共享变量的原子操作,可以把多个变量放在一个对象里来进行CAS操作

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

    JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

    从源代码到指令序列的重排序:

    1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
    的执行顺序。
    ​ 2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
    Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
    机器指令的执行顺序。
    ​ 3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
    去可能是在乱序执行。

    指令重排序对多线程的影响:

    class ReorderExample {
        int a = 0;
        boolean flag = false;
        
        public void writer() {
            a = 1; // 1
            flag = true; // 2
        }
        Public void reader() {
            if (?flag) { // 3
            int i = a * a; // 4
            ……
        }
    }
    

    flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行
    writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,<font color=#FF0000>不一定</font>能看到线程A在操作
    1对共享变量a的写入

    volatile的特性:

    • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写
      入。
    • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
      具有原子性。

    并发编程工具类学习:

    1、Unsafe

    简书地址-不安全类讲解

    脚本之家-不安全类通俗详解

    1、直接内存访问

    Unsafe的直接内存访问:用Unsafe开辟的内存空间不占用Heap空间,当然也不具有自动内存回收功能。做到像C一样自由利用系统内存资源。

    2、Unsafe类源码分析

    Unsafe的大部分API都是native的方法,主要包括以下几类:

    1)Class相关。主要提供Class和它的静态字段的操作方法。

    2)Object相关。主要提供Object和它的字段的操作方法。

    3)Array相关。主要提供数组及其中元素的操作方法。

    4)并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等。

    5)Memory相关。提供了直接内存访问方法(绕过Java堆直接操作本地内存),可做到像C一样自由利用系统内存资源。

    6)系统相关。主要返回某些低级别的内存信息,如地址大小、内存页大小。

    2、AQS

    AQS深度剖析

    3、CountDownLatch

    CountDownLatch的用法:

    CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

    CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

    4、Semaphore

    Semaphore(信号量)用来控制并发访问数量,当一个任务进入时,资源计数器会减一(获得许可证),当任务离开时,资源计数器加一(归还许可证),当计数器为0时,如果有任务需要获取许可才能执行操作,则此任务会阻塞。

    5、CyclicBarrier

    多个线程相互等待,同时执行。

    相关文章

      网友评论

          本文标题:并发基本原理以及常用工具类介绍

          本文链接:https://www.haomeiwen.com/subject/yobszqtx.html