JUC并发编程

什么是 JUC

JUC就是 java.util.concurrent 工具包的简称。这是一个处理线程的工具包,JDK1.5 开始出现的。

jdk 在线文档 https://tool.oschina.net/apidocs/apidoc?api=jdk-zh

进程和线程

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

总的来说

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位

例如,打开 QQ ,那么整个 QQ 的打开,就可以看作开启了一个进程,我们在 QQ 里面进行聊天,发文件等操作,各个操作就可以看为多个线程在运行,这些线程属于 QQ 这个进程

线程的状态

NEW 新建

RUNNABLE 准备就绪

BLOCKED 阻塞

WAITING 不见不散

某个线程规定了等待的时间,到达等待时间后,若没有被处理,则继续等待

TIMED_WAITING 过时不候

某个线程规定了等待的时间,到达等待时间后,若没有被处理,则不再等待

TERMINATED 终结

wait | sleep 区别

sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。

sleep 不会释放锁,它也不需要占用锁,wait 会释放锁,但调用它的前提是 当前线程占有锁 (即代码要在 synchronized 中)

二者都可以被 interrupted 方法中断,二者在哪里 sleep | wait ,就会在哪里重新唤醒

补充:与 wait() 经常一起使用的 notify() 或者 notifyAll() 在调用时,调用的对象本身并不会立即释放对象锁,必须等到 synchronized 方法或者语法块执行完才真正释放锁

并发和并行

串行模式

多个任务,按照先后顺序进行执行,后面的任务需要等待前面的任务执行完,串行一次只能取得 一个 任务,并执行完这个任务,才能进行下一个任务

并行模式

多个任务同时取得多个任务并同时运行,并行相当于将串行中,长长的一条任务队列,分成多段来同时运行,可以理解为多个串行在同时运行,并行的效率从代码层次上强依赖于多进程 | 多线程代码,从硬件角度上则依赖于多核 CPU

并发模式

并发(concurrent) 指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行,并发的重点在于它是一种现象,并发描述的是多进程同时运行的现象(多任务),对于单核心 cpu ,同时只能运行一个线程,所以,这里的【同时运行】表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会

要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象

可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果

可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务

可以多进程 | 多线程的方式并行执行这些小任务。也可以单进程 | 单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率

总结

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点,如:春运抢票

并行:多项工作一起执行,之后再汇总,如:泡方便面,电水壶烧水,一边撕调料倒入桶中,最终泡面完成

管程

管程(monitor),也就是通常所说的锁,管程是一种同步机制,它保证在同一时刻,只有一个线程对保护的资源进行访问

JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着Java 对象一同创建和销毁,执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

用户线程 | 守护线程

用户线程:自定义的线程

守护线程:如 jvm 中,垃圾回收所属的线程

补充

主线程:当一个 Java 程序启动的时候,会有一个线程立即开始运行,这个线程通常被我们叫做程序中的主线程,因为它是在我们程序开始的时候就被执行的线程。

子线程都从该线程中被孵化

通常它都是最后一个执行结束的线程,因为它会执行各种的关闭操作

public class Demo {
    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            /*
            Thread.currentThread().isDaemon() 判断当前线程,是否为守护线程
             */
            System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().isDaemon());

            while (true) {

            }
        }, "aa");

        aa.start();// 创建,运行线程

        System.out.println("主线程:" + Thread.currentThread().getName() + " 运行结束了");
    }
}

如上代码,主线程已经结束了,而用户线程还在运行,则 jvm 也会一直处于存活状态

若将线程 aa 设置为守护线程后再运行,则主线程结束后,守护线程也随之结束了,此时没有其他线程运行了,jvm 也随之结束,注意,守护线程的设置,需要在线程执行之前设置

public class Demo {
    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            /*
            Thread.currentThread().isDaemon() 判断当前线程,是否为守护线程
             */
            System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().isDaemon());

            while (true) {

            }
        }, "aa");

        aa.setDaemon(true); // 设置为守护线程
        aa.start();// 创建,运行线程

        System.out.println("主线程:" + Thread.currentThread().getName() + " 运行结束了");
    }
}

Lock 接口

Synchronized 关键字回顾

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 { } 括起来的代码,作用的对象是调用这个代码块的对象;

修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了

修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用的主要对象是这个类的所有对象

Synchronized 卖票案例

多线程的编程步骤:

第一步,创建资源类,在资源类创建属性和操作方法

第二步,创建多个线程,调用资源类的操作方法

代码演示

创建多线程,有继承 Thread 的方式 | 实现 Runnable 的方式,其中实现 Runnable 接口的方式,较多使用

package com.dhj.juc.syncdemo;

/**
 * synchronized 关键字,买票演示
 */

// 第一步,创建资源类
class Ticket {

    // 创建属性
    private int number = 30;

    // 创建操作方法
    public synchronized void sale() {
        if (number > 0) {
            number--;
            System.out.format("%s 卖出一张票,当前剩余票数: %s\n", Thread.currentThread().getName(), number);
        }
    }
}

public class SaleTicket {

    // 第二步,创建多个线程
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        // 创建线程 aa
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    // 调用资源类中的操作方法
                    ticket.sale();
                }
            }

        }, "aa").start();

        // 创建线程 bb
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    // 调用资源类中的操作方法
                    ticket.sale();
                }
            }

        }, "bb").start();

        // 创建线程 cc
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    // 调用资源类中的操作方法
                    ticket.sale();
                }
            }

        }, "cc").start();
    }
}

Lock 接口概述

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,Lock 替代了被 synchronized 修饰的方法和语句的使用

与 Lock 经常一起出现的,还有 Condition 接口,Condition 替代了 Object 监视器方法:wait() | notify() 等的使用

所有的已知实现类

ReentrantLock | ReentrantReadWriteLock.ReadLock | ReentrantReadWriteLock.WriteLock

Lock 和 synchronized 的区别

Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问

Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象

Lock 中的常用方法

lock()

该方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待

采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}finally{}块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生

unlock()

释放锁

newCondition()

关键字 synchronized 与 wait() | notify() 这两个方法一起使用可以实现等待 | 通知模式,Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待 | 通知模式

前面提到的 Condition 实例实质上需要被绑定到一个 Lock 锁上,要为特定 Lock 实例获得 Condition 实例,只能使用其 newCondition() 方法

用 notify() 通知时,JVM 会随机唤醒某个等待的线程(jdk1.8 是按照等待的顺序进行唤醒),使用 Condition 类也是如此,Condition 比较常用的几个方法:

await():会使当前线程等待,同时会释放锁,当其他线程使用同一个 Condition 对象调用 signal() 时,线程会重新获得锁并继续执行

signal():在该方法调用后会从当前 Condition 对象的等待队列中,唤醒一个线程,在当前 JDK 版本为 1.8.0_291 的情况下,唤醒线程的顺序为线程等待的顺序(先进先出),不同版本的 JDK 可能不一样
具体细节可以参考 https://blog.csdn.net/thlzjfefe/article/details/109961385

signalAll():唤醒当前 Condition 对象的等待队列中,所有的等待线程

注意:在调用 Condition 的 await() | signal() 方法前,也需要线程持有相关的 Lock 锁,调用 await() 后线程会释放这个锁,在 singal() 调用后会从当前 Condition 对象的等待队列中,按照线程等待的顺序唤醒一个线程,唤醒的线程尝试获得锁,获得锁成功后,才能继续执行

await | signal | signalAll | unlock 等方法的执行,都需要先获取锁,否则会抛出异常 java.lang.IllegalMonitorStateException

关于 signal 唤醒顺序的实验代码

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

public class ConditionTest {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public void print() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("线程:" + Thread.currentThread().getName() + " 等待");
            condition.await();
            System.out.println("线程:" + Thread.currentThread().getName() + " 被唤醒");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {

        ConditionTest conditionTest = new ConditionTest();

        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        conditionTest.print();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Thread-" + (i + 1)).start();
        }

        try {
            Thread.sleep(1000); // 休眠是为了给当前线程一个空闲时间,确保之前的线程创建完成,启动且被等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();


        for (int i = 0; i < 20; i++) {
            try {
                // 休眠是为了给当前线程一个空闲时间,确保当前线程唤醒的前一个 wait 的线程有时间被唤醒且打印输出
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            condition.signal(); // 唤醒需要先获取锁
            lock.unlock();
        }
    }
}

执行代码后,从控制台可以看出,唤醒的线程的顺序就为线程被等待时的顺序

包括 notify() 方法,也进行了测试

public class NotifyTest {

    private final Object obj = new Object();

    public void print(int i) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("线程: " + Thread.currentThread().getName() + " 等待");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程: " + Thread.currentThread().getName() + " 被唤醒");
                }
            }
        }, "Thread-" + (i + 1)).start();
    }

    public static void main(String[] args) {

        NotifyTest notifyTest = new NotifyTest();

        for (int i = 0; i < 20; i++) {
            notifyTest.print(i);
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();


        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (notifyTest.obj) {
                notifyTest.obj.notify();
            }
        }
    }
}

测试后发现,不论是 notify() 还是 signal() 唤醒线程时,在 JDK1.8 中,都是根据线程等待时的先后顺序**(先进先出)**进行唤醒的

ReentrantLock 可重入锁

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法,下面通过代码具体看一下其基本使用

import java.util.concurrent.locks.ReentrantLock;

// 第一步,创建资源类
class LTicket {
    // 票数
    private int number = 300;

    // 创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();

    // 卖票方法
    public void sale() {
        lock.lock();// 上锁
        try {
            if (number > 0) {
                // 模拟异常情况下,释放 lock 锁
                if ("cc".equalsIgnoreCase(Thread.currentThread().getName())) {
                    System.out.println("线程 cc 异常");
                    throw new RuntimeException();
                }
                number--;
                System.out.format("%s 卖出一张票,当前剩余票数: %s\n", Thread.currentThread().getName(), number);
            }
        } finally {
            // 防止出现异常,无法执行解锁操作
            lock.unlock();// 解锁
        }
    }
}

public class LSaleTicket {
    public static void main(String[] args) {
        LTicket lTicket = new LTicket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 400; i++) {
                    lTicket.sale();
                }
            }
        }, "aa").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 400; i++) {
                    lTicket.sale();
                }
            }
        }, "bb").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 400; i++) {
                    lTicket.sale();
                }
            }
        }, "cc").start();
    }
}

补充:调用了 start() 方法,线程就会马上创建吗?

start() 方法的源码如下

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            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 */
            }
        }
    }

观察源码,发现,只有当 start0() 方法执行完成后,才会将 started 置为 true,根据 started 的值来判定线程是否创建成功

而 start0 方法定义如下,该方法被 native 关键字修饰

private native void start0();

关于 native 关键字,定义如下:一个 native method 就是一个 Java 调用非 Java 代码的接口。一个 native method 是这样一个 Java 的方法:该方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如 C 和 C++)实现的文件中

这说明 Java 中线程的启动,实际上不是由 Java 自身来决定的,是通过调用外部(在这里是操作系统,线程的运行需要本地操作系统的支持,此方法调用的是本地操作系统的函数) 来决定是否要创建当前这样一个线程,创建成功后,才会执行 started = true;,就可以通过判断该值来判定线程是否创建成功

结论就是,尽管调用了 start() 方法,线程也不会马上被创建出来,需要由操作系统决定

相关链接 https://blog.csdn.net/bigcakewshwl/article/details/50536994

线程通信

线程间通信的模型有两种:共享内存和消息传递

之前提到过,多线程编程的上半部分为

第一步,创建资源类在资源类创建属性和操作方法

第二步,创建多个线程,调用资源类的操作方法

下面补充完整其中部

第一步,创建资源类在资源类创建属性和操作方法

第二部,(1) 判断,(2) 干活,(3) 通知

第三步,创建多个线程,调用资源类的操作方法

代码示例

/*
第一步 创建资源类,定义属性和操作方法
 */
class Share {
    private int number = 0;

    // +1
    public synchronized void incr() throws Exception {
        /*
         第二步 判断 干活 通知
         */
        // 判断
        if (number != 0) {
            this.wait(); // 线程等待 释放锁
        }
        number++; // 干活
        System.out.format("线程:%s 执行 +1 操作,当前 number 值为:%d\n", Thread.currentThread().getName(), number);
        this.notifyAll(); // 通知 唤醒其他等待的线程
    }

    // -1
    public synchronized void decr() throws Exception {
        if (number != 1) {
            this.wait();
        }
        number--;
        System.out.format("线程:%s 执行 -1 操作,当前 number 值为:%d\n", Thread.currentThread().getName(), number);
        this.notifyAll();
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Share share = new Share();

        // +1 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        share.incr();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "aa").start();

        // -1 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        share.decr();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "bb").start();
    }
}

通过两个线程 incr | decr,交替的操作同一资源,当满足 incr 线程执行条件时时,该线程会执行资源操作方法,然后调用 notifyAll() 唤醒其他等待的线程,注意:notify() 或者 notifyAll() 调用时并不会真正释放对象锁, 必须等到 synchronized 方法或者语法块执行完才真正释放锁,这里的 notifyAll() 在语法的最后执行,因此会释放锁

incr 线程释放锁后,decr 也被唤醒了,判断是否满足线程的执行条件,若不满足,则会 wait(),wait 会直接释放锁且当前线程等待,直到被唤醒

由此实现了交替的对资源进行符合规则的操作,线程之间也建立了基本的通信

虚假唤醒问题

此时,我们若是再多起几个线程,如 cc、dd 也是分别交替执行 +1、-1 的操作,会发现 number 的值变得没有规律甚至为负数,这是虚假唤醒问题的体现

使用 wait() 方法时,可能存在这样一种情况,当 aa 线程进行了 +1 操作,唤醒其他线程且释放锁后,被 cc 线程抢到了锁,cc 线程执行的是 +1 操作,此时,number 值为 1,不满足条件,cc 线程 wait,释放了锁,又被 aa 线程抢到,此时 number 值依然为 1,aa 线程 wait,释放锁,还是被 cc 线程抢到

注意,此时情况发送了变化,cc 线程刚刚判断不符合操作条件时,一直处于 wait 状态,而 wait 有一个特点,从哪里等待,就从哪里执行,根据上面代码的语法,cc 在之前 wait 时,已经执行过了 if 判断,被再次唤醒时,不会经过 if 条件来判断线程执行的操作是否符合要求,直接就会执行对资源的操作:number++,导致 number 的值不再是规定的 0、1 两种状态,而是变为了这里的 2 或者是其他的一些值

if (number != 1) {
	this.wait();
}
number--;

这就是线程的虚假唤醒问题

针对这种问题,我们可以将 wait 操作放入 while 循环中,当线程被 wait,下次唤醒时,其依然会被 while 循环困住,在 while 中我们就可以判断线程的操作是否可以继续执行,否则就 wait,下次再被唤醒时,也是一样,避免了线程的虚假唤醒问题

修改后的代码如下

/*
第一步 创建资源类,定义属性和操作方法
 */
class Share {
    private int number = 0;

    // +1
    public synchronized void incr() throws Exception {
        /*
         第二步 判断 干活 通知
         */
        // 判断
        while (number != 0) {
            this.wait();
        }
        number++; // 干活
        System.out.format("线程:%s 执行 +1 操作,当前 number 值为:%d\n", Thread.currentThread().getName(), number);
        this.notifyAll(); // 通知
    }

    // -1
    public synchronized void decr() throws Exception {
        while (number != 1) {
            this.wait();
        }
        number--;
        System.out.format("线程:%s 执行 -1 操作,当前 number 值为:%d\n", Thread.currentThread().getName(), number);
        this.notifyAll();
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Share share = new Share();

        // +1 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        share.incr();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "aa").start();

        // -1 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        share.decr();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "bb").start();
    }
}

多线程编程三步

虚假唤醒问题,也就是多线程编程:上步 | 中步 | 下步 | 中的下步,完整步骤如下

第一步,创建资源类在资源类创建属性和操作方法

第二步,(1) 判断,(2) 干活,(3) 通知

第三步,创建多个线程,调用资源类的操作方法

第四步,防止虚假唤醒问题

Lock 实现

将刚刚的线程通信案例,使用 Lock 来实现

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

// 资源类
class Share {
    private static int number = 0;
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    // +1
    public void incr(int i) throws InterruptedException {
        lock.lock(); // 加锁
        try {
            while (number != 0) {
                condition.await();// 等待
            }
            number++;
            System.out.println("当前线程: " + Thread.currentThread().getName() +
                    "执行 +1 操作:" + i + " 次," + "number 值为: " + number);
            condition.signalAll();// 唤醒所有等待的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }

    // -1
    public void decr(int i) throws InterruptedException {
        lock.lock(); // 加锁
        try {
            while (number == 0) {
                condition.await();// 等待
            }
            number--;
            System.out.println("当前线程: " + Thread.currentThread().getName() +
                    " 执行 -1 操作: " + i + " 次," + "number 值为: " + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Share share = new Share();

        // +1
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.incr(i + 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "aa").start();

        // -1
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.decr(i + 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "bb").start();
    }
}

线程定制化通信

前面只实现了线程之间的通信,而没有做到一个定制化的效果(指定唤醒或者等待线程而不是随机的),现在需要 aa 线程在控制台打印 5 次,bb 打印 10 次,cc 打印 15 次,整体循环 10 次,又该怎么实现呢?

比较常用的方法,就是通过标志位来实现

代码演示

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

/**
 * 线程定制化通信
 */

class Print2 {
    private static int flag = 1;

    private static final ReentrantLock lock = new ReentrantLock();

    private static final Condition condition1 = lock.newCondition();// 对应线程 aa
    private static final Condition condition2 = lock.newCondition();// 对应线程 bb
    private static final Condition condition3 = lock.newCondition();// 对应线程 cc

    // aa 线程打印
    public void aaPrint(int loop) throws InterruptedException {
        lock.lock(); // 上锁
        try {
            while (flag != 1) {
                System.out.println("当前线程: " + Thread.currentThread().getName() + " 被休眠");
                condition1.await();// 使用 condition1 休眠当前线程,释放锁
            }
            for (int i = 0; i < flag * 5; i++) {
                System.out.println("线程: " + Thread.currentThread().getName()
                        + " 打印 " + (i + 1) + " 次");
            }
            System.out.println("线程: " + Thread.currentThread().getName() + " 第 " + loop + " 轮打印结束");
            flag = 2;
            condition2.signal();// 唤醒被 condition2 休眠的线程中的任意一个
        } finally {
            lock.unlock();
        }
    }

    // bb 线程打印
    public void bbPrint(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                System.out.println("当前线程: " + Thread.currentThread().getName() + " 被休眠");
                condition2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("线程: " + Thread.currentThread().getName()
                        + " 打印 " + (i + 1) + " 次");
            }
            System.out.println("线程: " + Thread.currentThread().getName() + " 第 " + loop + " 轮打印结束");
            flag = 3;
            condition3.signal();
        } finally {
            lock.unlock();
        }
    }

    // cc 线程打印
    public void ccPrint(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                System.out.println("当前线程: " + Thread.currentThread().getName() + " 被休眠");
                condition3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println("线程: " + Thread.currentThread().getName()
                        + " 打印 " + (i + 1) + " 次");
            }
            System.out.println("线程: " + Thread.currentThread().getName() + " 第 " + loop + " 轮打印结束");
            flag = 1;
            condition1.signal();// 继续循环唤醒线程 aa
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo2_1 {

    public static void main(String[] args) {
        Print2 print2 = new Print2();

        // aa 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print2.aaPrint(i + 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "aa").start();

        // bb 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print2.bbPrint(i + 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "bb").start();

        // cc 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print2.ccPrint(i + 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "cc").start();
    }
}

定义了标志位 flag,不同的线程通过判断标志位进行休眠,注意,这里休眠时,不同的线程使用的 Condition 对象是不一样的,一个线程对应了一个 Condition 对象,这样,尽管 singal() 方法在调用后会从当前 Condition 对象的等待队列中,按照等待的顺序唤醒一个线程,但是我们只为每个 Condition 对象绑定了一个线程,相当于就是唤醒指定线程了

由此就可以达到,按照规定的顺序唤醒线程来切换标志位,线程根据标志位来执行指定的操作,实现线程的定制化通信

上面是教程中的方式,通过不同的 Condition 来实现线程的切换(定制化通信)

本人也有一种通过传参来切换线程打印的方法,虽然没有定制化线程通信,但也是一种解决问题的思路,代码如下

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

/**
 * 线程定制化通信问题(个人思路,没有涉及到定制化,只是一种解决问题的思路)
 */
class Print {
    private static int flag = 1;// 标志位
    private static int aaforNum = 0;// aa 线程整体循环计数
    private static int bbforNum = 0;// bb 线程整体循环计数
    private static int ccforNum = 0;// cc 线程整体循环计数

    private static final ReentrantLock lock = new ReentrantLock(); // ReentrantLock 可重入锁
    private static final Condition condition = lock.newCondition();//

    public void print(int i) throws InterruptedException {
        lock.lock();// 加锁
        try {
            while (flag != i) {
                condition.await();// 等待
            }
            for (int j = 0; j < i * 5; j++) {
                System.out.println("线程: " + Thread.currentThread().getName()
                        + " 打印: " + (j + 1) + " 次");
            }
            switch (i) {
                case 1:
                    aaforNum++;
                    System.out.println("线程: " + Thread.currentThread().getName()
                            + " 整体循环: " + aaforNum + " 次");
                    break;
                case 2:
                    bbforNum++;
                    System.out.println("线程: " + Thread.currentThread().getName()
                            + " 整体循环: " + aaforNum + " 次");
                    break;
                case 3:
                    ccforNum++;
                    System.out.println("线程: " + Thread.currentThread().getName()
                            + " 整体循环: " + aaforNum + " 次");
                    break;
                default:
                    System.out.println("参数出错!");
            }
            flag = i == 3 ? 1 : i + 1;
            condition.signalAll();// 唤醒所有线程
        } finally {
            lock.unlock();// 解锁
        }
    }
}

public class ThreadDemo2 {

    public static void main(String[] args) {
        Print print = new Print();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print.print(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "aa").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print.print(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "bb").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        print.print(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "cc").start();
    }
}

集合的线程安全问题

ArrayList 集合线程不安全

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * List 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                }
            }, String.valueOf(i)).start();
        }
    }
}

当多个线程并发的对一个 ArrayList 集合进行访问,修改等操作,因为是并发访问的,可能在 add 数据时,其他线程又在取出数据或者访问数据,亦或是,在访问数据的同时,又在 add 数据且数据还没有完全 add,就会抛出如下异常 java.util.ConcurrentModificationException 并发修改异常

解决方案

针对上述问题,有三种解决方案

方案 1

使用 List 接口的另一个实现类 Vector ,该类中的方法被 synchronized 关键字修饰,在操作 | 访问集合元素的过程中,能够保证并发访问的线程安全问题

/**
 * List 集合线程不安全问题解决方案 1
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        List<String> list = new Vector<>();

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                }
            }, String.valueOf(i)).start();
        }
    }
}

方案 2

使用 java.util.Collections 工具类中的 synchronizedList 方法,返回指定列表支持的同步(线程安全的)列表

/**
 * List 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        // Collections.synchronizedList 返回指定列表支持的同步列表(线程安全)
        List<String> list = Collections.synchronizedList(new ArrayList<>());

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                }
            }, String.valueOf(i)).start();
        }
    }
}

方案 3

使用 java.util.concurrent 包下的 CopyOnWriteArrayList 类来解决线程不安全问题

它相当于线程安全的 ArrayList,和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的是,它具有以下特性

/**
 * List 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                }
            }, String.valueOf(i)).start();
        }
    }
}

使用 CopyOnWriteArrayList 进行读操作时,使用的依然是并发读的方式,但是在写操作时,使用的是一种【写时复制技术】

在 CopyOnWriteArrayList 中,写操作不是并发的写,而是独立的写,过程:准备一个跟【之前集合】大小相同的【新集合】,复制原来的集合中所有的数据到新集合中且将新的数据也写入新集合,在写入操作完成之前,若有读操作进来,依然读取【之前的集合】,写入操作完成后,将指向【之前集合】的引用指向【新集合】,此时,并发的读操作访问的就是【新集合】

多次读写元素的流程,也是如此,相关源码如下

public boolean add(E e) {
        final ReentrantLock lock = this.lock; // 使用 Lock
        lock.lock();// 上锁 保证并发时的线程安全
        try {
            Object[] elements = getArray(); // 获取原数组
            int len = elements.length; // 获取数组长度
            
            // 将原数组中的数据 copy 到新数组 len + 1 是给新添加的元素 e 留的索引位置
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e; // 在新数组中添加数据
            setArray(newElements); // 将原数组的引用指向新数组
            return true;
        } finally {
            lock.unlock(); // 解锁
        }
    }

通过上述方式来写入数据,尽管在并发方式下访问,也能保证在当前线程添加 | 修改 | 删除完数据后,其他线程对数据进行访问时,能够得到最新的数据且使用 ReentrantLock 来保持线程的安全

HashSet | HashMap 的线程不安全

HashSet 的线程不安全

/**
 * HashSet 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {
    
        Set<String> set = new HashSet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    set.add(UUID.randomUUID().toString().substring(0,8));
                    System.out.println(set);
                }
            }, String.valueOf(i)).start();
        }
    }
}

对于 HashSet 集合,使用多个线程来并发访问和变动元素时,会出现 ConcurrentModificationException 异常,说明其存在线程不安全问题

HashSet 的线程不安全问题,可以通过使用 CopyOnWriteArraySet 来避免,CopyOnWriteArraySet 又通过 ReentrantLock 来解决线程安全问题

/**
 * HashSet 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        Set<String> set = new CopyOnWriteArraySet<>(new HashSet<>());

        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    set.add(UUID.randomUUID().toString().substring(0,8));
                    System.out.println(set);
                }
            }, String.valueOf(i)).start();
        }
    }
}

补充:HashSet 的底层实际是通过 HashMap 来实现元素的不重复,存入 HashSet 的元素,是被作为 HashMap 的 key 来存储的

HashMap 的线程不安全

不安全演示

/**
 * Map 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < 30; i++) {
            String key = String.valueOf(i);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    map.put(String.valueOf(key), UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(map);
                }
            }, String.valueOf(i)).start();
        }
    }
}

以上,Map 集合在并发访问和变动元素时,也会出现线程安全问题,抛出异常:ConcurrentModificationException

针对 Map 集合,可以使用 ConcurrentHashMap 来保证线程安全

/**
 * HashMap 集合线程不安全问题 demo
 */
public class ThreadDemo3 {
    public static void main(String[] args) {

        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            String key = String.valueOf(i);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    map.put(String.valueOf(key), UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(map);
                }
            }, String.valueOf(i)).start();
        }
    }
}

ConcurrentHashMap 是通过 synchronized 关键字来实现线程安全

总结

针对 ArrayList 的线程安全问题,使用 java.util.concurrent.CopyOnWriteArrayList 解决

针对 HashSet 的线程安全问题,使用 java.util.concurrent.CopyOnWriteArraySet 解决

针对 HashMap 的线程安全问题,使用 java.util.concurrent.ConcurrentHashMap 解决

Synchronized 八锁

准备资源类

class Phone {

    public synchronized void sedSMS() throws Exception {
        System.out.println("-----sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("-----sendEmail");
    }


    public void getHello() {
        System.out.println("-----getHello");
    }
}

根据资源类,针对不同的情况进行线程调用

情况一

使用两个线程,进行标准访问,先短信还是先邮件 ?

public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
   
        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendSMS
-----sendEmail

原因:

被 synchronized 修饰的同步方法,锁的是调用该方法的 Phone 对象,这里使用的同一个 Phone 对象,因为在创建线程 AA 后,还休眠了 100ms 保证线程 AA 的创建,因此线程 AA 大概率会提前被创建且抢到锁,锁住 Phone 对象,使得调用 sendEmail() 方法的 BB 线程只能等待,造成了短信先,邮件后的效果

情况二

在短信方法中,停留 2 秒,先短信还是邮件 ?

class Phone {

    public synchronized void sedSMS() throws Exception {
        // 停留 2 秒
        TimeUnit.SECONDS.sleep(2);
        System.out.println("-----sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("-----sendEmail");
    }


    public void getHello() {
        System.out.println("-----getHello");
    }
}

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
       
        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendSMS
-----sendEmail

原因:

和上面的情况一样,BB 线程必须要等 AA 线程执行完释放锁后,才能抢到被 AA 线程锁住的 Phone 对象执行邮件方法,因此短信先,邮件后

情况三

新增调用普通的 getHello() 方法,与调用短信方法比较,先 hello 还是先短信 ?

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.getHello();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----getHello
-----sendSMS

原因:

getHello() 作为一个普通方法,不会因为锁的问题而等待其他线程,肯定会最新执行

情况四

两部手机,先短信还是邮件 ?

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendEmail
-----sendSMS

原因:

synchronized 方法,只会锁住当前调用该方法的对学校,不同 Phone 对象,同步方法的锁之间互不干扰,因此,BB 线程不会等待 AA 线的锁释放而执行,二者在 start() 方法后,根据操作系统的调度决定先后顺序,因为 AA 线程休眠了 2 秒,所以大概率 BB 线程会先执行,也就是邮件先于短信

情况五

两个静态同步方法,1 部手机,先打印短信还是邮件

class Phone {
    public static synchronized void sedSMS() throws Exception {
        // 停留 2 秒
        TimeUnit.SECONDS.sleep(2);
        System.out.println("-----sendSMS");
    }

    public static synchronized void sendEmail() throws Exception {
        System.out.println("-----sendEmail");
    }


    public void getHello() {
        System.out.println("-----getHello");
    }
}

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendSMS
-----sendEmail

原因:对于静态的同步方法,锁的是当前类的 Class 对象,简单理解也就是锁住了当前类,所有的静态同步方法使用的都是同一把锁,这就好比一个静态的成员变量,不管 new 多少个对象,静态的成员变量都是共享的,既然锁都是同一把,那么就是先短信后邮件

情况六

两个静态同步方法,2 部手机,先打印短信还是邮件

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendSMS
-----sendEmail

原因:

情况六和情况五没有本质区别,因为使用的都是静态的同步锁,多个对象之间,都是共享的,因此先短信后邮件

情况七

1 个静态同步方法,1 个普通同步方法,1 部手机,先打印短信还是邮件

class Phone {

    public static synchronized void sedSMS() throws Exception {
        // 停留 2 秒
        TimeUnit.SECONDS.sleep(2);
        System.out.println("-----sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("-----sendEmail");
    }

    public void getHello() {
        System.out.println("-----getHello");
    }
}

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendEmail
-----sendSMS

原因:

既有静态同步方法,又有普通同步方法,我们可以先从静态入手,所有的静态同步方法使用的是同一把锁,而普通同步方法使用的锁是当前调用该方法的对象本身(this) 这两把锁不是同一把,因此先邮件,后短信

情况八

1 个静态同步方法,1 个普通同步方法,2 部手机,先打印短信还是邮件

class Phone {

    public static synchronized void sedSMS() throws Exception {
        // 停留 2 秒
        TimeUnit.SECONDS.sleep(2);
        System.out.println("-----sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("-----sendEmail");
    }

    public void getHello() {
        System.out.println("-----getHello");
    }
}

/**
 * 锁的八种情况
 */
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        // 线程 AA
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone.sedSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        // 休眠一定时间,等待线程 AA 创建完成
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程 BB
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

结果:

-----sendEmail
-----sendSMS

原因:

根据情况七,静态同步与普通同步方法使用的不是一把锁,两个 Phone 使用的更不是一把锁,毫无疑问,先邮件后短信

总结

一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,【某一个时刻内】,只能有【唯一 一个线程】去访问这些 synchronized 方法

synchronized 方法锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法中,普通方法与同步锁无关,换成两个对象后,使用的就不是同一把锁了,【锁不相同】的情况下,线程之间就【无需等待】

synchronized 实现同步的基础:Java 中的【每一个对象】都可以【作为锁】具体表现为以下三种形式:

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的 Class 对象
  • 对于同步代码块,锁是括号里配置的对象,如 synchonized(lock){ } 的锁为 lock

当一个线程试图访问【同步代码块】时,它首先必须【得到锁】,退出或抛出异常时必须【释放锁】。也就是说如果一个实例对象的【非静态】同步方法获取锁后,该实例对象的其他【非静态同步方法】必须【等待】获取锁的方法【释放锁后】才能【获取锁】

可是【别的实例对象】的【非静态同步方法】因为跟【该实例对象】的【非静态同步方法】用的是【不同的锁】,所以【毋须等待】该实例对象【已获取锁的非静态同步方法释放锁】就可以获取【他们自己】的锁

所有的【静态同步方法】用的是【同一把锁——>类对象本身】,与【非静态的同步方法】相比,这二者的锁是【两个不同的对象】,所以【静态同步方法】与【非静态同步方法】之间是【不会有竞态条件】的

但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须【等待该方法释放锁后】才能【获取锁】,而【不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法】之间,只要它们是同一个类的实例对象即可

补充:关于锁的修饰符,当将锁对象修饰为 static 后,当前类的多个对象之间都使用的同一把锁,修饰为 final 后,锁不会被更改

公平锁和非公平锁

在之前的卖票程序中,通过空参构造器 new ReentrantLock(); 的方式来获取 lock 对象而获取锁使用,默认使用的实际上是一种非公平锁,通过源码来看

public ReentrantLock() {
	sync = new NonfairSync();
}

空参构造器底层,使用的实际上是一个名为 NonfairSync 的内部类来创建非公平锁,使用非公平锁存在一个现象:线程之间存在竞争关系,一旦某一个线程效率过高或者先启动,那么就会抢了其他线程的饭碗,使得其他线程就算没有被休眠或等待,也无事可做

相对的,有非公平锁,也就有公平锁,通过 new ReentrantLock(true); 在创建锁时,开启公平锁,同样的,将参数中的布尔值置为 false,就为非公平锁,开启公平锁的情况下,多个线程之间,不会出现像非公平锁那样,被某一个线程强行占用所有的执行机会,而是会将执行的机会进行一个较为公平的分配

公平和非公平的特点

非公平锁

存在一些线程无事可做

效率高

公平锁

多个线程之间能较为公平的分配到任务

效率低

二者没有绝对的好坏,根据使用的情况来选择

可重入锁

可重入锁,顾名思义,可以重复进入的锁,例如现在有一个可重入锁,锁住的区域之中,还存在多层区域,那么,我们只需要在最外层获取锁且进入,后续里面的其他层就可自由进出,因为实际上它们使用的都是同一把锁,而且一个线程能够多次重复的进入该锁,可重入锁也叫递归锁

可重入锁是一种特殊的互斥锁,它可以被同一个线程多次获取,而不会产生死锁。

  • 首先它是互斥锁:任意时刻,只有一个线程锁。即假设 A 线程已经获取了锁,在 A 线程释放这个锁之前,B 线程是无法获取到这个锁的,B 要获取这个锁就会进入阻塞状态。
  • 其次,它可以被同一个线程多次持有。即,假设 A 线程已经获取了这个锁,如果 A 线程在释放锁之前又一次请求获取这个锁,那么是能够获取成功的

之前的 synchronized 关键字和 Lock 都属于可重入锁,二者的区别在于,synchronized 是隐式(自动的上锁解锁)的,而 Lock 是显式(手动的上锁解锁)的

代码演示

public synchronized void add() {
	add();
}

某一个线程 A 调用了该方法,执行完后该方法后释放锁,因为是递归调用而没有其他逻辑,所有在执行完的瞬间可能线程 A 释放锁又获取了锁重新进入递归方法(同步方法需要先获取锁才能够进入),循环往复,最终会造成 java.lang.StackOverflowError 异常,这就是可重入锁的体现,如果 synchronized 不具备可重入锁可重复进入的特性,那么也就不会获取到锁重复的进入递归方法,造成 java.lang.StackOverflowError 异常

可重入锁也可以有 Lock 来演示

/**
 * Lock 可重入锁演示
 */
public class LockDemo {
    public static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("外层");

                    try {
                        lock.lock();
                        System.out.println("内层");
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }, "t1").start();
    }
}

当前线程 t1 在外层上了锁,在 unlock 之前,其他线程是不能进入的,当前线程也不能重新获取锁,因为可重入锁的特性,在遇到内层的 lock 时,无需通过解锁再次获取锁,就可以自由进出内层的锁,再一次体现了可重入锁自由进出的特性,否则的话,内层的打印语句就不会输出

如果我们当前线程最终不释放锁,对于当前线程没有影响,因为当前线程自己已经获取到了可重入锁,就算不释放锁,后续的锁都可以无障碍进入,不影响当前线程的执行,但是,如果我们还有其他线程,那么,就必须要释放锁,否则就只有当前线程顺利执行完,而其他线程因为当前线程没有释放锁而被一直阻塞

死锁

什么是死锁

两个或者两个以上的进程在执行过程中,因为争夺资源而互相等待的现象,如果没有外力的干涉,相关进程就无法再执行下去

造成死锁的原因

系统资源不足

进程推进的顺序不合适

资源分配不当

代码演示

import java.util.concurrent.TimeUnit;

public class DeadLock {
    static final Object a = new Object(); // a 锁
    static final Object b = new Object(); // b 锁

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName() + "持有锁 a ,试图获取锁 b");

                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (b) {
                        System.out.println(Thread.currentThread().getName() + "持有锁 b");
                    }
                }

            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName() + "持有锁 b ,试图获取锁 a");

                    synchronized (a) {
                        System.out.println(Thread.currentThread().getName() + "持有锁 a");
                    }
                }
            }
        }, "t2").start();
    }
}

结果如下

t1持有锁 a ,试图获取锁 b
t2持有锁 b ,试图获取锁 a

在 t1 中,外层的同步块已经持有了锁 a ,内层的同步块需要持有锁 b ,但是因为被休眠了一秒,使其不能立即获取到锁 b,而锁 b 这时被启动的 t2 线程获取了还没有释放,锁 b 的内层同步块又需要获取锁 a 才能执行,但是锁 a 也已经被 t1 线程获取了,两个线程相持不下,程序一直运行,造成死锁的情况

死锁如何验证

上述的代码,是我们自己写的,我们知道它就是死锁,但是,在实际开发过程中,我们不确定这种情况是不是死锁,因为造成程序一直运行的情况有很多种,所以下,需要一种方法来判断是否是死锁的情况

验证死锁,可以使用到两个命令

jps,类似于 linux 中的 ps -ef

jstack,jvm 自带的堆栈跟踪工具

先使用 jps 查看当前进程,如下

15744 DeadLock
1600
1028 Jps
9044 Launcher
14072 KotlinCompileDaemon

其中,DeadLock 正是我们死锁类的进程,其进程 id 为 15744

通过进程 id ,使用 jstack 15744 命令来查询,结果如下

2021-07-14 13:53:05
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.291-b10 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000028c48556800 nid=0x3e10 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t2" #12 prio=5 os_prio=0 tid=0x0000028c642a4800 nid=0x25ec waiting for monitor entry [0x000000cb93dff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.dhj.juc.syncdemo.DeadLock$2.run(DeadLock.java:37)
        - waiting to lock <0x000000076b99ca28> (a java.lang.Object)
        - locked <0x000000076b99ca38> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

"t1" #11 prio=5 os_prio=0 tid=0x0000028c642a4000 nid=0x1a84 waiting for monitor entry [0x000000cb93cff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.dhj.juc.syncdemo.DeadLock$1.run(DeadLock.java:23)
        - waiting to lock <0x000000076b99ca38> (a java.lang.Object)
        - locked <0x000000076b99ca28> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000028c641ee800 nid=0x3270 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000028c637bb000 nid=0x329c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000028c63748800 nid=0x2d4c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000028c63747000 nid=0x3e08 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000028c63760800 nid=0x1c68 runnable [0x000000cb936fe000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x000000076b88fae8> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x000000076b88fae8> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000028c636b5800 nid=0x3004 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000028c636b4800 nid=0x2bc4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000028c62f5a000 nid=0x3b70 in Object.wait() [0x000000cb933fe000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b708ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x000000076b708ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000028c63685800 nid=0x3d5c in Object.wait() [0x000000cb932ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b706c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076b706c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x0000028c63661800 nid=0x29c8 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000028c4856d800 nid=0x3a84 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000028c48570000 nid=0x1350 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000028c48571000 nid=0x1b6c runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000028c48572800 nid=0x3d58 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x0000028c641f3800 nid=0x32fc waiting on condition

JNI global references: 12


Found one Java-level deadlock:
=============================
"t2":
  waiting to lock monitor 0x0000028c6368b258 (object 0x000000076b99ca28, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x0000028c6368c6f8 (object 0x000000076b99ca38, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2":
        at com.dhj.juc.syncdemo.DeadLock$2.run(DeadLock.java:37)
        - waiting to lock <0x000000076b99ca28> (a java.lang.Object)
        - locked <0x000000076b99ca38> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at com.dhj.juc.syncdemo.DeadLock$1.run(DeadLock.java:23)
        - waiting to lock <0x000000076b99ca38> (a java.lang.Object)
        - locked <0x000000076b99ca28> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

我们可以看到其完整的堆栈跟踪信息,Found 1 deadlock. 代表,当前进程,发现一个死锁,

其中

"t2":
  waiting to lock monitor 0x0000028c6368b258 (object 0x000000076b99ca28, a java.lang.Object),
  which is held by "t1"

代表 t2 在等待 0x0000028c6368b258 锁,但是该锁被 t1 持有,held(持有的意思)

后面的类似命令意思也是如此

通过以上方式,就能方便查看是否存在死锁问题

Callable 接口

目前,我们创建线程的方式有两种

一、继承 Thread 类

二、实现 Runnable 接口

在 jdk1.5 以后,又出现了第三种,第四种方式,分别为 Callable 接口和线程池的方式

之前 Runnable 缺少的一项功能是,当线程终止时(即 run() 完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了Callable 接口

Callable & Runnable 的比较

是否有返回值

Callable 有返回值,Runnable 无返回值

是否抛出异常

Callable 在方法无法计算出结果时,会抛出异常,而 Runnable 不会抛出异常

实现方法的名称不同

Callable 实现方法名为 call(),Runnable 实现方法的名称为 run()

FutureTask

使用 Callable 接口时,不能直接通过传参的方式创建线程,因为 Thread 类的构造器没有类型为 Callable 的,这时,需要找一个中间人,来完成线程的创建

Future 接口就是这样一个中间人,它的子接口 RunnableFuture 就继承了 Runnable 接口,它的子实现类 FutureTask 则实现了 RunnableFuture 接口且 FutureTask 的构造方法支持 Callable 类型

FutureTask 未来任务执行流程

当前线程运行过程中,需要执行其他任务,就可以单开一个线程,当前线程继续,在单开的线程运行完成时,当前线程可以随时通过 get 来取得运行结果,整个过程是异步的

未来任务的计算流程只会进行一次,第二次不会计算直接返回结果

常用方法

public boolean cancel(boolean mayInterrupt); 用于停止任务

如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true 时才会中断任务

public Object get(); 抛出 InterruptedException,ExecutionException

用于获取任务的结果,如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果

public boolean isDone();

如果任务完成,则返回 true,否则返回 false

主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态

一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果

仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法

一旦计算完成,就不能再重新开始或取消计算

get 方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常

get 只计算一次,因此 get 方法放到最后

总结

在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状

代码简单演示

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class FutureTaskDemo {

    public static void main(String[] args) {

        FutureTask<String> task = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                TimeUnit.SECONDS.sleep(4);
                return Thread.currentThread().getName();
            }
        });

        new Thread(task, "t1").start();

        // 循环判断任务是否执行完成
        while (!task.isDone()) {
            System.out.println("task 任务执行中");
        }

        // 异步获取任务结果
        try {
            System.out.println(task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

详细的说明可以参考 https://www.jianshu.com/p/55221d045f39 ,https://www.cnblogs.com/dennyzhangdd/p/7010972.html 或者 jdk 文档

JUC 辅助类

JUC中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时Lock锁的频繁操作

减少计数 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减1的操作,使用 await 方法等待计数器不大于0,然后继续执行 await 方法之后的语句

CountDownLatch 主要有两个方法

当一个或多个线程调用 await 方法时,这些线程会阻塞

其它线程调用 countDown 方法会将计数器减1(调用countDown方法的线程不会阻塞)

当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

简单演示

import java.util.concurrent.CountDownLatch;

/**
 * 演示 CountDownLatch
 */
public class CountDownLatchDemo {


    public static void main(String[] args) {

        // 设置初始值
        CountDownLatch count = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "离开教室");

                    // 计数 -1
                    count.countDown();
                }
            }, String.valueOf(i)).start();
        }
        try {
            // 计时器不为 0,继续等待
            count.await();
            System.out.println(Thread.currentThread().getName() + "锁门");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

循环栅栏 CyclicBarrier

CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await() 之后的语句。可以将 CyclicBarrier 理解为加1操作

代码演示

import java.util.concurrent.CyclicBarrier;

/**
 * CyclicBarrier 演示
 */
public class CyclicBarrierDemo {
    private static final int NUM = 7;// 设定一个目标障碍数

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM, new Runnable() {

            // 达到目标障碍数时,才执行 run 方法
            @Override
            public void run() {
                System.out.println("达到目标障碍数");
            }
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    try {
                        // 每一次 await 都会算作一次目标障碍数
                        cyclicBarrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, String.valueOf(i)).start();
        }
    }
}

若达不到目标障碍数,则线程会一直等待

信号灯 Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放,许可场景: 抢车位, 6 部汽车 3 个停车位

代码演示

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * SemaphoreDemo 信号灯
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 创建一个信号灯,信号量为 3
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获得一个信号量,当信号量获取到 3 次时,无法获取
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "获取信号量成功!");
                        TimeUnit.SECONDS.sleep(new Random().nextInt(3));// 随机休眠
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 在 finally 块中,保证信号量的释放
                        semaphore.release();// 释放信号量,使得当前获取的信号量小于 3 时,就可以再次获取信号量
                        System.out.println(Thread.currentThread().getName() + "释放信号量");
                    }
                }
            }, String.valueOf(i)).start();
        }
    }
}

读写锁

相关概念

悲观锁

悲观锁,顾名思义,很悲观,它认为当前线程在操作一个资源时,别的线程总会来进行修改,所以在使用悲观锁的情况下,涉及到共享资源的操作都会上锁且每次只允许一个线程进行操作,保证了线程的安全,但是效率低

乐观锁

与悲观锁相对,它认为每次操作共享资源时,不会有其他线程干扰,乐观锁有一个特点,它会给当前操作的资源加上一个版本号

例如此时 A 线程来操作资源,得到的版本号为 1 ,在这同一时间,线程 B 也来操作资源,得到版本号依然为 1 ,A 线程资源操作完成时,判断版本号是否还为 1 ,若相同,则资源操作成功,A 线程修改版本号为 2 ,此时 B 线程也操作完成了,它检查版本号,发现与当前获取的 1 不一致,则 B 线程的操作不会提交

表锁

A 在操纵数据库时,直接锁住整个表,不允许别人操作,哪怕 A 只是操作一行记录

行锁

A 在操作数据库时,只对其操作的某一行上锁,对于其他行,别人能够正常访问和操作

行锁可能会出现死锁的情况,具体为:A 需要操作第 11 行,A 当前锁住的是第 10 行,B 需要操作第 10 行,B 当前锁住的是第 11 行,双方都在等待对方释放自己需要的行

读写锁概述

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了

也就是说,在没有人写的情况下,多人可以一起读,但是一旦有人写,其他人都不能读,只能一个人写

线程进入读锁的前提条件

没有其他线程的写锁

没有写请求,或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)

线程进入写锁的前提条件

没有其他线程的读锁

没有其他线程的写锁

而读写锁有以下三个重要的特性

公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平

重进入:读锁和写锁都支持线程重进入

锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

对于读写锁,也会发生死锁的情况

有线程 A、B 二者同时读取某一资源,根据写锁的特性,对于同一线程 A ,在读取时,也可以进行修改,恰巧,同一时间 B 也在读取该资源,A 想要进入写锁,必须等待其他线程读完当前没有其他线程在读取时,才能进入写锁,而 B 此时读完资源后,也想要进行一个写的操作,B 也要等其他线程读完,双方互相等待对方读完,造成死锁

在 Java 的 juc 并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁

代码演示

 import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache {
    /*
    volatile 保证了不同线程对共享变量操作的可见性
    也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值
     */
    private static volatile Map<String, Object> cache = new HashMap<>();
    // 读写锁
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 写入数据
    public void put(String key, Object value) {
        rwLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "-正在写");
            TimeUnit.MICROSECONDS.sleep(300);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + "-写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // 读取数据
    public void get(String key) {
        rwLock.readLock().lock();
        Object val;
        try {
            System.out.println(Thread.currentThread().getName() + "-正在取出数据...");
            TimeUnit.MICROSECONDS.sleep(300);
            val = cache.get(key);
            System.out.println(Thread.currentThread().getName() + "-取出数据: " + val);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        for (int i = 1; i <= 10; i++) {
            final int num = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    cache.put(String.valueOf(num), num);
                }
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 10; i++) {
            final int num = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    cache.get(num + "");
                }
            }, String.valueOf(i)).start();
        }
    }
}

读写锁的降级

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)

原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程 升级 为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就 降级 为了读锁

一个资源可以被多个线程访问,被一个线程写,但是读写不能同时存在,二者互斥

代码演示

写锁降级为读锁

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁降级
 */
public class ReadWriteDemo2 {

    public static void main(String[] args) {
        // 可重入读写锁对象
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();// 读锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();// 写锁

        // 写锁降级读锁
        writeLock.lock();// 1.获取写锁
        System.out.println("获取写锁");
        readLock.lock();// 2.获取读锁
        System.out.println("获取读锁");

        writeLock.unlock();// 释放写锁,注意,此时已经降级为了读锁,否则下面的写锁无法释放
        readLock.unlock();// 最终释放读锁
    }
}

读锁转化为写锁失败

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁降级
 */
public class ReadWriteDemo2 {

    public static void main(String[] args) {
        // 可重入读写锁对象
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();// 读锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();// 写锁

        // 锁降级
        readLock.lock();// 1.获取读锁
        System.out.println("获取读锁");
        /*
         2.获取写锁,会获取失败(后续的打印语句不会输出),因为不论是当前或者其他的线程,此时正获取有读锁
         在执行写操作时,需要等待其他线程的读操作完成(释放读锁)
         */
        writeLock.lock();
        System.out.println("获取写锁");

        readLock.unlock();// 释放写锁,注意,此时已经降级为了读锁,否则下面的写锁无法释放
        writeLock.unlock();// 最终释放读锁
    }
}

读写锁的演变

没有锁的情况下

多个线程强制资源,十分混乱

使用 synchronized 和 Lock 锁

保证了线程的安全,但是不论是读操作还是写操作,都只允许一个线程,效率太低

使用 ReentrantReadWriteLock 锁

该锁可以多个线程共享读,写操作时,依然只允许一个线程进行,读写锁存在锁饥饿的问题,就是说,在写操作进行时,不能存在读操作,当前有读操作时,需要等待读操作完成后写操作才能进行,若读操作一直没有完成,写操作一直无法执行

阻塞队列

概述

Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全传输数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利;阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出

当队列是空的,从队列中获取元素的操作将会被阻塞

当队列是满的,从队列中添加元素的操作将会被阻塞

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素

试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

多线程环境中,通过队列可以很容易实现数据共享,比如经典的【生产者】和【消费者】模型中,通过队列可以很便利地实现两者之间的数据共享

假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然

当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列•当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒

BlockingQueue 核心方法

放入数据

offer(anObject)

表示如果可能的话,将 anObject 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则返回 false(本方法不阻塞当前执行方法的线程)

offer(E o,long timeout,TimeUnit unit)

可以设定等待的时间,如果在指定的时间内,还不能往队列中加入 BlockingQueue,则返回失败

put(anObject)

把 anObject 加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续

获取数据

poll(time)

取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null

poll(long timeout,TimeUnit unit)

从 BlockingQueue 取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败

take()

取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入

drainTo()

一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁

代码演示

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Demo1 {

    public static void main(String[] args) {
        // 创建阻塞队列
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.add("a"));
        System.out.println(queue.add("b"));
        System.out.println(queue.add("c"));

        // element() 检索头部但不删除该元素
        System.out.println(queue.element());
        // 当元素满时,再调用 add 方法添加,会抛出 java.lang.IllegalStateException: Queue full 异常
         System.out.println(queue.add("d"));

         System.out.println(queue.remove());
         System.out.println(queue.remove());
         System.out.println(queue.remove());

        // 当队列中没有元素时,再执行 remove() 操作,会抛出 java.util.NoSuchElementException 异常
         System.out.println(queue.remove());
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Demo1 {

    public static void main(String[] args) {
        // 创建阻塞队列
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("b"));
        System.out.println(queue.offer("c"));

        // 达到最大容量,返回 false 添加失败
        System.out.println(queue.offer("w"));

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        
        // 队列为空,没有元素,返回 null
        System.out.println(queue.poll());
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Demo1 {

    public static void main(String[] args) {
        // 创建阻塞队列
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        try {
            queue.put("a");
            queue.put("b");
            queue.put("b");

            // 队列已满,线程阻塞,直到队列中有空间
            queue.put("w");

            queue.take();
            queue.take();
            queue.take();

            // 队列中没有元素,一直阻塞,直到队列中有元素
            queue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class Demo1 {

    public static void main(String[] args) throws Exception {
        // 创建阻塞队列
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        queue.offer("a");
        queue.offer("b");
        queue.offer("c");

        /*
         当队列满时,继续放入元素会阻塞,阻塞指定时间后放弃
         参数1 放入的元素
         参数2 阻塞的时间长度
         参数3 阻塞的时间单位
         */
        queue.offer("w", 2, TimeUnit.SECONDS);

        queue.poll();
        queue.poll();
        queue.poll();
        
        /*
         线程中没有元素了,继续取出元素会阻塞,阻塞指定时间后放弃
         参数1 阻塞的时间长度
         参数2 阻塞的时间单位
         */
        queue.poll(2, TimeUnit.SECONDS);
    }
}

常见的 BlockingQueue 分类

ArrayBlockingQueue(常用)

基于数组的阻塞队列实现,在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整型变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于 LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue 完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。DougLea 之所以没这样去做,也许是因为 ArrayBlockingQueue 的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

ArrayBlockingQueue 和 LinkedBlockingQueue 间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的 Node 对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于 GC 的影响还是存在一定的区别。而在创建 ArrayBlockingQueue 时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁;

一句话总结: 由数组结构组成的有界阻塞队列。

LinkedBlockingQueue(常用)

基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理(当队列中没有数据时,消费者被阻塞)

而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以

一句话总结: 由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列

DelayQueue

DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

一句话总结: 使用优先级队列实现的延迟无界阻塞队列。

PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来决定),但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁

一句话总结: 支持优先级排序的无界阻塞队列。

SynchronousQueue

一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。

相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖),但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为

公平模式和非公平模式的区别

  • 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO(先进先出) 队列来阻塞多余的生产者和消费者,从而体系整体的公平策略
  • 非公平模式(SynchronousQueue默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO(后进先出)队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理

一句话总结: 不存储元素的阻塞队列,也即单个元素的队列。

LinkedTransferQueue

LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队列。

相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。LinkedTransferQueue 采用一种预占模式,意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上

后面生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回

一句话总结: 由链表组成的无界阻塞队列。

LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,这里的阻塞一般有两种情况

  • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再讲该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常
  • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数

一句话总结: 由链表组成的双向阻塞队列

小结

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

为什么需要 BlockingQueue 在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。使用后我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了

线程池

概述

线程池(英语:threadpool):一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建与销毁线程的代价

线程池不仅能够保证内核的充分利用,还能防止过分调度,例如:10 年前单核 CPU 电脑,假的多线程,像马戏团小丑玩多个球,CPU 需要来回切换。现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用切换效率高

线程池的优势

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行

特点

降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗

提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行

提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类

线程池的种类 & 创建

newFixedThreadPool(常用)

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程

在任意点,绝大多数线程会处于处理任务的活动状态,如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待

如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要),在某个线程被显式地关闭之前,池中的线程将一直存在

特点

线程池中的线程处于一定的量,可以很好的控制线程的并发量,一池多线程

线程可以重复被使用,在显示关闭之前,都将一直存在

超出一定量的线程被提交时候需在队列中等待

场景

适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景

代码演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程池演示
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        // 一池 5 线程
        ExecutorService pool = Executors.newFixedThreadPool(5);

        // 10 个线程任务
        try {
            for (int i = 1; i <= 10; i++) {
                pool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + "执行任务");
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            /* 
            启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则不产生其他作用。
            shutdown 只是将线程池的状态设置为 SHUTWDOWN 状态
            正在执行的任务会继续执行下去,没有被执行的则中断
            
            与之对应的还有 shutdownNo
            而 shutdownNow 则是将线程池的状态设置为 STOP 正在执行的任务则被停止 没被执行任务的则返回
            */
            pool.shutdown();
        }
    }
}

关于 shutdown() & shutdownNow() 的博客 https://www.cnblogs.com/aspirant/p/10265863.html

newSingleThreadExecutor(常用)

创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程,注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务,可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的,与其他等效的 newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程

特点

线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中依次执行

场景

适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景

代码演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程池演示
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        // 一池 1 线程
        ExecutorService pool = Executors.newSingleThreadExecutor();

        // 10 个线程任务
        try {
            for (int i = 1; i <= 10; i++) {
                pool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + "执行任务");
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则不产生其他作用。
            pool.shutdown();
        }
    }
}

newCachedThreadPool(常用)

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.

特点

线程池中数量没有固定,可达到最大值 Interger.MAX_VALUE

线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)

当线程池中,没有可用线程,会重新创建一个线程

场景

适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景

代码演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程池演示
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        // 一池可扩容线程
        ExecutorService pool = Executors.newCachedThreadPool();

        // 100 个线程任务
        try {
            for (int i = 1; i <= 100; i++) {
                pool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + "执行任务");
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则不产生其他作用。
            pool.shutdown();
        }
    }
}

实际上,不同线程池类型之间,底层使用的都是 new ThreadPoolExecutor() 构造器来创建,通过设置不同的参数来实现不同效果类型的线程池

常用参数

corePoolSize 线程池常驻的核心线程数

maximumPoolSize 能容纳的最大线程数

keepAliveTime 空闲线程存活时间长度

unit 空闲线程存活的时间单位

workQueue 存放提交但未执行任务的队列

threadFactory 创建线程的工厂类

handler 等待队列满后的拒绝策略

线程池中,有三个重要的参数,决定影响了拒绝策略:

  • corePoolSize-核心线程数,也即最小的线程数
  • workQueue 阻塞队列
  • maximumPoolSize 最大线程数当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中,当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置,此时,再多余的任务,则会触发线程池的拒绝策略了

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize),就会触发线程池的拒绝策略

拒绝策略

CallerRunsPolicy

当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务(就好比,【A:调用者线程】 叫我们去 B【线程池】 那儿讨债,此时 B 对我们说,谁让你来的找谁去,于是我们又回到【A:调用者线程】,由调用者线程自己处理),一般并发比较小,性能要求不高,不允许失败;但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大

AbortPolicy

丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略,必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行

DiscardPolicy

直接丢弃无法被处理的任务,不做任何其他的操作

DiscardOldestPolicy

当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

线程池工作流程

创建线程池对象时,线程池中的线程并不会理解创建,当执行 execute() 方法时才会创建线程

当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断

如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务

如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列

如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程(corePoolSize) 立刻运行这个任务

如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行

当一个线程完成任务时,它会从队列中取下一个任务来执行

当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断

如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉

所有线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

补充

当 corePoolSize 满了,阻塞队列中也满了,但 maximumPoolSize 还没有满,此时来了一个新的任务,那么线程池就会新建线程来优先处理新来的任务,阻塞队列中的任务则继续等待

注意事项

项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是 FixedThreadPool 和SingleThreadExecutor 底层都是用 LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE,容易导致OOM:Out Of Memory,翻译成中文就是【内存用完了】,所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池

创建线程池推荐适用 ThreadPoolExecutor 及其 7 个参数手动创建
corePoolSize 线程池的核心线程数
maximumPoolSize能容纳的最大线程数
keepAliveTime 空闲线程存活时间
ounit 存活的时间单位
workQueue 存放提交但未执行任务的队列
threadFactory 创建线程的工厂类
handler 等待队列满后的拒绝策略

为什么不允许使用 Executors 的方式手动创建线程池,如下图

自定义线程池

import java.util.concurrent.*;

/**
 * 自定义线程池
 */
public class ThreadPoolDemo2 {

    public static void main(String[] args) {

        ExecutorService pool = new ThreadPoolExecutor(
                4,// 常驻核心线程数
                10, // 最大线程数
                10L, // 空闲线程存活时间长度
                TimeUnit.SECONDS,//  空闲线程存活时间单位
                new ArrayBlockingQueue<>(4), // 阻塞队列
                Executors.defaultThreadFactory(), // 默认的线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略,默认 AbortPolicy
        );

        try {
            for (int i = 0; i < 100; i++) {
                pool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + ": 执行任务");
                });
            }
        } catch (Exception e) {
            System.out.println("线程池拒绝执行任务");
        } finally {
            pool.shutdown();
        }
    }
}

分支合并框架

概述

Fork / Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork / Join 框架要完成两件事情

Fork:把一个复杂任务进行分拆,大事化小

Join:把分拆任务的结果进行合并

任务分割:首先 Fork / Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割

执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据

在Java的 Fork / Join 框架中,使用两个类完成上述操作

ForkJoinTask

我们要使用 Fork / Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制,通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork / Join 框架提供了两个子类

  • RecursiveAction 用于没有返回结果的任务
  • RecursiveTask 用于有返回结果的任务

ForkJoinPool

ForkJoinTask 需要通过 ForkJoinPool 来执行

RecursiveTask

继承后可以实现递归(自己调自己)调用的任务

Fork & Join 方法

fork()

当我们调用 ForkJoinTask 的 fork 方法时,程序会把任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地执行这个任务,然后立即返回结果

pushTask 方法把当前任务存放在 ForkJoinTask 数组队列里。然后再调用 ForkJoinPool 的 signalWork() 方法唤醒或创建一个工作线程来执行任务

join()

Join 方法的主要作用是阻塞当前线程并等待获取结果

它首先调用 doJoin 方法,通过 doJoin() 方法得到当前任务的状态来判断返回什么结果,任务状态有4种

  • 已完成(NORMAL)
  • 被取消(CANCELLED)
  • 信号(SIGNAL)
  • 出现异常(EXCEPTIONAL)

如果任务状态是已完成,则直接返回任务结果

如果任务状态是被取消,则直接抛出 CancellationException

如果任务状态是抛出异常,则直接抛出对应的异常

doJoin() 方法流程如下

  • 首先通过查看任务的状态,看任务是否已经执行完成,如果执行完成,则直接返回任务状态;
  • 如果没有执行完,则从任务数组里取出任务并执行
  • 如果任务顺利执行完成,则设置任务状态为 NORMAL,如果出现异常,则记录异常,并将任务状态设置为 EXCEPTIONAL

Fork Join 的异常处理

ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的getException 方法获取异常

getException 方法返回 Throwable 对象,如果任务被取消了则返回 CancellationException,如果任务没有完成或者没有抛出异常则返回 null

关于 Throwable https://blog.csdn.net/mccand1234/article/details/51579425

代码演示

计算 1+2+3+…100,规定:相加的两个数之间的差值不能大于 10,否则将二者各自拆分,拆分后继续比较差值,直到符合条件时,二者相加,如 1~100,拆分为 【150,51100】,左边继续拆分【125,2550】,右边继续拆分【5175,76100】依次类推

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * 分支合并框架演示
 */
class MyTask extends RecursiveTask<Integer> {

    // 拆分的差值不能超过 10
    private static Integer VALUE = 10;
    private int begin; // 拆分的开始值
    private int end; // 拆分的结束值
    private int result; // 返回值

    // 创建有参构造
    public MyTask(Integer begin, Integer end) {
        this.begin = begin;
        this.end = end;
    }

    // 拆分与合并过程
    @Override
    protected Integer compute() {
        // 判断相加的两个数,是否大于 10
        if (end - begin <= VALUE) {
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        } else {
            /*
             拆分
             */
            // 获取数据中间值
            int middle = (begin + end) / 2;
            MyTask task01 = new MyTask(begin, middle);
            MyTask task02 = new MyTask(middle + 1, end);

            // 执行新的两个子任务 异步执行
            task01.fork();
            task02.fork();

            // 读取子任务的结果,合并到一起,尚未完成就等待
            result = task01.join() + task02.join();
        }
        return result;
    }
}

public class ForkJoinDemo {

    public static void main(String[] args) {

        // 创建任务对象
        MyTask task = new MyTask(1, 100);

        // 创建分支合并池对象
        ForkJoinPool pool = new ForkJoinPool();

        try {
            // 提交任务
            ForkJoinTask<Integer> submit = pool.submit(task);

            // 获取合并之后的结果
            Integer result = submit.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭池对象
            pool.shutdown();
        }
    }
}

当两个数只差小于 10 时,直接遍历求和 begin + (begin+1) + (begin+2) + … end 为止,若大于 10 ,则进行拆分,找出两个数的中间量 (begin + end) / 2,使用二分的方式,再创建两个新任务,如下

MyTask task01 = new MyTask(begin, middle);
MyTask task02 = new MyTask(middle + 1, end);

再调用 fork() 执行这两个新任务

然后使用 join 获取两个任务的结果

在 fork() 执行的过程中,是异步的,它不会等待当前任务执行完,直接就会开启下一个子任务,子任务会再次调用重写的 compute() 方法中的逻辑,如果两数差大于 10,又会继续拆分子任务,递归的调用,而在 join() 时,便会等待获取结果了

异步回调

同步 & 异步

同步:当我们执行一个任务时,该任务需要一定的时间,因此我们一直等待该任务执行完成

异步:我们将该任务交给对应的线程执行后,无需等待任务执行完成,我们可以继续去完成其他的任务,当该任务执行完成后,只需要通知我们,我们直接去获取结果或则只需要知道该任务的执行状态即可

CompletableFuture

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。

CompletableFuture 实现了 Future,CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类

代码演示

同步

import javax.swing.table.TableCellRenderer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * 异步调用 & 同步调用
 */
public class CompletableDemo {

    public static void main(String[] args) {
        /*
         同步调用
         Void 代表无返回值
         */
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName());
                    }
                }

        );
        try {
            System.out.println(completableFuture.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
}

异步

import javax.swing.table.TableCellRenderer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * 异步调用 & 同步调用
 */
public class CompletableDemo {

    public static void main(String[] args) {
        // 异步调用
        CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(
                new Supplier<Integer>() {
                    @Override
                    public Integer get() {
                        System.out.println(Thread.currentThread().getName());

                        return 1024;
                    }
                }
        );
        try {
            /*
            whenComplete():当异步任务完成时,才会调用该方法
            调用时,就会执行参数 BiConsumer 中的 accept 方法
             */
            completableFuture1.whenComplete(new BiConsumer<Integer, Throwable>() {
                /*
                参数1 执行正常时的返回值
                参数2 执行失败时的异常信息,无异常则为 null
                 */
                @Override
                public void accept(Integer integer, Throwable throwable) {
                    System.out.println(integer);
                    System.out.println(throwable);
                }
            }).get();// 最终也需要 get 来获取结果
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 2
    点赞
  • 2
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页

打赏作者

告别时光

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值