高并发教程三:线程与线程池

373 阅读16分钟

为什么需要多线程

线程是进程中的一个执行单元,是CPU调度和分派的基本单位,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

线程

线程创建

image.png

实现thread

public class T01_thread extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(i+" run1()");
        }
    }
    public static void main(String args[]){
        T01_thread t1 = new T01_thread();
        t1.start();
        for (int i = 1; i <= 10; i++) {
            System.out.println(i+" main1()");
        }
    }
}

实现runable

public class T02_runable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(i+" run()");
        }
    }
    public static void main(String args[]){
        T02_runable t2 = new T02_runable();
        Thread t = new Thread(t2);
        t.start();
        for (int i = 1; i <= 10; i++) {
            System.out.println(i+" main()");
        }
    }
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}


public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

匿名类创建

public class T02_runable2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread((new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名类执行");
            }
        }));
        thread1.start();

        Thread thread2 = new Thread((()-> {
            System.out.println("函数式执行");
        }));
        thread2.start();

    }
}

线程池创建

package com.lm.java.study;

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

/**
 * Hello world!
 */
public class App {
    public static void main(String[] args) {
        // 使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            if (i == 20) {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
            // 获取线程返回值
            System.out.println("子线程的返回值:" + task.get());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Thread和Runnable的区别

image.png

线程调度

程序运行原理

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
  • 抢占式调度(Java使用):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

上下文切换

  • 对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)
  • 虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素

调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会

线程核心状态

image.png

新建(New)

创建后尚未启动。

可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片。

包含了操作系统线程状态中的 Running 和 Ready。

阻塞(Blocking)

等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

无限期等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

进入方法退出方法
没有设置 Timeout 参数的 Object.wait() 方法Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法被调用的线程执行完毕
LockSupport.park() 方法-

限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

进入方法退出方法
Thread.sleep() 方法时间结束
设置了 Timeout 参数的 Object.wait() 方法时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法-
LockSupport.parkUntil() 方法-

死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束

线程核心方法

Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分,当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。

使用 setDaemon() 方法将一个线程设置为守护线程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

start

start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法

sleep

让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

yield

yield 方法使当前线程让出 CPU 占有权,让当前运行线程回到可运行状态,以允许具有相同优先级或更高的其他线程获得运行机会,让步的线程还有可能被线程调度程序再次选中

public void run() {
    Thread.yield();
}

join

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出,以下代码说明:

public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
//结果:
A
B

interrupt

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}

// 结果
Thread end

setPriority

setPriority()设置线程执行的优先级、优先级越高的线程越有可能执行

wait()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

image.png

  • 线程阻塞,JVM将该线程放置在目标对象的等待集合中。
  • 释放调用wait()对象的同步锁,但是除此之外的其他锁依然由该线程持有。
  • 即使是在wait()对象多次嵌套同步锁,所持有的可重入锁也会完整的释放。这样,后面恢复的时候,当前的锁状态能够完全地恢复。
  • object.wait() object.notify() object.notifyAll() 调用之前需要先拿到object锁

notify() notifyAll()

image.png

  • Java虚拟机从目标对象的等待集合中随意选择一个线程(称为T,前提是等待集合中还存在一个或多个线程)并从等待集合中移出T。当等待集合中存在多个线程时,并没有机制保证哪个线程会被选择到
  • notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁

等待-通知机制

等待-通知机制:避免自旋等待带来的性能损失、synchronized配合wait()、notify()、notifyAll() 可实现等待-通知

交替打印

/**
 * @author lm
 * @version 1.0
 * @desc PrintABCUsingWaitNotify
 * A 执行后,唤醒 B,B 执行后唤醒 C,C 执行后再唤醒 A,这样循环的等待
 * @created 2020/12/1 下午10:35
 **/
public class PrintABCUsingWaitNotify {
    private int times = 10;
    private int states;
    public void print(String name, int curStates, int nThread) {
        for (int i = 0; i < times; i++) {
            try {
                synchronized (this) {
                    while (states % nThread != curStates){
                        this.wait();
                    }
                    states++;
                    System.out.println(name);
                    this.notifyAll();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        PrintABCUsingWaitNotify printABCUsingWaitNotify = new PrintABCUsingWaitNotify();
        int nThread = 5;
        for (int i = 0 ; i< nThread ;i++){
            int finalI = i;
            new Thread(()->{
                printABCUsingWaitNotify.print(String.valueOf("线程")+String.valueOf(finalI), finalI,nThread);
            },"线程"+finalI).start();
        }
    }

}

结果:

线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4
线程0
线程1
线程2
线程3
线程4

多生产多消费

public class Comsumer {
    private List list;
    public Comsumer(){
    }
    public Comsumer(List list){
        this.list = list;
    }
    public void remove()  {
        while(true){
            synchronized (list){
                try {
                    Thread.sleep(1000);
                    if (CollectionUtils.isNotEmpty(list)){
                        Iterator iterator = list.iterator();
                        while (iterator.hasNext()){
                            System.out.println(Thread.currentThread().getName()+ "->消费i"+iterator.next());
                            iterator.remove();
                        }
                        System.out.println(Thread.currentThread().getName()+ "->消费者通知唤醒");
                        list.notifyAll();
                    } else {
                        System.out.println(Thread.currentThread().getName()+ "->消费者wait");
                        list.wait();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Producer {
    private List list;
    public Producer(){
    }
    public Producer(List list){
        this.list = list;
    }
    public void add()  {
        while(true){
            synchronized (list){
                try {
                    Thread.sleep(1000);
                    if (CollectionUtils.isNotEmpty(list)){
                        System.out.println(Thread.currentThread().getName()+ "->生产者wait");
                        list.wait();
                    } else {
                        int total = 3;
                        for (int i = 0 ; i< total ; i++){
                            System.out.println(Thread.currentThread().getName()+ "->生成i:"+i);
                            list.add(i);
                        }
                        System.out.println(Thread.currentThread().getName()+ "->生产者通知唤醒");
                        list.notifyAll();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
/**
 * @author lm
 * @version 1.0
 * @desc TestRun
 * @created 2020/12/1 下午4:58
 **/
public class TestRun {

        public static void main(String[] args) {
            List list = new ArrayList();
            for (int i =1 ;i <=5 ; i++){
                Thread pt = new Thread(() -> {
                    Producer p = new Producer(list);
                    p.add();});
                pt.setName("线程"+i);
                pt.start();

                Thread ct = new Thread(() -> {
                    Comsumer c = new Comsumer(list);
                    c.remove();
                });
                ct.setName("线程"+i);
                ct.start();
            }
        }
}

结果:

线程1->生成i:0
线程1->生成i:1
线程1->生成i:2
线程1->生产者通知唤醒
线程1->生产者wait
线程5->消费i0
线程5->消费i1
线程5->消费i2
线程5->消费者通知唤醒
线程5->消费者wait
线程5->生成i:0
线程5->生成i:1
线程5->生成i:2
线程5->生产者通知唤醒
线程5->生产者wait
线程4->消费i0
线程4->消费i1
线程4->消费i2
线程4->消费者通知唤醒
线程4->消费者wait
线程3->消费者wait
线程2->消费者wait
线程1->消费者wait
线程4->生成i:0
线程4->生成i:1
线程4->生成i:2
线程4->生产者通知唤醒
线程4->生产者wait
线程3->生产者wait
线程2->生产者wait
线程1->消费i0
线程1->消费i1
线程1->消费i2
线程1->消费者通知唤醒
线程1->消费者wait
线程2->消费者wait
线程3->消费者wait
线程4->消费者wait
线程5->生成i:0
线程5->生成i:1
线程5->生成i:2
线程5->生产者通知唤醒
线程5->生产者wait
线程5->消费i0
线程5->消费i1
线程5->消费i2
线程5->消费者通知唤醒
线程5->消费者wait
线程1->生成i:0
线程1->生成i:1
线程1->生成i:2
线程1->生产者通知唤醒
线程1->生产者wait
线程5->生产者wait
线程4->消费i0
线程4->消费i1
线程4->消费i2
线程4->消费者通知唤醒
线程4->消费者wait
线程3->消费者wait
线程2->消费者wait
线程1->消费者wait
线程2->生成i:0
线程2->生成i:1
线程2->生成i:2
线程2->生产者通知唤醒
线程2->生产者wait
线程3->生产者wait
线程4->生产者wait
线程1->消费i0
线程1->消费i1
线程1->消费i2
线程1->消费者通知唤醒
线程1->消费者wait
线程2->消费者wait
线程3->消费者wait
线程4->消费者wait
线程5->生成i:0
线程5->生成i:1
线程5->生成i:2
线程5->生产者通知唤醒
线程5->生产者wait
线程1->生产者wait

挂号看病

  1. 患者先去挂号,然后到就诊门口分诊,等待叫号;
  2. 当叫到自己的号时,患者就可以找大夫就诊了;
  3. 就诊过程中,大夫可能会让患者去做检查,同时叫下一位患者;
  4. 当患者做完检查后,拿检测报告重新分诊,等待叫号;
  5. 当大夫再次叫到自己的号时,患者再去找大夫就诊。
/**
 * @author lm
 * 患者先去挂号,然后到就诊门口分诊,等待叫号;
 * 当叫到自己的号时,患者就可以找大夫就诊了;
 * 就诊过程中,大夫可能会让患者去做检查,同时叫下一位患者;
 * 当患者做完检查后,拿检测报告重新分诊,等待叫号;
 * 当大夫再次叫到自己的号时,患者再去找大夫就诊。
 **/
public class T03_wait_notify {
    private int states;

    public void print(String name, int curStates, int nThread) throws InterruptedException {
        int times = 1;
        Random random = new Random();
        while (true) {
            synchronized (this) {
                while (states % nThread != curStates) {
                    this.wait();
                }
                states = random.nextInt(5);
                if (times == 1) {// 看病
                    System.out.println(name + "看病");
                }
                if (times == 2) {// 检查
                    System.out.println(name + "检查");
                }
                if (times == 3) {// 复查
                    System.out.println(name + "复查");
                }
                times++;
                this.notifyAll();
            }
        }
    }
    public static void main(String[] args) {
        T03_wait_notify t03_wait_notify = new T03_wait_notify();
        int nThread = 5;
        for (int i = 0; i < nThread; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    t03_wait_notify.print(String.valueOf("病人") +
                            String.valueOf(finalI), finalI, nThread);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "病人" + finalI).start();
        }
    }

}

结果:

image.png

线程池

为什么要有线程池

线程池能够对线程进行统一分配,调优和监控:

  • 降低资源消耗(线程无限制地创建,然后使用完毕后销毁)
  • 提高响应速度(无须创建线程)
  • 提高线程的可管理性

ThreadPoolExecutor类

原理分析

J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务

image.png

image.png

  • java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行

核心参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)
  • corePoolSize: 线程池核心线程数量,指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去

  • maximumPoolSize: 线程池最大线程数量,指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量

  • keepAliveTime与unit: 指该线程池中非核心线程闲置超时时长,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

  • workQueue: 任务队列,被添加到线程池中,但尚未被执行的任务;分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种

  • handler: 当任务太多来不及处理时,如何拒绝任务

ExecutorService

通常来说有两种方法来创建ExecutorService

  • 第一种方式是使用Executors中的工厂类方法,例如:
ExecutorService executor = Executors.newFixedThreadPool(10);
复制代码

除了newFixedThreadPool方法之外,Executors还包含了很多创建ExecutorService的方法。

  • 第二种方法是直接创建一个ExecutorService, 因为ExecutorService是一个interface,我们需要实例化ExecutorService的一个实现。
ExecutorService executorService =
            new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>());

执行流程

image.png

ThreadPoolExecutor 的内部工作原理,整个思路总结起来就是 5 句话:

  1. 如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务。

  2. 如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列

  3. 如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务。

  4. 如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策略来处理该任务。

  5. 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出(超出核心线程部分非线程)(设置allowCoreThreadTimeOut = true,则会作用于核心线程)

阻塞队列

线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务

image.png

image.png

拒绝策略

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池

image.png

Executors创建线程池

Executors类里面提供了一些静态工厂,帮助生成一些常用的线程池

newSingleThreadExecutor

newSingleThreadExecutor:线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务

image.png

newFixedThreadPool

newFixedThreadPool:线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待

image.png

newCachedThreadPool

newCachedThreadPool:SynchronousQueue队列,任务进来就执行,线程数量不够就创建,空闲的线程会被回收掉,空闲的时间是60s

image.png

newScheduledThreadPool

newScheduledThreadPool:创建大小无限的线程池。此线程池支持定时以及周期性执行任务的需求 image.png

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor:   主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
  • newCachedThreadPool和newScheduledThreadPool:   主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM

配置线程池需要考虑因素

任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。

性质不同的任务可用使用不同规模的线程池分开处理:

  • CPU密集型: 尽可能少的线程,Ncpu+1
  • IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
  • 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。