Java中的多线程(下)

目录

一、juc下的Lock

1.接口Lock

2.接口实现类——ReentrantLock类

2.1基本格式:

2.2其他方法的演示与区别

2.3 synchronized VS ReentrantLock

二、volatile机制

三、单例模式(懒汉、饿汉)

1.饿汉模式

2.懒汉模式 

四、wait() 和 notify()

五、阻塞队列

1.Who?

2.典型应用——生产者消费者模型

(1)什么是生产者-消费者模型

(2)引入阻塞队列的作用:

3.自己实现一个基于数组的阻塞队列

 4.生产者-消费者模型的应用

五、定时器

1.定时器的使用

2.定时器的内部原理实现

【面试题】sleep和wait的区别:

六、线程池

1.存在的意义及初识

2.实现一个线程池(Runnable command)——实现Executor接口


一、juc下的Lock

sycchronized锁是在JVM内部实现的,是一种非常早期就存在的锁,而在我们的jdk标准库中,也定义实现了类似的锁机制,以类、对象的形式给我们使用,不再是简单的语言层面

1.接口Lock

(1)juc包指的是 java.util.concurrent.*,现代写并发编程都尽量使用juc包下提供的工具

(2)Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition

(3)juc下的锁,定义了接口Lock

Lock接口下的方法定义如下图:

(1)lock()——普通的加锁操作,相当于synchronized加锁

(2)lockInterruptibly()——允许被打断的加锁,就是说,使用这种加锁操作,是允许被interrupted的,如果被打断,我就会停止我的加锁操作,而我们之前的加锁方式即使收到了interrupted信号,也是停不下来的

(3)tryLock()——是对加锁操作的结果处理,如果我没有加到锁,就返回false(可以在没加到锁的时间里先去做些别的事)

(4)tryLock(long time,TimeUnit unit)——在一定时间内如果都没加到锁,就返回false

(5)unLock()——解锁操作

Lock实现提供了使用synchronized方法和语句的附加功能,通过提供非阻塞尝试来获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly() ,以及尝试获取可以超时( tryLock(long, TimeUnit)

相较于synchronized锁,加锁策略更加的灵活

2.接口实现类——ReentrantLock类

目前的实现类有三个

这里我们主要学习ReentrantLock类(re-entrant——可重入),可重入的互斥锁 

2.1基本格式:

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


/**juc下lock的使用
 * @author sunny
 * @date 2022/05/02 08:52
 **/
public class Creat {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
//        加锁
        lock.lock();
        try {
//            在try语句块中放临界代码

        }  finally {
//            在finally中解锁,确保任何情况下都能解锁
            lock.unlock();
        }

    }
}

如之前我们写过的加1000次1,再减1000次1的例子,使用juc下的ReentrantLock写,如下:(lock()操作跟synchronized锁一样,只是写法不一样罢了) 

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

/**juc下的锁
 * @author sunny
 * @date 2022/05/02 10:11
 **/
public class Main66 {
    static int r = 0;
    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 1_0000_0000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    // r++ 和 r-- 互斥
    static class Add extends Thread {
        private Lock o;
        Add(Lock o) {
            this.o = o;
        }

        @Override
        public void run() {
//            加锁锁
            o.lock();
            try{
                for (int i = 0; i < COUNT; i++) {
                    r++;    // r++ 是原子的
                }
            }finally {
                o.unlock();
            }

        }
    }

    static class Sub extends Thread {
        private Lock o;

        Sub(Lock o) {
            this.o = o;
        }

        @Override
        public void run() {
            o.lock();
            try {
                for (int i = 0; i < COUNT; i++) {
                    r--;    // r-- 是原子的的
                }
            }finally {
                o.unlock();
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
//        利用构造传入同一把锁
        Lock o = new ReentrantLock();

        Main6.Add add = new Main6.Add(o);
        add.start();

        Main6.Sub sub = new Main6.Sub(o);
        sub.start();

        add.join();
        sub.join();

        System.out.println(r);
    }

}

2.2其他方法的演示与区别

(1)主线程lock()方法忘记解锁而导致子线程永远也拿不到锁

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

/**死锁
 * @author sunny
 * @date 2022/05/02 10:21
 **/
public class DeadlyLock {
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
//        主线程加锁而未解锁
        lock.lock();
//        子线程则是永远也得不到锁的
        MyThread t = new MyThread();
        t.start();
        t.join();
    }
    static class MyThread extends Thread{
        @Override
        public void run() {
            lock.lock();
            System.out.println("进入子线程");
        }
    }
}

(2)lock()方法即使使用interrupted中断信号,也是停不下来的 

(3)而使用lockInterruptibly方法则允许中断

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

/**
 * lockInterruptibly方法则允许中断
 *
 * @author sunny
 * @date 2022/05/02 10:36
 **/
public class LockInterruptibly {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
//        主线程加锁而未解锁
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);
//        中断子线程
        t.interrupt();


    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lockInterruptibly();
                System.out.println("进入子线程");
            } catch (InterruptedException e) {
                System.out.println("收到停止信号,停下来了");
            }

        }
    }
}

结果看到:程序是运行结束了的

 (4)tryLock()操作

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

/**tryLock操作
 * @author sunny
 * @date 2022/05/02 10:43
 **/
public class TryLock {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
//        主线程加锁而未解锁
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);
//        中断子线程
        t.interrupt();


    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = lock.tryLock();
            if (b == true){
                System.out.println("加锁成功,进入子线程");
            }else{
                System.out.println("加锁失败");
//                可以继续执行我要做的别的十二
                System.out.println("其他代码执行");
            }

        }
    }
}

(5)带时间的tryLock

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

/**带时间的tryLock
 * @author sunny
 * @date 2022/05/02 10:48
 **/
public class TryLockWithTime {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
//        主线程加锁而未解锁
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);
//        中断子线程
        t.interrupt();


    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = false;
            try {
                b = lock.tryLock(5, TimeUnit.SECONDS);
                if (b == true) {
                    System.out.println("加锁成功,进入子线程");
                } else {
                    System.out.println("加锁失败");
//                可以继续执行我要做的别的十二
                    System.out.println("其他代码执行");
                }
            } catch (InterruptedException e) {
                System.out.println("俺被打断了呀");
            }
        }
    }
}

结果:

ps:修改一下代码:

如果我把主线程的锁解锁,同时休眠时间2秒<5秒,所以会加锁成功

如果我把主线程的锁解锁,同时休眠时间修改为10秒,10>5,所以会加锁失败

2.3 synchronized VS ReentrantLock

🧁synchronized锁只有这一种锁,会自动请求加锁和解锁

🧁synchronized锁无法被中止(感受不到中止信号,一直在自动请求加锁)

🧁juc下的锁可能忘记写lock.unlock()导致锁一直不释放,需要手动unlock()解锁

🧁juc类型下的锁更灵活(公平锁/非公平锁;读写锁/独占锁)

🧁juc下加锁策略更灵活(带中断、try、带时间的try)

二、volatile机制

synchronized主要就是保护了原子性,而这里的violatile机制,则可以很好的保护内存可见性

volatile的作用有哪些呢?

(1)最主要的作用——保护变量的内存可见性

volatile中文意思是不稳定的易变的;

volatile修饰变量(表明该变量可变)如果没有该词修饰,JVM为了速度快会直接借助工作内存(缓存),不及时读写入主内存,导致多线程下数据不一致

而有该词修饰后,JVM不优化存入缓存,每次从主内存读,写回主内存,不再有内存可见性的问题

代码在写入 volatile 修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

但是注意,volatle不能修饰局部变量

(2)小范围的保护原子性

JVM基本操作长度是32位

int short byte float char 引用的赋值 都是原子的

但是long 、double是64位,不是原子的,用volatile修饰可保证局部原子性

volatile long  a = 4;

(3)保证代码重排序

A a = new A()——>根据类计算对象大小,在内存(堆)中分配空间给对象——>对象的初始化(构造代码块,属性的初始化赋值,构造方法)——>把对象的引用交给a

上述步骤为理论步骤,但实际中,顺序不能保证,可能会重排序,单线程即使重排序也不出错,但多线程可能会出错

用volatile修饰后,能保证一定是按照逻辑顺序执行的

volatile A a = new A();

三、单例模式(懒汉、饿汉)

何为单例模式?

通过代码保护一个类,使得该类在一个进程中有且只有一个对象,如配置对象,控制类

1.饿汉模式

所谓饿汉模式,就是一开始就初始化,饿汉模式天生就线程安全

/**饿汉模式
 * @author sunny
 * @date 2022/05/02 16:04
 **/
public class StarvMode {
//    类加载的同时创建类的实例
    private static StarvMode instance = new StarvMode();
    private StarvMode(){

    }

    public static StarvMode getInstance() {
        return instance;
    }
}

2.懒汉模式 

所谓懒汉模式,就是用时再初始化,但是呢因为全程就这一个对象实例,故而只初始化一次

在多线程中,懒汉模式是不安全的,我们需要加锁加volatile来保证线程安全

(1)先看单线程下的懒汉模式:

/**单线程下的懒汉模式
 * @author sunny
 * @date 2022/05/02 16:14
 **/
public class LazyMode1 {
    private static LazyMode1 instance = null;
    private LazyMode1(){

    }
//    当第一次用到这个实例时,我们再初始化

    public static LazyMode1 getInstance() {
//        当第一次调用这个方法时,才初始化,if控制只初始一次
        if(instance == null){
            instance = new LazyMode1();
        }
        return instance;
    }
}

(2)如果是多线程下,多个线程同时去调用getInstance方法,是线程不安全的,就可能导致创建出多个实例

所以我们需要加个锁

/**多线程下的懒汉模式
 * synchronized锁保护原子性
 * @author sunny
 * @date 2022/04/25 19:21
 **/
public class LazyMode2 {
    //    instance只会被实例化一次
    private volatile static LazyMode3 instance = null;

//    synchronized锁
    public synchronized static LazyMode3 getInstance() {
        if (instance == null) {
            instance = new LazyMode3();
        }
        return instance;
    }
}

(3)但是我们还可以发现,其实只有第一次会进if里进行实例化,后续调用是不用的

可是我们这样直接在方法上加锁或者在if那里加锁,导致多线程调用该方法时都在这把锁这里等着,即使早已被实例化好了,还是需要排队一个一个来,导致效率不高

所以我们又有了一个升级版;

if里面再if加锁
 

/**优化的多线程下的懒汉模式
 * @author sunny
 * @date 2022/04/25 19:16
 **/
public class LazyMode3 {
//    instance只会被实例化一次
//    加volatile,避免instance初始化时重排序,导致线程不安全
    private volatile static LazyMode3 instance = null;
    public static LazyMode3 getInstance(){
        if(instance == null){
//            只有当instance尚未被初始化时,才会走到这里
//            在分支里对if操作加锁保护,因为该类的每个线程都只有第一次会初始化
            synchronized (LazyMode3.class){
                if(instance == null){
                    instance = new LazyMode3();
                }
            }
        }
        return instance;
    }
}

四、wait() 和 notify()

🧁wait()和notify()

大多数情况下,线程与线程之间是要通信的,尤其在下面的生产者-消费者模式中,线程与线程之间需要互相等待与唤醒,就比如消费者原本是在阻塞的,生产者put元素后,就要去唤醒消费者,告诉它我生产好了,你可以用了

所以,我们这里先来介绍两个方法:wait()方法与notify()方法,顾名思义,等待与唤醒

  • 这两个方法位于Object类下:Object.wait(),Object.notify(),所以所有对象都带有这两个方法
  • 要使用这两个方法,必须先对使用这个方法的对象进行synchronized加锁,否则会运行时报错,
  • notify()唤醒机制——随机唤醒,从阻塞队列中随机选一个唤醒

  • notufyAll()——唤醒所有,但顺序仍是不固定的哟

    import java.util.concurrent.TimeUnit;
    
    /**notify的随机唤醒
     * @author sunny
     * @date 2022/04/25 20:37
     **/
    public class Notify_Demo {
        public static Object o = new Object();
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 5; i++) {
               MyThread t = new MyThread();
               t.start();
            }
    //        先让主线程休眠
            TimeUnit.SECONDS.sleep(5);
    //        再唤醒
           synchronized (o) {
    //           notifyAll是将所有都唤醒
    //           而notify是随机唤醒一个
               o.notifyAll();
    //         o.notify();
           }
    
        }
        static class MyThread extends Thread{
            @Override
            public void run() {
                synchronized (o){
                    try {
                        o.wait();
                        System.out.println(getName() + "被唤醒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

  • wait()方法其实内部是有释放锁的,在等待的过程中是不持有锁

    /**wait的不拥有锁
     * @author sunny
     * @date 2022/05/03 13:16
     **/
    public class Wait_Unlock {
        static Object o1 = new Object();
        static Object o2 = new Object();
        static Object o3 = new Object();
        public static void main(String[] args) throws InterruptedException {
            synchronized (o1){
                synchronized (o2){
                    synchronized (o3){
                        MyThread t = new MyThread();
                        t.start();
    //                    o3.wait(),wait会释放锁,这里,只会释放o3的
                        o3.wait();
                    }
                }
            }
        }
        static class MyThread extends Thread{
            @Override
            public void run() {
                synchronized (o3){
                    o3.notify();
                    System.out.println("o3唤醒");
                }
            }
        }
    }

    输出结果:o3唤醒

  • wait()方法结束的情况:(notify,被中止(异常),假唤醒,时间到了)

  • wait和notify是没有状态保存机制的,所以一定要先wait再notify,才有效果,如果先notify再去wait,还是会一直wait下去,感知不到有过notify动作

wait-notify使用示例:

import java.util.concurrent.TimeUnit;

/**wait-notify
 * @author sunny
 * @date 2022/04/25 20:10
 **/
public class Wait_Demo {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        MyThread t = new MyThread(o);
        t.start();
        synchronized (o){
            o.wait();
            System.out.println("wait后的语句");
        }


    }
    static class MyThread extends Thread{
        Object o;

        public MyThread(Object o) {
            this.o = o;
        }

        @Override
        public void run() {
//            2秒后唤醒o
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o){
                o.notify();
            }
        }
    }
}

 输出结果:
2秒后打印输出:

 最后,因为wait和notify必须搭配synchronized使用,所以juc下用Condition类下的系列方法来代替wait和notify

五、阻塞队列

1.Who?

(1)阻塞队列是一种特殊的队列,也遵循先进先出原则;

关键区别在于:额外增加了put,take操作,操作不成功不是返回,而是会阻塞等待

(2)在Java标准库中,阻塞队列也是在juc包下(java.util.concurrent),BlockingQueue是Queue的子接口,所以,Queue中有的方法,它都有

(3)BlockingQueue的常用实现类有:

 ArrayBlockingQueue

LinkedBlockingQueue

PriorityBlockingQueue

……

(4)接口内的方法:

💫💫💫put(e) 和 take() 操作

put操作是向阻塞队列中入队:当队列未满的时候,正常入队,当队列满的时候, 继续入队列就会阻塞在这里, 直到有其他线程从队列中取走元素

take操作是从阻塞队列中出队:当队列不空的时候,正常出队,当队列为空的时候,继续出队列也会阻塞在这里, 直到有其他线程往队列中插入元素.

💫💫💫offer(e,time,unit) 和 poll(time,unit) 操作

offer还是入队,poll是出队,这两个操作允许在规定的时间内阻塞等待,如果超过规定的时间仍然未成功,则按照正常形式返回

注意:InterruptException】当这几种方法处于阻塞时,也是可以被interrupt的,被Interrupt后该方法也会退出,但是是以抛出异常的形式结束

2.典型应用——生产者消费者模型

该模型在写多线程时非常常见

(1)什么是生产者-消费者模型

所谓生产者-消费者就是字面意思:生产者负责生产,消费者使用生产者生产出来的“数据”

为了降低生产者-消费者之间的强耦合,我们让生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

生产者-消费者之间通过阻塞队列来通信,以降低生产者-消费者之间的强耦合度

(2)引入阻塞队列的作用:

1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

比如在 "秒杀" 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 "削峰", 防止服务器被突然到来的一波请求直接冲垮.

2) 阻塞队列也能使生产者和消费者之间 解耦.

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 "生产者", 包饺子的人就是 "消费者". 擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).

3.自己实现一个基于数组的阻塞队列

(基于数组的阻塞队列,实则是一个基于数组的循环队列;主要实现put和take操作)

/**
 * 自己实现一个基于数组的阻塞队列
 *
 * @author sunny
 * @date 2022/04/25 21:06
 **/
public class MyArrayBlockingQueue {
    private long[] arr;
    //    始终指向队首
    private int firstIndex;
    //    指向下一个put的位置
    private int nextIndex;
    //    阻塞队列中的个数
    private int size;

    //    构造方法,传入队列大小
    public MyArrayBlockingQueue(int capacity) {
        arr = new long[capacity];
        firstIndex = 0;
        nextIndex = 0;
        size = 0;
    }

    //    put入队操作
    public synchronized void put(long a) throws InterruptedException {
//        先判满
//        这里用while防止被假唤醒
        while (arr.length == size) {
//            如果满了,先等待
            this.wait();
        }
//        不满了,再put
        arr[nextIndex] = a;
        nextIndex++;
        if (nextIndex == arr.length) {
            nextIndex = 0;
        }
        size++;
//        入队成功后去唤醒take
        notify();
    }
//    take出队操作
    public synchronized void take() throws InterruptedException {
        while (size == 0){
            this.wait();
        }
//        此时,一定不为空
        firstIndex ++;
        if(firstIndex == arr.length){
            firstIndex = 0;
        }
        size --;
//        唤醒put
        notify();
    }
}

 4.生产者-消费者模型的应用

🍭🍭🍭一个生产者,一个消费者:

下面这个例子中,主线程是生产者,子线程是消费者

为便于观察,我们让子线程等待输入,在我们输入后再消费

所以,主线程在放第四个数字时,阻塞

等我们输入数字后,子线程take,主线程才可以继续put

/**一个生产者,一个消费者
 * @author sunny
 * @date 2022/05/03 14:05
 **/

import java.util.Scanner;

public class Main2 {
    static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            long e = scanner.nextLong();

            try {
                queue.take();
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);   // 阻塞
        System.out.println("4 被放入队列中");
    }
}

控制台:

任意输入一个数字后,打印输出

“4被放入队列中” 

🍭🍭🍭在上述阻塞队列+生产消费模式下,如果是多个生产者,多个消费者,其实是有bug的

notify是随机唤醒机制,并不能保证生产者唤醒的一定是消费者,消费者唤醒的一定是生产者

所以当队列容量较小,生产者和消费者又非常多时,很可能导致消费者唤醒的全部是消费者,出现等待集中全部是生产者,而消费者看到的队列容量又是0,也全部进入等待集中,导致两方都在等待,没有唤醒的

所以,我们的阻塞队列可以更改一点点,就是把所有的notify更换为notifyAll,当然,这样是效率很低的

五、定时器

1.定时器的使用

Timer类——任务调度

TimerTask抽象类(继承该类,重写run方法,run方法就是待执行的任务)

多长时间后执行/周期性执行任务

定时器执行任务时不会占用我们当前的执行流

package practice_class.thread.timer_test;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @author sunny
 * @date 2022/04/27 18:40
 **/
public class UseTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("Timer 执行");
            }
        };
//        1000毫秒后闹钟响了
//        timer.schedule(task,1000);
//        1000毫秒后定时器开始执行任务,且每隔5000毫秒就执行一次,频率是5000毫秒
        timer.scheduleAtFixedRate(task,1000,5000);
//        主线程在死循环打印,说明定时器执行任务时不会占用我们当前的执行流
        while (true){

        }
    }
}

2.定时器的内部原理实现

先来一个一个定时器实现一个任务:

/**Timer的简单实现
 * @author sunny
 * @date 2022/04/27 18:51
 **/
public class Timer_implement {
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("Timer 执行");
            }
        };
        MyThread t = new MyThread(task,3000);
        t.start();

    }
    static class MyThread extends Thread{
        Runnable task;
        long delay;
        public MyThread(Runnable task,long delay){
            this.task = task;
            this.delay = delay;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(delay);
                task.run();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

上面这种简单的实现方式,一个定时任务就需要一个Thread线程,是不方便的

所以Java中不是这样实现的——优化:一个线程执行多个任务,也就是专门创建了一个工作线程

该工作线程使用优先级阻塞队列(delay时间最小的优先级最高),不断从优先级阻塞队列中取任务做任务

下面我们再来简单实现一下一个定时器执行多个任务的代码实现:

代码:

💌Timer——MyTimer类

import java.util.concurrent.PriorityBlockingQueue;

/**
 * @author sunny
 * @date 2022/04/27 19:19
 **/
public class MyTimer {
    private final PriorityBlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();
    private final Object newTask = new Object();
//    构造方法启动线程
    public MyTimer(){
        WorkerThread work = new WorkerThread();
        work.start();
    }
    //        schedule方法
    public void schedule(MyTimerTask task,long delay){
//        runTime计算的是闹钟响了的时间,用于去阻塞队列中比较优先级
        task.runTime = System.currentTimeMillis() + delay;
        queue.put(task);
        synchronized (newTask){
            newTask.notify();
        }
    }
    //    一个工作线程
    class WorkerThread extends Thread{
        @Override
        public void run() {
            MyTimerTask task = null;
            while(true){
//                从优先级阻塞队列中取任务
                try {
                    task = queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
//                有了task,我们应该知道什么时候执行该任务
                long now = System.currentTimeMillis();
                long delay = task.runTime - now;
                if(delay <= 0){
                    task.run();
                }else{
                    try {
                        synchronized (newTask){
                            newTask.wait(delay);
                        }
                        if(System.currentTimeMillis() >= task.runTime){
                            task.run();
                        }else{
                            queue.put(task);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

💌💌TimerTask——MyTimerTask抽象类

public abstract class MyTimerTask implements Comparable<MyTimerTask>{
    long runTime;
    void run(){};

    @Override
    public int compareTo(MyTimerTask o) {
        if(runTime - o.runTime < 0){
            return -1;
        }else if(runTime - o.runTime > 0){
            return 1;
        }else{
            return 0;
        }
    }
}

💌💌💌测试应用Main类

public class Main {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        MyTimerTask task1 = new MyTimerTask() {
            @Override
            void run() {
                System.out.println("一秒钟闹钟到了");
            }
        };
        MyTimerTask task2 = new MyTimerTask() {
            @Override
            void run() {
                System.out.println("0.5秒钟闹钟到了");
            }
        };
        timer.schedule(task1,1000);
        timer.schedule(task2,500);

    }
}

结果:

 上面实现的是一次性任务,对于周期性任务只要执行后再放入阻塞队列中然后重新计算时间即可

周期性:执行——再放入队列——更新时间

【面试题】sleep和wait的区别:

sleep是休眠,休眠固定时间,让线程进入阻塞状态——而wait是在等待,主要用于线程间的通信

wait常常和notify搭配使用,并且必须使用synchronized加锁,而sleep是不需要的

sleep是Thread类的静态方法,而wait是Object类的普通方法

wait的两个结束条件:超时时间已到,或者条件满足

sleep和锁无关,而wait会释放当前的锁

六、线程池

1.存在的意义及初识

创建销毁线程都是无意义的成本——>线程池模式,提前创建好很多线程,按需创建,有新任务直接交给准备好的线程

线程池最大的好处就是减少每次启动、销毁线程的损耗

ThreadPoolExecutor实现自ExecutorService接口(ExecutorService接口又实现自Executor接口)

(1)按需创建:

【正式员工指核心线程,这些线程是常驻线程,不会被轻易销毁,临时员工即临时线程,这些线程在工作完后空闲超出一定时间后就会被销毁】

一开始,线程池中一个工作线程都没有;

随着任务的提交:

如果正式员工的数量<正式员工上限,创建一个新的正式员工

如果数量=上限,暂时把任务放到阻塞队列中

如果队列也满了,雇佣临时工

如果员工总数到达上限了,队列也满了——拒绝策略

定义了四种拒绝策略,拒绝(抛出异常),交由调用者运行,丢弃最旧的,丢弃当前的

(2)ThreadPoolExecutor的构造方法

  • corePoolSize——(正式员工的名额上限)即核心线程的上限数,核心线程是常驻线程,这些线程被创建后便不会被消除
  • maximumPoolSize——(正式+临时的最大数量)核心线程加临时线程的总数不能超出这个最大值
  • keepAliveTime + unit——临时线程允许空闲的时间上限(unit是时间单位),超出这个时间,临时线程被销毁
  • handler——拒绝策略:以抛异常形式直接拒绝、交由调用者执行、删除最旧的、删除当前的

 

(3)简单应用

import java.util.Scanner;
import java.util.concurrent.*;

public class Demo {
    public static void main(String[] args) {
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);

        ThreadFactory tf = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "饭店厨师");
                return t;
            }
        };

        ExecutorService service = new ThreadPoolExecutor(
                3, // 正式员工 10
                9, // 临时员工 20
                10, TimeUnit.SECONDS,
                queue,
                tf,
                new ThreadPoolExecutor.AbortPolicy()
        );

        // 定义任务
        Runnable task = new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.DAYS.sleep(365);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 把任务提交给线程池对象(公司)

        Scanner s = new Scanner(System.in);
        for (int i = 1; i < 100; i++) {
            s.nextLine();
            service.execute(task);
            System.out.println(i);
        }
    }

}

如何解除摸鱼状态下的正式员工?

2.实现一个线程池(Runnable command)——实现Executor接口

MyThreadPoolExecutor类

import java.util.concurrent.*;

/**
 * @author sunny
 * @date 2022/05/05 19:05
 **/
public class MyThreadPoolExecutor implements Executor {
    // 创建线程的工厂对象
    private final ThreadFactory threadFactory;

    // 临时工摸鱼的时间上限
    private final long keepAliveTime;
    private final TimeUnit unit;

    // 当前正式员工的数量
    private int currentCoreSize;

    // 正式员工的数量上限
    private final int corePoolSize;

    // 当前临时员工的数量
    private int currentTemporarySize;

    // 临时员工的数量上限
    private final int temporaryPoolSize;

    // 传递任务的阻塞队列
    private final BlockingQueue<Runnable> workQueue;

    public MyThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue,
                                ThreadFactory threadFactory,
                                RejectedExecutionHandler handler) {
        this.corePoolSize = corePoolSize;
        this.temporaryPoolSize = maximumPoolSize - corePoolSize;
        this.workQueue = workQueue;
        this.threadFactory = threadFactory;
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
    }

    // 向线程池中提交任务
    @Override
    public void execute(Runnable command) {
        // 1. 如果正式员工的数量还低于正式员工的数量上限,则优先创建正式员工处理任务
        // 1.1 需要管理,当前正式员工有多少,正式员工的数量上限有多少?
        if (currentCoreSize < corePoolSize) {
            // 优先创建正式员工进行处理
            // 创建一个线程,这个线程中的任务就是不断地取任务-做任务,但是不需要考虑退出的问题
            CoreJob job = new CoreJob(workQueue, command);
//            Thread thread = new Thread(job);    // 不使用工厂创建的线程
            Thread thread = threadFactory.newThread(job);   // thread 代表的就是正式员工
            String name = String.format("正式员工-%d", currentCoreSize);
            thread.setName(name);

            thread.start();

            // 只是两种不同的策略,没有谁是正确的说法
            // 1. 把 command 放到队列中;command 的执行次序是在队列已有的任务之后
            // 2. 创建正式员工的时候,就把 command 提交给正式员工,让 command 优先执行
            // 我们这里采用第二种方案,主要原因就是 java 官方的就是使用的第二种策略

            currentCoreSize++;
            return;
        }

        // 走到这里,说明正式员工的数量 == 正式员工的上限了
        // 2. 优先把任务放入队列中,如果放入成功,execute 执行结束,否则还需要继续
        // 2.1 需要一个阻塞队列
//        workQueue.put(command); // 带阻塞的放入,是否满足这里的需求?
        // 我们这里希望的是立即得到结果
        boolean success = workQueue.offer(command);
        if (success == true) {
            // 说明放入队列成功
            return;
        }

        // 队列也已经放满了
        // 3. 继续判断,临时工的数量有没有到上限,如果没有到达,创建新的临时工来处理
        if (currentTemporarySize < temporaryPoolSize) {
            // 创建临时工进行处理
            TemporaryJob job = new TemporaryJob(keepAliveTime, unit, workQueue, command);
//            Thread thread = new Thread(job);    // 不使用工厂创建的线程
            Thread thread = threadFactory.newThread(job);   // thread 代表的就是临时员工
            String name = String.format("临时员工-%d", currentTemporarySize);
            thread.setName(name);

            thread.start();

            currentTemporarySize++;
            return;
        }

        // 4. 执行拒绝策略
        // 为了实现方便,暂时不考虑其他策略
        throw new RejectedExecutionException();
    }
}

TemporaryJob类——临时工

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

/**一个临时工要做的工作
 * @author sunny
 * @date 2022/05/05 20:08
 **/
public class TemporaryJob implements Runnable{
    // 需要阻塞队列
    private final BlockingQueue<Runnable> workQueue;
    private final long keepAliveTime;
    private final TimeUnit unit;
    private Runnable firstCommand;

    TemporaryJob(long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, Runnable firstCommand) {
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
        this.workQueue = workQueue;
        this.firstCommand = firstCommand;
    }

    @Override
    public void run() {
        try {
            firstCommand.run();     // 优先先把刚提交的任务先做掉了
            firstCommand = null;    // 这里设置 null 的意思是,不影响 firstCommand 对象被 GC 时的回收

            // 一旦超过一定时间没有任务,临时工是需要退出的
            // 1. keepAliveTime + unit 记录起来
            // 2. 怎么就知道超过多久没有任务了?如果一定时间内都无法从队列中取出来任务,则认为摸鱼时间够了
            while (!Thread.interrupted()) {
//                Runnable command = workQueue.take();
                Runnable command = workQueue.poll(keepAliveTime, unit);
                if (command == null) {
                    // 说明,没有取到任务
                    // 说明超时时间已到
                    // 说明该线程已经 keepAliveTime + unit 时间没有工作了
                    // 所以,可以退出了
                    break;
                }
                command.run();
            }
        } catch (InterruptedException ignored) {}
    }
}

CoreJob类——正式工

import java.util.concurrent.BlockingQueue;

/**一个正式工要做的工作
 * @author sunny
 * @date 2022/05/05 19:39
 **/
public class CoreJob implements Runnable{
    // 需要阻塞队列
    private final BlockingQueue<Runnable> workQueue;
    private Runnable firstCommand;

    CoreJob(BlockingQueue<Runnable> workQueue, Runnable firstCommand) {
        this.workQueue = workQueue;
        this.firstCommand = firstCommand;
    }

    @Override
    public void run() {
        try {
            firstCommand.run();     // 优先先把刚提交的任务先做掉了
            firstCommand = null;    // 这里设置 null 的意思是,不影响 firstCommand 对象被 GC 时的回收

            while (!Thread.interrupted()) {
                Runnable command = workQueue.take();
                command.run();
            }
        } catch (InterruptedException ignored) {}
    }
}

Main类

import java.util.concurrent.*;

/**
 * @author sunny
 * @date 2022/05/05 19:40
 **/
public class Main {
    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(15);
            } catch (InterruptedException ignored) {}
        }
    }

    static class MyThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    }

    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
        // 同时最多有 15 个任务
        // 3 个正式的
        // 5 个队列中
        // 7 个临时的
        // 提交第 16 个任务时就会出现拒绝服务
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(
                3, 10, 10, TimeUnit.SECONDS,
                workQueue,
                new MyThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        // [0, 1, 2] 交给正式员工在处理
        // [3, 4, 5, 6, 7] 暂时放在队列中
        // [8, 9, 10, 11, 12, 13, 14] 交给临时工处理
        // 过了 15s 之后,第一批任务执行结束
        // [0, 1, 2]、[8, 9, 10, 11, 12, 13, 14] 执行结束
        // 剩下的 [3, 4, 5, 6, 7] 任务具体怎么分配不确定,大概率是交给正式员工执行
        // 就算极端情况下,5 个全部给了临时工
        // 也至少还有 2 个临时工没有工作
        // 再过 10s,至少 2 个,最多 5 个临时工要被解雇
        for (int i = 0; i < 15; i++) {
            System.out.println("提交任务: " + i);
            Task task = new Task();
            executor.execute(task);
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨笨在努力

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值