JUC并发编程

1. 什么是JUC

JUC 指的是 java.util .concurrent 工具包。这是一个处理线程的工具包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统、包括线程池、异步 IO 和轻量级任务框架,还提供了设计用于多线程上下文中的 Collection 实现等;

JUC 是在 JDK 1.5 开始出现的。下面一起来看看它怎么使用。

2. 线程和进程

2-1. 进程正在进行中的程序(直译),一个进程至少包含一个或多个线程。

2-2. 线程:进程中一个负责程序执行的控制单元(执行路径),每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务;

1、Java默认有几个线程?

答:2个,main主线程、GC线程

2、Java 真的可以开启线程吗?

答:不可以,只能通过本地方法调用底层的C++去操作

public synchronized void start() {

    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
2-3. 并发、并行
并发编程:并发并行
并发(多线程操作同一个资源)
  • CPU一核,模拟出多条线程,快速交替
并行(多个人一起行走)
  • CPU多核,多个线程可以同时执行
// 获取CPU核数
// CPU密集型、IO密集型
Runtime.getRuntime().availableProcessors();

并发编程的本质:充分利用CPU的资源

2-4. 线程的几个状态
public enum State {

    // 新生
    NEW,

    // 运行
    RUNNABLE,

    // 阻塞
    BLOCKED,

    // 等待(死等,也是阻塞的等待)
    WAITING,

    // 超时等待(指定的时间内等待,过时不候)
    TIMED_WAITING,

    // 终止
    TERMINATED;
}
2-5. sleep / wait 区别
① 来自不同的类:
  - sleep来自Thread类;
  - wait来自Thread类; ② 是否释放锁:   - sleep方法不会释放锁;
  - wait会方法释放锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁); ③ 使用范围:   - sleep可以在任何地方使用;
  - wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用; ④ 异常捕获:   - sleep必须捕获异常;
  - wait,notify和notifyAll不需要捕获异常;

3. Lock锁同步

在JDK1.5之前,解决多线程安全问题有两种方式:synchronized 隐式锁

  • 同步代码块、同步方法

在JDK1.5之后,出现了更加灵活的方式:Lock 显式锁

  • 同步锁

3-1. Lock需要通过lock()方法上锁,通过unlock()方法释放锁。为了保证锁能释放,所有unlock方法一般放在finally中去执行。

Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); } 

○ Lock实现类:

 ○ ReentrantLock:默认为非公平锁

公平锁:十分公平:可以先来后到

非公平锁:十分不公平:可以插队 (默认)

3-2. 再来看一下卖票案例:
● 传统的Synchronized
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        // 并发:多线程操作同一个类,把资源类丢入线程就可以了
        Ticket ticket = new Ticket();

        // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
        new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "001").start();
        new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "002").start();
        new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "003").start();
    }
}

// 资源类 OOP
class Ticket {

    private int ticket = 30;

    // synchronized 本质: 队列,锁
    public synchronized void sale() {
        if (ticket > 0) {
            System.out.println("窗口 " + Thread.currentThread().getName() + " 完成售票,余票为:" + (--ticket));
        }
    }
}

多个线程同时操作共享数据ticket,所以会出现线程安全问题。会出现同一张票卖了好几次或者票数为负数的情况。以前用同步代码块和同步方法解决,现在看看用同步锁怎么解决。

● Lock接口:直接创建lock对象,然后用lock()方法上锁,最后用unlock()方法释放锁即可。

public class SaleTicketDemo02 {
    public static void main(String[] args) {
        // 并发:多线程操作同一个类,把资源类丢入线程就可以了
        Ticket2 ticket2 = new Ticket2();

        new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "001").start();
        new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "002").start();
        new Thread(() -> { for (int i = 0; i < 30; i++) ticket2.sale(); }, "003").start();
    }
}
// 资源类
class Ticket2 {

    private int ticket = 30;

    private Lock lock = new ReentrantLock(); // 1.创建lock锁

    public void sale() {
        lock.lock(); // 2.加锁
        try {
            if (ticket > 0) {
                System.out.println("Lock窗口 " + Thread.currentThread().getName() + " 完成售票,余票为:" + (--ticket));
            }
        } finally {
            lock.unlock(); // 3.解锁
        }
    }
}

3-3. Synchronized 和 Lock 的区别

Synchronized 内置的 Java 关键字Lock 是一个 Java 类

 Synchronized 无法判断获取锁的状态Lock 可以判断是否获取到了锁

③ Synchronized 会自动释放Lock 必须要手动释放锁!如果不释放 会产生死锁

④ Synchronized 线程 1(获得锁,阻塞),线程 2(等待获取,死等)Lock 锁就不一定会等待下去

⑤ Synchronized 可重入锁,不可以中断的,非公平Lock 可重入锁,可以判断锁,非公平(可以自己设置)

Synchronized 适合锁少量的代码同步问题Lock 适合锁大量的同步代码!

4. 等待唤醒机制

4-1. 虚假唤醒问题

生产消费模式是等待唤醒机制的一个经典案例,看下面的代码:

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

        Data data = new Data();

        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者01").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者02").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者03").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者04").start();
    }
}

class Data {

    private int product = 0; // 共享数据

    // product +1
    public synchronized void increment() {
        if (product != 0) {
            try {
                this.wait(); // 等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        product++;
        System.out.println(Thread.currentThread().getName() + " => " + product);
        // 通知其他线程,我 +1 完毕了
        this.notifyAll();
    }

    // product -1
    public synchronized void decrement() {
        if (product == 0) {
            try {
                this.wait(); // 等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        product--;
        System.out.println(Thread.currentThread().getName() + " => " + product);
        // 通知其他线程,我 -1 完毕了
        this.notifyAll();
    }
}

● 问题存在:当存在 2 个以上的线程时,可能会产生虚假唤醒

● 解决办法将 if 改为 while 判断

// product +1
public synchronized void increment() throws InterruptedException {
    while (product != 0)
        this.wait(); // 等待
    product++;
    System.out.println(Thread.currentThread().getName() + " => " + product);
    // 通知其他线程,我 +1 完毕了
    this.notifyAll();
}

// product -1
public synchronized void decrement() throws InterruptedException {
    while (product == 0)
        this.wait(); // 等待
    product--;
    System.out.println(Thread.currentThread().getName() + " => " + product);
    // 通知其他线程,我 -1 完毕了
    this.notifyAll();
}

只需要把 if 改成 while,每次都再去判断一下,就可以了。

4-2. 用Lock锁实现等待唤醒

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

        Data2 data = new Data2();
        
        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者01").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者02").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.decrement(); }, "生产者03").start();
        new Thread(() -> { for (int i = 0; i < 10; i++) data.increment(); }, "消费者04").start();
    }
}

class Data2 {

    private int product = 0; // 共享数据

    private Lock lock = new ReentrantLock(); //  创建锁对象
    Condition condition = lock.newCondition(); // 获取锁的监视器对象

    // product +1
    public void increment() {
        lock.lock(); // 加锁
        try {
            while (product != 0)
                condition.await(); // 等待
            product++;
            System.out.println("Lock" + Thread.currentThread().getName() + " => " + product);
            condition.signalAll();// 通知其他线程,我 +1 完毕了
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // product -1
    public void decrement() {
        lock.lock(); // 加锁
        try {
            while (product == 0)
                condition.await(); // 等待
            product--;
            System.out.println("Lock" + Thread.currentThread().getName() + " => " + product);
            // 通知其他线程,我 -1 完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

使用 lock 同步锁,就不需要 sychronized 关键字了,需要创建 lock 对象和 condition 实例。condition 的 await() 方法、signal() 方法和 signalAll() 方法分别与 wait() 方法、 notify() 方法和 notifyAll() 方法对应。

4-3. 线程按序交替

首先来看一道题:

编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,
每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。
如:ABCABCABC…… 依次递归
分析:
线程本来是抢占式进行的,要按序交替,所以必须实现线程通信,
那就要用到等待唤醒。可以使用同步方法,也可以用同步锁。

代码实现:

public class TestLoopPrint {
    public static void main(String[] args) {
        AlternationDemo alternationDemo = new AlternationDemo();

        new Thread(()->{for (int i = 0; i < 10; i++) alternationDemo.loopA(); },"线程A").start();
        new Thread(()->{for (int i = 0; i < 10; i++){alternationDemo.loopB();}},"线程B").start();
        new Thread(()->{for (int i = 0; i < 10; i++){alternationDemo.loopC();}},"线程C").start();
    }
}

class AlternationDemo {

    // 当前正在执行的线程的标记
    private int number = 1;
    // 创建锁对象,并且使用该锁创建三个监视器
    private Lock lock = new ReentrantLock();
    Condition condition_1 = lock.newCondition();
    Condition condition_2 = lock.newCondition();
    Condition condition_3 = lock.newCondition();

    public void loopA() {
        lock.lock();
        try {
            while (number != 1) { // 判断
                condition_1.await(); // 等待
            }
            System.out.println(Thread.currentThread().getName() + " => AAAAA");
            // 唤醒指定的监视器2
            number = 2;
            condition_2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void loopB() {
        lock.lock();
        try {
            while (number != 2) { // 判断
                condition_2.await(); // 等待
            }
            System.out.println(Thread.currentThread().getName() + " => BBBBBB");
            // 唤醒指定的监视器3
            number = 3;
            condition_3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void loopC() {
        lock.lock();
        try {
            while (number != 3) { // 判断
                condition_3.await(); // 等待
            }
            System.out.println(Thread.currentThread().getName() + " => CCCCCCC");
            // 唤醒指定的监视器1
            number = 1;
            condition_1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
以上编码就满足需求。创建三个线程,分别调用loopA、loopB和loopC方法,这三个线程使用condition进行通信。

5. 锁的8种问题

1)标准情况

按照普通的情况访问同步方法,查看输出

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

        Phone1 phone = new Phone1();

        new Thread(() -> {phone.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> { phone.call(); }, "B").start();
    }
}

class Phone1 {

    public synchronized void sendSms()  {
        System.out.println("发短信》》》");
    }

    public synchronized void call() {
        System.out.println("打电话!!!");
    }
}
View Code

 - 执行结果:1、发短信 2、打电话

2)在其中一种方法中添加sleep方法访问

在sendSms方法中添加sleep,查看修改后的打印顺序

class Phone1 {

    public synchronized void sendSms()  {
        try {
            TimeUnit.SECONDS.sleep(4); // 睡4秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    public synchronized void call() {
        System.out.println("打电话!!!");
    }
}
View Code

 - 执行结果: 1、发短信 2、打电话

synchronized 锁的是对象this,两个方法用的使用一个锁,所以谁先拿到谁先执行

3)添加一个未加锁方法,查看访问结果

在Phone类中添加hello方法,同时线程修改为访问同步方法和普通方法

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

        Phone2 phone = new Phone2();

        // 有锁
        new Thread(() -> { phone.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 没锁
        new Thread(() -> { phone.hello(); }, "B").start();
    }
}

class Phone2 {

    public synchronized void sendSms()  {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    public synchronized void call() { System.out.println("打电话!!!"); }

    // 这里没有锁,不是同步方法,不受锁的影响
    public void hello() {
        System.out.println("hello!");
    }
}
View Code

 - 执行结果:1、hello 2、发短信

4)执行时创建两个不同对象,通过不同对象访问加锁的方法

在创建线程时,通过不同对象执行同步方法,查看执行结果

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

        // 两个对象,两个调用者,两把锁!
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        // 有锁
        new Thread(() -> { phone1.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 有锁
        new Thread(() -> { phone2.call(); }, "B").start();
    }
}

class Phone2 {

    // synchronized 是 对象锁this
    public synchronized void sendSms()  {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    public synchronized void call() { System.out.println("打电话!!!"); }

    // 这里没有锁
    public void hello() { System.out.println("hello!"); }
}
View Code

 - 执行结果:1、打电话 2、发短信

synchronized 锁的是对象this。两个对象,两个调用者,两把锁!所以互不受影响

5)将加锁的方法改为静态方法,同一个对象执行

两个同步方法都改为静态方法,通过同一个对象执行方法,查看执行结果

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

        Phone3 phone = new Phone3();

        new Thread(() -> { phone.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> { phone.call(); }, "B").start();
    }
}

class Phone3 {

    // static synchronized 静态方法锁
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4); // 睡眠4秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    public static synchronized void call() { System.out.println("打电话!!!"); }

}
View Code

 - 执行结果: 1、发短信 2、打电话

static synchronized 静态同步方法,类一加载就有了,所以锁的是Class

6)通过两个对象访问静态同步方法

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

        // 两个对象的Class类模板只有一个,static,锁的是Class
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        new Thread(() -> { phone1.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> { phone2.call(); }, "B").start();
    }
}

class Phone3 {

    // static synchronized 静态方法,类一加载就有了,所以锁的是Class
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4); // 睡眠4秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    public static synchronized void call() {
        System.out.println("打电话!!!");
    }

}
View Code

 - 执行结果:1、打电话 2、发短信

两个对象的Class类模板只有一个,锁的是Class,只有一把锁

7)一个静态同步方法 一个普通同步方法,通过同一个对象执行

其中的一个方法去掉静态关键字,查看执行结果

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

        Phone4 phone = new Phone4();

        new Thread(() -> { phone.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> { phone.call(); }, "B").start();
    }
}

class Phone4 {

    // 静态同步方法 锁Class
    public static synchronized void sendSms() {
        try { // 睡眠4秒
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    // 普通同步方法 锁this
    public synchronized void call() { System.out.println("打电话!!!"); }

}
View Code

 - 执行结果:1、打电话 2、发短信

静态同步锁 Class,普通同步锁 this,两把不同的锁,所以互不受影响

8)一个静态同步方法 一个普通同步方法,两个对象执行

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

        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(() -> { phone1.sendSms(); }, "A").start();

        try { // 睡眠2秒
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> { phone2.call(); }, "B").start();
    }
}

class Phone4 {

    // 静态同步方法 锁Class
    public static synchronized void sendSms() {
        try { // 睡眠4秒
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信》》》");
    }

    // 普通同步方法 锁this
    public synchronized void call() {
        System.out.println("打电话!!!");
    }

}
View Code

 - 执行结果:1、打电话 2、发短信

两个对象 两把不同的锁,一个Class 一个this

● 总结:

① 对象锁:使用 synchronized 修饰非静态的方法以及 synchronized(this) 同步代码块使用的锁是对象锁。

② 类锁:使用 synchronized 修饰静态的方法以及 synchronized(class) 同步代码块使用的锁是类锁。

③ 私有锁在类内部声明一个私有属性如 private Object lock,在需要加锁的同步块使用 synchronized(lock)

它们的特性:

  • 对象锁具有可重入性。
  • 当一个线程获得了某个对象的对象锁,则该线程仍然可以调用其他任何需要该对象锁的 synchronized 方法或 synchronized(this) 同步代码块。
  • 当一个线程访问某个对象的一个 synchronized(this) 同步代码块时,其他线程对该对象中所有其它 synchronized(this) 同步代码块的访问将被阻塞,因为访问的是同一个对象锁。
  • 每个类只有一个类锁,但是类可以实例化成对象,因此每一个对象对应一个对象锁。
  • 普通方法与同步方锁法无关。
  • 类锁和对象锁不会产生竞争。
  • 私有锁和对象锁也不会产生竞争。
  • 使用私有锁可以减小锁的细粒度,减少由锁产生的开销。

6. 集合类不安全

多个线程共同操作一个线程不安全的类时报并发修改异常

6-1. List - 线程不安全

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

//        List<String> list = new Vector<>();
//        List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(Thread.currentThread().getName() + " - " + list);
            }, String.valueOf(i)).start();
        }
    }
}
● 解决方案:

① 使用 Vector 对象,实际上是加了 Synchronized 同步,实现同步需要很高的花费,效率较低

② 使用 Collections 集合工具类,给线程不安全的集合添加一层封装

③ 写入时复制 CopyOnWriteArrayList,加了写锁的集合,锁住的是整个对象,但读操作可以并发执行

  - 写入时复制:这种集合将数据放在固定的数组中,任何数据的改变,都会重新创建一个新的数组来记录值。

  - 这种集合被设计用在,读多写少的时候推荐使用!

6-2. Set - 线程不安全

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

//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(Thread.currentThread().getName() + " - " + set);
            }, String.valueOf(i)).start();
        }
    }
}

● 解决方案:

① 使用 Collections 集合工具类,给线程不安全的集合添加一层封装

② CopyOnWriteArraySet 写入时复制,读多写少的时候推荐使用

● HashSet 底层:

// HashSet是基于HashMap实现的
public HashSet() {
    map = new HashMap<>();
}

// add set 本质就是 map key 是无法重复的!
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

// PRESENT 是用来填充 map 中的 value,定义为 Object 类型。
private static final Object PRESENT = new Object();

6-3. Map - 线程不安全

public class MapTest {

    public static void main(String[] args) {

//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(Thread.currentThread().getName() + " - " + map);
            }, String.valueOf(i)).start();
        }
    }
}

解决方案:

① 使用 Collections 集合工具类,给线程不安全的集合添加一层封装

② 使用线程安全的 hash 表 ConcurrentHashMap,注意:1.7前采用是锁分段机制,1.8后采用了CAS算法

1、Map 是这样用的吗?
  不是,工作中不使用 HashMap
2、默认等价于什么?
  new HashMap<>(16, 0.75f)
  - 初始容量:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
  - 加载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f

7. 创建线程的方式 --- 实现Callable接口

● Callable:类似于Runnable

- call 方法有返回值,并且能够抛出异常。

- 有缓存,结果可能需要等待,会阻塞!

● FutureTask:获取 Callable 任务的返回值

使用 Future 我们可以得知 Callable 的运行状态,以及获取 Callable 执行完后的返回值。

Future 的方法介绍:

  • get() :阻塞式,用于获取 Callable/Runnable 执行完后的返回值。
  •     带时间参数的get()重载方法用于最多等待的时间后,如仍未返回则线程将继续执行。
  • cancel() :撤销正在执行 Callable 的 Task。
  • isDone():是否执行完毕。
  • isCancelled():任务是否已经被取消。

● 使用实例:

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

        CallableDemo callableDemo = new CallableDemo();
        for (int i = 0; i < 10; i++) {
            // 执行callable方式,需要FutureTask实现类的支持,用来接收运算结果
            FutureTask<Integer> task = new FutureTask<>(callableDemo);
            // 执行线程任务
            new Thread(task, String.valueOf(i)).start();
            // 接收线程运算结果
            try {
                // 需要等到线程执行完调用get方法才会执行,也可以用于闭锁操作
                System.out.println(task.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class CallableDemo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " => call()");
        TimeUnit.SECONDS.sleep(1); // 睡眠1秒
        return new Random().nextInt(100);
    }
}

Callable接口和实现Runable接口的区别就是,Callable带泛型,其call方法有返回值。使用的时候,需要用FutureTask来接收返回值。而且它也要等到线程执行完调用get方法才会执行,也可以用于闭锁操作。

8. 读写锁 - ReadWriterLock

我们在读数据的时候,可以多个线程同时读,不会出现问题,但是写数据的时候,如果多个线程同时写数据,那么到底是写入哪个线程的数据呢?

所以,如果有两个线程,读-写 和 写-写 需要互斥,读-读 不需要互斥。这个时候可以用读写锁。

独占锁(写锁):一次只能被一个线程占有

共享锁(读锁):多个线程可以同时占有

代码实现

public class ReadWriteLockDemo {

    public static void main(String[] args) {
        RWLTest rwlTest = new RWLTest();

        // 写入
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                rwlTest.set(String.valueOf(finalI), finalI + "");
            }, String.valueOf(i)).start();
        }

        // 读取
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                rwlTest.get(String.valueOf(finalI));
            }, String.valueOf(i)).start();
        }
    }
}

class RWLTest {

    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 读(可以多个线程同时操作)
    public void get(String key) {
        readWriteLock.readLock().lock();// 上锁
        try {
            System.out.println(Thread.currentThread().getName() + " -> 读取 key[" + key + "]");
            Object obj = map.get(key);
            System.out.println(Thread.currentThread().getName() + " -> 获得 value[" + obj + "]");
        } finally {
            readWriteLock.readLock().unlock();// 释放锁
        }
    }

    // 写(一次只能有一个线程操作)
    public void set(String key, Object value) {
        readWriteLock.writeLock().lock();// 上锁
        try {
            System.out.println(Thread.currentThread().getName() + " -> 写入 key[" + key + "]");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " -> 写入成功!!!");
        } finally {
            readWriteLock.writeLock().unlock();// 释放锁
        }
    }
}

执行结果:

9. 阻塞队列 - BlockingQueue

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

① 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

② 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

9-1. 阻塞队列的四种方法

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

● 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。

● 有返回值,不抛出异常:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null

● 阻塞等待:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。

● 超时等待:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出等待。

代码演示:

public class BlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {

//        test01();
//        test02();
//        test03();
        test04();
    }

    /* 抛出异常 */
    public static void test01() {
        // 队列的大小
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(3);

        System.out.println(queue.add("a"));
        System.out.println(queue.add("b"));
        System.out.println(queue.add("c"));
//        System.out.println(queue.add("d")); // 队列已满,抛出异常 IllegalStateException

        System.out.println("队首 = " + queue.element()); // 检测队首元素

        System.out.println("==================");
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
//        System.out.println(queue.remove()); // 队列置空,没有元素,抛出异常 NoSuchElementException
    }

    /* 有返回值,不抛出异常 */
    private static void test02() {

        ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

        System.out.println(queue.offer("111"));
        System.out.println(queue.offer("222"));
        System.out.println(queue.offer("333"));
        System.out.println(queue.offer("444")); // false,不抛出异常

        System.out.println("队首 = " + queue.peek()); // 检测队首元素

        System.out.println("======================================");

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll()); // null,不抛出异常

    }

    /* 等待,阻塞(一直阻塞) */
    private static void test03() throws InterruptedException {

        ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

        // 一直阻塞
        queue.put("AAA");
        queue.put("BBB");
        queue.put("CCC");
//        queue.put("DDD"); // 队列已满,等待添加 -> 阻塞

        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
//        System.out.println(queue.take()); // 队列为空,等待获取 -> 阻塞
    }

    /* 有返回值,不抛出异常,等待,阻塞(等待超时) */
    private static void test04() throws InterruptedException {

        ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

        System.out.println(queue.offer("A1", 2, TimeUnit.SECONDS));
        System.out.println(queue.offer("B2", 2, TimeUnit.SECONDS));
        System.out.println(queue.offer("C3", 2, TimeUnit.SECONDS));
        System.out.println(queue.offer("D4", 2, TimeUnit.SECONDS)); // 等待超过两秒,就返回false

        System.out.println("================================");

        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS)); // 等待超过两秒,就返回null
    }
}
BlockingQueueTest

9-2. Java里的阻塞队列

JDK7提供了7个阻塞队列。分别是:

① ArrayBlockingQueue:是用数组实现的有界阻塞队列,初始化时必须传入容量,是FIFO队列,支持公平和非公平锁。

LinkedBlockingQueue:是用链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,是FIFO队列,因为入队和出队方法各是一把锁,所以一般情况下并发性能优于ArrayBlockingQueue。

LinkedBlockingDeque:是双向链表实现的有界阻塞队列,初始化时如果没有传入容量,则容量时Intger.MAX_VALUE,可以实现FIFO队列,也可以实现LIFO队列。

PriorityBlockingQueue:是数组实现的具有优先级的无界阻塞队列,有两个注意事项,

  1. 该队列是具有优先级的,不是按先进先出或者后进先出,是按优先级的高低,所以入队的对象必须是实现了Comparable接口的。
  2. 队列虽然逻辑上是无界的,但因为是数组实现的,不能无限扩容,作者在代码里限制了,最大容量是Intger.MAX_VALUE-8。

DelayQueue:延迟队列可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素, 元素必须是Delayed的子类

SynchronousQueue:不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。

LinkedTransferQueue:由链表结构组成的无界阻塞队列,对比LinkedBlockingQueue除了有put、take等方法外,还提供了transfer方法,该方法会阻塞线程,直到元素被消费,才返回。

上面列举了阻塞队列的异同点,我们可以根据自己的业务场景选择合适的阻塞队列。

简单介绍下其中两个队列:

9-2-1.  ArrayBlockingQueue

public interface BlockingQueue<E> extends Queue<E> {
    //入队,入队失败会抛出异常
    boolean add(E e);

    //入队,入队成功返回true,否则返回false
    boolean offer(E e);

    //入队,入队成功返回,否则进行等待
    void put(E e) throws InterruptedException;

    //入队,不成功等待一段时间,仍然不能入队成功返回false,入队成功返回true
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

    //出队,队列为空进行等待,否则将元素出队
    E take() throws InterruptedException;

    //出队,队列为空进行一段时间的等待,仍然为空返回null,否则将元素出队
    E poll(long timeout, TimeUnit unit) throws InterruptedException;

    //返回剩余余量
    int remainingCapacity();

    //将某个元素出队,存在返回true,否则返回false
    boolean remove(Object o);

    //判断是否包含某个元素
    public boolean contains(Object o);

    //将队列中所有可用元素出队到集合C中
    int drainTo(Collection<? super E> c);

    //将队列中最多maxElements个可用元素出队到集合C中
    int drainTo(Collection<? super E> c, int maxElements);
}
BlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列。

什么是公平的访问队列?

所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

访问者的公平性是使用可重入锁实现的,代码如下:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

9-2-2. SynchronousQueue

SynchronousQueue 是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

适用场景:线程之间数据传递,一个线程使用过的数据,传给另外一个线程使用。

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

public SynchronousQueue() {
    this(false);
}

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

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

- 公平模式:采用公平锁,在获取锁时,增加了isFirst(current)判断,当且仅当,等待队列为空或当前线程是等待队列的头结点时,才可尝试获取锁。

- 非公平模式(默认):采用非公平锁,那些尝试获取锁且尚未进入等待队列的线程会和等待队列head结点的线程发生竞争。

代码演示:

/**
 * 同步队列 - SynchronousQueue
 * 和其它的BlockingQueue不一样,SynchronousQueue不存储元素
 * put一个元素,必须take取出一个元素出来,否则不能再put进去
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {

        SynchronousQueue queue = new SynchronousQueue();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " Put 111");
                queue.put("111");
                System.out.println(Thread.currentThread().getName() + " Put 222");
                queue.put("222");
                System.out.println(Thread.currentThread().getName() + " Put 333");
                queue.put("333");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();

        new Thread(() -> {

            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " - Take " + queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " - Take " + queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " - Take " + queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}
SynchronousQueueDemo

10. 线程池

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

● 线程池工作机制

① 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

② 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

● 为什么使用?

多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

10-1. 线程池的好处

降低系统资源消耗。通过重用已存在的线程,降低线程创建和销毁造成的消耗;

提高系统响应速度。当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;

方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

提供更强大的功能。延时执行、定时循环执行的策略等

10-2. java中提供的线程池

Executors类提供了4种不同的线程池:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool

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

适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

可缓存线程池:

  1. 线程数无限制
  2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  3. 终止并从缓存中移除那些已有 60 秒钟未被使用的线程
  4. 一定程序减少频繁创建/销毁线程,减少系统开销

创建方法:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

示例代码:

public class NewCachedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可缓存线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    // 打印正在执行的缓存线程信息
                    System.out.println(Thread.currentThread().getName()
                            + "正在被执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
View Code

② newFiexedThreadPool(int Threads):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

定长线程池:

  1. 可控制线程最大并发数(同时执行的线程数)
  2. 超出的线程会在队列中等待

创建方法:

//nThreads => 最大线程数即 maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);

//threadFactory => 创建线程的方
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);

源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

示例代码:

public class NewFixedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可重用固定个数的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        // 打印正在执行的缓存线程信息
                        System.out.println(Thread.currentThread().getName() + "正在被执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
View Code

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

适用于需要保证顺序执行各个任务。

单线程化的线程池:

  1. 有且仅有一个工作线程执行任务
  2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

创建方法:

ExecutorService singleThreadPool = Executors.newSingleThreadPool();

源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}

示例代码:

public class NewSingleThreadExecutorTest {


    public static void main(String[] args) {
        //创建一个单线程化的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        //结果依次输出,相当于顺序执行各个任务
                        System.out.println(Thread.currentThread().getName() + "正在被执行,打印的值是:" + index);
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
View Code

④ newScheduledThreadPool(int corePoolSize):创建一个定长线程池,支持定时及周期性任务执行。

适用于执行延时或者周期性任务。

定长线程池:

  1. 支持定时及周期性任务执行。

创建方法:

//nThreads => 最大线程数即maximumPoolSize
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
}

示例代码:

public class NewScheduledThreadPoolTest {

    public static void main(String[] args) {
        //创建一个定长线程池,支持定时及周期性任务执行——延迟执行
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //延迟1秒执行
                 /*scheduledThreadPool.schedule(new Runnable() {
                     public void run() {
                        System.out.println("延迟1秒执行");
                     }
                 }, 1, TimeUnit.SECONDS);*/


        //延迟1秒后每3秒执行一次
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("延迟1秒后每3秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);

    }
}
View Code

10-3. ThreadPoolExecutor(重要)

在阿里巴巴开发手册中,明确规定线程池不允许使用 Executors 去创建,所以我们需要使用 ThreadPoolExecutor 的方式创建线程池。

● ThreadPoolExecutor提供了四个构造函数:

// 五个构造参数
public
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } // 六个构造参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } // 六个构造参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } // 七个构造参数,本质的ThreadPoolExecutor() public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小 int maximumPoolSize,// 最大核心线程池大小 long keepAliveTime, // 超时了没有人调用就会释放 TimeUnit unit, // 超时单位 BlockingQueue<Runnable> workQueue, // 阻塞队列 ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动 RejectedExecutionHandler handler){ // 拒绝策略 if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }

● 线程池的主要参数: 

① int corePoolSize:该线程池中核心线程数最大值

线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程

核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉

② int maximumPoolSize:该线程池中线程总数最大值

线程总数 = 核心线程数 + 非核心线程数。

③ long keepAliveTime:该线程池中非核心线程闲置超时时长

当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

④ TimeUnit uni:keepAliveTime的超时单位,TimeUnit是一个枚举类型

NANOSECONDS : 1微毫秒 = 1微秒 / 1000

MICROSECONDS : 1微秒 = 1毫秒 / 1000

MILLISECONDS : 1毫秒 = 1秒 /1000

SECONDS : 秒

MINUTES : 分

HOURS : 小时

DAYS : 天

⑤ BlockingQueue<Runnable> workQueue:阻塞队列

该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

⑥ ThreadFactory threadFactory:创建线程的方式(一般用不上)

这是一个接口,你new他的时候需要实现他的 Thread newThread(Runnable r) 方法

⑦ RejectedExecutionHandler handler:拒绝策略,线程池和队列都满了,再加入线程会执行此策略。

这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,你不指定他也有个默认的

○ 代码演示

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

        ExecutorService threadExecutor = new ThreadPoolExecutor(
                2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 丢弃任务,并抛出RejectedExecutionException异常 【 默认 】
            );

        try {
            // 最大承载:maximumPoolSize + workQueue,超过则抛出 RejectedExecutionException 异常
            for (int i = 0; i < 9; i++) {
                // 使用线程池创建线程
                threadExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " => OK");
                });
            }
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadExecutor.shutdown();
        }
    }
}

10-4. 线程池的四种拒绝策略

① ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。

如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

② ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。

③ ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

使用此策略,可能会使我们无法发现系统的异常状态。

建议是一些无关紧要的业务采用此策略。

例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。

④ ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

此拒绝策略,是一种喜新厌旧的拒绝策略。

是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。

10-5. 如何配置线程池

● CPU密集型任务

尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

● IO密集型任务

可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

● 混合型任务

可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失

11. 异步函数式编程 - CompletableFuture

使用 Future 获得异步执行结果时,要么调用阻塞方法 get() ,要么轮询看 isDone() 是否为 true ,这两种方法都不是很好,因为主线程也会被迫等待。

从 Java 8 开始引入了 CompletableFuture ,它针对 Future 做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

11-1. 创建 CompletableFuture 对象

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)

Asynsc 表示异步,而 supplyAsync  runAsync 不同在与前者异步返回一个结果,后者是 void 。第二个函数第二个参数表示是用我们自己创建的线程池,否则采用默认的 ForkJoinPool.commonPool() 作为它的线程池.其中 Supplier 是一个函数式接口,代表是一个生成者的意思,传入 0 个参数,返回一个结果。
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
            return "hello world";
  });
System.out.println(future.get());  //阻塞的获取结果  ''helllo world"

11-2. Demo

public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 没有返回值的 runAsync 异步回调
        // runAsync();
        // 有返回值的 supplyAsync 异步回调
        supplyAsync();
    }

    // 没有返回值的 runAsync 异步回调
    public static void runAsync() throws ExecutionException, InterruptedException {
// 创建异步执行任务 CompletableFuture
<Void> future = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " runAsync => Void"); }); System.out.println("11111111"); // 获取阻塞执行结果 future.get(); } // 有返回值的 supplyAsync 异步回调 public static void supplyAsync() throws ExecutionException, InterruptedException { // 创建异步执行任务: CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + " runAsync => Void"); // int i = 10 / 0; // 异常调用失败回调 return 2048; }); // 成功和失败的回调 Integer result = supplyAsync // 如果执行成功: .whenComplete((t, u) -> { System.out.println("t => " + t); // 正常的返回结果 System.out.println("u => " + u); // 错误描述信息:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero }) // 如果执行异常: .exceptionally((e) -> { System.out.println(e.getMessage()); return 404; // 错误的返回结果 }) // 获取执行结果 .get(); System.out.println("result => " + result); } }

12 深入单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

多种单例的特性:

单例模式 是否推荐 懒加载 反序列化单例 反射单例 克隆单例 性能、失效问题
饿汉式 Eager加载推荐 x x x x 类加载时就初始化,浪费内存
懒汉式(同步方法) x x x

x

存在性能问题,每次获取示例都会进行同步
双重检测锁(DCL) 可用 x x x

JDK < 1.5 失效

安全且在多线程情况下能保持高性能

静态内部类 推荐 x x x  和双检锁差不多,但这种方式只适用于静态域的情况
枚举 最推荐 x

JDK < 1.5 不支持

自动支持序列化机制,绝对防止多次实例化

12-1. 单例模式的几种实现方式

① 饿汉式:该模式在类被加载时就会实例化一个对象。

该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)

// 饿汉式单例
public class Hungry {

    // 在类装载时就实例化
    private static Hungry HUNGRY = new Hungry();

    // 私有化构造方法
    private Hungry() { }

    // 提供方法让外部获取实例
    public static Hungry getInstance() {
        return HUNGRY;
    }
}

这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显。

因为这句代码 private static Hungry HUNGRY = new Hungry(); 的关系,所以该类一旦被jvm加载就会马上实例化!

那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 懒汉式。

② 懒汉式:该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

public class LazyMan {

    private static LazyMan LAZY_MAN;

    private LazyMan() { }

    public synchronized static LazyMan getInstance() {
        if (LAZY_MAN== null)
            return new LazyMan();
        return LAZY_MAN;
    }
}

从线程安全性上讲,不加同步的懒汉式是线程不安全的,比如说:有两个线程,一个是线程A,一个是线程B,它们同时调用getInstance方法,那就可能导致并发问题。

所以只要加上 Synchronized 即可,但是这样一来,会降低整个访问的速度,而且每次都要判断,也确实是稍微慢点。

那么有没有更好的方式来实现呢?双重检查加锁,可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受到大的影响。

③ 双检锁/双重校验锁(DCL,即 double-checked locking):这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

双重检查加锁机制 并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查 这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

public class DoubleCheck {

    private volatile static DoubleCheck DOUBLE_CHECK;

    private DoubleCheck() { }

    public static DoubleCheck getInstance() {
        // 先检查实例是否存在,如果不存在才进入下面同步块
        if (DOUBLE_CHECK == null) {
// 同步块,线程安全的创建实例
synchronized (DoubleCheck.class) { // 再次检查实例是否存在,如果不存在则创建实例
if (DOUBLE_CHECK == null) return new DoubleCheck(); } } return DOUBLE_CHECK; } }

volatile关键字:将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

注意:在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。

这种实现方式既可使实现线程安全的创建实例,又不会对性能造成太大的影响,它 只在第一次创建实例的时候同步,以后就不需要同步了,从而加快运行速度

 由于 volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高 ,因此一般建议,没有特别的需要,不要使用。

也就是说,虽然可以使用双重加锁机制来实现线程安全的单例,但并不建议大量采用,根据情况来选用吧。

④ 静态内部类:采用类级内部类,在这个类级内部类里面去创建对象实例,只要不使用到这个类级内部类,那就不会创建对象实例。

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

当 getInstance 方法第一次被调用的时候,它第一次读取 SingletonHolder.instance ,导致 SingletonHolder 类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建 Singleton 的实例,由于是静态的域,因此只会被虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

public class Holder {

    // 私有化构造器,保证外部的类不能通过构造器来实例化
    private Holder() { }
    
    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
     * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
     */
    private static class InnerClass {
        // 静态初始化器,由JVM来保证线程安全
        private static final Holder HOLDER = new Holder();
    }

    // 获取单例对象实例
    public static final Holder getInstance() {
        return InnerClass.HOLDER;
    }
}

这个模式的优势在于,getInstance 方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

⑤ 枚举:枚举实现单例是最为推荐的一种方法,因为它更简洁并且就算通过序列化,反射等也没办法破坏单例性

  • Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法;
  • Java枚举类型的基本思想:通过公有的静态final域为每个枚举常量导出实例的类
  • 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

不能通过 reflection attack 来调用私有构造方法。

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

使用枚举来实现单实例控制,会更加简洁,而且无偿的提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

13. 公平锁、非公平锁、重入锁、自旋锁

13-1. 公平锁、非公平锁

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

1)是什么

公平锁就是先来后到、非公平锁就是允许加塞, Lock lock = new ReentrantLock(Boolean fair); 默认非公平。

  • 公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭。

  • 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。

2)两者区别

  • 公平锁:Threads acquire a fair lock in the order in which they requested it

    公平锁,就是很公平,在并发环境中,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

  • 非公平锁:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.

    非公平锁比较粗鲁,上来就直接尝试占有额,如果尝试失败,就再采用类似公平锁那种方式。

3)other

对 Java ReentrantLock 而言,通过构造函数指定该锁是否公平,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。
对 Synchronized 而言,是一种非公平锁。

15-2. 重入锁(递归锁)

1)递归锁是什么

指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

2)ReentrantLock / Synchronized 就是一个典型的可重入锁

3)可重入锁最大的作用是避免死锁

4)代码示例

● synchronized

public class Demo01 {

    public static void main(String[] args) {

        Phone phone = new Phone();

        new Thread(() -> { phone.sms(); }, "A").start();
        new Thread(() -> { phone.call(); }, "B").start();
    }
}

class Phone {

    // 锁 1
    public synchronized void sms() {
        System.out.println(Thread.currentThread().getName() + " => sms");
        try {
//            TimeUnit.SECONDS.sleep(3); // 线程睡眠,锁不释放
//            wait(); // 线程等待,释放锁
        } catch (Exception e) {
            e.printStackTrace();
        }
        call(); // 锁2 - 这里也有锁
    }

    // 锁 2
    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + " => call");
//        notify(); // 唤醒 A
    }
}

● ReentrantLock

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

        Phone2 phone2 = new Phone2();

        new Thread(() -> { phone2.sms(); }, "A").start();
        new Thread(() -> { phone2.call(); }, "B").start();
    }
}

class Phone2 {

    Lock lock = new ReentrantLock();

    public void sms() {
        lock.lock(); // 🔒1 细节问题:lock.lock(); lock.unlock(); - lock锁必须配对(成对出现),否则会死在里面
        lock.lock(); // 🔒2
        try {
            System.out.println(Thread.currentThread().getName() + " => sms");
            TimeUnit.SECONDS.sleep(3); // 睡眠3秒
            call(); // 也有锁
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 🔒1
            lock.unlock(); // 🔒2
        }
    }

    public void call() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " => call");
        } finally {
            lock.unlock();
        }
    }

}

13-3. 自旋锁

自旋锁(Spin lock) 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:

  • 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。

因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

1)加锁原理

线程一直是 running(加锁 ---> 解锁) ,死循环检测锁的标志位,机制不复杂。 互斥锁属于 sleep-waiting 类型的锁。例如在一个双核的机器上有两个线程 (线程 A 和线程 B ) ,它们分别运行在 Core0 和 Core1 上。假设线程 A 想要通过 pthread_mutex_lock 操作去得到一个临界区的锁,而此时这个锁正被线程 B 所持有,那么线程 A 就会被阻塞 (blocking),Core0 会在此时进行上下文切换 (Context Switch) 将线程 A 置于等待队列中,此时 Core0 就可以运行其他的任务 (例如另一个线程 C ) 而不必进行忙等待。而自旋锁则不然,它属于 busy-waiting 类型的锁,如果线程 A 是使用 pthread_spin_lock 操作去请求锁,那么线程 A 就会一直在 Core0 上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

2)应用

因为自旋锁是死循环检测,加锁全程消耗 cpu ,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

所有自旋锁主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器

3)spinlock

自旋尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

4)手写自旋锁

通过 CAS 操作完成自旋锁,A 线程先进来调用 mylock 方法自己持有锁3秒钟,B 随后进来发现当前有线程持有锁,不是 null,所以只能通过自旋等待,知道 A 释放锁后 B 随后抢到 
public class SpinLockDemo {

    public static void main(String[] args) {

        // 底层使用的自旋锁 CAS
        SpinLock spinLock = new SpinLock();

        new Thread(() -> {
            spinLock.myLock(); // 1 - A1获取锁
            try {
                TimeUnit.SECONDS.sleep(3); // 持锁睡眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnLock(); // 3 - 释放锁
            }
        }, "A1").start();

        try {
            TimeUnit.SECONDS.sleep(1); // 让出线层执行权
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLock.myLock(); // 2 - 自旋 等待A1释放锁,A1释放锁 S1获得
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.myUnLock(); // 4 - 释放S1锁
            }
        }, "S1").start();
    }
}

class SpinLock {

    // 原子引用线程
    AtomicReference<Thread> reference = new AtomicReference<>();

    // 加锁
    public void myLock() {

        Thread thread = Thread.currentThread(); // 获取当前的线程
        System.out.println(Thread.currentThread().getName() + " => myLock");
        // 自旋,期望值不为null -> 自旋
        while (!reference.compareAndSet(null, thread)) {
        }
    }

    // 解锁
    public void myUnLock() {
        Thread thread = Thread.currentThread(); // 获取当前的线程
        System.out.println(Thread.currentThread().getName() + " => myUnLock");
        reference.compareAndSet(thread, null); // 解锁 / 期望 !null 更新 null
    }
}

14. 死锁编码及定位分析

14-1. 死锁是什么

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

graph TD
 threadA(线程A)
 threadB(线程B)
 lockA((锁A))
 lockB((锁B))
 threadA--持有-->lockA
 threadB--试图获取-->lockA
 threadB--持有-->lockB
 threadA--试图获取-->lockB

14-2. 产生死锁的主要原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

14-3. 死锁示例

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,

public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldThread(lockA,lockB),"Thread-AAA").start();
        new Thread(new HoldThread(lockB,lockA),"Thread-BBB").start();
    }
}

class HoldThread implements Runnable {

    private String lockA;
    private String lockB;

    public HoldThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得:" + lockA);
            }
        }
    }
}

14-4. 解决死锁问题

1)使用 jps -l 定位进程号

2)jstack 进程号 找到死锁查看

3)jdk自带的 jconsole 工具

posted @ 2020-07-23 22:14  ohmok  阅读(353)  评论(0)    收藏  举报