Java核心技术 卷1 Ch.14 70000字长篇入门Java并发

文章目录

  • 跳过的部分:
  • Ch.XIV 并发:
    • 14.1:并发基础知识:
      • 14.1.1 线程和进程简介:
      • 14.1.2 线程与进程的关系
      • 14.1.3 并发和并行
      • 14.1.4 并发中常见的问题:
        • 线程安全问题:
        • 上下文切换:
        • 线程死锁:
    • 14.2 实现多线程:
      • 14.2.1 在单独的一个线程中执行任务的简单操作:
      • 14.2.2 一个被嫌弃的实现多线程的方法:
    • 14.3 线程状态与常规操作:
      • 14.3.1 几个简单方法的介绍:
        • start()方法:
        • sleep()方法:
        • join()方法:
        • yield()方法:
      • 14.3.2 线程死锁:
      • 14.3.3 线程中断:
        • 标志中断:
        • interrupt()中断:
      • 14.3.4 synchronized关键字:
        • **监视器monitor 或 锁lock 的概念:**
        • **synchronized 关键字**
        • **synchronized使用方法:**
      • 14.3.5 wait()方法:
      • 14.3.6 notify()方法 & notifyAll()方法:
    • 14.4 线程属性:
      • 14.4.1 优先级和守护线程:
        • 线程优先级:
        • 守护线程:
        • 示例代码:
      • 14.4.2 未捕获异常处理器:
    • 14.5 线程同步:
      • 14.5.1 ReentrantLock对象:
        • APIの区别:
          • 各种方法简介:
        • 可重入性:
        • 等待可中断性:
        • 公平锁:
        • 绑定条件:
        • 其他:
        • ReentrantLock & synchronized的选择:
      • 14.5.2 volatile关键字:
        • **从Java内存模型上理解volatile:**
        • **volatile特点:**
        • **volatile使用:**
      • 14.5.3 原子性:
        • 基本类原子更新
        • 数组类原子更新:
        • 引用类原子更新:
        • 字段类类原子更新:
      • 14.5.4 ThreadLocal:
      • 14.5.5 ReentrantReadWriteLock:
        • ReentrantReadWriteLock的特点:
    • 14.6 阻塞队列:
      • 14.6.1 常用的阻塞队列:
      • 14.6.2 常用方法:
    • 14.7 线程安全的集合:
      • 上古の线程安全集合:
      • 优秀の线程安全集合:
    • 14.8 线程异步:
      • 14.8.1执行器Executors:
        • Executor简介:
        • Excutor接口:
        • Callable接口:
        • Future接口:
        • ExecutorService接口:
        • 线程池 & Executors类:
        • 伪异步の实例测试:
          • Future实例测试:
          • invokeAny实例测试:
          • invokeAll()测试:
        • CompletableFuture & 真异步:
      • 14.8.2 Fork-Join 框架:

跳过的部分:

  • 字段类原子更新
  • 阻塞队列的实现原理
  • 优秀的线程安全集合, 书中14.7.2 ~ 14.7.6
  • Fork-Join框架
  • CompletableFuture 更多的使用方法
  • 同步器

Ch.XIV 并发:

由于个人觉得书中讲解的有些小乱, 对于我这种鶸鸡极不友好, 所以按照个人的学习顺序稍微调整了下内容, 并扩充了部分知识

14.1:并发基础知识:

先了解一些基础知识, 便于后头并发的学习

14.1.1 线程和进程简介:

  1. 进程

    是程序的一次执行过程,是系统运行程序的基本单元(就比如打开某个应用,就是开启了一个进程),因此进程是动态的。系统运行一个程序即是一个程序从创建、运行到消亡的过程。

    在 Java 中,当我们启动 main 函数时其实就是启动了 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

  2. 线程

    线程是进程的一个执行路径,一个进程中至少有一个线程

    与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个进程,或是在各个进程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

14.1.2 线程与进程的关系

从 JVM 角度说进程和线程之间的关系

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第1张图片

图中可以看出线程之间的资源分配状态

其中:

  • 程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
  • :用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
  • :是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
  • 方法区 (jdk1.8之后为元空间):则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的
  1. 程序计数器为什么是私有的?

    首先明确程序计数器的作用:

    • **字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制。**如:顺序执行、选择、循环、异常处理。
    • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程运行到哪了。

    需要注意的是:如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

    所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置。

  2. 虚拟机栈和本地方法栈为什么是私有的?

    • 虚拟机栈:

      每个Java 方法在执行的同时会创建一个帧栈用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至完成的过程,就对应一个帧栈在 Java 虚拟机中入栈和出栈的过程。

    • 本地方法栈:

      和虚拟机的作用非常相似。区别是:虚拟机为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

    所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

  3. 堆和方法区

    **堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,**主要用来存放新创建的对象(所有的对象都在这里分配内存);方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。

14.1.3 并发和并行

  • 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行);
  • 并行:单位时间内,多个任务同时执行。

**并发的关键是你有处理多个任务的能力,不一定要同时。 **
而并行的关键是你有同时处理多个任务的能力。

14.1.4 并发中常见的问题:

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁等,还有受限于硬件和软件和资源闲置问题

线程安全问题:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第2张图片

多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题

具体是这样产生的:

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量

对应实际的CPU模型:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第3张图片

上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器

  1. 线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。
  2. 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
  3. 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核的个数,而一个 CPU 核在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。也就是:当任务执行完, CPU 时间片切换到另一个任务之前会先保存自己的状态,以便于再切换回这个任务时,可以加载这个任务的状态。任务从保持到再加载的过程就是一个上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

线程死锁:

两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

Java 运行多线程并发控制,当多个线程同时操作一个共享的资源变量时(如数据的增删改查),将会导致数据出现不正确的结果,相互之间产生冲突,因此加入锁保证了该变量的唯一性和准确性。

线程死锁的具体例子间下头, 在了解了其他相关概念后可以更好的理解代码, 现在先暂时理解概念

14.2 实现多线程:

无论如何, 先让多线程跑起来, 有点效果才有动力

14.2.1 在单独的一个线程中执行任务的简单操作:

  1. 实现Runnable接口:

    函数式接口, 非常简单

    其中的run函数会在单独创建的线程中运行, 所以将需要跑的代码放里头

    @FunctionalInterface
    public interface Runnable{
     	void run();   
    }
    
  2. 将上头的类实例化

  3. 创建Thread对象:

    其中Thread构造函数:

    Thread() 
    //分配一个新的 Thread对象。  
    Thread(Runnable target) 
    //分配一个新的 Thread对象。  
    Thread(Runnable target, String name) 
    //分配一个新的 Thread对象。  
    
  4. 使用start()启动线程

    不要直接调用对象的run ()方法, 这会在同一个线程中执行, 使用start()才是正确的打开方式

举个栗子:

public class RunableTest implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName()+" good time");
        }
    }
    public static void main(String[] args) {
        RunableTest runableTest1 = new RunableTest();
        RunableTest runableTest2 = new RunableTest();
        new Thread(runableTest1,"th1").start();
        new Thread(runableTest1,"th2").start();
        new Thread(runableTest2,"th3").start();
    }
}

14.2.2 一个被嫌弃的实现多线程的方法:

定义Thread的子类, 并覆盖run()方法, 同样可以实现多线程

只不过有如下劣势:

  • Java无多继承, 继承了Thread就无法继承其他超类, 所以此方法自由度很小

  • 需要将并行运行的任务与运行机制解耦合, 将二者分离

    否则任务一多, 就需要为每个任务单独编写子类, 代价太大

但是其优势是方便传参, 这个后头再讲

举个栗子:

public class ThreadRuning extends Thread{

    public ThreadRuning(String name){  
//重写构造,可以对线程添加名字
        super(name);
    }
    @Override
    public void run() {
        while(true){
            System.out.println("good time");
//在run方法里,this代表当前线程
            System.out.println(this);
        }
    }
    public static void main(String[] args){
        ThreadRuning threadRuning = new ThreadRuning("1111");
        threadRuning.start();
    }
}

其实还有个实现interface Callable的方法, 这会在下头学习

14.3 线程状态与常规操作:

public static enum Thread.State
extends Enum<Thread.State>

线程状态。线程可以处于以下状态之一:

  • NEW:

    状态是指线程刚创建, 尚未启动

  • RUNNABLE:

    状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等

  • BLOCKED:

    这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区

  • WAITING:

    这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束

  • TIMED_WAITING:

    这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态

  • TERMINATED:

    这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。

RUNNABLE状态说明:

可以看见RUNNABLE中有两个状态, RUNNING & READY, 当调用线程的start()方法后, 线程会进入READY状态, 等待系统内部资源的分配, 此状态的线程获得了 CPU 时间片 (timeslice) 后就会进入RUNNING运行状态

这个过程是系统自动调度的 ,暂时先不管它

14.3.1 几个简单方法的介绍:

以下介绍上头使线程状态转移的几个常用的方法, 其余的在后头逐一介绍:

start()方法:

public synchronized void start() {
        /**
         * 此方法并不会被主要方法线程or由虚拟机创建的系统组线程所调用.
         * 任何向此方法添加的新功能方法在未来都会被添加到虚拟机中.
         * 0状态值代表了NEW的状态.
         */
        if (threadStatus != 0) // 线程不能重复start
            throw new IllegalThreadStateException();
 
        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. 
         */
        group.add(this);
 
        boolean started = false;
        try {
            start0(); //本地方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
 
    private native void start0();

sleep()方法:

简单理解就是使当前线程进入超时等待TIMED_WAITING状态, 超时后自动进入RUNNABLE状态

/**
     * 此方法会引起当前执行线程sleep(临时停止执行)指定毫秒数.
     * 此方法的调用不会引起当前线程放弃任何监听器(monitor)的所有权(ownership).
     */
public static native void sleep(long millis) throws InterruptedException;
 
public static void sleep(long millis, int nanos)
throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
 
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }
 
    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }
 
    sleep(millis);
}

测试代码:

class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread mt = new MyThread("MyThread");

        //推荐
        MyRunnable mr = new MyRunnable();
        Thread t2 = new Thread(mr,"MyRunnable");

        mt.start();//启动线程
        t2.start();


        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 实现线程的第一种方式:继承thread类
 */
class MyThread extends Thread {
    public MyThread(){
        super();
    }
    public MyThread(String threadName){
        super(threadName);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (this.isInterrupted()) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "-" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
                this.interrupt();
            }

        }
    }
}

/**
 * 实现线程的第二种方式:实现Runnable接口
 */
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

main-0
MyRunnable-0
MyThread-0
MyThread-1
main-1
MyRunnable-1
MyThread-2
main-2
MyRunnable-2
MyRunnable-3
main-3
MyThread-3
MyRunnable-4
MyThread-4
main-4
MyThread-5
main-5
MyRunnable-5
MyRunnable-6
main-6
MyThread-6
main-7
MyThread-7
MyRunnable-7
MyThread-8
MyRunnable-8
main-8
MyRunnable-9
MyThread-9
main-9

Process finished with exit code 0

join()方法:

在线程B中调用线程A的join()方法,会使得线程B进入等待,直到线程A结束,或者到达给定的时间,那么期间线程B处于BLOCKED的状态,而不是线程A

/**
     * 最多等待参数millis(ms)时长当前线程就会死亡.参数为0时则要持续等待.
     * 此方法在实现上:循环调用以this.isAlive()方法为条件的wait()方法.
     * 当线程终止时notifyAll()方法会被调用.
     * 建议应用程序不要在线程实例上使用wait,notify,notifyAll方法.
     */
    public final synchronized void join(long millis)
            throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
 
        //如果等待时间<0,则抛出异常
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        //如果等待时间为0
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
 
    //等待时间单位为纳秒,其它解释都和上面方法一样
    public final synchronized void join(long millis, int nanos)
            throws InterruptedException {
 
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                    "nanosecond timeout value out of range");
        }
 
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
 
        join(millis);
    }
 
    //方法功能:等待一直到线程死亡.
    public final void join() throws InterruptedException {
        join(0);
    }

举个栗子:

class ThreadDemo2 {
    public static void main(String[] args) {

        MyRunable2 mr2 = new MyRunable2();
        Thread t = new Thread(mr2, "MyRunable2");
        t.start();

        MyRunable3 mr3 = new MyRunable3();
        Thread t2 = new Thread(mr3, "MyRunable3");
        t2.start();

        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName() + "--" + i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 5) {
                try {  			//这些打开用来测试join
                    t.join();	//让t线程执行完毕
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        mr3.flag = false;
    }
}

class MyRunable2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//标记中断
class MyRunable3 implements Runnable {
    public boolean flag;

    public MyRunable3() {
        flag = true;
    }

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println(Thread.currentThread().getName() + "=" + (i++));
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

MyRunable2-0
main–0
MyRunable3=0
main–1
MyRunable3=1
MyRunable2-1
main–2
MyRunable3=2
MyRunable2-2
main–3
MyRunable2-3
MyRunable3=3
main–4
MyRunable2-4
MyRunable3=4
MyRunable3=5
MyRunable2-5
main–5
MyRunable3=6
MyRunable2-6
MyRunable2-7
MyRunable3=7
MyRunable3=8
MyRunable2-8
MyRunable3=9
MyRunable2-9
MyRunable2-10
MyRunable3=10
MyRunable3=11
MyRunable2-11
MyRunable2-12
MyRunable3=12
MyRunable2-13
MyRunable3=13
MyRunable3=14
MyRunable2-14
MyRunable3=15
main–6
main–7
MyRunable3=16
main–8
MyRunable3=17
MyRunable3=18
main–9
main–10
MyRunable3=19
main–11
MyRunable3=20
MyRunable3=21
main–12
MyRunable3=22
main–13
main–14
MyRunable3=23

Process finished with exit code 0

yield()方法:

public static native void yield();

yield()是一个本地方法,提示线程调度器当前线程愿意放弃当前CPU的使用。如果当前资源不紧张,调度器可以忽略这个提示。

实际上可以理解线程从RUNNING状态转换到RUNNABLE的就绪状态

14.3.2 线程死锁:

上头已经介绍过了线程死锁的概念, 这里给出测试DEMO:

举个栗子:

class DeadThreadDemo implements Runnable{
    public static void main(String[] args) {
        try {
            DeadThreadDemo dtd1 = new DeadThreadDemo();
            dtd1.setFlag("a");
            Thread thread1 = new Thread(dtd1, "DeadThreadDemo_1");
            thread1.start();
            Thread.sleep(100);
            dtd1.setFlag("b");
            Thread thread2 = new Thread(dtd1,"DeadThreadDemo_2");
            thread2.start();
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    public String username;
    public Object lock1 = new Object();
    public Object lock2 = new Object();
    public void setFlag(String username) {
        this.username = username;
    }
    @Override
    public void run(){
        if(username.equals("a")) {
            synchronized (lock1) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("按 lock1->lock2代码 顺序执行了");
                }
            }
        }
        if(username.equals("b")) {
            synchronized (lock2) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("按lock2->lock1代码顺序执行了");
                }
            }
        }
    }
}

本例中 ,线程 a 通过 synchronized (lock1) 获得 lock1 的监视器锁,然后通过thread.sleap(3000) 休眠 3000ms , 于此同时, 线程b也开始执行, 通过synchronized (lock2)获得lock2 的监视器锁, 然后通过thread.sleap(3000) 休眠 3000ms

线程a首先苏醒, 开始尝试获得lock2的监视器锁, 但是此时线程b正在休眠, 无响应
而后线程b苏醒, 开始尝试获得lock1 的监视器锁…

线程a 与 线程b 互相争抢资源, 死锁由此产生

输出结果:

username = a
username = b

程序一直没有退出, 处于死锁状态

遗憾的是 ,Java语言没有任何东西可以避免或打破这种死锁状态
所以必须谨慎设计程序防止死锁

14.3.3 线程中断:

终止线程的情况:

  1. run() 执行到return返回, 自动退出

  2. run() 中出现没有捕获的异常

  3. 老版本中可以调用线程类Thread的stop()来终止, 现以弃用

具体原因后头讲, 总之这一类方法不安全

其中第一种是常用的手段

下头介绍中断线程的常用方法

标志中断:

在线程中设置一个public全局标志flag, 而线程在运行期间时不时的检测flag是否被置位, 如是, 则执行相应的代码

当需要中断线程时, 其他线程将本线程的flag置位, 即可进入该线程的中断程序

class InterruptFlagTestDemo implements Runnable {
    public static void main(String[] args) {
        InterruptFlagTestDemo v1 = new InterruptFlagTestDemo();
        Thread th1 = new Thread(v1, "V1");
        try {
            th1.start();
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        v1.interruptFlag=true;
    }

    public boolean interruptFlag = false;

    @Override
    public void run() {
        int i = 0;
        while (!interruptFlag) {
            
        }
        System.out.println(Thread.currentThread().getName() + " is interrupted.\n");
        System.out.println(Thread.currentThread().getName() + " end.");
    }
}

本方法可较为简单的中断线程, 但是:
当线程处于阻塞BLOCKED状态时, 此方法无效

class InterruptFlagTestDemo implements Runnable {
    public static void main(String[] args) {
        InterruptFlagTestDemo v1 = new InterruptFlagTestDemo();
        Thread th1 = new Thread(v1, "V1");
        try {
            th1.start();
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        v1.interruptFlag = true;
    }

    public boolean interruptFlag = false;

    @Override
    public void run() {
        int i = 0;
        while (!interruptFlag) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " is interrupted.\n");
        System.out.println(Thread.currentThread().getName() + " end.");
    }
}

可以看到, 线程没能及时响应外部的中断请求, 而是在退出BLOCKED状态后才exit

解决此种情况需要使用下头介绍的interrupt()方法

interrupt()中断:

与标志中断相似, 使用的是Thread内置的标志位来判定是否中断

首先了解一下相关的方法:

Thread类中有三个中断相关的方法:

  • interrupt(),在一个线程中调用另一个线程的interrupt()方法,即会将目标线程的中断状态置位

    API:

    public void interrupt();
    

    中断此线程。
    除非当前线程一直在中断自身(通常允许这样做),否则将调用此线程的checkAccess方法,这可能会引发SecurityException。

    如果在调用Object类的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)的方法时阻塞了此线程,此类的sleep(long)或sleep(long,int)方法,则其中断状态将被清除,并将收到InterruptedException

    如果此线程在InterruptibleChannel上的I / O操作中被阻止,则该通道将关闭,该线程的中断状态将被设置,并且该线程将收到ClosedByInterruptException。

    如果此线程在选择器中被阻塞,则该线程的中断状态将被设置,并且它将立即从选择操作中返回,可能具有非零值,就像调用选择器的唤醒方法一样。

    如果以上条件均不成立,则将设置该线程的中断状态。

    中断未激活的线程不会产生任何效果

  • interrupted()是个Thread的static方法,用来恢复中断状态

    API:

    public static boolean interrupted();
    

    测试当前线程是否已被中断。 通过此方法可以清除线程的中断状态。 换句话说,如果要连续两次调用此方法,则第二个调用将返回false(除非在第一个调用清除了其中断状态之后且在第二个调用对其进行检查之前,当前线程再次被中断)。
    由于此方法返回false,因此将反映线程中断,因为该线程在中断时尚未处于活动状态而被忽略。

    return:
    true如果这个线程已被中断; false否则。

  • isInterrupted(),用来判断当前线程的中断状态(true or false)

    API:

    public boolean isInterrupted();
    

    测试这个线程是否被中断。 线程的中断状态不受此方法的影响。
    如果线程处在非活动状态, 则会忽略线程中断,并返回false

    return:
    true如果这个线程已被中断; false否则。

常规操作:

  • 调用线程类Thread的interrupt()方法将相应线程的中断标志置位
  • 线程执行期间时不时的使用Thread.currentThread().isInterrupted()检测是否被中断
  • 使用interrupted()方法将中断标志位置零(恢复)

通常需要中断的线程的run() 中需要这么整:

    public void run() {
        ...
        try{
            while(!Thread.currentThread().isInterrupted() && ... ){
				...
            }
        }catch (InterruptedException e) {
            ...
        }finally {
			...
        }
        ...
    }

当线程处于BLOCKED状态时, 如执行到sleep(10000); 此时外部中断线程, 会使sleep()抛出InterruptedException, 并把线程从中断状态中解救出来

注意上头的API解释, 此时不会将中断标志置位!

class interruptTestDemo implements Runnable {
    public static void main(String[] args) {
        interruptTestDemo v1 = new interruptTestDemo();
        Thread th1 = new Thread(v1, "V1");
        try {
            th1.start();
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        th1.interrupt();
    }
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()+" catch "+e.getMessage());
                System.out.println(Thread.currentThread().getName() + " is now " + Thread.currentThread().isInterrupted());
                break;
            }
        }
        System.out.println(Thread.currentThread().getName() + " is interrupted.\n" +
                "Thread sleep 3000ms");
        if (Thread.interrupted()) {
            System.out.println("interrupted() return TRUE");
            System.out.println(Thread.currentThread().getName() + " is now " + Thread.currentThread().isInterrupted());
        }
        System.out.println(Thread.currentThread().getName() + " exit");
    }
}

14.3.4 synchronized关键字:

还剩余仨方法: wait() 和 notify() & notifyAll()
学习他们需要先了解一下synchronized关键字

监视器monitor 或 锁lock 的概念:

Java中的每个对象都有一个监视器monitor, 或者称之为锁lock,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用

锁和监视器指的是同一个东西

synchronized 关键字

synchronized 关键字解决的是多线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

synchronized使用方法:

  1. 代码块形式:

    在多线程环境下,synchronized块中的方法获取了lock实例的monitor, 如果有多个线程中都有synchronized(lock), 即synchronized的实例相同, 那么只有一个线程能够执行该同步块的代码:

    //线程a中:
    public void run() {  
        //do something
           synchronized(lock){
             //..do something
           }
       }
    

    此时如果有:

    //线程b中:
    public void run() {  
        //do something
           synchronized(lock){	//执行到此后, 线程b将进入阻塞BLOCKED状态
             //..do something
           }
       }
    
  2. 直接用于方法:

    用synchronized直接修饰相应的方法, 此时获得的是方法所在的类的实例的monitor, 更进一步,如果修饰的是static方法,则获得的是该类所有实例的monitor

    public class Thread1 implements Runnable {
       public synchronized void run() {  
            ..do something
       }
    }
    

    如上, 实际获取的是Thread1类的monitor

测试代码:

class SynchronizedExample implements Runnable {
    public static void main(String[] args){
        SynchronizedExample se1=new SynchronizedExample();
//        SynchronizedExample se2=new SynchronizedExample();
        Thread th1=new Thread(se1,"se1");
        Thread th2=new Thread(se1,"se2");
        th1.start();
        th2.start();
    }
    @Override
    public void run() {
        synchronized (this) {
            try{
                System.out.println(Thread.currentThread().getName());
                for (int i = 0; i < 10; i++) {
                    System.out.print(i + " ");
                    Thread.sleep(200);
                }
                System.out.println("");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

输出结果:

se1
0 1 2 3 4 5 6 7 8 9
se2
0 1 2 3 4 5 6 7 8 9

Process finished with exit code 0

14.3.5 wait()方法:

源码:

public final native void wait(long timeout) throws InterruptedException; //本地方法 参数为毫秒
public final void wait(long timeout, int nanos) throws InterruptedException {//参数为毫秒和纳秒
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
 
        if (nanos > 0) {
            timeout++;
        }
 
        wait(timeout);
    }
    public final void wait() throws InterruptedException {
        wait(0);
    }

可见其中都调用了本地方法wait(long timeout)

wait() 的几个特点:

  • 对于wait(long timeout), 其会引起当前线程阻塞,直到另外一个线程在对应的对象上调用notify或者notifyAll()方法,或者达到了方法参数中指定的时间
  • 调用wait方法的当前线程一定要拥有对象的监视器锁, 即wait必须存在于synchronized块中
  • wait之后,其他线程可以进入同步块执行

wait方法会把当前线程T放置在对应的object上的等待队列中,在这个对象上的所有同步请求都不会得到响应。线程调度将不会调用线程T,在以下四件事发生之前,线程T会被唤醒(线程T是在其代码中调用wait方法的那个线程)

  1. 当其他的线程在对应的对象上调用notify方法,而在此对象的对应的等待队列中将会任意选择一个线程进行唤醒。
  2. 其他的线程在此对象上调用了notifyAll方法
  3. 其他的线程调用了interrupt方法来中断线程T
  4. 等待的时间已经超过了wait中指定的时间。如果参数timeout的值为0,不是指真实的等待时间是0,而是线程等待直到被另外一个线程唤醒为止。

线程唤醒之后, 被唤醒的线程T会被从对象的等待队列中移除并且重新能够被线程调度器调度, 之后,线程T会像平常一样跟其他的线程竞争获取对象上的锁;一旦线程T获得了此对象上的锁,那么在此对象上的所有同步请求都会恢复到之前的状态,也就是恢复到wait被调用的情况下。然后线程T从wait方法的调用中返回。因此,当从wait方法返回时,对象的状态以及线程T的状态跟wait方法被调用的时候一样。

线程在没有被唤醒,中断或者时间耗尽的情况下仍然能够被唤醒,这叫做伪唤醒。虽然在实际中,这种情况很少发生,但是程序一定要测试这个能够唤醒线程的条件,并且在条件不满足时,线程继续等待。换言之,wait操作总是出现在循环中,就像下面这样:

synchronized(对象){
    while(条件不满足){
     对象.wait();
  }
  对应的逻辑处理
}

如果当前的线程被其他的线程在当前线程等待之前或者正在等待时调用了interrupt()中断了,那么会抛出InterruptedExcaption异常。直到这个对象上面的锁状态恢复到上面描述的状态以前,这个异常是不会抛出的。
要注意的是,wait方法把当前线程放置到这个对象的等待队列中,解锁也仅仅是在这个对象上;当前线程在其他对象上面上的锁在当前线程等待的过程中仍然持有其他对象的锁。
这个方法应该仅仅被持有对象监视器的线程调用。
wait(long timeout, int nanos)方法的实现中只要nanos大于0,那么timeout时间就加上一毫秒,主要是更精确的控制时间,其他的跟wait(long timeout)一样

例子:

来自与上头的例子, 并做了一定的修改:

class SynchronizedExample implements Runnable {
    public static void main(String[] args){
        SynchronizedExample se1=new SynchronizedExample();
        Thread th1=new Thread(se1,"se1");
        Thread th2=new Thread(se1,"se2");
        th1.start();
        th2.start();
    }
    @Override
    public void run() {
        synchronized (this) {
            try{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+" "+i);
                    Thread.sleep(200);
                    if(i==4){
                        if(Thread.currentThread().getName()=="se1"){
                            wait(2000);
                        }else if(Thread.currentThread().getName()=="se2"){
                            wait(500);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

输出结果:

se1 0
se1 1
se1 2
se1 3
se1 4
se2 0
se2 1
se2 2
se2 3
se2 4
se2 5
se2 6
se2 7
se2 8
se2 9
se1 5
se1 6
se1 7
se1 8
se1 9

Process finished with exit code 0

14.3.6 notify()方法 & notifyAll()方法:

public final native void notify(); //本地方法

通知可能等待该对象的对象锁的其他线程。由JVM(与优先级无关)随机挑选一个处于wait状态的线程

注意:

  • 在调用notify()之前,线程必须获得该对象的对象的监视器monitor锁
  • 执行完notify()方法后,不会马上释放锁,要直到退出synchronized代码块,当前线程才会释放锁
  • notify()一次只随机通知一个线程进行唤醒
public final native void notifyAll();//本地方法

和notify()差不多,只不过是使所有正在等待池中等待同一共享资源的全部线程从等待状态退出,进入可运行状态
让它们竞争对象的锁,只有获得锁的线程才能进入就绪状态
每个锁对象有两个队列:就绪队列和阻塞队列

  • 就绪队列:存储将要获得锁的线程
  • 阻塞队列:存储被阻塞的线程

举个栗子:

还是上头的例子的修改

class SynchronizedExample implements Runnable {
    public static void main(String[] args){
        SynchronizedExample se1=new SynchronizedExample();
        Thread th1=new Thread(se1,"se1");
        Thread th2=new Thread(se1,"se2");
        Thread th3=new Thread(se1,"se3");
        th1.start();
        th2.start();
        th3.start();
    }
    @Override
    public void run() {
        synchronized (this) {
            try{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+" "+i);
                    Thread.sleep(200);
                    if(i==4){
                        if(Thread.currentThread().getName().equals("se1")){
                            wait();
                        }else if(Thread.currentThread().getName().equals("se2")){
                            wait(500);
                            notify();
                        }else if(Thread.currentThread().getName().equals("se3")){
                            System.out.println(Thread.currentThread().getName()+"wait 10000ms");
                            wait(10000);
                            System.out.println(Thread.currentThread().getName()+"等待结束");
                        }
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

se1 0
se1 1
se1 2
se1 3
se1 4
se3 0
se3 1
se3 2
se3 3
se3 4
se3wait 10000ms
se2 0
se2 1
se2 2
se2 3
se2 4
se2 5
se2 6
se2 7
se2 8
se2 9
se1 5
se1 6
se1 7
se1 8
se1 9
se3等待结束
se3 5
se3 6
se3 7
se3 8
se3 9

Process finished with exit code 0

线程th3直到wait(10000)超时后才退出

将notify()换成notifyAll()之后:

class SynchronizedExample implements Runnable {
    public static void main(String[] args){
        SynchronizedExample se1=new SynchronizedExample();
        Thread th1=new Thread(se1,"se1");
        Thread th2=new Thread(se1,"se2");
        Thread th3=new Thread(se1,"se3");
        th1.start();
        th2.start();
        th3.start();
    }
    @Override
    public void run() {
        synchronized (this) {
            try{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+" "+i);
                    Thread.sleep(200);
                    if(i==4){
                        if(Thread.currentThread().getName().equals("se1")){
                            wait();
                        }else if(Thread.currentThread().getName().equals("se2")){
                            wait(500);
                            notifyAll();
                        }else if(Thread.currentThread().getName().equals("se3")){
                            System.out.println(Thread.currentThread().getName()+"wait 10000ms");
                            wait(10000);
                            System.out.println(Thread.currentThread().getName()+"等待结束");
                        }
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

输出结果:

se1 0
se1 1
se1 2
se1 3
se1 4
se3 0
se3 1
se3 2
se3 3
se3 4
se3wait 10000ms
se2 0
se2 1
se2 2
se2 3
se2 4
se2 5
se2 6
se2 7
se2 8
se2 9
se3等待结束
se3 5
se3 6
se3 7
se3 8
se3 9
se1 5
se1 6
se1 7
se1 8
se1 9

线程th3由于线程th2 中的notifyAll() 而提前从wait(10000)中退出, 开始与th1争抢监视器锁

14.4 线程属性:

14.4.1 优先级和守护线程:

线程优先级:

Java中的每个线程都有个优先级:

  • 新建线程的优先级继承自父线程(如果有的话), 否则默认设置为NORM_PRIORITY=5

  • Thread中, 优先级定义有10个等级, 从MIN_PRIORITY=1, 到NORM_PRIORITY=5, 到MAX_PRIORITY=10

  • 可以通过Thread.setPriority()方法调整线程的优先级

    public final void setPriority(int newPriority);
    

线程调度器在选择新线程执行时, 趋向于更高优先级的线程, 所以优先级高的线程获得CPU时间片的概率更大

但是, 优先级受限于系统的实现, 在不同系统中, 优先级不同, JVM会将设置的优先级映射到系统上, 不同的系统导致不同的结果
所以不要将实现依赖于优先级, 这损伤了移植性

使用优先级的注意事项:

如果高优先级的线程一直没有进入非活动状态, 低优先级的线程可能永远不会被执行, 最终将导致线程被饿死

设计时应当避免出现这种情况

守护线程:

Java线程中分为用户线程和守护线程, 其中:

  • 用户线程就是普通的线程
  • 守护线程的意识是为其他线程提供服务, 比如计时线程

当JVM中没有用户线程时, JVM也就没有必要继续运行下去了, 所以直接停机

使用守护线程的注意事项:

不要在守护线程中访问固有资源, 包括文件, 数据库等, 因为守护线程会在任何时候甚至一个操作的中间发生中断

示例代码:

举个栗子:

class ThreadDemo3 {

    public static void main(String[] args){
        MyRunnable4 mr4 = new MyRunnable4();
        Thread t = new Thread(mr4,"MyThread4");
        //优先级高可以提高该线程抢点CPU时间片的概率大
        t.setPriority(Thread.MIN_PRIORITY);
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        //线程可以分成守护线程和 用户线程,当进程中没有用户线程时,JVM会退出
        t.setDaemon(true);//把线程设置为守护线程
        System.out.println(t.isAlive());
        t.start();
        System.out.println(t.isAlive());

        for (int i = 0; i < 50; i++) {
            System.out.println("main--"+i);
//            try {
//                Thread.sleep(200);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            if (i>=5&&i<=10){
                Thread.yield();//让出本次CPU执行时间片
            }
        }


    }
}


class MyRunnable4 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName()+"--"+i);
//            try {
//                Thread.sleep(200);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
    }
}

并发执行俩线程时, 不设置sleep()对比效果较好:

输出结果:

false
true
main–0
main–1
MyThread4–0
MyThread4–1
main–2
MyThread4–2
MyThread4–3
main–3
MyThread4–4
MyThread4–5
main–4
MyThread4–6
main–5
MyThread4–7
MyThread4–8
MyThread4–9
MyThread4–10
MyThread4–11
MyThread4–12
MyThread4–13
MyThread4–14
MyThread4–15
main–6
MyThread4–16
main–7
MyThread4–17
main–8
MyThread4–18
main–9
MyThread4–19
main–10
MyThread4–20
main–11
MyThread4–21
MyThread4–22
MyThread4–23
MyThread4–24
MyThread4–25
MyThread4–26
MyThread4–27
MyThread4–28
MyThread4–29
MyThread4–30
MyThread4–31
main–12
main–13
main–14
main–15
main–16
main–17
main–18
main–19
main–20
main–21
main–22
main–23
main–24
main–25
main–26
main–27
main–28
main–29
MyThread4–32
main–30
MyThread4–33
MyThread4–34
MyThread4–35
MyThread4–36
MyThread4–37
MyThread4–38
MyThread4–39
main–31
MyThread4–40
main–32
MyThread4–41
MyThread4–42
MyThread4–43
MyThread4–44
MyThread4–45
MyThread4–46
MyThread4–47
MyThread4–48
MyThread4–49
main–33
main–34
main–35
main–36
main–37
main–38
main–39
main–40
main–41
main–42
main–43
main–44
main–45
main–46
main–47
main–48
main–49

Process finished with exit code 0

14.4.2 未捕获异常处理器:

之前说到引发线程终止的原因之一是run() 中出现没有捕获的异常

但是run()方法不能抛出任何异常, 所以这个异常会滚到一个用于未捕获异常的处理器

此处理器是可以自定义, 也可以使用java默认的处理器:

  1. 自定义未捕获异常处理器:

    只需要实现Thread.UncaughtExceptionHandler接口 , 这个接口中只有一个方法:

    @FunctionalInterface
    public static interface Thread.UncaughtExceptionHandler{
    	void uncaughtException(Thread t, Throwable e)  ;  
    }
    

    而后使用Thread的setUncaughtExceptionHandler()方法为指定线程设置对应的未捕获异常处理器

    public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh);
    
  2. 使用默认的未捕获异常处理器:

    直接使用Thread的static方法Thread.setDefaultUncaughtExceptionHandler();
    为所有的线程设置默认的未捕获异常处理器

如果没有设置未捕获线程处理器, 则此时的处理器就是该线程的ThreadGroup对象

ThreadGroup类与Thread的关系类似于对象与集合的关系

由于现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组ThreadGroup

ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口
它的uncaughtException 方法对异常做如下操作:

  1. 如果该线程组有父线程组, 那么父线程组的 uncaughtException 方法被调用。

  2. 否则, 如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器, 则调用该处理器

  3. 否则, 如果 Throwable 是 ThreadDeath 的一个实例, 什么都不做。

  4. 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err 上

    这是你在程序中肯定看到过许多次的栈轨迹。

14.5 线程同步:

上头其实已经介绍了很多线程同步的操作, 这里进行进一步扩充:

14.5.1 ReentrantLock对象:

Java SE 5.0引入了ReentrantLock对象, 用于控制线程安全

类似于synchronized监视器锁, ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能, 是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

由于这玩意水好像挺深, 暂时先通过与synchronized监视器锁的对比来学习使用

APIの区别:

ReentrantLock的标准使用方法:

public class test(){
    private Lock lock=new ReentrantLock();	//创建ReentrantLock对象
    public void testMethod(){
        try{
            lock.lock();
            //code
        } 
        finally{
            lock.unlock();
        }   
    }  
} 

ReentrantLock的继承关系UML图:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第4张图片

可以看到ReentrantLock继承自Lock接口

其中Lock接口的API:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第5张图片

其中:

方法 说明
lock() 获取锁。如果锁已经被占用,则等待
tryLock() 尝试获取锁,拿到锁返回true,没拿到返回false,并立即返回
tryLock(long time, TimeUnit unit) 在指定时间内会等待获取锁,如果一直拿不到就返回false,并立即返回。在等待过程中可以进行中断
lockInterruptibley() 获取锁。如果线程interrupted了,则跟着抛出异常中断
unLock() 释放锁
newCondition() 创建一个与此 Lock 实例一起使用的 Condition 实例。

将lock() 与unlock()方法放到try-catch语句中的目的:

由于锁的释放是手动的, 如果当前线程没有unlock()释放锁, 则其他线程永远不会得到锁, 所以需要保证锁的释放

相比于synchronized, 会在抛出异常时自动释放锁

各种方法简介:

ReentrantLock的特性之一:

  • tryLock()尝试获取锁, 如果得到, 则返回true, 否则, 立刻返回false, 此时线程无需等待就可以立刻去做其他事

  • tryLock(long time, TimeUnit unit) 尝试获取锁, 功能与tryLock()相似, 只不过加上了超时时间, 后头的unit是时间单位, 为枚举变量:

    Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第6张图片

    其中 ,tryLock(long time, TimeUnit unit) 支持interrupt中断, 抛出InterruptedException异常

  • lockInterruptibley(), 与tryLock(long time, TimeUnit unit)相似
    相当于设置了一个超时时间为无限的tryLock()

可重入性:

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞

ReentrantLock与synchronized都是可重入锁

而ReentrantLock内部通过一个计数器来维护重入锁的功能, 当一个线程重复获得锁时, 计数器自增, 释放锁并被其他线程获取时, 计数器归零

等待可中断性:

等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

等待可中断性对处理执行时间非常长的同步块很有帮助

具体说来,假如有两个线程,Thread1 Thread2.
当Thread1获取了对象object的锁,Thread2将等待Thread1释放object的锁。

  • 使用synchronized:

    如果Thread1不释放,Thread2将一直等待,不能被中断。synchronized也可以说是Java提供的原子性内置锁机制。内部锁扮演了互斥锁(mutual exclusion lock ,mutex)的角色,一个线程引用锁的时候,别的线程阻塞等待。

  • 使用ReentrantLock:

  • 如果Thread1不释放,Thread2等待了很长时间以后,可以中断等待,转而去做别的事情。

公平锁:

公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁, 即等待时间最长的线程将第一个获得锁
而非公平锁则不能保证这一点

以synchronized监视器锁为例, 其为非公平锁, 在一个线程退出synchronized块之后(即释放锁之后) , 所有等待该锁的线程都有可能获得锁

而ReentrantLock可以设置为公平锁或非公平锁:

ReentrantLock fairLock = new ReentrantLock(true); // 初始化一个公平锁

ReentrantLock fairLock = new ReentrantLock(); // 初始化一个非公平锁
ReentrantLock fairLock = new ReentrantLock(false); // 初始化一个非公平锁

由于非公平锁的性能比公平锁的性能好很多,所以ReentrantLock默认是非公平锁

非公平锁的优点:

  1. 如果直接拿到了锁,就避免了维护node链表队列
  2. 如果直接拿到了锁,就避免了线程休眠和唤醒的上下文切换

但并非所有的应用场景中, 非公平锁都比公平锁要好, 是具体情况而定

绑定条件:

在synchronized中,锁对象的wait和notify() 或notifyAll()方法可以实现一个隐含的条件。但如果还需要其他条件关联的时候就不得不额外添加一个锁

而ReentrantLock可以同时绑定多个Condition对象,只需多次调用newCondition方法即可

首先整明条件关联(水深, 简单理解):

以synchronized为例, 所有的使用同一个锁的synchronized块线程相当于互相关联, 其中只能有一个线程获得锁并执行synchronized内容, 而等待的线程都会被置于当前锁对象的等待队列中 ,当锁被释放时, 所有关联的线程都有可能获得锁

而对于ReentrantLock, 所有使用相同Condition对象的线程才会被相互关联, 每一个Condition对象都有一个等待队列, 线程之前的等待和唤醒只在各自的队列中进行

由于wait(), notifyAll(), notify() 是万类祖师Object() 的final方法, 所以Condition中的方法必须得改个名字

对比项 synchronized Condition
前置条件 获取对象的监视器锁 调用Lock.lock()获取锁
调用Lock.newCondition() 获取Condition对象
线程等待队列个数 一个 多个
使当前线程
进入等待状态
object.wait() condition.await()
使其他一个线程苏醒 object.notify() condition.signal()
使其他所有线程苏醒 object.notifyAll() condition.signalAll()

其次介绍interface Condition的API:

public interface Condition {
    void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
}

其中:

void await() 使当前线程进入等待状态, 直到接收到signal或interrupt
boolean await(long time, TimeUnit unit) 使当前线程进入等待状态, 直到接收到signal或interrupt或timeout
long awaitNanos(long nanosTimeout) 使当前线程进入等待状态, 直到接收到signal或interrupt或timeout
void awaitUninterruptibly() 使当前线程进入等待状态, 直到接收到signal (忽略interrupt)
boolean awaitUntil(Date deadline) 使当前线程进入等待状态, 直到接收到signal或interrupt或到达deadline
void signal() 使对应的线程苏醒
void signalAll() 使所有线程苏醒

所以, 可以通过多次调用Lock.newCondition()方法创建多个Collection对象, 为多个线程建立不同的关联:

举个栗子:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static class NumberWrapper {
        public int value = 1;
    }

    public static void main(String[] args)  {
        //初始化可重入锁
        final Lock lock = new ReentrantLock();

        //第一个条件当屏幕上输出到3
        final Condition reachThreeCondition = lock.newCondition();
        //第二个条件当屏幕上输出到6
        final Condition reachSixCondition = lock.newCondition();

        //NumberWrapper只是为了封装一个数字,一边可以将数字对象共享,并可以设置为final
        //注意这里不要用Integer, Integer 是不可变对象
        final NumberWrapper num = new NumberWrapper();
        //初始化A线程
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                //需要先获得锁
                lock.lock();
                try {
                    System.out.println("threadA start write");
                    //A线程先输出前3个数
                    while (num.value <= 3) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //输出到3时要signal,告诉B线程可以开始了
                    reachThreeCondition.signal();
                } finally {
                    lock.unlock();
                }
                lock.lock();
                try {
                    //等待输出6的条件
                    reachSixCondition.await();
                    System.out.println("threadA start write");
                    //输出剩余数字
                    while (num.value <= 9) {
                        System.out.println(num.value);
                        num.value++;
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }

        });


        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();

                    while (num.value <= 3) {
                        //等待3输出完毕的信号
                        reachThreeCondition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                try {
                    lock.lock();
                    //已经收到信号,开始输出4,5,6
                    System.out.println("threadB start write");
                    while (num.value <= 6) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //4,5,6输出完毕,告诉A线程6输出完了
                    reachSixCondition.signal();
                } finally {
                    lock.unlock();
                }
            }

        });

        //启动两个线程
        threadB.start();
        threadA.start();
    }
}

其他:

  1. 性能方面

    JDK 1.5中,synchronized还有很大的优化余地。JDK 1.6 中加入了很多针对锁的优化措施,synchronized与ReentrantLock性能方面基本持平

  2. 持有的锁方面:

    ReentrantLock和synchronized持有的对象锁不同

ReentrantLock & synchronized的选择:

  • 如果 synchronized 能够满足需求, 那么请尽量使用它, 这样可以减少编写的代码数量,减少出错的几率
  • 如果特别需要 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition

14.5.2 volatile关键字:

从Java内存模型上理解volatile:

volatile关键字可以确保对一个变量的更新对其他线程马上可见
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存
当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值

volatile特点:

volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性与并发的正确性

volatile不能保证原子性
即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,i++的操作仍有可能被中断在其中的某一个原子操作上, volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况

实际上就是说, 用volatile修饰的变量, 仍然有可能读取到错误的值

解决这个问题需要使用原子类型的变量

所以volatile一般情况下不能替代synchronized

volatile使用:

volatile用于修饰变量, 通常满足这样的条件时, 变量需要用volatile修饰:

  • 如果向一个变量写入值, 而这个变量接下来可能会被另一个线程读取, 或者, 从一个变量读值, 而这个变量可能是之前被另一个线程写入的, 此时必须使用同步

举个栗子:

class volatileTestDemo implements Runnable {
    public static void main(String[] args) {
        volatileTestDemo v1 = new volatileTestDemo();
        volatileTestDemo v2 = new volatileTestDemo();
        volatileTestDemo v3 = new volatileTestDemo();
        Thread th1 = new Thread(v1, "V1");
        Thread th2 = new Thread(v1, "V2");
        Thread th3 = new Thread(v1, "V3");
        try {
            th1.start();
            Thread.sleep(100);
            th2.start();
            Thread.sleep(100);
            th3.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    volatile private int variable1 = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10; ++i) {
            System.out.println(Thread.currentThread().getName() + " " + variable1++);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

V1 0
V2 1
V1 2
V2 3
V3 4
V1 5
V3 6
V2 6
V1 7
V3 8
V2 9
V1 10
V2 11
V3 11
V1 12
V3 13
V2 14
V1 15
V3 16
V2 17
V1 18
V2 19
V3 19
V1 20
V2 22
V3 21
V1 23
V3 24
V2 24
V3 25

Process finished with exit code 0

可以看到程序依然没有正确执行…

14.5.3 原子性:

解决之前volatile的问题, 一种是对多线程访问变量使用锁, 但是锁比较消耗性能, 所以在JDK1.5之后, 添加了原子操作类, 到现在使用的JDK1.8 , 一共已经有这些原子操作类:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第7张图片

这些原子操作类够提供了一些不可分割的原子操作, 在多线程操作时可性能高效、线程安全地更新一个变量

基本类原子更新

  • AtomicBoolean、AtomicInteger、AtomicLong
    元老级的原子更新,方法几乎一模一样
  • DoubleAdder、LongAdder
    对Double、Long的原子更新性能进行优化提升
  • DoubleAccumulator、LongAccumulator
    支持自定义运算

举个栗子:

将上头volatile的例子中的变量修改为AtomicInteger, 就没问题了

class volatileTestDemo implements Runnable {
    public static void main(String[] args) {
        volatileTestDemo v1 = new volatileTestDemo();
        volatileTestDemo v2 = new volatileTestDemo();
        volatileTestDemo v3 = new volatileTestDemo();
        Thread th1 = new Thread(v1, "V1");
        Thread th2 = new Thread(v1, "V2");
        Thread th3 = new Thread(v1, "V3");
        try {
            th1.start();
            Thread.sleep(100);
            th2.start();
            Thread.sleep(100);
            th3.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

//    volatile private int variable1 = 0;
    private AtomicInteger variable1= new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10; ++i) {
            System.out.println(Thread.currentThread().getName() + " " + variable1);
            variable1.incrementAndGet();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

V1 0
V2 1
V1 2
V2 3
V3 4
V1 5
V2 6
V3 6
V1 8
V2 9
V3 9
V1 11
V3 12
V2 12
V1 14
V2 15
V3 15
V1 17
V3 18
V2 18
V1 20
V3 21
V2 21
V1 23
V2 24
V3 24
V1 26
V2 27
V3 27
V3 29

Process finished with exit code 0

数组类原子更新:

  • AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

三个类的用法基本一致, 以下是AtomicIntegerArray的几个常用方法, 剩余的看API:

  • addAndGet(int i, int delta):
    以原子更新的方式将数组中索引为i的元素与输入值相加;
  • getAndIncrement(int i):
    以原子更新的方式将数组中索引为i的元素自增加1;
  • compareAndSet(int i, int expect, int update):
    将数组中索引为i的位置的元素进行更新

引用类原子更新:

  • AtomicReference:原子更新引用类型;
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
  • AtomicMarkableReference:原子更新带有标记位的引用类型;

举个栗子:

首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为User{userName='b', age=2},返回的是原来的user对象User{userName='a', age=1}

class AtomicDemo {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

程序输出:

User{userName=‘a’, age=1}
User{userName=‘b’, age=2}

Process finished with exit code 0

字段类类原子更新:

  • AtomicIntegeFieldUpdater:原子更新整型字段类;
  • AtomicLongFieldUpdater:原子更新长整型字段类;
  • AtomicStampedReference:原子更新引用类型
    这种更新方式会带有版本号, 为了解决CAS的ABA问题;

这个先放着, 用的不多, 以后在看

14.5.4 ThreadLocal:

先掌握基本用法, 之后有时间在深入学习

变量值的共享可以使用public static的形式,这会使得所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量, 则需要使用ThreadLocal

在多线程环境下,ThreadLocal可以保证各个线程之间的变量互相隔离、相互独立

在线程中,可以通过get() & set()方法来访问&修改变量

举个栗子:

class ThreadLocalTestDemo {
    public static void main(String[] args) {
        Thread threadA=new Thread(new MyThread(),"Thread A");
        Thread threadB=new Thread(new MyThread(),"Thread B");
        threadA.start();
        threadB.start();
    }
    static class MyThread implements Runnable{
        private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
//        private static int threadLocal=0;
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                threadLocal.set(i);
                System.out.println(Thread.currentThread().getName() + " threadLocal.get() = " + threadLocal.get());
//                threadLocal=i;
//                System.out.println(Thread.currentThread().getName()+" threadLocal = "+threadLocal);
            }
        }
    }
}

输出结果:

Thread B threadLocal.get() = 0
Thread A threadLocal.get() = 0
Thread B threadLocal.get() = 1
Thread A threadLocal.get() = 1
Thread B threadLocal.get() = 2
Thread A threadLocal.get() = 2
Thread B threadLocal.get() = 3
Thread A threadLocal.get() = 3
Thread B threadLocal.get() = 4
Thread A threadLocal.get() = 4
Thread B threadLocal.get() = 5
Thread A threadLocal.get() = 5
Thread B threadLocal.get() = 6
Thread A threadLocal.get() = 6
Thread B threadLocal.get() = 7
Thread A threadLocal.get() = 7
Thread B threadLocal.get() = 8
Thread A threadLocal.get() = 8
Thread B threadLocal.get() = 9
Thread A threadLocal.get() = 9

Process finished with exit code 0

14.5.5 ReentrantReadWriteLock:

ReentrantReadWriteLockinterface Lock的另一种实现方式

类比于ReentrantLock:

前者是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。
相对于排他锁,ReentrantReadWriteLock提高了线程并发性, 在共享数据(如缓存)访问的读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量

ReentrantReadWriteLock的特点:

  1. 支持公平和非公平的获取锁的方式
  2. 支持可重入
    读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁
  3. 还允许从写入锁降级为读取锁
    其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的
  4. 读取锁和写入锁都支持锁获取期间的中断
  5. Condition支持
    仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon, readLock().newCondition() 会抛出UnsupportedOperationException异常

举个栗子:

class RWDictionary {
    private final TreeMap<String, Integer> m = new TreeMap<String, Integer>();
    /**
     * 构造ReentrantReadWriteLock对象rwl
     */
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    /**
     *从rwl中抽取读锁和写锁
     */
    private final Lock r = rwl.readLock();    //读锁
    private final Lock w = rwl.writeLock();   //写锁

    /**
     * 通过对读锁和写锁的单独锁定来提升Collection的并发性
     */
    public int get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
}

14.6 阻塞队列:

阻塞队列是一类泛型容器, 类似于将Ch.09的List系列容器与多线程并发进行了一系列封装, 使得对容器类实例进行操作时, 同时保证线程安全, 而省去了编写额外的代码, 这样提供了极大的方便性

由于这玩意水也挺深, 暂时先做功能了解, 底层实现原理以后有时间在回来看

14.6.1 常用的阻塞队列:

自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,主要有以下几个:

  • ArrayBlockingQueue:

    基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列

  • LinkedBlockingQueue:

    基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE

  • PriorityBlockingQueue:

    以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列

  • DelayQueue:

    基于PriorityQueue,一种延时阻塞队列 (所以元素需要实现comparable接口)
    DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞

14.6.2 常用方法:

  • add(E e):

    将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常

  • remove():

    移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常

  • offer(E e):

    将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false

  • poll()

    移除并获取队首元素,若成功,则返回队首元素;否则返回null

  • peek()

    获取队首元素,若成功,则返回队首元素;否则返回null

  • put(E e)

    方法用来向队尾存入元素,如果队列满,则线程阻塞(等待)

  • take()

    方法用来从队首取元素,如果队列为空,则线程阻塞(等待)

  • offer(E e,long timeout, TimeUnit unit)

    方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true

  • poll(long timeout, TimeUnit unit)

    方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素

一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果

方法对比:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

14.7 线程安全的集合:

上古の线程安全集合:

首先先了解一下Java中早期的线程安全集合, 其实就是Ch.09里介绍的Vector与HashTable:

  1. Vector
    Vector是线程安全的,因为它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用

  2. HashTable
    HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

取代这俩的是ArrayList与HashMap, 虽然这俩以及Ch.09中介绍的大部分容器都不是线程安全的, 但是可以通过使用Collection的同步包装器变成线程安全的:

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

//...

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

优秀の线程安全集合:

这里仅仅了解一下集合, 需要使用时再进行详细拓展学习

java.util.concurrent包中定义了好多线程安全的集合, 这些集合的性能比同步包装器产生的集合的性能更加优秀, 所有优先使用后者

  1. ConcurrentHashMap

    //构造函数:
    ConcurrentHashMap();
    ConcurrentHashMap(int initialCapacity);
    ConcurrentHashMap(int initialCapacity, float loadFactor);
    ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel);
    ConcurrentHashMap(Map<? extends K,? extends V> m);
    
    参数 简介
    initialCapacity 集合的初始容量。默认值为 16
    loadFactor 控制调整: 如果每一个桶的平均负载超过这个因子,表的大 小会被重新调整。默认值为 0.75
    concurrencyLevel 并发写者线程的估计数目

    ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
    在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
    JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

  2. CopyOnWriteArrayList和CopyOnWriteArraySet

    它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

  3. 除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等

    至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

14.8 线程异步:

上头介绍了Java线程同步的相关操作, 类比于同步, 介绍一下异步:

很多时候都希望能够最大的利用资源,比如在进行IO操作的时候尽可能的避免同步阻塞的等待,因为这会浪费CPU的资源。如果在有可读的数据的时候能够通知程序执行读操作甚至由操作系统内核帮助我们完成数据的拷贝,这再好不过了。从NIO到CompletableFuture、Lambda、Fork/Join,java一直在努力让程序尽可能变的异步甚至拥有更高的并行度,这一点一些函数式语言做的比较好,因此java也或多或少的借鉴了某些特性。

考虑有一个耗时的操作,操作完后会返回一个结果(不管是正常结果还是异常),程序如果想拥有比较好的性能, 不可能由线程去等待操作的完成,而是应该采用listener模式。

通常, 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率

14.8.1执行器Executors:

Executor简介:

在Java 5之后,并发编程引入了一堆新的启动、调度和管理线程的API
Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
因此,在Java 5之后,通过Executor来启动线程比使用Thread的start()方法更好,更易管理,效率更好(用线程池实现,节约开销), 还能够避免一些其他问题

Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制

Executor的UML图:

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第8张图片

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第9张图片

Excutor接口:

public interface Executor{
	void execute(Runnable command);
}

其定义了一个接收Runnable对象的方法executo(),它用来执行一个任务,任务即一个实现了Runnable接口的类

之前使用Thread开辟新线程的操作为使用start() 方法
但在Executor中,可以使用Executor而不用显示地创建线程:

executor.execute(new RunnableTask()); // 异步执行

但是, 可以看到, Executor接口中的execute()方法只能接受Runnable对象, 所以, 通常使用下头的ExecutorService接口, 其中定义的submit()可以接受Runnable与Callable, 更为广泛

Callable接口:

类比于interface runnable, callable相当于有返回值和throw的runnable

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

注意, 子类继承Callable接口时需要指明参数列表V的类型, 否则会给出警告

Future接口:

public interface Future<V>{
    boolean	cancel(boolean mayInterruptIfRunning);
	V	get();
	V	get(long timeout, TimeUnit unit);
	boolean	isCancelled();
	boolean	isDone();
}

其中:

  • V get() :
    获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
  • V get(Long timeout , TimeUnit unit) :
    获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
  • boolean isDone() :
    如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
  • boolean isCanceller() :
    如果任务完成前被取消,则返回true。
  • boolean cancel(boolean mayInterruptRunning) :
    如果任务还没开始,执行cancel(…)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(…)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。

ExecutorService接口:

ExecutorService继承自Executor接口 ,并拓展了大量的方法用于管理线程

一般用该接口来实现和管理多线程

public interface ExecutorService extends Executor{
    boolean	awaitTermination(long timeout, TimeUnit unit);
	<T> List<Future<T>>	invokeAll(Collection<? extends Callable<T>> tasks);
	<T> List<Future<T>>	invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
	<T> T				invokeAny(Collection<? extends Callable<T>> tasks);
	<T> T				invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
	boolean	isShutdown();
	boolean	isTerminated();
	void	shutdown();
	List<Runnable>	shutdownNow();
	<T> Future<T>	submit(Callable<T> task);
	Future<?>		submit(Runnable task);
	<T> Future<T>	submit(Runnable task, T result);
}

常用方法简介:

  • submit(Runnable) & submit(Callable) & submit(Runnable task)

    submit(Callable)submit(Runnable)类似,也会返回一个Future对象,但是除此之外,submit(Callable)接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值, 所以这里是一个迷惑的Future

    第三个版本则是在使用get()时返回指定的result对象

  • shutdown() & shutdownNow():

    前者启动的是线程池的有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务。 如果已关闭,则调用不会产生任何其他影响。
    此方法不等待先前提交的任务完成执行。 使用awaitTermination可以做到这一点

    后者则是尝试停止所有正在执行的任务,中止正在等待的任务的处理,并返回正在等待执行的任务的列表, 但是有可能它们都会停止,也有可能直到执行完成。
    此方法不等待主动执行的任务终止。 使用awaitTermination可以做到这一点。

  • invokeAny():

    invokeAny(...)方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,有可能是耗时最少的, 但也有可能是耗时最多的

    当愿意接受任何一种解决方案时, 可以使用invokeAny()方法

    还有一个带超时时间Timeout的重载版本, 其实都差不多

    此方法不能实现异步, 需要等待任务完成

  • invokeAll():

    与invokeAny相似, 此方法会返回所有的Callable任务到一个Future类型的列表中, 可以通过范围for访问所有结果, 但是顺序是乱的

    此方法不能实现异步, 需要等待任务全部完成

线程池 & Executors类:

构建一个新的线程是有一定代价的, 因为涉及与操作系统的交互
如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool ):

  • 一个线程池中包含许多准备运行的空闲线程
    将 Runnable 对象交给线程池, 就会有一个线程调用 run 方法。 当 run 方法退出时, 线程不会死亡,而是在池中准备为下一个请求提供服务。
  • 使用线程池可以有效减少并发线程的数目
    创建大量线程会大大降低性能甚至使虚拟机崩溃。 如果有一个会创建许多线程的算法, 应该使用一个线程数“ 固定的” 线程池以限制并发线程的总数

Java通过Executors提供四种线程池,分别为:

  1. newCachedThreadPool

    • 缓存型线程池,先查看池中有没有以前建立的线程,如果有,就 reuse. 如果没有,就建一个新的线程加入池中
    • 缓存型池子通常用于执行一些生存期很短的异步型任务
      因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选
    • 能reuse的线程,必须是timeout 空闲等待内的池中线程,缺省timeout是60s,超过这个空闲时长,线程实例将被终止及移出池
      注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止
  2. newFixedThreadPool

    • newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
    • 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
    • 和cacheThreadPool不同,FixedThreadPool没有空闲机制,所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
    • 从方法的源代码看,cache池和fixed 池调用的是同一个底层池,只不过参数不同:
      fixed池线程数固定,并且是无空闲机制
  3. newSingleThreadExecutor
    创建只有一个线程的"池", 该线程顺序执行每一个提交的任务, 相当于是前头的多线程线程池的退化版本

  4. newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行, 替代java.util.Timer

  5. newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

伪异步の实例测试:

Future实例测试:

此例使用jdk1.8之前的Future实现"异步", 实际上显得有点鸡肋,并不能实现真正的异步, 通常称之为Future模式, 需要阻塞的获取结果,或者不断的检测结果是否生成

class ExecutorsTestDemo implements Callable{
    public static void main(String[] args) throws Throwable, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        ExecutorsTestDemo etd1=new ExecutorsTestDemo();
        Future<String> f = executor.submit(etd1);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" task started!");
                try {
                    Thread.sleep(5000);//表示一个耗时较长的任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" task finished!");
            }
        }); ;

        int rowTimeCounter=1;
        while((!f.isDone())){
            //此处main()可以不用阻塞, 转而去完成其他任务
            System.out.println(Thread.currentThread().getName()+" row "+rowTimeCounter+"Times.");
            rowTimeCounter++;
            Thread.sleep(200);  //模拟其他任务
        }
        System.out.println(f.get());
        System.out.println("main thread is blocked" +
                "\nShutdown Executor Pool");
        executor.shutdown();
    }
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" task started!");
        Thread.sleep(3000);//表示一个耗时较长的任务
        System.out.println(Thread.currentThread().getName()+" task finished!");
        return "I'm Callable.";
    }
}

输出结果:

pool-1-thread-1 task started!
main row 1Times.
pool-1-thread-2 task started!
main row 2Times.
main row 3Times.
main row 4Times.
main row 5Times.
main row 6Times.
main row 7Times.
main row 8Times.
main row 9Times.
main row 10Times.
main row 11Times.
main row 12Times.
main row 13Times.
main row 14Times.
main row 15Times.
pool-1-thread-1 task finished!
I’m Callable.
main thread is blocked
Shutdown Executor Pool
pool-1-thread-2 task finished!

Process finished with exit code 0

可以看到使用shutdown()后, 线程池没有被立刻关闭, 而是等待到Runnable任务完成后才关闭

invokeAny实例测试:

举个栗子:

class ExecutorsTestDemo2 implements Callable<String>{
    private final int flag;

    ExecutorsTestDemo2(int newI) throws ExecutionException, InterruptedException {
        flag = newI;
    }
    public static void main(String[] args) throws Exception {

        ExecutorService executorService = Executors.newCachedThreadPool();

        Set<Callable<String>> callables = new HashSet<Callable<String>>();

        for (int i = 0; i < 10; i++) {
            callables.add(new ExecutorsTestDemo2(i));
        }
        System.out.println(callables.size());

        String result = executorService.invokeAny(callables);
        System.out.println("Ends.\n" +
                "result = " + result);
        executorService.shutdown();
    }
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " start."+ Integer.toString(flag));
        Thread.sleep(300 * flag);
        return new String("Task " + Integer.toString(flag));
    }
}

输出结果:

10
pool-1-thread-1 start.2
pool-1-thread-3 start.9
pool-1-thread-2 start.6
pool-1-thread-4 start.5
pool-1-thread-5 start.8
pool-1-thread-6 start.4
pool-1-thread-7 start.0
pool-1-thread-8 start.3
pool-1-thread-9 start.7
pool-1-thread-10 start.1
Ends.
result = Task 0

可以看到返回的是Callable Set任务中时间最短的一个
并且main在invokeAny之后等待任务完成, 并没有实现异步

invokeAll()测试:

举个栗子:

class ExecutorsTestDemo3 implements Callable<String> {
    private final int flag;
    ExecutorsTestDemo3(int newI) throws ExecutionException, InterruptedException {
        flag = newI;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Set<Callable<String>> callables = new HashSet<Callable<String>>();
        for (int i = 0; i < 10; i++) {
            callables.add(new ExecutorsTestDemo3(i));
        }
        System.out.println(callables.size());
        List<Future<String>> result = executorService.invokeAll(callables);
        System.out.println("End invokeAll");
        for (
                Future<String> temp : result) {
            System.out.println("future.get = " + temp.get());
        }
        executorService.shutdown();
    }

    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " start." + Integer.toString(flag));
        Thread.sleep(300 * flag);
        return new String("Task" + Integer.toString(flag));
    }
}

输出结果:

10
pool-1-thread-1 start.2
pool-1-thread-2 start.6
pool-1-thread-3 start.9
pool-1-thread-4 start.5
pool-1-thread-5 start.8
pool-1-thread-6 start.4
pool-1-thread-7 start.0
pool-1-thread-8 start.3
pool-1-thread-9 start.7
pool-1-thread-10 start.1
End invokeAll
future.get = Task2
future.get = Task6
future.get = Task9
future.get = Task5
future.get = Task8
future.get = Task4
future.get = Task0
future.get = Task3
future.get = Task7
future.get = Task1

Process finished with exit code 0

其中, main() 在invokeAll() 提交任务列表之后, 等待任务完成, 没有实现异步

CompletableFuture & 真异步:

这里仅做简单的使用方法了解, 有时间在深入

直到jdk1.8才算真正支持了异步操作,jdk1.8中提供了lambda表达式, 同时借助jdk1.8 提供的CompletableFuture可以实现异步的操作

这里

创建异步操作:

CompletableFuture 提供了四个静态方法来创建一个异步操作

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

其中:

  • 没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码
    如果指定线程池,则使用指定的线程池运行
  • runAsync方法不支持返回值
  • supplyAsync可以支持返回值

获取计算结果:

public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);

指定异步完成后的回调函数
当异步完成后, 结果会被传递给我们提供的异步函数中进一步执行

public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn,Executor executor);

指定异步完成后的回调函数
当异步完成后, 结果会被传递给我们提供的异步函数中进行下一步的转化, 并将回调函数的返回值返回

举个栗子:

class JavaPromise {
    public static void main(String[] args) throws Throwable, ExecutionException {
        // 两个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        //jdk1.8之前的实现方式
        CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                System.out.println("task started!");
                try {
                    //模拟耗时操作
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "task finished!";
            }
        }, executor);

        //采用lambada的实现方式
        future.thenAccept((String e)-> {
            System.out.println(e + " ok");
            executor.shutdown();
        });

        System.out.println("main thread is running");
    }
}

14.8.2 Fork-Join 框架:

先放着, 后头有时间在学

实际上就是使用了分而治之的思想:

规模为N的问题,N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解

Java核心技术 卷1 Ch.14 70000字长篇入门Java并发_第10张图片

你可能感兴趣的:(Java学习笔记)