JUC-java并发编程的艺术

一、并发问题

  1. 上下文切换:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
    1. 多线程不一定快:多线程有线建和上下文切的开
    2. 减少上下文切换:
      1. 并发:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
      2. CAS算法
      3. 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
      4. 使用协程:在单线程里实现多任度,并在单线程里持多个任务间的切
  2. 死锁:线程A持有资源,线程B持有资源;他们都想申请对方的资源,这两个线程就会相互等待而进入死锁状态。(互相等待
    1. 代码:
      public class Test {
          public static void main(String[] args) {
              Object o1 = new Object();
              Object o2 = new Object();
              new Thread(() ->{
                  synchronized (o1) {
                      try {
                          System.out.println("get o1");
                          Thread.sleep(1000);
                      } catch (Exception ignored){}
                      synchronized (o2) {
                          System.out.println("get o2");
                      }
                  }
              }).start();
      
              new Thread(() ->{
                  synchronized (o2) {
                      try {
                          System.out.println("get o2");
                          Thread.sleep(1000);
                      } catch (Exception ignored){}
                      synchronized (o1) {
                          System.out.println("get o1");
                      }
                  }
              }).start();
          }
      }

      避免死锁的办法

      1. 避免一个 线 程同 时获 取多个锁
      2. 避免一个 线 程在 内同 占用多个 源,尽量保 每个 只占用一个
      3. 尝试 使用定 时锁 ,使用 lock.tryLock timeout )来替代使用内部 机制
      4. 于数据 库锁 ,加 和解 在一个数据 库连 接里,否 会出
  3. 资源限制
    1. 程序的执行速度受限于计算机硬件或者是软件资源。
    2. 问题:导致并发执行变为串行执行,开启并发线程速度可能会很慢。因为上下文切换占用了大量的时间。

二、java并发底层实现

Java 编译 后会 Java 节码 ,字 节码 器加 JVM 里, JVM 行字节码 ,最 需要 为汇编 指令在 CPU 行, Java 中所使用的并 机制依 JVM 实现和CPU 的指令。

2.1、volatile

  • volatile是轻量级的synchronized,保证多处理器开发的共享变量的可见性。
  • 可见性的意思是一个线程修改共享变量的时候,其它线程可以读到这个值。
  • volatile的成本比synchronized成本更低,不会引起线程的上下文切换和调度。
1、volatile的定义与实现原理
  • 如果一个字段被声明为volatile,那么java线程的内存模型确保所有线程看到这个变量的值是一致的。
  • volatile相关的CPU术语介绍,在下面。

    JUC-java并发编程的艺术_第1张图片

2、volatile如何保证可见性
  • 当一个线程修改了一个volatile变量的值,会将这个值刷新到主内存中,并且会使用某种机制通知其他线程该变量发生变化。其他线程需要读取这个变量时,则会去主内存中读取最新的值。
  • 具体机制:volatile共享变量进行写操作的时候多出lock前缀的汇编
    • lock前缀的指令在多核处理器引发的事件

      • 把当前的处理器缓存行的数据写到系统的内存

      • 这个写回内存的操作会使其它CPU里面缓存的该地址的数据无效。

    • 即使写回了内存,其它的处理器缓存的数据其实还是旧的。

      • 每个处理器通过嗅探总线传播的数据,检查自己的缓存是不是过期了。

      • 如果发现过期了那么就把缓存行设置为无效的状态。并且重新去内存读取数据。

  • 小结:volatile的底层原理其实就是通过lock信号和MESI协议通知所有的处理器缓存失效,并且把数据更新到了内存。

2.2、Synchronized

  • Java SE1.6对synchronized进行优化,引入偏向锁和轻量级锁,以及锁存储结构和升级过程。
  • synchronized实现同步的基础
    • 普通方法锁的是当前实例对象
    • 静态同步方法锁的是当前类Class对象
    • 同步方法块,锁住的是synchronized括号里面的对象
  • 线程访问同步代码的时候必须要得到锁,退出或抛出异常须释
  • 到底存在哪里呢? 里面会存 什么信息呢?
    • 同步方法块使用的是monitorenter和monitorexit,monitorenter指令指向同步代码块开始的位置,monitorexit指向同步代码块结束的位置;线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
    • 同步方法:使用ACC_SYNCHRONIZED标识,指明该方法是一个同步方法
    • 对一个 象的 监视 器( monitor 取,而 程是排他的,也就是同一 时刻只能有一个线 取到由 synchronized 所保 护对 象的 监视
    • monitor对象存在每个java对象的对象头中

2.2.1对象头

  • synchronized 用的 存在 Java 里。如果 象是数 组类 型, 拟机用12个字节 储对 ,如果 象是非数 组类 型, 8个字节 储对象头

    JUC-java并发编程的艺术_第2张图片

  • Java里的Mark Word里默储对象的HashCode、分代年龄和锁标记位

    JUC-java并发编程的艺术_第3张图片

  • ​​​在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

    JUC-java并发编程的艺术_第4张图片

2.2.2锁升级

锁状态:无 、偏向 级锁状态 和重量 级锁 几个状 会随着 争情况逐 可以升 但不能降 级, 目的是 为了提高获 的效率
  • 偏向锁
    • 若不存在多线程竞争,经常由某个线程获取。为了让次线程获取锁的代价更低,引入了偏向锁

    • 线程访问获取锁的时候,会在对象头和栈帧的锁记录存储锁偏向的线程ID,那么该线程获取锁的时候就不需要去CAS来加锁和解锁。只需要检测Mark Word里面的线程ID是否指向当前线程

    • 偏向锁的撤销

      • 偏向锁是等待要竞争的时候才会释放锁的机制。

      • 如果有其他线程竞争锁,那么首先是暂停持有锁的线程,并且检查是不是存活,如果是拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程。要么就是恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。

      • 如果持有的线程结束,那么就标记为无锁的状态。

      • 另一个线程如果CAS失败那么就会暂停持有的线程,持有的线程会解锁,并且把锁的线程ID设置为空。然后恢复线程。

  • 轻量级锁
    • 轻量级加锁

      • 线程执行同步块之前,JVM会在线程里面创建锁记录的空间,并且把对象头的Mark Word复制到锁记录中。

      • 然后通过CAS来把Mark Word替换为指向锁记录的指针。

      • 如果成功那么就获取到轻量级的锁。

      • 如果失败说明有其他线程竞争,那么线程就尝试自旋获取锁。

    • 轻量级解锁

      • 通过CAS把Mark Word恢复到对象头。

      • 如果成功,那么说明没有竞争,失败说明有竞争。这个时候释放锁,并且唤醒等待的线程。因为已经锁膨胀所以Mark Word指向的是重量级锁的

      • 如果失败那么就会膨胀为重量级的锁。这个时候竞争的线程2会把锁改为10也就是重量级锁。

  • 锁的优缺点

    JUC-java并发编程的艺术_第5张图片

2.3、java中实现原子操作

  • 使用循环CAS实现原子操作:实际上就是检查和赋值合成一个原子操作。通过CMPXCHG指令完成自旋。

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

      • ABA问题

      • 循环时间长开销大。如果CAS不成功,就会一直自旋。可

      • 只能保证一个共享变量的原子操作,对于多个变量它是无法处理的。

  • 使用锁机制实现原子操作:保证了只有获取锁的线程才能够操作锁定的内存区域。

三、java内存模型

3.1、JMM基础

3.2、重排序

3.2.1、数据依赖性

  • 如果两个操作对一个共享变量操作,而且有一个是写操作,那么两个操作就是数据依赖的。
  • 编译器和处理器都是遵循数据依赖性的(单个处理器中)

3.2.2、as-if-serial

  • as-if-serial语义就是不管怎么重排序,执行结果都是不会变的。
  • 所以编译器和处理器不会对数据依赖的指令进行重排序

3.2.3、程序顺序规则

  • 只要指令之间有可见性的关系,那么就不能够重排序。如果前一个操作不需要对后面的操作可见,那么就可以重排序

3.2.4、重排序对多线程的影响

  • 重排序可能会把多线程执行的结果修改。

3.3、顺序一致性

3.4、volatile的内存语义

插入内存屏障保证禁止指令重排

3.5、锁的内存语义

3.5.1、 锁的释放-获取建立的happens-before关系

3.5.2、锁的释放和获取的内存语义

  • 线程释放锁的时候,JMM会把该线程对应的本地内存共享变量刷新到主内存

  • 线程获取锁的时候,JMM会把线程的本地内存设置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

  • 可以看到锁释放的内存语义和volatile写的是一样的。获取锁语义和volatile读是一样的

    • 线程A释放锁,实质是线程A对某个要获取锁的线程发出要对共享变量修改的消息

    • 线程B获取锁,实质是线程B接收了某个线程发出的修改消息

    • 线程A释放锁和线程B获取锁,实质上是线程A通过主内存向线程B发送消息

3.6、final域的内存语义

3.7、happens-before

3.7.1、happens-before的定义

  • 如果线程A的写操作a和线程B的读操作b存在happens-before,那么JMM可以保证a一定是对于b是可见的。

  • JSR-133的定义

    • 如果一个操作happens-before另一个操作,第一个操作对第二个操作的可见,并且第一个操作在第二个操作之前

    • 两个操作存在happens-before关系,并不意味着Java平台具体实现要按照这个顺序执行。如果重排序之后结果一致,那么这种重排序是可以的。

  • JMM的承诺

    • JMM对程序员承诺A happens-before B,也就是A的结果对B是可见的。A的执行顺序在B之前。

    • JMM对编译器和处理器的承诺,只要不改变结果就可以优化。

  • as-if-serial语义保证单线程内执行结果不被改变,happens-before关系保证同步的多线程执行结果不会改变。

  • as-if-serial语义给程序员创建了幻境,单线程程序按照程序顺序执行,happens-before的幻境就是正确的同步多线程是按照happens-before指定的顺序执行的。

3.7.2、happens-before规则

  1. 程序顺序规则
  2. 监视器锁规则
  3. volatile变量规则
  4. 传递性
  5. start()规则
  6. join()规则

3.8、DCL和延迟初始化

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getUniqueInstance(){
        //判断对象是否实例过
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • uniqueInstance = new Singleton();分三步

  1. 为uniqueInstance分配内存空间

  2. 初始化uniqueInstance

  3. 将uniqueInstance指向分配好的内存地址;

JVM具有指令重排,指向顺序可能变成1->3->2。如,线程T1执行了1和3,T2调用方法发现uniqueInstance不为null直接返回,然而uniqueInstance还没被初始化。volatile可以禁止指令重排

3.9、JMM综述

四、多线程

4.1、线程简介

4.1.1、什么是线程
现代操作系 在运行一个程序 ,会 建一个 进程。现 代操作系 统调 度的最小 元是 线 程,也叫 级进 程(Light Weight Process ),在一个 程里可以 建多个 线 程, 线 程都 有各自的 数器、堆 栈和局部 量等属性,并且能 够访问 共享的内存 量。 理器在 线 程上高速切 使用者感觉到 线 程在同 时执 行(并发)。
4.1.2、为什么使用多线程
  • 更多的处理器核心,以及超线程技的广泛运用
  • 更快的响应时间
4.1.3、线程优先级
  • 代操作系基本采用分的形式度运行的线程,操作系会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会线度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用理器源的多少,而线就是决定线程需要多或者少分配一些理器源的线程属性。
  • Java中,可以使用Thread类的setPriority()方法来设置线程的优先级。线程的优先级介于1到10,其中10是最高优先级,1是最低优先级。默认情况下,线程的优先级为5。
  • 注:线程优先级是提示给操作系统的,操作系统可能会根据自己的调度策略来决定线程的实际执行顺序。因此,即使设置了线程的优先级,也不能保证线程一定会按照优先级顺序执行。
4.1.4、线程状态
  • NEW:初始转态,线程被构建,但还没有调用start()方法
  • RUNNABLE:运行状态
  • BLOCKED:阻塞状态,表示线程阻塞于锁
  • WAITING:等待状态,进入该状态表示当前线程需要等待其他线程通知或中断
  • TIME_WAITING:超时等待,在指定的时间自行返回
  • TERMINATED:终止状态,表示当前线程已经执行完毕

    JUC-java并发编程的艺术_第6张图片

  • 线建之后,start()方法开始运行。当线wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状,而超等待状相当于在等待状的基上增加了超限制,也就是超时时间到达会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞线程在Runnablerun()方法之后将会入到止状
4.1.5、Daemon线程

Daemon线程是一种支持型线程(守护线程),主要被用作程序中后台度以及支持性工作。这 意味着,当一个Java机中所有用户线程结束时Java拟机将会退出,所有Daemon线程都需要立即终止。可以通过调用Thread.setDaemon(true)线Daemon线程。

  • Daemon 属性需要在启 动线 程之前 置,不能在启 动线 程之后 置。
  • 在Java虚拟机退出时Daemon线程中的finally块并不一定会执行

4.2、启动和终止线程

4.2.1、构造线程
  • 在运行线程之前首先要构造一个线象,线象在构造的候需要提供线程所需要的属性,如线程所属的线线、是否是Daemon线程等信息。

    • Thread中对线程进行初始化
        private Thread(ThreadGroup g, Runnable target, String name,
                       long stackSize, AccessControlContext acc,
                       boolean inheritThreadLocals) {
            if (name == null) {
                throw new NullPointerException("name cannot be null");
            }
            //当前线程就是该线程的父线程
            Thread parent = currentThread();
            this.group = g;
             将daemon、priority属性设置为父线程的对应属性
            this.daemon = parent.isDaemon();
            this.priority = parent.getPriority();
            this.target = target;
            setPriority(priority);
            // 将父线程的InheritableThreadLocal复制过来
            if (inheritThreadLocals && parent.inheritableThreadLocals != null)
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
            /* Set thread ID */
            this.tid = nextThreadID();
        }
    在上述 程中,一个新构造的 线 象是由其 parent 线 程来 行空 分配的,而 child 线程继 承了 parent 是否 Daemon 和加 载资 源的 contextClassLoader 以及可 继承的 ThreadLocal ,同 时还 会分配一个唯一的 ID 标识这 child 线 程。至此,一个能 运行的 线 程对 象就初始化好了,在堆内存中等待着运行
4.2.2、启动线程
线 在初始化完成之后, start() 方法就可以启 动该 线 程。s tart() 方法的含义是:当前 线 程(即 parent 线 程)同步告知 Java 机,只要 线 划器空 立即启 动调用start() 方法的 线 程。
4.2.3、中断
  • 中断可以理解 为线 程的一个 标识 位属性,它表示一个运行中的 线 程是否被其他 线 进行了中断操作。中断好比其他 线 对该线 程打了个招呼,其他 线 程通 过调 该线 程的interrupt()方法 行中断操作。
4.2.4、过期的suspend()、resume()、和stop()
  • suspend() resume() stop() 方法完成了 线 程的 停、恢复和终止工作
  • 不建 使用的原因主要有: suspend() 方法 用后, 线 程不会 放已 占有的资源(比如 ),而是占有着 入睡眠状 这样 容易引 锁问题 。同 stop() 方法在终结一个 线 不会保 证线 程的 源正常 放,通常是没有 线 程完成 释放工作的机会,因此会 致程序可能工作在不确定状

4.3、线程间通信

4.3.1、volatile和synchronized关键字
  • volatile可以用来修字段(成员变量),告知程序任何对该变量的访问均需要从共享内存中取,而它的改同步刷新回共享内存,它能保所有线对变量访问的可见性。过多地使用volatile是不必要的,因为它会降低程序执行的效率。
  • synchronized 可以修 方法或者以同步 的形式来 行使用,确保多个 线程在同一个 刻,只能有一个 线 于方法或者同步 中,保 线 对变 访问 的可 见性和排他性。
    • 象、 象的 监视 器、同步 列和 线 程之 的关系
    • 任意 线 Object synchronized )的 访问 ,首先要 获得Object 监视 器。如果 取失 线 入同步 列, 线 程状 态变为 BLOCKED 。当 访问Object的前 得了 线 程) 放了 则该释 放操作 醒阻塞在同步 列中的 线程,使其重新尝试对监视 器的获取。

      JUC-java并发编程的艺术_第7张图片

4.3.2、等待/通知机制
一个 线 程修改了一个 象的 ,而另一个 线 程感知到了 化,然后 行相 应的操作,整个过 程开始于一个 线 程,而最 终执 行又是另一个 线 程。前者是生 者,后者就是消 者, 这种模式隔离了 做什么 what )和 怎么做 How ),在功能 面上 实现 了解耦,体系 构上具 备了良好的伸 性。
  • 等待 / 通知的相关方法是任意 Java 象都具 的,因 为这 些方法被定 在所有 对象的父类java.lang.Object
  • 等待 / 通知机制,是指一个 线 A 用了 O wait() 方法 入等待状 ,而另一个 线 程B调 用了 O notify() 或者 notifyAll() 方法, 线 A 收到通知后从 O wait() 方法返回, 进而执 行后 操作。上述两个 线 程通 过对 O 来完成交互,而 象上的 wait() notify/notifyAll()的关系就如同开关信号一 ,用来完成等待方和通知方之间的交互工作。

    JUC-java并发编程的艺术_第8张图片

  1. 使用 wait() notify() notifyAll() 需要先 对调 象加 锁。
  2. wait() 方法后, 线 程状 RUNNING WAITING ,并将当前 线 程放置到 对象的等待 队列。
  3. notify() notifyAll() 方法 用后,等待 线 程依旧不会从 wait() 返回,需要 notify()或 notifAll() 线 之后,等待 线 程才有机会从 wait() 返回。
  4. notify() 方法将等待 列中的一个等待 线 程从等待 列中移到同步 列中,而notifyAll()方法 是将等待 列中所有的 线 程全部移到同步 列,被移 线 程状 WAITING变为 BLOCKED。
  5. wait() 方法返回的前提是 得了 象的 锁。
  6. 从上述 细节 中可以看到,等待 / 通知机制依托于同步机制,其目的就是确保等待 线程从wait() 方法返回 感知到通知 线 对变 量做出的修改。
过程:WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行

JUC-java并发编程的艺术_第9张图片

4.3.3、等待/通知的今典范式
  • 等待方
    • 象的
    • 如果条件不 足,那么 象的 wait() 方法,被通知后仍要 检查条件
    • 条件 则执 对应 逻辑
      synchronized(对象) {
          while(条件不满足) {
              对象.wait();
          }
          对应的处理逻辑
      }
  • 通知方
    • 象的
    • 条件
    •  通知所有等待在对象上的线程

      synchronized(对象) {
          改变条件
          对象.notifyAll();
      }
4.3.4、Thread.join()
  • 如果一个线A行了thread.join()句,其含是:当前线A等待thread线终止之后才从thread.join()返回。线Thread除了提供join()方法之外,提供了join(long millis)和join(long millis,int nanos)两个具特性的方法。两个超方法表示,如果线thread定的超时时间里没有止,那么将会从方法中返回
  • JDK Thread.join() 方法的源
    public final synchronized void join(final long millis) throws InterruptedException {
        while (isAlive()) {
            wait(0);
        }
    }
    线 ,会 线 程自身的 notifyAll() 方法,会通知所有等待在 该线 象上的 线
    程。可以看到 join() 方法的 逻辑结 构与 等待 / 通知 典范式一致
4.3.5、ThreadLocal
  • ThreadLocal,即线量,是一个以ThreadLocal为键、任意为值的存储结构。个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个程上的一个可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

五、java中的锁

5.1、Lock接口

  • 是用来控制多个 线 访问 共享 源的方式,一般来 ,一个 防止多个 线 程同时访问 共享 源(但是有些 可以允 多个 线 程并 访问 共享 源,比如 )。在 Lock接口出 之前, Java 程序是靠 synchronized 实现锁 功能的,而 Java SE 5 之后,并 发包中新增了L ock 接口(以及相关 实现类 )用来 实现锁 功能,它提供了与 synchronized 似的同步功
    能,只是在使用 需要 式地 取和 然它缺少了(通 synchronized 块或者方法所提 供的) 的便捷性,但是却 有了 锁获 取与 放的可操作性、可中断的 锁以及超 时获 等多种 synchronized 字所不具 的同步特性
  •  使用

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
    
    } finally {
        //在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放
        lock.unlock();
    }
  • Lock接口提供的synchronized关键字所不具备的特性

    JUC-java并发编程的艺术_第10张图片

  • Lock是一个接口,它定锁获取和释放的基本操作

    JUC-java并发编程的艺术_第11张图片

5.2、队列同步器-ASQ

  • 队列同步器AbstractQueuedSynchronizer用来构建锁,或者其它同步组件。用一个int成员变量表示同步状态。通过内置的FIFO队列完成资源获取线程的排队工作。

  • 同步器的实现主要是继承,同步器需要提供(getState()、setState(int newState)和compareAndSetState(int expect,int update))方法来获取同步的状态。

  • 同步器支持独占或者是共享地获取锁。

  • 同步器是 实现锁 (同步 件)的关 ,在 实现中聚合同步器,利用同步器 实现锁 语义 。可以 这样 理解二者之 的关系: 是面向使用者的,它定 了使用者与 锁交互的接口(比如可以允 两个 线 程并行 访问 ), 藏了 实现细节 ;同步器面向的是 实现者, 它 化了 实现 方式,屏蔽了同步状 管理、 线 程的排 、等待与 醒等底 操作。 锁和同步器很好地隔离了使用者和 实现 者所需关注的 域。
5.2.1、AQS接口
  • 同步器的设计是基于模板方法模式的,也就是,使用者需要继承同步器并重写指定的方法,随后将同步器合在自定同步件的实现中,并用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

    JUC-java并发编程的艺术_第12张图片

  • 实现 自定 同步 ,将会调用同步器提供的模板方法

    JUC-java并发编程的艺术_第13张图片

    同步器提供的模板方法基本上分 3 :独占式 取与 放同步状 、共享式 取与
    同步状 查询 同步 列中的等待 线程情况

5.2.2、AQS的实现

  1. 同步队列:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态
  2. 独占式同步状态获取与释放

  3. 共享式同步状态获取与释放:共享式取与独占式取最主要的区在于同一刻能否有多个线程同时获取到同步状 态

  4. 独占式超时获取同步状态:过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内取同步状,如果取到同步状态则返回true,否,返回false

5.3、重入锁

重入 ReentrantLock 名思 ,就是支持重 入的 ,它表示 该锁 支持一个 线 程对资 源的重复加 。除此之外, 该锁 支持 锁时 的公平和非公平性 选择
  1. 实现重进入:是指任意线程在取到之后能再次该锁而不会被所阻塞
    1. 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再
      次成功 取。
    2. 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到
      该锁 的最 终释 放要求 锁对 数自增, 数表示当前 被重复 取的次数,而锁被 数自减,当 数等于 0 表示 成功 放。
  2. 公平与非公平获取锁
    1. 公平锁:如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
    2. 非公平锁:

5.4、读写锁

  • 在同一刻可以允多个读线访问,但是在写线访问时,所有的读线程和其他写线程均被阻塞。锁维护了一对锁,一个读锁和一个写,通分离读锁和写锁,使得并性相比一般的排他有了很大提升;
  • 一般情况下, 的性能都会比排它 好,因 大多数 是多于写的。在 读多于写的情况下, 提供比排它 更好的并 性和吞吐量。 Java 包提供 锁的实现是 ReentrantReadWriteLock
  1. 写锁的获取与释放:是一个支持重入的排它。如果当前线程已经获取了写增加写状态。如果当前线程在取写锁时读锁取(0)或者该线程不是已经获取写线程,则当前线入等待状态。
  2. 读锁的获取与释放:读锁是一个支持重入的共享,它能被多个线程同时获取,在没有其他写线程访问(或者写状态为0读锁总会被成功地取,而所做的也只是(线程安全的)增加态。如果当前线程已经获取了读锁增加。如果当前线程在读锁时,写已被其他线程获取,则进入等待状
  3. 锁降级:指的是写为读锁。锁是指把持住(当前有的)写,再获取到读锁,随后放(先前有的)写

5.5、LockSupport

当需要阻塞或 醒一个 线 程的 候,可使用 LockSupport 工具 来完成相应工作。 LockSupport 了一 的公共静 方法, 些方法提供了最基本的 线 程阻塞和 唤醒功能,而 LockSupport 也成 构建同步 件的基 础工具。LockSupport 了一 park 的方法用来阻塞当前 线 程,以及unpark(Thread thread) 方法来 醒一个被阻塞的 线 程。

5.6、Condition

任意一个 Java 象,都 有一 组监视 器方法(定 java.lang.Object 上),主要包括 wait()、wait(long timeout) notify() 以及 notifyAll() 方法, 些方法与 synchronized 同步关 键字配合,可以 实现 等待 / 通知模式。 Condition 接口也提供了 Object 监视 器方法,与 Lock 配合可以 实现等待/通知模式

JUC-java并发编程的艺术_第14张图片

5.6.1Condition接口
Condition 了等待 / 通知两种 型的方法,当前 线 些方法 ,需要提前 获取到Condition 象关 Condition 象是由 Lock 象( Lock 象的 newCondition() 方法)创建出来的, 话说 Condition 是依 Lock 象的。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

5.6.2、Condition的实现

ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部 ,因 Condition的操作需要获 取相关 ,所以作 同步器的内部 较为 合理。每个 Condition 象都包含着一个队列(以下称 等待 列), 该队 列是 Condition 实现 等待 / 通知功能的关 键。 下面将分析 Condition 实现 ,主要包括:等待 列、等待和通知
  1. 等待队列:等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
  2. 等待:调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中
  3. 通知:调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

    JUC-java并发编程的艺术_第15张图片

六、java并发容器和框架

6.1、ConcurrentHashMap的实现原理与使用

ConcurrentHashMap线程安全且高效的HashMap

6.1.1、为什么使用ConcurrentHashMap

  • HashMap不安全
  • HashTable效率低(synchronized)
  • CurrentHashMap采用锁分段技术可有效提升并发访问

6.1.2、ConcurrentHashMap的结构

6.1.3、ConcurrentHashMap初始化

6.1.4、定位Segment

6.1.5、ConcurrentHashMap的操作

6.2、ConcurrentLinkedQueue

  • 在并发编程中,有候需要使用线程安全的列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的列可以用一个锁(入和出用同一把)或两个(入和出用不同的)等方式来实现。非阻塞的实现方式可以使用循CAS的方式来实现
  • ConcurrentLinkedQueue是一个基于点的无界线程安全列,它采用先先出的则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元,它会返回部的元素。

6.3、阻塞队列

  • 满时列会阻塞插入元素的线程,直到队列不满;在队取元素的线程会等待变为非空。
  • 阻塞 列常用于生 者和消 者的 景,生 者是向 列里添加元素的 线 程,消 费者是从 列里取元素的 线 程。阻塞 列就是生 者用来存放元素、消 者用来获取元素的容器

    JUC-java并发编程的艺术_第16张图片

  • ​​​​​​​​​​​​​​​​​​​ArrayBlockingQueue:一个由数组结成的有界阻塞
  • LinkedBlockingQueue:一个由成的有界阻塞
  • PriorityBlockingQueue:一个支持排序的无界阻塞
  • DelayQueue:一个使用级队实现的无界阻塞
  • SynchronousQueue:一个不存元素的阻塞队列,每一个put操作必须等待一个take操作,
    不能 继续 添加元素
  • LinkedTransferQueue:一个由成的无界阻塞
  • LinkedBlockingDeque:一个由成的双向阻塞​​​​​​​

6.3.1、阻塞队列实现原理:

  • 使用通知模式 实现 。所 通知模式,就是当生 者往 列里添加元素 会阻塞住生
    者,当消 者消 了一个 列中的元素后,会通知生 者当前 列可用。
    • ​​​​​​​​​​​​​​ArrayBlockingQueue使用了Condition来实现,当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现

6.4、Fork/Join框架​​​​​​​

把大任 务分割成若干个小任 ,最 终汇总 每个小任 务结 果后得到大任 务结 果的框架

七、java中的原子类

原子更新基本 型、原子更新数 组、原子更新引用和原子更新属性,Atomic 包里的 基本都是使用 Unsafe 实现 的包装 类。采用CAS
  • 原子更新基本类型:AtomicBoolean、AtomicInteger、AtomicLong
    • ​​​​​​​​​​​​​Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现
  • 原子更新数组:AtomicIntegerArrayAtomicLongArray、AtomicReferenceArray
  • 原子更新引用:AtomicReference
  • 原子更新属性

八、java中的并发工具

8.1、CountDownLatch

一个或多个 线 程等待其他 线 程完成操作
  • 使用:CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,里就N。 当我们调CountDownLatchcountDown方法N就会减1CountDownLatchawait方法 会阻塞当前线程,直到N成零。由于countDown方法可以用在任何地方,所以N个 点,可以是N线程,也可以是1线程里的N行步。用在多个线,只需要把这个 CountDownLatch的引用传递线程里即可
  • 如果有线理得比慢,我不可能线程一直等待,所以可以使用另外一个带指定时间的await方法——await(long time,TimeUnit unit),这个方法等待特定时后,就会不再阻塞当前线
  • 数器必 大于等于 0 ,只是等于 0 候, 数器就是零, await 方法 时不会阻塞当前 线 程。 CountDownLatch 不可能重新初始化或者修改 CountDownLatch 象的内部 计数 器的 。一个 线 countDown 方法 happen-before 另外一个 线 await 方法。

8.2、CyclicBarrier

让一组线 程到达一个屏障(同步点) 被阻塞,直到最后一个 线 程到达屏障 时,屏障才会开 ,所有被屏障 截的 线 程才会 继续 运行。
  • 使用:CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
  • 场景:可以用于多线算数据,最后合并果的
  • CyclicBarrier和CountDownLatch的区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景

8.3、Semaphore

是用来控制同 时访问 特定 源的 线 程数量,它通 过协调 各个 线程,以保 合理的使用公共
应用场景:流量控制

8.4、Exchanger

是一个用于 线 间协 作的工具 Exchanger 用于 线 间的数据交换 。它提供一个同步点,在 个同步点,两个 线 程可以交 彼此的数据。 两个 线 程通过exchange 方法交 数据,如果第一个 线 程先 exchange() 方法,它会一直等待第二个 线程也执 exchange 方法,当两个 线 程都到达同步点 两个 线 程就可以交 数据,将本 线 程生产出来的数据 传递给对
  • 应用场景:用于遗传算法

九、java中的线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

9.1、线程池的实现原理

ThreadPoolExecutor执行:

  1. 如果当前运行的 线 程少于 corePoolSize 则创 建新 线 程来 行任 (注意, 一步
    需要 取全局 锁)。
  2. 如果运行的 线 程等于或多于 corePoolSize 将任 加入 BlockingQueue。
  3. 如果无法将任 加入 BlockingQueue 列已 ), 则创 建新的 线 程来 理任 务(注意,执行 一步 需要 取全局 锁)。
  4. 如果 建新 线 程将使当前运行的 线 程超出 maximumPoolSize ,任 将被拒 ,并
    RejectedExecutionHandler.rejectedExecution()方法

    JUC-java并发编程的艺术_第17张图片

ThreadPoolExecutor 采取上述步 设计 思路,是 了在 execute() 方法 时,尽可能地避免 取全局 (那将会是一个 重的可伸 )。在 ThreadPoolExecutor 完成 预热之后(当前运行的 线 程数大于等于 corePoolSize ),几乎所有的 execute() 方法 用都是 行步 2,而步 2 不需要 取全局

9.2、线程池的使用

ThreadPoolExecutor创建线程池

参数

  • corePoolSize (核心线程数 ):当提交一个任 线 程池 线 程池会 建一个线程来 行任 ,即使其他空 的基本 线 程能 够执 行新任 也会 线 程,等到需要 执行的任 务 数大于 线 程池基本大小 就不再 建。如果 用了 线 程池的 prestartAllCoreThreads()方法,线 程池会提前 建并启 所有核心 线 程。
  • BlockingQueue(阻塞 列):用于保存等待 行的任 的阻塞 列。可以 选择以下几个阻塞 队列。
    • ArrayBlockingQueue :是一个基于数 组结 构的有界阻塞 列,此 列按 FIFO (先 进先出)原则对 元素 进行排序。
    • LinkedBlockingQueue :一个基于 构的阻塞 列,此 列按 FIFO排序元素,吞吐量通常要高于 ArrayBlockingQueue 。静 工厂方法 Executors.newFixedThreadPool() 使用了 队列。
    • SynchronousQueue :一个不存 元素的阻塞 列。每个插入操作必 等到另一个 线 调用移除操作,否 插入操作一直 于阻塞状 ,吞吐量通常要高于 Linked-BlockingQueue ,静 态工厂方法 Executors.newCachedThreadPool 使用了 队列。
    • PriorityBlockingQueue :一个具有 的无限阻塞 列。
  • maximumPoolSize (最大 线 程数): 线 程池允 许创 建的最大 线 程数。如果 满了,并且已 建的 线 程数小于最大 线 程数, 则线 程池会再 建新的 线 行任 值得注意的是,如果使用了无界的任 务队 个参数就没什么效果。
  • ThreadFactory:用于 线 程的工厂,可以通 过线 程工厂 每个 建出来的 线 程设置更有意 的名字。
  • RejectedExecutionHandler (拒绝 策略):当 列和 线 程池都 了, 线 程池 饱和状 态 ,那么必 采取一种策略 理提交的新任务。
    • ​​​​​​​​​​​​​​​​​​​​AbortPolicy:直接抛出异常(默认)

      CallerRunsPolicy:让调用者所在线程来运行任务

      DiscardOldestPolicy列里最近的一个任

      DiscardPolicy:不理,弃掉

  • keepAliveTime(线程存活时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  • TimeUnit 线 程活 保持 时间 位)​​​​​​​

提交任务

  • execute():用于提交不需要返回的任,所以无法判断任是否被线程池行成功
  • subimit():用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线
    程一段 时间 后立即返回, 这时 候有可能任 没有 行完

关闭线程池:

  • 过调线程池的shutdownshutdownNow方法来关闭线程池。它的原理是遍历线程池中的工作线程,然后逐个线程的interrupt方法来中断线程,所以无法响中断的任务可能永无法止。但是它存在一定的区shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在行或停任线程,并返回等待行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN,然后中断所有没有正在行任的线程
  • 只要用了两个关方法中的任意一个,isShutdown方法就会返回true。当所有的任都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任特性决定,通常shutdown方法来关线程池,如果任务不一定要执行完,则可以调用shutdownNow方法

合理配置线程数

  • 的性CPU密集型任IO密集型任和混合型任
  • :高、中和低
  • 时间、中和短
  • 的依性:是否依其他系统资源,如数据库连
  • 不同的任 可以用不同 模的 线 程池分开 理。 CPU 密集型任 务应配置尽可能小的线 程,如配置 N cpu +1 线 程的 线 程池。由于 IO 密集型任 务线 程并不是一直在 行任 则应配置尽可能多的 线 程,如 2*N cpu 。混合型的任
  • 数据库连接池的任,因为线程提交SQL后需要等待数据返回果,等待的时间越 长CPU闲时间就越,那么线程数应该设置得越大,这样才能更好地利用CPU
    ​​​​​

线程池监控

如果在系 中大量使用 线 程池, 有必要 对线 程池 控,方便在出 现问题时,可以根据 线 程池的使用状况快速定位 问题
  • t askCount 线 程池需要 行的任 数量。
  • completedTaskCount 线 程池在运行 程中已完成的任 数量,小于或等于 taskCount
  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如等于线程池的最大大小,表示线程池曾经满过
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销​​​​​​​,所以个大小只增不减。
  • getActiveCount:获取活动的线程数

十、Executor框架

Java 中,使用 线 程来异步 行任 Java 线 程的 建与 需要一定的开 ,如果我们为 每一个任 务创 建一个新 线 程来 行, 线 程的 建与 将消耗大量的 源。同 时,为 每一个任 务创 建一个新 线 程来 行, 种策略可能会使 于高 荷状 用最 溃。 Java 线 程既是工作 元,也是 行机制。从 JDK 5 开始,把工作 元与 执行机制分离开来。工作 元包括 Runnable Callable ,而 行机制由 Executor 框架提供。

10.1、Executor

HotSpot VM 线 程模型中, Java 线程(java.lang.Thread )被一 一映射 本地操作系统线 程。 Java 线 程启 动时 建一个本地操作系 统线 程;当 Java 线 个操作系 统线程 也会被回收。操作系 度所有 线 程并将它 分配 可用的 CPU。在上 Java 线 程程序通常把 用分解 若干个任 ,然后使用用 户级 调度器( Executor 框架)将 些任 映射 固定数量的 线 程;在底 ,操作系 内核将 线程映射到硬件 理器上。
结构
  • 。包括被 行任 需要 实现 的接口: Runnable 接口或 Callable接口。
  • 行。包括任 务执 行机制的核心接口 Executor ,以及 承自 Executor的 ExecutorService 接口。 Executor 框架有两个关 键类实现 ExecutorService接口 ( ThreadPoolExecutor ScheduledThreadPoolExecutor)。
  • 异步 算的 果。包括接口 Future 实现 Future 接口的 FutureTask
Executor 框架包含的主要的 与接口
  • Executor 是一个接口,它是 Executor 框架的基 ,它将任 的提交与任 执行分离开来。
  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
  • ScheduledThreadPoolExecutor是一个实现类,可以在定的延后运行命令,或者定期
    行命令。 ScheduledThreadPoolExecutor Timer 更灵活,功能更 强大。
  • Future 接口和 实现 Future 接口的 FutureTask ,代表异步 算的 结果。
  • Runnable 接口和 Callable 接口的 实现类 ,都可以被 ThreadPoolExecutor 或Scheduled和ThreadPoolExecutor 行。
Executor框架成员
  • ThreadPoolExecutor:ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor
    • SingleThreadExecutor:单个线程,corePoolSizemaximumPoolSize为1,用无界
    • FixedThreadPool:固定线程数,KeepAliveTime为0L,用无界
    • CachedThreadPool:大小无界的线程池,corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE
    • ScheduledThreadPoolExecutor:要用来在给定的延迟之后运行任务,或者定期执行任务。DelayQueue无界
  • Future接口
  • Runnable接口:无返回结果
  • ​​​​​​​Callable接口:有返回结果
  • Executors

10.2、FutureTask

Future 接口和 实现 Future 接口的 FutureTask ,代表异步 算的 果。

面试题​​​​​​​

你可能感兴趣的:(并发,java)