美文网首页
JUC框架&多线程

JUC框架&多线程

作者: 任笙_8b8c | 来源:发表于2020-08-05 21:01 被阅读0次

    5 Volatile 关键字

    • 保证内存可见和有序向性,但是不保证原子性
    • 虽然synchronized,jvm对它做了很多优化,但是它还是一个重量级的锁,而我们的Volatile 是一个轻量级的锁,因为它不会引起线程上下文的切换和调度.
    • Java语言规范对volatile的定义如下:
            Java允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
     private volatile boolean flag = true;
    

    Volatile实现内存可见性的过程 线程写Volatile变量的过程: 1. 改变线程本地内存中Volatile变量副本的值; 2. 将改变后的副本的值从本地内存刷新到主内存
    线程读Volatile变量的过程:

    1. 从主内存中读取Volatile变量的新值到线程的本地内存中
    2. 从本地内存中读取Volatile变量的副本
    Volatile实现内存可见性原理:

           写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中
    读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值

    • 内存屏障(Memory Barrier):
          是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序
    • volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个优布置来小化插入内存 屏障的总数几乎是不可能的.
    • 所以,JMM采用了保守策略。如下:
      StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
      StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
      LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
      LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
    如果我们用这个关键字替代 synchronized 重量级锁,那怎么保证它的原子性呢?
    • 解决方案
      1. 使用synchronized (基本特性都保证)
      1. 使用ReentrantLock(可重入锁)
      1. 使用AtomicInteger(原子操作)
    演示:
    使用synchronized
    
    public synchronized void addCount() {   
     for (int i = 0; i < 10000; i++) {   
         count++; 
       } 
    
    使用ReentrantLock(可重入锁)
    //可重入锁 private Lock lock = new ReentrantLock();
    public void addCount() { 
       for (int i = 0; i < 10000; i++) {   
         lock.lock();    
        count++;      
      lock.unlock();    } 
    }
    
    使用AtomicInteger(原子操作)
    public static AtomicInteger count = new AtomicInteger(0); public void addCount() {   
     for (int i = 0; i < 10000; i++) { 
           //count++; 
           count.incrementAndGet(); 
       } 
    
    
    5.3 Volatile 适合使用场景
    • a)对变量的写入操作不依赖其当前值
      不满足:number++、count=count*5等
      满足:boolean变量、直接赋值的变量等
    • b)该变量没有包含在具有其他变量的不变式中
      不满足:不变式 low<up
    • 总结:变量真正独立于其他变量和自己以前的值,在单独使用的时候,适合用volatile
    5.4 synchronized和volatile的区别

    a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程
    b)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,和有序性,无法保证原子性
    与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁 的性能和伸缩性。如果严格遵循 volatile 的使用条件(变量真正独立于其他变量和自己以前的值 ) 在 某些情况下可以使用 volatile 代替 synchronized 来优化代码提升效率

    6 J.U.C之CAS

    J.U.C 即 java.util.concurrent 是 Java 5 中引入的,是 JSR 166 标准规范的一个实现;
    内容:
    我们熟悉的线程池机制就在这个包,J.U.C 框架包含的内容有:

    • AbstractQueuedSynchronizer(AQS框架),J.U.C 中实现锁和同步机制的基础;
    • Locks & Condition(锁和条件变量),比 synchronized、wait、notify 更细粒度的锁机制;
    • Executor 框架(线程池、Callable、Future),任务的执行和调度框架; Synchronizers(同步器),主要用于协助线程同步,有 CountDownLatch、CyclicBarrier、 Semaphore、Exchanger; Atomic Variables(原子变量),方便程序员在多线程环境下,无锁的进行原子操作,核心操作是 CAS 原子操作,所谓的 CAS 操作,即 compare and swap,指的是将预期值与当前变量的值比较 (compare),如果相等则使用新值替换(swap)当前变量,否则不作操作;
    • BlockingQueue(阻塞队列),阻塞队列提供了可阻塞的入队和出对操作,如果队列满了,入队 操作将阻塞直到有空间可用,如果队列空了,出队操作将阻塞直到有元素可用;
    • Concurrent Collections(并发容器),说到并发容器,不得不提同步容器。在 JDK1.5 之前,为 了线程安全,我们一般都是使用同步容器,同步容器主要的缺点是:对所有容器状态的访问都串行 化,严重降低了并发性;某些复合操作,仍然需要加锁来保护;迭代期间,若其它线程并发修改该 容器,会抛出 ConcurrentModificationException 异常,即快速失败机制; Fork/Join 并行计算框架,这块内容是在 JDK1.7 中引入的,可以方便利用多核平台的计算能力,简 化并行程序的编写,开发人员仅需关注如何划分任务和组合中间结果; TimeUnit 枚举,TimeUnit 是 java.util.concurrent 包下面的一个枚举类,TimeUnit 提供了可读 性更好的线程暂停操作,以及方便的时间单位转换方法;
    6.1 CAS介绍悲观锁策略)
    • CAS,Compare And Swap,即比较并交换。同步组件中大量使用CAS技术实现了Java多线程的并发操 作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的 版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
    • CAS原理剖析
      循环的次数调整为一亿(保证在一秒之内不能遍历完成,从而测试三 种原子操作的性能)
      1.synchronized
      2.Volatile
      3.AtomicInteger原子操作类
      我们发现,AtomicInteger原子操作性能高,他是用的就是CAS
    CAS原理
    • 而同步保证了原子操作。但 是不可避免的是性能较低。CAS是如何提高性能的呢?
    • CAS的思想很简单:
      三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期 值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失 败,通过自旋的方式等待并再次尝试,直到成功。
    • CAS在 先比较后修改 这个CAS过程中,根本没有获取锁,释放锁的操作,是硬件层面的原子操作,跟 JMM内存模型没有关系。
    • JUC下的atomic类都是通过CAS来实现的,下面就是一个AtomicInteger原子操作类的例子,在其中使用 了Unsafe unsafe = Unsafe.getUnsafe()。Unsafe 是CAS的核心类,它提供了硬件级别的原子操作。
    • Unsafe 是CAS的核心类 他下面有一个compareAndSwapInt方法来完成原子操作
    6.3 native关键词

    前面提到了sun.misc.Unsafe这个类,里面的方法使用native关键词声明本地方法,为什么要用native

    • Java无法直接访问底层操作系统,但有能力调用其他语言编写的函数or方法,是通过JNI(Java Native Interfface)实现。
    • Java毕竟不是一个完整的系统,它经常需要一些底层的支持,通过JNI和native method我们就可以 实现jre与底层的交互
    多CPU的CAS处理
    • CAS可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现 就有点儿复杂了。
    • CPU提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。
    • 总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此 信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其 他内存地址的数据,其开销有点儿大。
    • 缓存加锁:缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器 不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致 性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当cpu1修改了缓存的数据,发布到主内存,会告知cpu2,cpu2的缓存全部失效,如果还要继续执行就必须重主内存重新拉取下来,到缓存.这样就能保证原子性.


      jvm.png

    CAS的缺陷

    主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
    • 循环时间太长:如果CAS一直不成功的话,它会一直自旋下去,如果自旋也一直你不成功,这样非常耗费cpu的资源.
      解决:限制自旋次数. 在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的 SynchronousQueue
    • 只能保证一个共享变量原子操做:cas只能针对一个共享变量,如果是多个共享变量就只能使用锁了。
      解决:1.使用锁. 2.从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
    • ABA问题 (脏读,脏取)
      比如A(未婚),B(已婚)
      比如当我们的老王在放暑假前通过朋友介绍了一个妹子她未婚(A) old=A ,但是老王出去打暑假工了,妹子可能有别的追求者,然后别的线程进来跟她嘿嘿嘿,于是他们就结婚了,主内存的妹子就变成了(B),但是一个月内他们闹矛盾,男的时间太短,离婚了,妹子又单身了,于是呢老王打工回来了,这是CAS匹配主内存发现未婚(A),这时老王就和她结婚了.
      解决:
      Java提供了AtomicStampedReference来解决。AtomicStampedReference 通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题
      使用原子操作类加版本号,就是在每次比较交换之前加加入版本号,一旦修改了值后版本号加一
    //这个是ABA问题的展示
     private static AtomicInteger ai = new AtomicInteger(100);   
    
    //解决ABA问题
     private static AtomicStampedReference air = new AtomicStampedReference(100, 1);
     
     //ABA问题演示:   
     //1. 线程1先对数据进行修改 A-B-A过程   
            public void run() {           
         ai.compareAndSet(100, 110); 
         ai.compareAndSet(110, 100);       
         }      
      });
     //2. 线程2也对数据进行修改 A-C的过程
      //AtomicStampedReference可以看到每次修改都需要设置标识Stamp,相当于进行了1A-2B3A的操作       
     //线程2进行操作的时候,虽然数值都一样,但是可以根据标识很容易的知道A是以前的1A,还是现 在的3A 
       // 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识 getStamp() + 1
        air.compareAndSet(100, 110, air.getStamp(), air.getStamp() + 1);               
        air.compareAndSet(110, 100, air.getStamp(), air.getStamp() + 1);      
          }  
          });
     
    
    jvm.png

    7 J.U.C之atomic包

    atomic
    • 到AtomicInteger的工作原理,它们的内部都维护者一个对应的基本类 型的成员变量value,这个变量是被volatile关键字修饰的,保证多线程环境下看见的是同一个(可见 性)。
    • AtomicInteger在进行一些原子操作的时候,依赖Unsafe类里面的CAS方法,原子操作就是通过自旋方式,不断地使用CAS函数进行尝试直到达到自己的目的。
    • 除了AtomicInteger类以外还有很多其他的类也有类似的功能,在JUC中有一个包 java.util.concurrent.atomic存放原子操作的类
    atomic里的类主要包括:

    基本类型 使用原子的方式更新基本类型

    • AtomicInteger:整形原子类
      api介绍(简单介绍几个):
      getAndDecrement() //减少1,返回减少前的数据
      getAndIncrement() //增加1,返回增加前的数据
      getAndSet(int) //设置指定的数据,返回设置前的数据
      compareAndSet(int, int)//尝试新增后对比,若增加成功则返回true否则返回false

    • AtomicLong:长整型原子类
      AtomicLong主要API和AtomicInteger,只是类型不是int,而是long

    • AtomicBoolean :布尔型原子类
      compareAndSet(boolean, boolean) //参数1为原始值,参数2为修改的新值,若修改成功返回true, 否则返回false getAndSet(boolean)// 尝试设置新的boolean值,直到成功为止,返回设置前的数据

    引用类型

    • AtomicReference:引用类型原子类
    • AtomicStampedReference:原子更新引用类型里的字 段原子类(加版本号)
    • AtomicMarkableReference :原子更新带有标记位的引用类型(true,flase)
    public static void main(String[] args) throws InterruptedException {   
       User u1 = new User("张三", 22);     
      User u2 = new User("李四", 33);
           AtomicReference ar = new AtomicReference(u1);    
       ar.compareAndSet(u1, u2);
           System.out.println(ar.get());
       }
    
    -  AtomicMarkableReference :原子更新带有标记位的引用类型(true,flase)
     public static void main(String[] args) throws InterruptedException {        
    User u1 = new User("张三", 22);    
       User u2 = new User("李四", 33);
           //和AtomicStampedReference效果一样,用于解决ABA的       
          //区别是表示不是用的版本号,而只有true和false两种状态。相当于未修改和已修改  
              AtomicMarkableReference<User> amr = new AtomicMarkableReference(u1, true);     
             amr.compareAndSet(u1, u2, false, true); 
    

    区别的是:它描述更加简单的是与否的关系。通常ABA问题只有两种状态,而AtomicStampedReference是多种状态。

    数组类型 使用原子的方式更新数组里的某个元素

    • AtomicIntegerArray:整形数组原子类

    • AtomicLongArray:长整形数组原子类

    • AtomicReferenceArray :引用类型数组原子类
      对象的属性修改类型

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

    • AtomicLongFieldUpdater:原子 更新长整形字段的更新器

    • AtomicReferenceFieldUpdater :原子更新引用类形字段的更新 器
      7.3 数组类型
      使用原子的方式更新数组里的某个元素
      AtomicIntegerArray:整形数组原子类 AtomicLongArray:长整形数组原子类 AtomicReferenceArray :引用类型数组原子类 AtomicIntegerArray主要API如下:
      System.out.println(amr.getReference());
      JDK1.8新增类

    • DoubleAdder:双浮点型原子类

    • LongAdder:长整型原子类

    • DoubleAccumulator:类似 DoubleAdder,但要更加灵活(要传入一个函数式接口) LongAccumulator:类似 LongAdder,但要更加灵活(要传入一个函数式接口)

    • 虽然涉及到的类很多,但是原理和AtomicInteger都是一样,使用CAS进行的原子操作,其方法和使用 都是大同小异的。

    • 为在非常高 的并发请求下AtomicLong的性能不能让他们接受,虽然AtomicLong使用CAS但是CAS失败后还是通过 无限循环的自旋锁不断尝试。

    • 在高并发下N多线程同时去操作一个变量会造成大量线程CAS失败然后处于自旋状态,这大大浪费了cpu 资源,降低了并发性。那么既然AtomicLong性能由于过多线程同时去竞争一个变量的更新而降低的, 那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源那么性能问题不就解决了?是 的,JDK8提供的LongAdder就是这个思路


      cas.png
    public class Demo9Compare {
        public static void main(String[] args) {    
        AtomicLong atomicLong = new AtomicLong(0L);       
        LongAdder longAdder = new LongAdder();
            long start = System.currentTimeMillis();    
        for (int i = 0; i < 50; i++) {        
        new Thread(new Runnable() {          
         @Override 
        public void run() {            
            for (int j = 0; j < 1000000; j++) {                        //atomicLong.incrementAndGet();                        longAdder.increment();             
           }                }       
         }).start();   
         }
            while (Thread.activeCount() > 2) {        }
            System.out.println(atomicLong.get());        System.out.println(longAdder.longValue());
            System.out.println("耗时:" + (System.currentTimeMillis() - start));
        } }
    
    cas.png
    • 在并发比较低的时候,LongAdder和AtomicLong的效果非常接近。但是当并发 较高时,两者的差距会越来越大,。上图中在线程数为1000,每个线程循环数为100000时,LongAdder 的效率是AtomicLong的6倍左右

    8 J.U.C之AQS

    • AQS,即队列同步器。它是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望 它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件
    • 在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为 低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:它 缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,而且独占式在高并发场景下性能大打折扣。
      AQS解决了实现同步器时涉及到的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建 同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争 问题。
    AQS定义两种资源共享方式:

    Exclusive(独占,只有一个线程能执行,如ReentrantLock) 竞争不激烈
    Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

    AQS核心图解:
    cas.png
    图解:
    • 首先明确一点,AQS内部其实就是一个CLH同步队列,遵循FIFO原则(先进先出)双向队列
    出列:

    首节点的线程释放同步就是将state的值-1,它的值为0表示无锁状态,然后会去唤醒它的后继节点,当后继节点拿到同步状态就是将state的值+1,后他把自己设置为首节点.这个过程不用cas来保证.

    入列:

    CLH队列入列,就是tail指向新节点、新节点的prev指向当前后的节点,当前后一个节点 的next指向当前节点


    cas.png
    总:

    当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点 (Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒 (公平锁),使其再次尝试获取同步状态

    • 它提供了三个方法来对同步状态state进行操作:
      getState():返回同步状态的当前值
      setState():设置当前同步状态
      compareAndSetState():使 用CAS设置当前状态,该方法能够保证状态设置的原子性
      这三种操作均是CAS原子操作,其中compareAndSetState的实现依赖于Unsafe的 compareAndSwapInt()方法

    J.U.C之锁

    9.1.1 互斥锁

    在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

    9.1.2 阻塞锁

    阻塞锁,是让线程进入阻塞状态进行等待(wait()等方法),当获得相应的信号(唤醒,时间) 时,才可以进入线 程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。

    9.1.3 自旋锁

    自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时,才能进入 临界区。
    由于自旋锁只是将当前线程不停地执行循环体执行一段没有意义的代码,不进行线程状态的改变,所以响应速度更快。但当线程 数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且 保持锁的时间段。适合使用自旋锁。

    9.1.4 读写锁

    读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行 读访问,写者则需要对共享资源进行写操作。
    读写锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享 资源,大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多 个读者(与CPU数相关),但不能同时既有读者又有写者。

    9.1.5 公平锁

    公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得,

    9.1.6 非公平锁(Nonfair):

    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
    非公平锁性能比公平锁高,因为公平锁需要在多核的情况下维护一个队列

    9.2 ReentrantLock (可重入锁)

    ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是 ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。
    ReentrantLock还提供了公平锁和非公平锁的选择,构造方法接受一个可选的公平参数(默认非公平 锁),当设置为true时,表示公平锁,否则为非公平锁。
    实底层就是使用AQS同步队列。当同步队列的状态state == 0 时,则将锁持有线程设 置为null,free= true,表示释放成功。

    public ReentrantLock() {
        //非公平锁    sync = new NonfairSync();
     }
    public ReentrantLock(boolean fair) {  
      //公平锁    sync = fair ? new FairSync() : new NonfairSync();
     }
    
    9.2.3 公平锁与非公平锁原理
    public final boolean hasQueuedPredecessors() {   
     Node t = tail;  //尾节点  
      Node h = head;  //头节点  
      Node s;
        //头节点 != 尾节点  
      //同步队列第一个节点不为null 
       //当前线程是同步队列第一个节点 
       return h != t &&       
     ((s = h.next) == null || s.thread != Thread.currentThread()); 
    }
    

    该方法主要做一件事情:
    主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回true, 否则返回false。

    9.2.4 ReentrantLock与synchronized的区别

    到ReentrantLock提供了比synchronized更加灵活和强大的锁机制,那么它的灵活和强大之处在 哪里呢?
    他们之间又有什么相异之处呢?

      1. 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如: 时间锁等候,可中断锁等候,锁投票。
      1. ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个 条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
      1. ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下 次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而 言,ReentrantLock会不容易产生死锁些。
      1. ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个 synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可 能会产生严重的后果。
      1. ReentrantLock支持中断处理,且性能较synchronized会好些
    9.3 读写锁ReentrantReadWriteLock

    可重入锁ReentrantLock是互斥锁,互斥锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景 下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一 个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。
    读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较 大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会 被阻塞。
    读写锁的主要特性:

    1. 公平性:支持公平性和非公平性。
    2. 重入性:支持重入。读写锁多支持65535个递归写入锁和65535个递归读取锁。
    3. 锁降级:写锁能够降级成为读锁,遵循获取写锁、获取读锁在释放写锁的次序。读锁不能升级为写 锁。

    读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只 读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是 独占的。
    ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。

    • 获取写锁
      写锁就是一个支持可重入的互斥锁。
      写锁的获取终会调用tryAcquire(int arg),该方法在内部类Sync中实现.
    • 写锁的释放
      获取了写锁用完了则需要释放,WriteLock提供了unlock()方法释放写锁,放终还是会调用AQS的模板方法release(int arg)方法,
    • 读锁的获取
      读锁为一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是获取成 功。
      读锁的获取可以通过ReadLock的lock()方法
    • 读锁的释放
      与写锁相同,读锁也提供了unlock()释放读锁
    • 锁降级
      读写锁有一个特性就是锁降级,锁降级就意味着写锁是可以降级为读锁的。锁降级需要遵循以下顺序:
      获取写锁=>获取读锁=>释放写

    J.U.C之Condition

    • Condition,对线程的等待、唤醒操作更加详细和灵活。
    Condition提供了一系列的方法来对阻塞和唤醒线程:
    1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
    2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前 一直处于等待状态。
    3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之 前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout – 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
    4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对 中断不敏感】。
    5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定后期限之前一直 处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回 false。
    6. signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
    7. signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。 Condition是一种广义上的条件队列(等待队列)。他为线程提供了一种更为灵活的等待/通知模 式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。 Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个 Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
    10.2.2 等待状态

    调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放 锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。

    10.2.3 通知

    调用Condition的signal()方法,将会唤醒在等待队列中等待长时间的节点(条件队列里的首节点), 在唤醒节点前,会将节点移到CLH同步队列中中


    11 J.U.C之并发工具类

    11.1 CyclicBarrie(同步屏障可理解为栅栏)

    CyclicBarrier也叫同步屏障,在JDK1.5被引入的一个同步辅助类,
    官方解释:允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这 些线程必须偶尔等待彼此。 屏障被称为循环,因为它可以在等待的线程被释放之后重新使用。

    CyclicBarrier自己理解: 它的屏障就好比一扇门,当所有人都到达了这扇门时才会开门一起进去. 1.png
    原理分析:
    • CyclicBarrie内部是用Reentrantlock重入锁和Condition

    它有俩个构造方法:
    CyclicBarrier(int parties):它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在 启动屏障时执行预定义的操作。parties表示拦截线程的数量。
    CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数 量的线程处于等待状态时启动,并在启动屏障时执行给定的屏障操作,该操作由后一个进入屏障的线程执行。
    await()方法的逻辑:如果该线程不是到达的后一个线程,则他会一直处于等待状态,除非发生以下情 况:
    1. 后一个线程到达,即index == 0
    2. 超出了指定时间(超时等待)
    3. 其他的某个线程中断当前线程
    4. 其他的某个线程中断另一个等待的线程
    5. 其他的某个线程在等待屏障超时 .
    6. 其他的某个线程在此屏障调用reset()方法。reset()方法用于将屏障重置为初始状态。

    案例介绍:
    /*
            *CyclicBarrier案例
            *叫同步屏障
            **/
            publicclassDemo1CyclicBarrier{
            
            publicstaticvoidmain(String[]args){
            //指定屏障发行的数量
            CyclicBarriercyclicBarrier=newCyclicBarrier(5);
            List<Thread>threadList=newArrayList<>();
            for(inti=0;i<5;i++){
            Threadt=newThread(newAthlete(cyclicBarrier,"运动员"+i));
            threadList.add(t);
            }
            for(Threadt:threadList){
            t.start();
            }
            }
            
            staticclassAthleteimplementsRunnable{
            
            privateCyclicBarriercyclicBarrier;
            privateStringname;
            
            publicAthlete(CyclicBarriercyclicBarrier,Stringname){
            this.cyclicBarrier=cyclicBarrier;
            this.name=name;
            }
            
            @Override
            publicvoidrun(){
            System.out.println(name+"就位");
            try{
            //屏障,栅栏所有线程都准备好达到预期的数量放可放行
            cyclicBarrier.await();
            System.out.println(name+"跑到终点。");
            }catch(Exceptione){
            
            }
            }
            }
            }
    
    CountDownLatch :
    • CountDownLatch是一个计数的闭锁,作用与CyclicBarrier有点儿相似。
      官方解释: 用给定的计数 初始化 CountDownLatch。由于调用countDown() 方法,所以在当前计数到达 零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立 即返回。
      这种现象只出现一次,计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier

    • CountDownLatch和CyclicBarrier 的区别
      CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行,await()才会被唤醒;
      CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

    • CountDownLatch内部依赖Sync实现,而Sync继承AQS。

    CountDownLatch仅提供了一个构造方法: CountDownLatch(int count) : 构造一个用给定计数初始化的 CountDownLatch
    public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
    sync为CountDownLatch的一个内部类,通过这个内部类Sync可以知道CountDownLatch是采用共享锁 来实现的。长用的两个方法是await()和countDown():
    CountDownLatch提供await()方法来使当前线程在锁存器倒计数至零之前一直等待,除非线程被 中断。内部使用AQS的getState方法获取计数器,如果计数器值不等于0,则会以自旋方式会尝试 一直去获取同步状态。
    CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待 的线程。内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态。

    案例:
    public class Demo2CountDownLatch {
        public static void main(String[] args) {    
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
            List<Thread> threadList = new ArrayList<>();    
        for (int i = 0; i < 5; i++) {     
           CountDownLatch countDownLatch = new CountDownLatch(1);  
              //起点运动员         
       Thread t1 = new Thread(new Athlete(cyclicBarrier, countDownLatch, "起点运动员" + i));
                //接力运动员     
           Thread t2 = new Thread(new Athlete(countDownLatch, "接力运动员" + i));
                threadList.add(t1);     
           threadList.add(t2);        }
            for (Thread t : threadList) {    
            t.start();      
      }  
      }
        static class Athlete implements Runnable {
            private CyclicBarrier cyclicBarrier;    
        private String name;
            CountDownLatch countDownLatch; 
            //起点运动员     
       public Athlete(CyclicBarrier cyclicBarrier, CountDownLatch countDownLatch, String name) {       
         this.cyclicBarrier = cyclicBarrier;      
          this.countDownLatch = countDownLatch;   
             this.name = name;   
         }
            //接力运动员   
         public Athlete(CountDownLatch countDownLatch, String name) {  
              this.countDownLatch = countDownLatch;        
        this.name = name;    
        }
            @Override    
        public void run() {       
         //判断是否是起点运动员       
         if (cyclicBarrier != null) {
    
                    System.out.println(name + "就位"); 
                   try {           
             cyclicBarrier.await();   
                     System.out.println(name + "到达交接点。");
                        //已经到达交接点                 
       countDownLatch.countDown();       
             } catch (Exception e) {    
                }       
         }
                //判断是否是接力运动员     
           if (cyclicBarrier == null) {     
               System.out.println(name + "就位");       
             try {                  
      countDownLatch.await();   
                     System.out.println(name + "到达终点。"); 
                   } catch (Exception e) {       
             }        
        }    
        } 
       } 
    }
    

    Semaphore 信号量

    • Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享 锁”。 Semaphore维护了一个信号量许可集。线程可以获取信号量的许可;当信号量中有可用的许可时,线 程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以释放它所持有的信号量许可, 被释放的许可归还到许可集中,可以被其他线程再次获取。

    • 举个例子:
      假设停车场仅有5个停车位,一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场 车位够,安排进去停车,然后又来三辆,这个时候由于只剩两个停车位,所有只能停两辆,其余一辆必 须在外面候着,直到停车场有空车位,当然以后每来一辆都需要在外面等着。当停车场有车开出去,里 面有空位了,则安排一辆车进去(至于是哪辆车 要看选择的机制是公平还是非公平)

    • Semaphore常用于约束访问一些(物理或逻辑)资源的线程数量。
      当信号量初始化为 1 时,可以当作互斥锁使用,因为它只有两个状态:有一个许可 能使用,或没有许可 能使用。当以这种方式使用时,“锁”可以被其他线程控制和释放,而不是主线程控制释放。

    • Semaphore内部包含公平锁(FairSync)和非公平锁(NonfairSync),继承内部类 Sync,其中Sync继承AQS。

    • Semaphore默认是非公平锁,acquire()方法来获取一个许可。

    public void acquire() throws InterruptedException {   
     sync.acquireSharedInterruptibly(1);
     }
    [图片上传中...(图片.png-c689a2-1597328192438-0)]
    
    
    

    release()来释放许可。

    public void release() {  
      sync.releaseShared(1);
     }
    

    J.U.C之并发容器ConcurrentHashMap

    HashMap: 是线程不安全的集合
    • 它是线程不安全的。并且在多线程环境下,put操作是 有可能产生死循环和数据丢失,不过在JDK1.8的版本中更换了数据插入的顺序,已经解决了这个问题。 为了解决该问题,提供了Hashtable和Collections.synchronizedMap(hashMap)两种解决方案,但是这 两种方案都是对读写加锁,独占式。一个线程在读时其他线程必须等待,吞吐量较低,性能较为低下。 而J.U.C给我们提供了高性能的线程安全HashMap:ConcurrentHashMap。
    • 在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思 路,而是利用CAS+Synchronized来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结 构。
    JDK7的hashMap:
    map.png
    • HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。每个绿色的实体是嵌套类 Entry 的 实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

    • public HashMap(int initialCapacity, float loadFactor) 初始化方法的参数说明:
      capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。 (便于位与运算)
      loadFactor:负载因子,默认为 0.75。
      threshold:扩容的阈值,等于 capacity * loadFactor 16x0.75=12
      计算index方法:index = hash & (tab.length – 1)

    扩容原理

    扩容方法 数组长度直接扩容至原来的两倍,,hashMap中元素的存放位置取决于元素哈希值与数组长度-1的位与&运算,默认情况下,长度为16,即后四位1111与元素hash值的比较,在扩容之后,长度为32,将第五位也变为1(11111=31),将各个元素与新长度进行位与&运算只需观察元素hash值和数组长度-1的第五位值的比较,为1就保持原地址不动,为0就会将该元素地址索引增加16移至新空间,如此扩容之后平分原有元素。
    在jdk1.8版本之后,hashMap底层加入了红黑树机制,当链表的长度大于8,数组长度大于64时,链表将会转换为红黑树(不过链表的形式依然存在,便于后期扩容或者删减之后该红黑树退化成链表),因为从时间复杂度来说,红黑树优于链表,便于查询,不过红黑树的节点占据空间是普通节点两倍,只有在节点足够多时才会采用红黑树,当链表节点超过8的时候几率大约千分之一,链表性能很差了,采用红黑树是很好的应对策略.当红黑树节点小于6时,又会转换为链表
    Hashmap最多存储键值对 1<<30(230),应该为int值最大值(231-1)之下,又是2的幂次数.

    • put 过程

      数组初始化,在第一个元素插入 HashMap 的时候做一次数组的初始化,先确定初始的数组大小, 并计算数组扩容的阈值。
      计算具体数组位置,使用key进行hash值计算,根据hash值计算应该放在哪个数组中。
      找到数组下标后,会先进行 key 判断是否重复,如果没有重复,就准备将新值放入到链表的表头 (在多线程操作中,这种操作会造成死循环和数据丢失,在jdk1.8已解决)。
      数组扩容,在插入新值的时候,如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经 有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍。(便于位与运算)
      扩容就是用一个新的大数组替换 原来的小数组,并将原来数组中的值迁移到新的数组中

    • get过程

    根据 key 计算 hash 值。
    根据hash值找到相应的数组下标。
    遍历该数组位置处的链表,直到找到相等的 key。

    JDK8 HashMap
    • JDK8做了很大的修改:最重要要的修改是加入了红黑树,所以它的组成就是数组+链表+红黑树

    • 为什么要加入红黑树呢?
      因为当我们链表长度非常长的时候我们要去查找对应的值,需要一个个去比较,时间复杂度和链表长度成正比.为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度。


      map.png
    • jdk7 中使用 Entry 来代表每个 HashMap 中的数据节点,jdk8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。 我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树 的。

    • put过程 和jdk7的put差不多
      和 Jdk7 不一样的地方就是,jdk7是先扩容后插入新值的,jdk8 先插值再扩容 先使用链表进行存放数据,当数量超过8个的时候,将链表转为红黑树

    • get 过程分析

    1. 计算 key 的 hash 值,根据 hash 值找到对应数组下标。
    2. 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步。
    3. 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步。
    4. 遍历链表,直到找到相等(==或equals)的 key
    • 1.7版本put会产生的问题:
      1 .就是形成链表的时候第一个元素next执行第二个元素,第二个next指向第三个元素,多线程中会出现第三个元素的next指向第一个元素 形成一个闭环 其他的元素想要加入进来就不行了.形成死循环 解决:Hashtable和Collections.synchronizedMap(hashMap)两种解决方案,但是这 两种方案都是对读写加锁,独占式。一个线程在读时其他线程必须等待,吞吐量较低,性能较为低下
      2.put可能会丢失数据,当来个线程同时过来put数据 因为是一起过来 同时存入到一个对象中,会造成后一个吧前一个覆盖掉的情况.
    1.8中更换了数据插入的顺序修复上述问题

    12.3 JDK7 ConcurrentHashMap

    • ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。 整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多 人都会将其描述为分段锁。简单的说,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的 map.png

      再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,每次操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的

    • 初始化
    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
     初始化方法
    
    • initialCapacity:整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。

    • concurrencyLevel:并发数(或者Segment 数,有很多叫法,重要的是如何理解)。默认是 16,也 就是说 ConcurrentHashMap 有 16 个 Segments,所以这个时候,多可以同时支持 16 个线程 并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他 值,但是一旦初始化以后,它是不可以扩容的。

    • loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使 用的

    • 用 new ConcurrentHashMap() 无参构造函数进行初始化的,那么初始化完成后:Segment 数组长度为 16,不可以扩容 Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元 素不会触发扩容,插入第二个会进行第一次扩容 这里初始化了 segment[0],其他位置还是 null

    • put过程
      根据 hash 值能找到相应的 Segment,之后就是 Segment 内部的 put 操作了。 Segment 内部是由 数组+链表 组成的,由于有独占锁的保护,所以 segment 内部的操作并不复 杂。保证多线程安全的,就是做了一件事,那就是获取该 segment 的独占锁。 Segment 数组不能扩容,rehash方法扩容是 segment 数组某个位置内部的数组 HashEntry[] 进 行扩容,扩容后,容量为原来的 2 倍。
      get过程
      计算 hash 值,找到 segment 数组中的具体位置 segment 数组中也是数组,再根据 hash 找到数组中具体值的位置 到这里是链表了,顺着链表进行查找即可
    JDK8 ConcurrentHashMap :
    • 在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思 路,而是利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

    • 12.6 使用场景
      ConcurrentHashMap通常只被看做并发效率更高的Map,用来替换其他线程安全的Map容器,比如 Hashtable和Collections.synchronizedMap。线程安全的容器,特别是Map,很多情况下一个业务中 涉及容器的操作有多个,即复合操作,而在并发执行时,线程安全的容器只能保证自身的数据不被破 坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确(不保证结果正确)。


      map.png
    ConcurrentHashMap总结:
    • HashMap是线程不安全的,ConcurrentHashMap是线程安全的,但是线程安全仅仅指的是对容 器操作的时候是线程安全的
    • ConcurrentHashMap的public V get(Object key)不涉及到锁,也就是说获得对象时没有使用锁 put、remove方法,在jdk7使用锁,但多线程中并不一定有锁争用,原因在于 ConcurrentHashMap将缓存的变量分到多个Segment,每个Segment上有一个锁,只要多个线程 访问的不是一个Segment就没有锁争用,就没有堵塞,各线程用各自的锁,
    • ConcurrentHashMap 缺省情况下生成16个Segment,也就是允许16个线程并发的更新而尽量没有锁争用。
    • 而在jdk8中 使用的CAS+Synchronized来保证线程安全,比加锁的性能更高 ConcurrentHashMap线程安全的,允许一边更新、一边遍历,也就是说在对象遍历的时候,也可 以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化
    Hashtable和ConcurrentHashMap的不同点:
    • Hashtable对get,put,remove都使用了同步操作,它的同步级别是正对Hashtable来进行同步的, 也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性 能和吞吐量造成影响,从而形成单点。
    • 而ConcurrentHashMap则不同,它只对put,remove操作 使用了同步操作,get操作并不影响。 Hashtable在遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操 作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的 话,就不用考虑这方面的问题

    相关文章

      网友评论

          本文标题:JUC框架&多线程

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