【学习笔记】Java多线程

148 阅读12分钟

1 线程

1.1 相关方法

方法作用备注
getId()获取线程的ID1)某线程执行完毕后,其ID可能会被后续新的线程使用
2)重启JVM后,同一个线程的ID可能不一样
setPriority()设置线程的优先级,取值范围是[1, 10]优先级越高,得到CPU调度的概率越高
setDaemon(true)设置线程为守护线程1)守护线程不能单独运行,如果所有用户线程执行结束,则JVM会关闭,守护线程也将结束
2)该方法需要在start()之前执行才有效
interrupt()给线程设置一个中断标志1)并不是真正中断线程,而是打上了一个标志
2)可通过isInterrupted()判断该标志来手动退出,如下例子
3)该方法会中断wait()方法,见章节4.1的例1
// 通过判断中断标志来手动退出
public class MyThead extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100000; i++) {
            System.out.println("子线程:" + i);
            if (this.isInterrupted()) {
                return;
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThead t1 = new MyThead();
        t1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:" + i);
        }
        t1.interrupt();
    }
}

1.2 生命周期

可通过getState()获取所在状态:

  • NEW:新建状态,还未执行start()方法

  • RUNNABLE:可运行状态,实际上包含READY和RUNNING两个状态。当执行start()方法后,变为READY,获取CPU执行权后,则是RUNNING。当调用Thread.yield()后,由RUNNING变为READY。

  • BLOCKED:阻塞状态,如等待IO操作或者申请其他线程的独占资源时,都会变成阻塞状态,此时不占用CPU资源。当IO结束或声请到资源后,变为RUNNABLE

  • WAITING:等待状态,当执行了 Object.wait() 方法或者 其他线程.join() 方法,则该线程变为WAITING状态。当执行 Object.notify() 或者加入的线程执行完毕,变为RUNNABLE

  • TIMED_WAITING:与WAITING相似,但它是带有计时的等待状态,即不会无限等待。

  • TERMINATED:终止状态,线程运行结束

线程的生命周期.png

1.3 多线程的风险

  • 线程安全问题:没有正确处理并发访问逻辑,使得出现脏数据、丢失数据更新等问题

  • 线程活性问题:由于程序缺陷或资源缺陷,导致线程一直处于非RUNNABLE状态。常见的有以下几种

    1. 死锁:类似于鹬蚌相争,谁也不让谁
    2. 活锁:指线程一直处于运行状态,但是其任务一直无法进展的一种活性故障。产生活锁的线程一直在做无用功,如小猫一直追着自己的尾巴咬但总是咬不到
    3. 饥饿:线程一直无法获得资源来运行。比如在CPU繁忙的情况下,优先级低的线程执行的概率低,就可能发生线程“饥饿”
  • 上下文切换:CPU从一个线程切换到另一个线程执行,需要耗费时间

2 线程安全

2.1 原子性

对变量的操作不可分割

  1. 线程访问(读、写)某个共享变量时,对于其他线程来说,这个操作要么执行完毕,要么未执行,其他线程无法看到中间结果
  2. 访问共享变量的原子操作,不能交错执行

在Java中有两种方式实现原子性:

  1. 使用锁:保证共享变量在某一时刻只能被一个线程访问
  2. 使用处理器的CAS指令

2.2 可见性

在多线程环境中,一个线程对某个共享变量进行更新后,其他线程能够立刻看到这个新的值

2.3 有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

可通过volatile、synchronized、lock保证有序性

2.4 Java内存模型

  Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。与JVM的内存划分不是同一个概念

线程-JMM.png

JMM规定:

  • 每个线程的共享变量存储在主内存
  • 每个线程都有自己的工作内存。工作内存是一个抽象的概念,并非真实存在。涵盖写缓冲器、寄存器等
  • 每个线程访问变量时,都要从主内存中拷贝一份副本到自己的工作内存里,操作完成后再将变量刷写回主内存。这个过程其他线程是不可见的

3 线程同步

  线程同步机制是一套用于协调线程之间的数据访问的机制。该机制可以保障线程安全。Java平台提供同步机制的有:锁、volatile、final、static等等。

3.1 volatile

  volatile关键字的作用:使变量在多个线程之间可见。线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存;当其他线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

  由于该关键字不保证原子性,所以只有当新值不依赖当前值时,才可以使用volatile;否则如果依赖当前值,则是 取值、计算、写值 三步操作,这三步操作不是原子性的,而volatile不保证原子性。例子如下:

public class Test {
    public static void main(String[] args) {
        MyInt myInt = new MyInt();

        // 开启子线程,无限循环
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 子线程开始运行...");
            while (myInt.getVal() == 0) {
            }
            System.out.println(Thread.currentThread().getName() + " 子线程结束运行...");
        }).start();

        // main线程休眠1秒后修改myInt的值
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myInt.setVal(1);
        System.out.println(Thread.currentThread().getName() + ": val is " + myInt.getVal());
    }

    private static final class MyInt {
        // 使用volatile关键字,子线程会看到main线程修改后的最新值,因此会结束循环
        // 若没有该关键字,则子线程不会结束
        private volatile int val;

        public int getVal() {
            return val;
        }

        public void setVal(int val) {
            this.val = val;
        }
    }
}

volatile与synchronized的比较:

  • volatile是线程同步的轻量级实现,性能比后者好
  • volatile只能修饰变量,后者可以修饰代码块、方法
  • 多线程访问volatile变量不会发送阻塞,而后者会阻塞
  • volatile能保证可见性、有序性;但不能保证原子性(例子如下)。synchronized则均能保证这三个特性
public class Test {

    public static void main(String[] args) {
        MyInt myInt = new MyInt();
        int end = 100000;
        // 开启两个子线程,每个线程自增100000次
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < end; j++) {
                    myInt.increment();
                }
            }).start();
        }

        // main线程休眠2秒,保证子线程跑完
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 此时值不等于200000,就是因为volatile不保证原子性
        System.out.println(myInt.getVal());
    }

    private static final class MyInt {

        private volatile int val;

        public int getVal() {
            return val;
        }

        public void increment() {
            // 分取值、计算、写值三步操作,非原子操作
            val++;
        }
    }
}

3.2 CAS

  CAS(Compare And Swap),含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值;否则,放弃本次操作或者重试。下面是基于CAS实现的一个计数器:

public class CASCounter {

    private volatile long val;

    public long incrementAndGet() {
        long oldVal;
        long newVal;
        do {
            oldVal = val;
            newVal = oldVal + 1;
        } while (!compareAndSwap(oldVal, newVal));
        return newVal;
    }

    private boolean compareAndSwap(long expectVal, long newVal) {
        // 这里使用同步代码块是模拟处理器提供的CAS指令
        synchronized (this) {
            if (val == expectVal) {
                val = newVal;
                return true;
            }
            return false;
        }
    }

    public static void main(String[] args) {
        CASCounter counter = new CASCounter();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    System.out.println(Thread.currentThread().getName() + ": " + counter.incrementAndGet());
                }
            }).start();
        }
    }
}

ABA问题:当进行compareAndSwap操作时,此时当前值看似等于期望值,但实际上是已经被更新过的了,即ABA中前一个A和后一个A的时间戳(或者版本号)不同,但是当前线程会认为该值没有被改过,这种情况能否被接受?

3.3 锁

  将多个线程对共享数据的访问改为串行访问,即一个共享数据一次只能被一个线程访问。锁就是利用这个思路来保障线程安全,即保障了原子性、可见性、有序性。Java的锁有两种:内部锁(synchronized关键字)、显示锁(java.util.concurrent.locks.Lock接口的实现类)

  • 一个线程只有在拥有锁(许可证)的情况下才能访问共享变量
  • 一个锁只能被一个线程拥有,称为排他锁互斥锁
  • 线程访问结束后释放锁,运行过程中出现异常也会释放锁

锁的其他概念:

  1. 可重入性:一个线程在拥有锁的时候能否再次申请该锁,若能则是可重入锁。
void methodA() {
    申请A锁;
    methodB();
    释放A锁;
}

void methodB() {
    申请A锁; // 如果这里也能申请成功,就是可重入锁
    doSomething;
    释放A锁;
}
  1. 根据锁的争用与调度机制可分:公平锁和非公平锁
  2. 锁的粒度:锁保护的共享数据的大小。若锁保护的共享数据量大,则称该锁的粒度粗;反之粒度小。粒度过粗会导致申请锁时进行不必要的等待,过小则会增加锁调度的开销

3.3.1 synchronized

  Java中每个对象都能充当一个内部锁,这种锁也被称为监视器(Monitor),是一种排他锁。synchronized有以下3种使用场景

  1. 修饰代码块。进入同步代码块时,会清空工作内存,从主内存读取最新值;退出同步代码块时,则会把工作内存的值写回主内存
synchronized (对象锁) {
	// 同步代码块            
}
public class Test {
    public static void main(String[] args) {
        final Object mutex = new Object();
        // 此时第一个线程打印完后,第二个线程才会开始打印。不会出现交错打印的情况
        new Thread(() -> {
            synchronized (mutex) {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (mutex) {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }).start();
    }
}
  1. 修饰实例方法,此时锁是this
  2. 修饰静态方法,此时锁是运行时类,即XXX.class

3.3.2 Lock

java.util.concurrent.locks.Lock接口的常用方法有:

方法作用备注
lock()等待获取锁
unlock()释放锁通常放在try/finally中来获取、释放锁
lockInterruptibly()等待获取锁在等待的过程中如果被中断了(调用线程的interrupt()方法),则抛异常结束
tryLock(long, TimeUnit)等待获取锁在指定等待时间中,如果锁没有被其他线程持有,且当前线程也没被中断,才能获取锁,并返回true
newCondition()返回Condition对象Condition对象也能实现等待通知机制,见章节4.1的例2

Lock与synchronized的区别:

  • 对于synchronized内部锁来说,如果一个线程在等待锁,则只有两种结果:要么获得锁继续执行,要么继续等待。
  • 而Lock则提供了另外一种可能:在等待锁的过程中,可以根据需求中断对锁的等待,只要把lock()方法改为lockInterruptibly()方法即可。表示如果在等待锁的过程中,被中断了(调用线程的interrupt()方法),则抛异常结束。合理使用lockInterruptibly()方法可解决死锁问题。

Lock接口的常用实现类:ReentrantLock,该类的常用方法有:

方法作用备注
getHoldCount()返回当前线程调用lock()的次数
getQueueLength()返回正在等待获取锁的线程预估数量是一个预估值,不保证准确
getWaitQueueLength(Condition)返回正在等待指定Condition对象的线程预估数量
hasQueuedThread(Thread)查询指定线程是否在等待锁
hasQueuedThreads()查询是否有线程在等待锁
hasWaiters(Condition)查询是否有线程在等待指定Condition对象
isHeldByCurrentThread()判断该锁是不是被当前线程持有常用于释放锁之前的判断
isLocked()判断该锁是否被线程持有
public class LockTest {

    private static final Lock lock = new ReentrantLock();

    public static void method1() {
        try {
            // 先获取锁,通常用try/finally包住
            lock.lock();
            // 这中间的代码就相当于同步代码块
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        } finally {
            // 最后释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = LockTest::method1;

        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

3.3.3 公平锁和非公平锁

  多数情况下,锁的申请都是非公平的。当多个线程在申请同一个锁时,只是从阻塞队列中随机取一个线程获取锁,这是非公平的。而公平锁则按照时间顺序保证先到先得,从而避免线程饥饿问题。synchronizedned是非公平锁;ReentrantLock则提供了一个构造方法可以指定为公平锁,但需要维护一个有序队列,性能较低。

3.3.4 ReadWriteLock

  synchronizedned内部锁和ReentrantLock都是排他锁,同一时间只允许一个线程持有,以此保证线程安全,但是执行效率较低。而读写锁是一种改进的共享/排他锁,允许多个线程同时读取共享数据,但每次只允许一个线程修改数据。通过读锁写锁来实现:线程读取数据前必须先持有读锁,读锁可以被多个线程持有,即读锁是共享的;线程在修改数据前必须先持有写锁,写锁是排他的,当线程持有写锁时,其他线程无法获取任何锁,不管是读还是写。保证了在读取数据期间,数据不会被修改。

其他线程能否获取读锁其他线程能否获取写锁
某线程获取读锁YESNO
某线程获取写锁NONO

java.util.concurrent.locks.ReadWriteLock接口定义了两个方法:readLock()返回一个读锁、writeLock()返回一个写锁。注意:这两个方法返回的是同一个锁的两个不同角色,而不是两个不同的锁。常用实现类:ReentrantReadWriteLock

4 线程通信

4.1 等待通知机制

  在多线程编程中,A线程的运行条件可能并不满足,此时可以将A线程暂停,等待其他线程更新这个条件,直到A线程满足运行条件后再将它唤醒。

相关的实现方法如下:

方法作用备注
Object.wait()使得当前线程暂停,直到被唤醒为止1)只能在同步代码块中由锁对象调用
2)调用方法后,当前线程会立即释放锁
3)interrupt()方法会中断线程的wait()方法
Object.notify()可以唤醒一个处于wait状态的线程1)只能在同步代码块中由锁对象调用
2)如果由多个等待的线程,只能随机唤醒其中一个
3)调用方法后,需要执行完同步代码块的内容才会释放锁
4)被唤醒的线程需要重新获得锁后才能继续往下执行
Object.notifyAll()可以唤醒全部处于wait状态线程
Condition.await()使得当前线程暂停1)Condition对象通过Lock.newCondition()方法获取
2)调用await()/signal()前需要获取对应的Lock锁
3)可创建多个Condition对象来绑定线程,达到唤醒指定线程的效果,更为灵活
Condition.signal()可以唤醒一个线程

例1interrupt()方法会中断线程的wait()方法

public class WaitTest {
    public static void main(String[] args) {
        final Object mutex = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (mutex) {
                try {
                    System.out.println("开始wait...");
                    mutex.wait();
                    System.out.println("结束wait...");
                } catch (InterruptedException e) {
                    System.out.println("wait()方法被中断");
                }
            }
        });
        t1.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 此时会中断子线程的wait()方法
        t1.interrupt();
    }
}

例2:使用Condition对象实现等待通知机制

public class ConditionTest {
    public static void main(String[] args) {
        final Lock lock = new ReentrantLock();
        final Condition condition0 = lock.newCondition();
        final Condition condition1 = lock.newCondition();

        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获得锁后开始等待...");
                condition0.await();
                System.out.println(Thread.currentThread().getName() + "被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获得锁后开始等待...");
                condition1.await();
                System.out.println(Thread.currentThread().getName() + "被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        try {
            Thread.sleep(1000);
            // main线程获得锁后,将子线程唤醒
            lock.lock();
            // 调用的是condition0的方法,达到只唤醒线程0的效果
            condition0.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4.2 生产者消费者模型

  1. 编写产品类
public class Product {

    private int num;

    public Product(int num) {
        this.num = num;
    }

    public synchronized void addProduct() {
        if (num < 10) {
            num++;
            System.out.println(Thread.currentThread().getName() + "添加产品,现有" + num + "个");
            notifyAll();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void delProduct() {
        if (num > 0) {
            num--;
            System.out.println(Thread.currentThread().getName() + "减少产品,现有" + num + "个");
            notifyAll();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 编写生产者线程
public class ProducerThread extends Thread {

    private final Product product;

    public ProducerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        super.run();
        while (true) {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.addProduct();
        }
    }
}
  1. 编写消费者线程
public class ConsumerThread extends Thread {

    private final Product product;

    public ConsumerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        super.run();
        while (true) {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.delProduct();
        }
    }
}
  1. 测试类
public class Test {
    public static void main(String[] args) {
        Product product = new Product(10);

        ProducerThread t0 = new ProducerThread(product);
        ConsumerThread t1 = new ConsumerThread(product);
        ConsumerThread t2 = new ConsumerThread(product);

        t0.start();
        t1.start();
        t2.start();
    }
}

5 ThreadLocal

  ThreadLocal主要是为每个线程绑定自己的值,通过ThreadLocalMap实现,即每个线程都有自己的ThreadLocalMap。这个Map的key是ThreadLocal变量实例,value则是set()进去的值。如下例子所示:

public class Test {
    // 定义ThreadLocal变量时,一般加上static关键字
    static ThreadLocal<String> name = new ThreadLocal<>();

    static ThreadLocal<String> phone = new ThreadLocal<>();

    static ThreadLocal<Integer> age = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            name.set("小明");
            phone.set("123xxxx6666");
            age.set(20);
            System.out.println(Thread.currentThread().getName() + "_" + name.get());
            System.out.println(Thread.currentThread().getName() + "_" + phone.get());
            System.out.println(Thread.currentThread().getName() + "_" + age.get());
        });

        Thread t2 = new Thread(() -> {
            name.set("小红");
            phone.set("123xxxx9999");
            age.set(18);
            System.out.println(Thread.currentThread().getName() + "_" + name.get());
            System.out.println(Thread.currentThread().getName() + "_" + phone.get());
            System.out.println(Thread.currentThread().getName() + "_" + age.get());
        });

        t1.start();
        t2.start();
    }
}

ThreadLocal结构.png

6 线程管理

6.1 线程组

  类似于计算机中使用文件夹来管理文件。在线程组中,可以定义一组相似(相关)的线程,也可以定义子线程组。创建线程时可定义线程组,若不指定则默认属于父线程所在的线程组。现在的开发已经不常用线程组,一般会将相关的线程放在一个数组或集合中。

6.2 处理线程异常

  在线程运行时,如果有受检异常则需要进行捕获处理,如果有运行时异常,则可以通过设置未捕获异常处理器(UncaughtExceptionHandler)来处理,当线程出现运行时异常后,则会调用设置好的Handler来处理异常。

例1:设置全局的异常处理器

public class ExceptionHandlerTest {
    public static void main(String[] args) {
        // 设置全局的异常处理器,所有线程都按照以下逻辑处理异常
        Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
            System.out.println(thread.getName() + "线程抛出异常:" + exception.getMessage());
        });

        // main线程出现异常,然后被上述处理器处理
        int i = 10 / 0;
    }
}

例2:为某个线程设置单独的异常处理器

public class ExceptionHandlerTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "开始运行...");
            int i = 10 / 0;
        });
        // 为t1线程设置单独的异常处理器
        t1.setUncaughtExceptionHandler((thread, exception) -> {
            System.out.println(thread.getName() + "线程抛出异常:" + exception.getMessage());
        });
        t1.start();
    }
}

6.3 注入Hook子线程

  JVM退出的时候会执行Hook线程。基于这个特性,我们可以在程序启动时创建一个.lock文件,用于校验程序是否启动,在程序结束时执行的Hook线程中删除该文件,以此防止程序重复启动。除外,Hook线程也常用于做资源释放的操作。注意:Hook线程只有在程序正常退出时才会执行,强制关闭(如kill -9)则不会执行Hook线程。

public class HookTest {

    private static final String FILE_PATH = "E:\\tmp.lock";

    public static void main(String[] args) {
        // 程序运行时,检查lock文件是否存在
        File file = new File(FILE_PATH);
        if (file.exists()) {
            // 如果已经存在则退出
            throw new RuntimeException("程序已启动!");
        } else {
            // 不存在则创建
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 注入一个Hook线程,用于删除lock文件
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("程序即将退出,将执行Hook线程");
            file.delete();
        }));

        // 模拟程序运行
        try {
            System.out.println("程序正在运行...");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

6.4 线程池

  上文的例子中都是通过new Thread()的方式来创建线程,这样创建的线程对象在执行完run()方法后会被GC回收。而线程的创建、调度、销毁都有一定的开销,这会导致整个应用的性能降低,所以我们需要线程池来有效使用线程。

  线程池内部维护一定数量的工作线程,开发人员将任务作为一个对象提交给线程池,线程池则把这些任务缓存在工作队列中,然后工作线程不断从队列中取出任务来执行。在开发中,一般使用第三方提供的线程池,或使用java.util.concurrent.Executors的相关方法创建线程池。

6.4.1 基本使用

例1:提交普通任务

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 创建线程池,有5个线程
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        // 向线程池提交10个任务
        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getId() + "执行任务");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

例2:提交计划任务

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 创建具有调度功能的线程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
        // 提交一个任务,2秒后执行
        executorService.schedule(() -> {
            System.out.println(Thread.currentThread().getId() + "执行任务");
        }, 2, TimeUnit.SECONDS);

        // 提交定时执行的任务,3秒后,每隔5秒就执行一次
        executorService.scheduleAtFixedRate(() -> {
            System.out.println(Thread.currentThread().getId() + "执行定时任务");
        }, 3, 5, TimeUnit.SECONDS);
    }
}

6.4.2 各个参数

以ThreadPoolExecutor的构造方法为例,各个参数的含义如下

  • corePoolSize:核心线程数量

    参考公式:线程大小 = CPU数量 * 目标CPU的使用率 * (1 + 等待时间与计算时间的比)

  • maximumPoolSize:最大线程数量

  • keepAliveTime:当线程数量超过核心线程数量时,多余线程的存活时长

  • unit:上述keepAliveTime的单位

  • workQueue:任务队列,任务会提交到该队列等待执行

  • threadFactory:线程工厂,用于创建线程

  • handler:拒绝策略,当任务过多来不及处理时使用策略拒绝

6.4.3 任务队列

其中任务队列是由阻塞队列(BlockingQueue)实现,一般有以下几种:

  • 直接提交队列:如SynchronousQueue。该队列没有容量,提交给线程池的任务不会被真实保存,而是将新的任务交给线程执行。如果没有空闲线程则尝试创建新的线程,当线程数量达到maximumPoolSize后执行拒绝策略。
  • 有界任务队列:如ArrayBlockingQueue。该队列有具体容量,处理新任务的逻辑如下图所示

有界任务队列逻辑.png

  • 无界任务队列:如LinkedBlockingQueue。有新任务时,当线程数小于corePoolSize则创建新线程来执行任务,否则把任务加入队列。默认情况下该队列大小为Integer.MAX_VALUE,所以被视为是无界的。

  • 任务优先队列:如PriorityBlockingQueue。与无界队列相似,只不过由于任务具有优先级,所以不是按照先进先出的顺序来执行任务。

6.4.4 拒绝策略

JDK提供了4种拒绝策略

  • AbortPolicy:直接抛出RejectedExecutionException异常(默认策略)
  • CallerRunsPolicy:只要线程池不关闭,则使用调用者线程来执行当前新任务
  • DiscardPolicy:直接丢弃任务
  • DiscardOldestPolicy:将队列中最老的任务丢弃,再次尝试提交新任务

6.4.5 扩展线程池

线程池ThreadPoolExecutor提供了以下方法来让我们增强线程池功能:

方法作用备注
beforeExecute(Thread t, Runnable r)执行任务前调用该方法
afterExecute(Runnable r, Throwable t)任务结束后/异常退出后执行该方法
terminated()线程池关闭后执行该方法
public class ThreadPoolTest {

    private static final class MyTask implements Runnable {

        private final String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("执行任务" + name + "中...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 自定义线程池,重写beforeExecute、afterExecute等方法来增强功能
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5,
                0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("线程" + t.getId() + "即将执行" + ((MyTask) r).name);
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println(((MyTask) r).name + "执行结束");
            }

            @Override
            protected void terminated() {
                System.out.println("线程池退出...");
            }
        };
        // 提交5个任务
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(new MyTask("task-" + i));
        }
        // 关闭线程池:不再接收新的任务,已接收的任务仍会继续执行
        threadPoolExecutor.shutdown();
    }
}