JUC快速入门

1.JUC概述

1.1什么是Juc

JUC就是java.util .concurrent工具包的简称 Java1.5 开始出现

1.2 线程和进程的概念

1.2.1进程和线程

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

1.2.2线程的状态

NEW,(新建) RUNNABLE,(准备就绪) BLOCKED,(阻塞)

WAITING,(不见不散) TIMED_WAITING,(过时不候)

TERMINATED;(终结)

1.2.3wait和sleep

  • sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
  • sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
  • 它们都可以被interrupted方法中断。

1.2.4并发和并行

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点

并行:多项工作一起执行,之后再汇总

1.2.5管程

管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着java对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

1.2.6用户线程和守护线程

用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
主线程
结束后,用户线程还在运行
,JVM
存活
如果没有用户线程,都是守护线程,JVM结束

2.Lock接口

2.1 Synchronized

2.1.1 Synchronized 作用范围

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

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

    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
    复制代码
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

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

2.1.2 多线程编程步骤

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

第二、创建多线程调用资源类的方法

2.2 什么是Lock接口

Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。

Lock和synchronized有以下几点不同:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

2.3 创建线程的多种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable接口
  4. 使用线程池

2.4 使用Lock实现卖票例子

private final ReentrantLock lock = new ReentrantLock();

public  void sale() {
    lock.lock();
    try {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + " : 卖出: " + (num --) + "剩下: " + num);
        }
    } finally {
        lock.unlock();
    }
}
复制代码

3.线程间通信

sychronized

public synchronized void incr() throws InterruptedException {
    while (number != 0) {
        this.wait();
    }
    number ++;
    System.out.println(Thread.currentThread().getName() + ": : " + number);
    this.notifyAll();
}

public synchronized void decr() throws InterruptedException {
    while (number != 1) {
        this.wait();
    }
    number --;
    System.out.println(Thread.currentThread().getName() + ": : " + number);
    this.notifyAll();
}
复制代码

Lock()

private Lock lock = new ReentrantLock();

private Condition condition = lock.newCondition();

public void incr() throws InterruptedException {
    lock.lock();
    try {
        while (number != 0) {
            condition.await();
        }
        number ++;
        System.out.println(Thread.currentThread().getName() + ": : " + number);
        condition.signalAll();
    } finally {
        lock.unlock();
    }
}

public void decr() throws InterruptedException {
    lock.lock();
    try {
        while (number != 1) {
            condition.await();
        }
        number --;
        System.out.println(Thread.currentThread().getName() + ": : " + number);
        condition.signalAll();
    } finally {
        lock.unlock();
    }
}
复制代码

4.线程间的定制化通信

private Lock lock = new ReentrantLock();

private Condition c1 = lock.newCondition();

private Condition c2 = lock.newCondition();

private Condition c3 = lock.newCondition();

public void print5(int loop) throws InterruptedException {
    lock.lock();
    try {
        while (flag != 1) {
            c1.await();
        }
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
        }
        flag = 2;
        c2.signal();
    }finally {
        lock.unlock();
    }
}

public void print10(int loop) throws InterruptedException {
    lock.lock();
    try {
        while (flag != 2) {
            c2.await();
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
        }
        flag = 3;
        c3.signal();
    }finally {
        lock.unlock();
    }
}

public void print15(int loop) throws InterruptedException {
    lock.lock();
    try {
        while (flag != 3) {
            c3.await();
        }
        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i + "轮数: " + loop);
        }
        flag = 1;
        c1.signal();
    }finally {
        lock.unlock();
    }
}
复制代码

5.集合间的线程安全

5.1 ArrayList线程不安全

演示

List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
    new Thread(() -> {
        list.add(UUID.randomUUID().toString().substring(0, 8));
        System.out.println(list);
    }, String.valueOf(i)).start();
}
复制代码

image-20210808220759990

解决方案一 Vector

List<String> list = new Vector<>();
复制代码

解决方案二 Collections

List<String> list = Collections.synchronizedList(new ArrayList<>());
复制代码

解决方案三 CopyOnWriteArrayList

List<String> list = new CopyOnWriteArrayList<>();
复制代码

image-20210808222054448

5.2 Hashset线程不安全

解决方案

Set set = new CopyOnWriteArraySet();
复制代码

5.3 HashMap线程不安全

解决方案

Map map = new ConcurrentHashMap();
复制代码

6.多线程锁

6.1 演示锁的八种情况

class Phone {
    public static synchronized void sendSMS() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

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

    public void getHello() {
        System.out.println("------getHello");
    }
}
复制代码

1 标准访问,先打印短信还是邮件

——sendSMS

——sendEmail

2 停4秒在短信方法内,先打印短信还是邮件

——sendSMS

——sendEmail

3 新增普通的hello方法,是先打短信还是hello

——getHello

——sendSMS

4 现在有两部手机,先打印短信还是邮件

——sendEmail

——sendSMS

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

——sendSMS

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

—–sendSMS

——sendEmail

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

——sendEmail

——sendSMS

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

——sendEmail

——sendSMS

结论

对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象

6.2 公平锁和非公平锁

非公平锁:线程饿死 效率高

公平锁:阳光普照,效率相对高

6.3 可重入锁

sychronized演示

Object o = new Object();
new Thread(() -> {
    synchronized (o) {
        System.out.println(Thread.currentThread().getName() + " 外层");
        synchronized (o) {
            System.out.println(Thread.currentThread().getName() + " 中层");
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + " 内层");
            }
        }
    }
}, "t1").start();
复制代码

lock演示

Lock lock = new ReentrantLock();
new Thread(() -> {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + " 外层");
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 中层");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 内层");
            }finally {
                lock.unlock();
            }
        }finally {
            lock.unlock();
        }
    }finally {
        lock.unlock();
    }
}, "t1").start();
复制代码

6.4 死锁

1、什么是死锁

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

image-20210808234402022

2、产生死锁原因

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

3、死锁的演示

4、验证是否是死锁

  • jps 类似linux中 ps -ef
  • jstack jvm自带堆栈跟踪工具

7.Callable接口&Future接口

7.1、Callable接口

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法
  • call方法可以引发异常,run()不能
  • 为实现Callable而必须重写call方法
  • 不能直接替换runnable,因为Thread类的构造方法根本没有Callable

7.2、Future接口

当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。。要实现此接口,必须重写5种方法,这里列出了重要的方法,如下:

  • public boolean cancel(boolean mayInterrupt)

    用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务
    复制代码
  • public Object get()throws InterruptedException,ExecutionException

    用于获取任务的结果。如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果
    复制代码
  • public boolean isDone()

    如果任务完成,则返回true,否则返回false
    复制代码

7.3、FutureTask

Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。

核心原理

在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成

  • 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
  • 一旦计算完成,就不能再重新开始或取消计算
  • get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
  • get只计算一次,因此get方法放到最后

案例

public class Demo1 {
    static class MyThread1 implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "线程进入了run方法");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    static class MyThread2 implements Callable {

        @Override
        public Long call() throws Exception {
            try {
                System.out.println(Thread.currentThread().getName() + "线程进入了call方法,开始sleep");
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "睡醒了");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return System.currentTimeMillis();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runnable runnable = new MyThread1();
        Callable callable = new MyThread2();
        FutureTask futureTask = new FutureTask(callable);

        new Thread(futureTask, "线程二").start();
        for (int i = 0; i < 10; i++) {
            Long res = (Long) futureTask.get();
            System.out.println(res);
        }

        new Thread(runnable, "线程一").start();

    }
}
复制代码

7.4、小结

  • 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成, 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
  • 只计算一次

8.JUC的三大辅助类

8.1、减少计数CountDownLatch

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

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行

场景

//场景:六个同学全部离开教室,班长关门
public class CountDownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "号同学离开了教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("班长给教室锁门");
    }

}
复制代码

image-20210812231811288

8.2、循环栅栏CyclicBarrier

CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作

场景

//场景:集齐七颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {

    private static final int NUMBER = 7;

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("集齐" + NUMBER + "召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "星龙珠被收集");
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }

}
复制代码

image-20210812235147317

8.3、信号灯 Semaphore

emaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用acquire方法获得许可证,release方法释放许可

场景

//6辆汽车抢3个停车位
public class SemaphoreDemo {

    public static void main(String[] args) {
        //创建3个车位
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {

                try {
                    //抢占车位
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "号汽车抢到了车位");
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName() + "离开了");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    //空出车位
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}
复制代码

image-20210813001747390

9.读写锁

9.1、读写锁介绍

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

1 线程进入读锁的前提条件:

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

2 线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

3 读写锁三个重要的特性

  1. 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  2. 重进入:读锁和写锁都支持线程重进入。
  3. 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

9.2、ReentrantReadWriteLock

ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。

9.3、案例

class MyCache {

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

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //放数据
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始写");
            TimeUnit.MICROSECONDS.sleep(300);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写完了");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //读数据
    public Object get(String key) {
        Object res = null;
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始读");
            TimeUnit.MICROSECONDS.sleep(300);
            res = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读完了");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return res;
    }
}

public class Demo {

    public static void main(String[] args) {
        MyCache m = new MyCache();
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                m.put(num + "", num);
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                m.get(num + "");
            }, String.valueOf(i)).start();
        }
    }
}
复制代码

10.BlockingQueue阻塞队列

10.1 阻塞队列概述

Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

10.2 阻塞队列的架构

image-20210813162744141

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

10.3 阻塞队列分类

  1. ArrayBlockingQueue 由数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列

10.4 阻塞队列核心方法

  1. 放入数据
    • offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)
    • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败
    • put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
  2. 获取数据
    • poll(time): 取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
    • .poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败
    • take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
    • drainTo(): 一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

11.ThreadPool线程池

11.1、线程池简介

优势

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

特点

  • 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

11.2、 线程池的参数说明

1 常用参数

  • corePoolSize线程池的核心线程数
  • maximumPoolSize能容纳的最大线程数
  • keepAliveTime空闲线程存活时间
  • unit 存活的时间单位
  • workQueue 存放提交但未执行任务的队列
  • threadFactory 创建线程的工厂类
  • handler 等待队列满后的拒绝策略

当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略

2 拒绝策略

CallerRunsPolicy:当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大

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

DiscardPolicy: 直接丢弃,其他啥都没有

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

11.3 线程池的种类和创建

1 newCachedThreadPool(常用)

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

特点:
• 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
• 线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
• 当线程池中,没有可用线程,会重新创建一个线程

创建方式

public static ExecutorService newCachedThreadPool(){ 
    /** 
    * corePoolSize线程池的核心线程数
    * maximumPoolSize能容纳的最大线程数 
    * keepAliveTime空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(0, 
            Integer.MAX_VALUE, 
            60L, 
            TimeUnit.SECONDS,
            new SynchronousQueue<>(),                    				Executors.defaultThreadFactory(), 
            new ThreadPoolExecutor.AbortPolicy()); 
}
复制代码

场景

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

2 newFixedThreadPool(常用)

作用

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

特征

• 线程池中的线程处于一定的量,可以很好的控制线程的并发量
• 线程可以重复被使用,在显示关闭之前,都将一直存在
• 超出一定量的线程被提交时候需在队列中等待

创建方式

public static ExecutorService newFixedThreadPool(){ 
    /** 
    * corePoolSize线程池的核心线程数 
    * maximumPoolSize能容纳的最大线程数 
    * keepAliveTime空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(10,
           10, 
           0L, 
           TimeUnit.SECONDS, 
           new LinkedBlockingQueue<>(),
           Executors.defaultThreadFactory(), 
           new ThreadPoolExecutor.AbortPolicy()); 
}
复制代码

场景

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

3 newSingleThreadExecutor(常用)

作用

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

特征

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

创建方式

public static ExecutorService newSingleThreadExecutor(){ 
    /** 
    * corePoolSize线程池的核心线程数 
    * maximumPoolSize能容纳的最大线程数 
    * keepAliveTime空闲线程存活时间 
    * unit 存活的时间单位 
    * workQueue 存放提交但未执行任务的队列 
    * threadFactory 创建线程的工厂类:可以省略 
    * handler 等待队列满后的拒绝策略:可以省略 
    */ 
    return new ThreadPoolExecutor(1,
           1, 
           0L, 
           TimeUnit.SECONDS, 
           new LinkedBlockingQueue<>(),
           Executors.defaultThreadFactory(), 
           new ThreadPoolExecutor.AbortPolicy()); 
}
复制代码

4 newScheduleThreadPool(了解)

作用

线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池

特征

  • 线程池中具有指定数量的线程,即便是空线程也将保留

  • 可定时或者延迟执行线程活动

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { 
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); 
}
复制代码

场景

适用于需要多个后台线程执行周期任务的场景

5 newWorkStealingPool

jdk1.8提供的线程池,底层使用的是ForkJoinPool实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu核数的线程来并行执行任务

创建方式

public static ExecutorService newWorkStealingPool(int parallelism) { 
    /** 
    * parallelism:并行级别,通常默认为JVM可用的处理器个数 
    * factory:用于创建ForkJoinPool中使用的线程。 
    * handler:用于处理工作线程未处理的异常,默认为null 
    * asyncMode:用于控制WorkQueue的工作模式:队列---反队列
	*/ 
    return new ForkJoinPool(parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); 
}
复制代码

场景

适用于大耗时,可并行执行的场景

11.4、线程池案例演示

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadService = new ThreadPoolExecutor(3, 3, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque()
                , Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());

        try{
            for (int i = 1; i <= 10; i++) {
                threadService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "开始卖票");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖票结束");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadService.shutdown();
        }
    }
}
复制代码

11.5、线程池的底层工作原理

image-20210813210512295

  1. 在创建了线程池后,线程池中的线程数为零
  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
    2. 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

11.6 注意事项

创建线程池推荐适用ThreadPoolExecutor及其7个参数手动创建

image-20210813211014572

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享