一、什么是多线程

在操作系统中,一个应用程序被看做是一个进程,而进程内部又可以开启多个线程。
每个线程都会在操作系统中的任务调度器等待被调度,实际处理任务的是CPU,同一时刻能够处理的线程数取决于CPU的核心数。
若线程数超过了CPU的核心数,那么这些线程会轮流拥有CPU的时间片(取决于时间片轮转调度算法)。
串行:同一时刻只能有一个任务执行,其它任务在等待。
并行:两个线程在同一时刻可以一起执行任务。
并发:两个线程在多个时刻交替执行,但这个间隔非常短,几乎是同时执行。

线程间的通信

方法名 描述
join() 在线程中调用另一个线程的join(),阻塞当前线程,等待目标线程结束。
wait()、notify()、notifyAll() 调用wait()使线程等待某个条件,线程等待时会被挂起。当其他线程使条件满足时,其他线程调用notify()或notifyAll()来唤醒挂起的线程。
await()、signal()、signalAll() juc类库中提供了Condition类来实现线程之间的协调,可通过Condition实例调用await()使线程等待,其他线程调用signal()或signalAll()唤醒等待的线程。
await()可以设置等待的条件,比wait()更灵活

二、线程的创建方式

操作系统实现线程主要有三种方式:

  • 内核线程(Kernel-Level Thread,KLT),内核线程指的是直接由操作系统内核支持的线程,程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口叫轻量级进程(Light Weight Process,LWP)。轻量级进程与内核线程之间是1:1的关系。HotSpot就是采用这种方式。
  • 用户线程(User Thread,UT),用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。进程与用户线程之间是1:N的关系。
  • 混合实现(N:M):用户线程加轻量级进程的混合实现,在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。用户线程与轻量级进程之间是M:N的关系。

JDK提供了四种方式可以创建线程

  • Thread类
  • Runnable接口
  • Callable接口
  • 线程池类

    1. Thread 类

    创建一个类并继承Thread类,重写Thread类的run方法(run方法里的代码就是线程执行的代码),调用Thread类的start方法使线程启动。
    缺点:Java具有单继承的特性,一个对象只能继承一个类,相较于实现Runable接口,扩展性稍差。

    1. public class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 5; i++) {
    5. System.out.println("创建的线程:" + Thread.currentThread().getName());
    6. }
    7. }
    8. public static void main(String[] args) throws IOException {
    9. //创建一个MyThread实例并调用start方法启动线程
    10. new MyThread().start();
    11. }
    12. }

    2. Runable 接口

    创建一个类,实现Runable接口,实现Runable接口中的run方法(run方法里的代码就是线程执行的代码)。在使用时以实现Runable接口类的实例作为参数创建一个Thread(Thread线程启动后会执行实例的run方法中的程序),再调用Thread类的start方法启动线程。

    1. public class MyRunnable implements Runnable {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 5; i++) {
    5. System.out.println("创建的线程:" + Thread.currentThread().getName());
    6. }
    7. }
    8. public static void main(String[] args) throws IOException {
    9. //创建一个线程,创建一个MyRunnable做为线程的参数,调用线程的start方法启动线程
    10. new Thread(new MyRunnable()).start();
    11. }
    12. }

    3. Callable 接口

    与Runable不同,Callable接口的实例需要配合FutureTask类使用,它的特点是可以通过FutureTask类的get方法获取线程执行完成后的返回值(此方法是阻塞方法,会阻塞当前线程)。与Runable相同之处是,都需要创建Thread类实例并调用start方法启动线程。 ```java public class MyCallable implements Callable {

    @Override public String call() throws Exception { for (int i = 0; i < 5; i++) {

    1. System.out.println("创建的线程:" + Thread.currentThread().getName());

    } return Thread.currentThread().getName() + “ thread over”; }

    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException { //创建一个MyCallable MyCallable myCallable = new MyCallable(); FutureTask futureTask = new FutureTask<>(myCallable); //将FutureTask作为Thread的参数,并调用start启动线程 new Thread(futureTask).start(); //FutureTask的get方法是获取Callable的返回值,是阻塞方法(需要等待线程执行完成) System.out.println(futureTask.get()); }

}

  1. <a name="v8fm2"></a>
  2. ## 4. Executor 线程池
  3. JDK中的JUC包(java.util.concurrent)提供了线程池的方式创建线程
  4. ```java
  5. public class ExecutorDemo {
  6. public static void main(String[] args) {
  7. ExecutorService executorService = Executors.newSingleThreadExecutor();
  8. executorService.execute(new Runnable() {
  9. @Override
  10. public void run() {
  11. System.out.println("线程池执行 Runnable匿名内部类实例:" + Thread.currentThread().getName());
  12. }
  13. });
  14. executorService.execute(() -> System.out.println("线程池执行 lambda表达式创建的Runnable匿名内部类:" + Thread.currentThread().getName()));
  15. }
  16. }

在阿里巴巴Java开发手册中【强制】规定了不允许使用Executors创建线程池,因为可能会导致OutOfMemoryError,其原文如下:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
通过以上说明我们可以得知不建议使用Executors创建线程池的原因是需要规避资源耗尽的风险,那么如何去规避呢?我们可以通过ThreadPoolExecutor类来创建一个定制化的线程池。
7个参数配置项如下:

  • corePoolSize:线程核心数5,当线程数超过这个数值时,会回收超过存活时间未活动的线程
  • maximumPoolSize:最大线程数10,总线程数不能超过这个数
  • keepAliveTime:存活时间30,当线程没有工作时能存活的时间
  • unit:存活时间的单位
  • workQueue:工作队列,当所有核心线程都在忙,则先把任务加入工作队列
  • threadFactory(非必填):创建线程的工厂,所有线程通过工厂创建,缺省值DefaultThreadFactory
  • handler(非必填):拒绝策略,当线程数满了,工作队列也满了之后任务的拒绝策略,缺省值AbortPolicy ```java public class ExecutorDemo {

    public static void main(String[] args) {

    1. //创建线程池
    2. ExecutorService executorService = new ThreadPoolExecutor(
    3. 5,
    4. 10,
    5. 30,
    6. TimeUnit.MINUTES,
    7. new LinkedBlockingQueue<>(1000),
    8. new BasicThreadFactory.Builder().build(),
    9. new ThreadPoolExecutor.DiscardPolicy());
    10. //通过线程池执行代码
    11. executorService.execute(() -> System.out.println(Thread.currentThread().getName()));

    }

}

  1. <a name="HxTY8"></a>
  2. # 三、多线程安全
  3. <a name="uw7dj"></a>
  4. ## 可见性(volatile)
  5. 看过Java虚拟机的同学应该知道,线程所使用的内存是在虚拟机栈中,是线程隔离的,线程对共享变量操作时,会将共享变量复制一份到线程的工作内存中(栈),计算完成后再写回主内存中(堆)。<br />![线程工作流程图](https://cdn.nlark.com/yuque/0/2022/png/25938361/1645519836532-97e08ec9-daa5-4ae4-b656-f21c6aac1632.png#clientId=u6051282e-cfb8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u37422f33&margin=%5Bobject%20Object%5D&originHeight=264&originWidth=749&originalType=url&ratio=1&rotation=0&showTitle=true&status=done&style=none&taskId=ucd3b4486-f30a-4daa-b0ba-e7f157965d9&title=%E7%BA%BF%E7%A8%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E5%9B%BE "线程工作流程图")<br />线程对共享变量的操作:从主内存Load到工作内存,进行运算,完成后再Store回主内存;这个操作在多线程情况下是不安全的,因为线程的执行受调度器控制,所以每个线程Load和Store操作的时机和顺序是不可预测的。<br />![多线程工作流程图](https://cdn.nlark.com/yuque/0/2022/png/25938361/1645519823259-9607f0a5-3b9c-4559-b12b-5eebcdf2e6bc.png#clientId=u6051282e-cfb8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=R1TGw&margin=%5Bobject%20Object%5D&originHeight=530&originWidth=997&originalType=url&ratio=1&rotation=0&showTitle=true&status=done&style=none&taskId=u9d45e944-5eaa-445a-b199-e88a84842f3&title=%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E5%9B%BE "多线程工作流程图")<br />一个程序案例:创建A、B两个线程和一个boolean值共享变量flag值为true,线程A进行循环工作,当flag为false则退出,通过线程B去修改flag为false,先启动线程A,稍等后启动线程B。当你运行此程序时会发现线程A出现无法退出的情况,这是因为线程A对线程B的修改不具有可见性,无法感知flag在主内存中被修改为false,线程A的工作内存中flag的值还是为true。<br />volatile关键字可用于解决可见性问题,当线程A读取共享变量flag后,线程B修改了flag值,此时线程A拷贝的flag将失效,线程A对flag值进行任何操作,都必须立刻从主内存重新Load。<br />volatile通过lock指令实现,可规避指令重排序。
  6. <a name="rMwbW"></a>
  7. ## 原子性(synchronized)
  8. 假设A和B线程同时对v进行加一操作(其中一种可能的执行顺序):<br />A Load v=1;B Load v=1;A还没将运算完的结果Store回主内存,B又读取了v。<br />A Store v=2;B Store v=2;v的结果不符合预期的值1+1+1=3。<br />我们预期的Load v=1;newV=v+1;Store v=newV;在多线程中并不是一个原子性操作。
  9. ```java
  10. public class SynchronizedDemo {
  11. static int v = 0;
  12. public static void main(String[] args) throws InterruptedException {
  13. new Thread(() -> {
  14. for (int i = 0; i < 100000; i++) {
  15. v++;
  16. }
  17. }).start();
  18. new Thread(() -> {
  19. for (int i = 0; i < 100000; i++) {
  20. v++;
  21. }
  22. }).start();
  23. Thread.sleep(1000);//等待两个线程执行完成
  24. System.out.println(v);//v的值小于正确值200000
  25. }
  26. }

Java提供了synchronized关键字来解决这一问题,synchronized是非公平锁,也就是说竞争锁的线程是无序执行的,这里还可以深入到锁粗化以及操作系统调度算法。
synchronized通过monitorenter、monitorexit指令实现。

  1. public class SynchronizedDemo {
  2. static int v = 0;
  3. public static void main(String[] args) throws InterruptedException {
  4. new Thread(() -> {
  5. //不建议在循环内写同步代码块,因为会重复申请锁
  6. for (int i = 0; i < 100000; i++) {
  7. //对SynchronizedDemo的Class对象进行加锁,只有持有锁的线程能执行同步代码块
  8. synchronized (SynchronizedDemo.class) {
  9. v++;
  10. }
  11. //JVM会在代码块执行完成后自动释放(申请100000次则需要释放100000次)
  12. }
  13. }).start();
  14. new Thread(() -> {
  15. //对SynchronizedDemo的Class对象进行加锁,只有持有锁的线程能执行同步代码块
  16. synchronized (SynchronizedDemo.class) {
  17. for (int i = 0; i < 100000; i++) {
  18. v++;
  19. }
  20. }
  21. //JVM会在代码块执行完成后自动释放
  22. }).start();
  23. Thread.sleep(1000);
  24. //不使用synchronized同步代码块时v的值小于正确值200000
  25. //使用synchronized同步代码块时v的值等于正确值200000
  26. System.out.println(v);
  27. }
  28. }

若需要同时锁定多个共享资源、更大范围的代码块、更灵活的控制锁,Java中JUC包(java.util.concurrent)中Lock接口提供了扩展实现。

有序性

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前
半句是指“线程内似表现为串行的语义”(Within-Thread As-If-SerialSemantics)
后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本
身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对
其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

参考文献

jdk的内置锁
cyc2018 Java并发
《深入理解Java虚拟机》第三版