Java并发编程之基础知识

40 阅读17分钟

本文主要介绍了Java并发编程的基础知识,包括进程、线程、协程的概念及关系,并发、并行、串行的含义,CPU 核心数和线程数的关系、上下文切换,线程创建方式及原理,常用方法如start与run方法、线程让步方法等,还涉及线程中断、合并、存活状态、守护线程、状态转换以及通过Callable和FutureTask创建有返回结果的线程等内容。梳理本文的目的是为了加深对java线程的理解。

一、进程、线程、协程

1、什么是进程?

在操作系统中,进程是基本的资源分配单位,操作系统通过进程来管理计算机的资源,如CPU、内存、磁盘等。每个进程都有一个唯一的进程标识符(PID),用于区分不同的进程。通俗说法:可看做是正在执行的程序如QQ.exe、 微信、浏览器等。

2、什么是线程?

线程是操作系统中的基本执行单元(能够直接执行的最小代码块),它是进程中的一个实体,是CPU调度和分派的基本单位。一个进程可以包含多个线程,每个线程都可以独立执行不同的任务,但它们共享进程的资源。同一时刻,一个CPU核心只能运行一个线程,也就是CPU内核和同时运行的线程数是1:1的关系,也就是说8核CPU同时可以执行8个线程的代码。

3、什么是协程?

协程又叫虚拟线程、纤程,可以在一个线程内部创建多个协程,这些协程之间可以共享同一个线程的资源。协程是在同一个进程内部运行的,不需要操作系统的介入,可以在用户空间内实现协作式多任务处理。因此协程的创建和销毁开销很小,可以更高效地利用系统资源。Java19才开始支持协程

4、进程、线程、协程之间的关系

首先需要有进程,然后在进程可以创建多个线程,线程是依附在进程里面的,线程里面可以包含多个协程,进程之间不共享全局变量,线程之间共享全局变量,但是要注意资源竞争的问题(线程安全问题),协程之间共享同一个线程的资源。

二、并发、并行、串行

1、什么是并发?

在操作系统中,安装了多个程序,并发的是同一时间段内宏观上有多个程序同时运行,这在单CPU系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

2、什么是并行?

在多核CPU系统中,这些同一时刻的程序可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

3、什么是串行?

如单核CPU,同一时刻只能运行一个程序,如果存在多个程序,需要按照先后顺序执行。我打开qq后,不能再同时打开微信,只能等qq执行完成(关闭)后才能打开微信,线程的串行亦是如此,一次只能执行一个线程代码指令,其他线程需要排队等待。

三、CPU核心数和线程数的关系

目前主流CPU都是多核的,线程是CPU调度的最小单位。同一时刻,一个CPU核心只能运行一个线程,也就是CPU内核和同时运行的线程数是1:1的关系,也就是说8核CPU同时可以执行8个线程的代码。但Intel引入超线程技术后,产生了逻辑处理器的概念,使核心数与线程数形成1:2的关系。在Java中提供了Runtime.getRuntime().availableProcessors(),可以让我们获取当前的CPU核心数,注意这个核心数指的是逻辑处理器数。获得当前的CPU核心数在并发编程中很重要,并发编程下的性能优化往往和CPU核心数密切相关。

四、CPU上下文切换

1、为了提高并发性,启动线程越多越好?

由于现在大多计算机都是多核CPU,多线程往往会比单线程更快,更能够提高并发,但提高并发并不意味着启动更多的线程来执行。更多的线程意味着线程创建销毁开销加大、上下文非常频繁,你的程序反而不能支持更高的TPS。

2、什么是时间片?

多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢?操作系统的设计者巧妙地利用了时间片轮转的方式,时间片是CPU分配给各个任务(线程)的时间。

3、什么是CPU上下文切换?

线程上下文是指某一时间点CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停地切换线程执行。换言之,单CPU这么频繁,多核CPU一定程度上可以减少上下文切换。

五、线程创建方式及创建原理

1、线程创建方式

(1)、继承Thread类

  • 显示定义
// 定义Thread类
public class TestThread extends Thread {

    @Override
    public void run() {
        // 线程执行的业务代码...
        System.out.println("线程Thread");
    }
}

// 开启异步线程
TestThread testThread = new TestThread();
testThread.start(); 
  • 匿名定义
public static void createThread() {
    // new Thread匿名内部类
    Thread t2 = new Thread() {
        @Override
        public void run() {
            // 线程执行的业务代码...
            System.out.println("线程Thread");
        }
    };
    t2.start();
}

(2)、实现Runnable接口

实际使用的话,建议采用实现Runnable接口的方式创建线程, 因为继承Thread类存在父类限制的问题,java只支持继承一个父类, 而可以实现多个接口,可扩展性更强。

  • 显示定义
public class TestRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的业务代码...
        System.out.println("线程Runnable");
    }
}

// 开启异步线程
Thread thread = new Thread(new TestRunnable());
thread.start();
  • 匿名定义
public static void createRunnable() {
    // new Runnable匿名内部类
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            // 线程执行的业务代码...
            System.out.println("线程Runnable");
        }
    });
    t1.start();
}

// 因为Runnable 是函数式接口,所以可以用lambda表达式来简化以上代码,如下所示:
public static void createRunnable() {
    // new Runnable匿名内部类
    Thread t1 = new Thread(() -> {
        // 线程执行的业务代码...
        System.out.println("线程Runnable");
    });
    t1.start();
}

2、线程创建原理

image.png 第一种:继承Thread,并重写Thread的run方法,启动过程为:thread.start() -> 中间过程 -> thread.run()

第二种:实例化Thread,传递一个Runnable任务,启动过程为:thread.start() -> 中间过程 -> thread.run() -> runnable.run()。 注意两处标粗的thread.run(),此run非彼run。第一处run方法已经被我们重写了,是真正的业务逻辑,而第二处是Thread类里面的默认逻辑,它会调用runnable.run()方法,业务逻辑都在runnable.run()里面。

六、线程常用方法

1、start与run方法

  • run方法是同步方法,run方法的作用是存放任务代码,执行run方法它不会产生新线程,run方法可以被执行无数次
  • start方法是异步方法,start方法是启动线程,start方法会产生新线程,start方法只能被执行一次,原因就在于线程不能被重复启动
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.info("2.子线程启动...");
    });
    log.info("1.开始创建线程");
    // t1.run();    // 同步执行
    t1.start();    // 异步执行
    t1.start();    // 异步执行,测试重复启动一个线程
    log.info("3.主线程结束");
}

// 报错信息如下:
15:22:56.280 [Thread-0] INFO StartAndRun - 2.子线程启动...
Exception in thread "main" java.lang.IllegalThreadStateException Create breakpoint
    at java.lang.Thread.start(Thread.java:708)
    at StartAndRun.main(StartAndRun.java:17)

2、setName、getName与sleep方法

(1)、setName与getName方法

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName());
        log.info("2.子线程启动...");
    });
    log.info("1.开始创建线程");
    t1.setName("eagle");
    t1.start();
    log.info("3.主线程结束");
    System.out.println(Thread.currentThread().getName());
}

// 执行结果如下:
09:16:15.751 [main] INFO StartAndRun - 1.开始创建线程
09:16:15.761 [main] INFO StartAndRun - 3.主线程结束
main
eagle
09:16:15.762 [徐庶1] INFO StartAndRun - 2.子线程启动...

(2)、sleep方法

  • sleep简单使用
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        try {
            // 单位毫秒
            // Thread.sleep(1000);
            // 这种方式可读性更好
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // 其他线程可以使用interrupt方法打断正在睡眠的线程,使其抛InterruptedException异常
            e.printStackTrace();
        }
        System.out.println("线程执行完毕, 从睡眠中醒来, 线程结束");
    });
}
  • sleep解决cpu飙升问题
public class SleepThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                try { 
                    // 让出cpu时间片, 让其他线程去执行,减少死循环一直对cpu的消耗
                    Thread.sleep(2000); 
                } catch (InterruptedException e) { 
                    e.printStackTrace(); 
                }
            }
        });
        t1.start();
    }
}
// springboot中内嵌的tomcat采用sleep方式创建阻塞的非守护线程
// Start the server to trigger initialization listeners
this.tomcat.start();

// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();

try {
    ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
} catch (NamingException ex) {
    // Naming is not enabled. Continue
}

// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown


// 重点看这个方法的实现
startDaemonAwaitThread();

// startDaemonAwaitThread具体实现如下:
private void startDaemonAwaitThread() {
    Thread awaitThread = new Thread(new Runnable() {
        @Override
        public void run() {
            TomcatWebServer.this.tomcat.getServer().await();
        }
    }, "container-" + (containerCounter.get()));
    awaitThread.setContextClassLoader(getClass().getClassLoader());
    awaitThread.setDaemon(false);
    awaitThread.start();
}
// 继续看下26行 await()方法的实现- 44行 while循环中处理 降低cpu的消耗
@Override
public void await() {
    // Negative values - don't wait on port - tomcat is embedded or using a different port
    if (getPortWithOffset() == -2) {
        // undocumented yet - for embedding apps that are around, also for tests
        return;
    }
    if (getPortWithOffset() == -1) {
        try {
            awaitThread = Thread.currentThread();
            while (!stopAwait) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException ex) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
    }
    return;
    // ... 
}

3、线程让步方法

(1)、sleep方法实现让步

// 让出当前的CPU使用权,允许其他线程运行。这并不会让线程休眠,而是立即让操作系统重新进行一次CPU时间片的分配,即CPU调度
Thread.sleep(0)

(2)、yield方法实现让步

Thread.yield() 方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的CPU资源),并执行其他线程。

yield() 做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

yield() 方法并不能保证线程一定会让出CPU资源,它只是一个提示,告诉调度器当前线程愿意让出CPU资源。具体是否让出CPU资源,还是由线程调度器决定

class Task1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("A:"+i);
        }
    }
}

class Task2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            Thread.yield();
            System.out.println("B:"+i);
        }
    }
}

Thread thread1 = new Thread(new Task2());
thread1.start();
Thread thread2 = new Thread(new Task1());
thread2.start();

// 这里Thread.yield() 会提示线程调度器当前线程(thread1)愿意让出对CPU的使用权,但最终是否能让出,取决于线程调度器及当前cpu的使用情况, cpu越空闲,线程调度器重新进行CPU调度的几率越小, 所以这里执行结果是随机的。

4、线程执行优先级设置方法

class Task1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("线程2:"+i);
        }
    }
}

class Task2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程1:"+i);
        }
    }
}

Thread thread1 = new Thread(new Task2());
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.start();
Thread thread2 = new Thread(new Task1());
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.start();

// 这里使用setPriority可以设置线程的调度优先级,虽然线程thread1的优先级设置的最高,但并不能表示线程thread1一直会被执行在thread2的前面,和Thread.yield()一样,具体也取决于线程调度器及当前cpu的使用情况,所以这里执行结果也是随机的。

5、打断线程方法

public static boolean interrupted(): 判断当前线程是否被打断, 并清除打断标记

public boolean isInterrupted(): 判断当前线程是否被打断, 不清除打断标记

  • interrupted()示例:
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
       try {
           // 会清除打断标记 --> 撤销打断
           log.info(Thread.interrupted() + "");
           // 单位毫秒 完成延迟任务,用的少,更多自己测试 模拟业务执行
           // Thread.sleep(1000);
           TimeUnit.SECONDS.sleep(2);
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
       System.out.println("睡眠完毕");
    });
    t1.start();
    // 线程中断
    t1.interrupt();
}
// 输出结果:
19:53:22.783 [Thread-0] INFO SleepThread - true
睡眠完毕
  • isInterrupted()示例:
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
       try {
           // 单位毫秒 完成延迟任务,用的少,更多自己测试 模拟业务执行
           // Thread.sleep(1000);
           TimeUnit.SECONDS.sleep(2);
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
       System.out.println("睡眠完毕");
    });
    t1.start();
    // 不清除打断标记 --> 不撤销打断
    log.info(t1.isInterrupted()+"");
    // 线程中断
    t1.interrupt();
    // 不清除打断标记 --> 不撤销打断
    log.info(t1.isInterrupted()+"");
}
// 输出结果:
false
true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:342)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at cn.iocoder.springboot.labs.lab10.springdatarediswithjedis.Test01.lambda$main$0(Test01.java:55)
	at java.lang.Thread.run(Thread.java:750)
睡眠完毕

6、线程中断

public void interrupt(): 仅仅是设置线程的中断状态为true,不会停止线程

注意: 当中断睡眠抛出InterruptedException异常时会清除中断标记,设置为false

// 优雅中断线程
public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        while (true) {
            // 每隔1s将时间片清除
            try {
                Thread.sleep(millis: 1000);
            } catch (InterruptedException e) {
                // 注意:当出现InterruptedException会清除中断标记 false
                e.printStackTrace();
                // 这里再次加上中断标记
                Thread.currentThread().interrupt(); // true
            }
            // 如果中断的标记为true
            // 获取线程中断标记,并且会清除标记
            System.out.println(Thread.currentThread().isInterrupted());
            if (Thread.interrupted()) {
                System.out.println(Thread.currentThread().isInterrupted());
                break;
            }
            // 长任务 - 定时监控
            System.out.println("定时监控");
        }
    });

    thread.start();
    // 只是通知线程需要中断,线程不会立马中断,只是给线程做个标记,给线程打了中断标记=true
    thread.interrupt();
}

// 执行结果如下:
java.lang.InterruptedException Create breakpoint: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.tl.juc.InterruptThread.lambda$main$0(InterruptThread.java:19) <1 internal line>
true
false

7、线程合并join

public final void join(): 等待这个线程结束

public final void join(long millis): 等待这个线程millis毫秒,0意味着永远等待

public class JoinThread {
    static int value = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // todo... 业务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            value = 10;
            System.out.println("线程Runnable");
        });
        t1.start(); // 异步
        t1.join(); // 主线程等待 t1线程执行结束,以下执行结果为:10
        // t1.join(1000); // 主线程等待t1线程执行1s之后继续执行,以下执行结果为:1
        System.out.println("主线程:" + value);
    }
}

8、线程存活状态

public final native boolean isAlive(): 线程是否存活(还没运行完毕)

public class JoinThread {
    static int value = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // todo... 业务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start(); // 异步
        System.out.println(t1.isAlive());
        t1.join();
        System.out.println(t1.isAlive());  
    }
}

// 执行结果:
true
false

9、守护线程

public final void setDaemon(boolean on): 将此线程标记为守护线程或用户线程(又叫普通线程)

  • 默认情况下我们创建的线程都是用户线程(普通线程),进程需要等待所有的线程执行完毕后,进程才会结束。
  • 守护线程.setDaemon(true): 设置守护线程
  • 想要查看线程到底是用户线程还是守护线程,可以通过 Thread.isDaemon() 方法来判断,如果返回的结果是 true 则为守护线程,反之则为用户线程。
  • 当所有的用户线程退出后,守护线程会立马结束。
public static void main(String[] args) {
    // 创建线程(默认前台线程)
    Thread d1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    // 设置线程为守护线程
    d1.setDaemon(true); // 主线程结束 d1线程会立即结束
    d1.start();
    System.out.println("主线程结束");
}
  • 应用场景
    • 垃圾回收器线程属于守护线程:当进程中断时,垃圾回收立马停止
    • tomcat用来接受处理外部的请求的线程就是守护线程:假如tomcat 关闭了,所有tomcat接收请求全部停止。

10、线程的状态与转换

public state getState(): 获取线程状态,java中线程状态共6个,如下图所示: image.png

  • NEW: 初始状态,线程被创建,但还没调用start()方法
  • RUNNABLE: 运行状态,java线程将操作系统中的就绪状态和运行状态统称为“运行中”
  • BLOCKED: 阻塞状态
  • WAITING: 等待状态
  • TIME_WAITING: 超时等待状态
  • TERMINATED: 终止状态,表示当前线程已执行完毕
  • 线程的状态是按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
  • 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。
  • 所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可转换。也就是这两个状态不会参与相互转化。
public static void main(String[] args) {
    // 创建线程(默认前台线程)
    Thread d1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    
    System.out.println(d1.getState());
    d1.start();
    System.out.println(d1.getState());
}

// 输出结果
NEW
RUNNABLE

11、第三种创建线程的方法callable和futureTask

一般情况下,使用Runnable接口、Thread实现的线程我们都无法返回结果的。但是如果对一些场合需要线程返回的结果,就要使用CallableFuture这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。

  • Callable源码
package java.util.concurrent;

/**
 * A task that returns a result and may throw an exception.
 * Implementors define a single method with no arguments called
 * {@code call}.
 *
 * <p>The {@code Callable} interface is similar to {@link
 * java.lang.Runnable}, in that both are designed for classes whose
 * instances are potentially executed by another thread.  A
 * {@code Runnable}, however, does not return a result and cannot
 * throw a checked exception.
 *
 * <p>The {@link Executors} class contains utility methods to
 * convert from other common forms to {@code Callable} classes.
 *
 * @see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

// 可以看到 Callable接口是一个函数式接口, 可以使用Lambda表达式来简化创建Callable接口
// 1、不需要实现抛异常的场景
Callable<String> callable = () -> {
    // 在这里执行一些计算,然后返回一个字符串结果
    return "Hello, World!";
};

// 2、需要实现抛异常的场景
Callable<Void> callable = () -> {
    if (某个条件存在) {
        throw new Exception("Something went wrong");
    }
    // 执行其他逻辑
    return null;
} throws Exception;
  • 基础使用
public static void main(String[] args) {
    /* class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            return 5;
        }
    }
    Task task = new Task(); // 第2步,创建Callable实现类实例*/

    // 第3步,使用FutureTask类来包装Callable对象,可以创建匿名对象
    // 也可以直接用lambda省略1、2步
    FutureTask<Integer> future = new FutureTask<>(() -> {
        System.out.println("2.子线程运行中...");
        Thread.sleep(5000);
        return 20;
    });

    // 第4步,使用Future' Task对象作为Thread对象的target创建、并启动新线程。
    new Thread(future).start();
    System.out.println("1.已启动...");

    try {
        // FutureTask的get()方法会自动阻塞,直到得到任务执行结果为止
        Integer value = future.get(); // 第5步,调用FutureTask对象的方法来获取子线程执行结束后的返回值
        System.out.println("3.返回值" + value);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}
// 执行结果:
1.已启动...
2.子线程运行中...
3.返回值20
  • 结合线程池使用
public static void main(String[] args) {
    ExecutorService executor = Executors.newSingleThreadExecutor();

    Callable<Integer> callable = () -> {
        Thread.sleep(5000);
        System.out.println("子线程执行...");
        // 执行一些计算任务
        return 42;
    };

    Future<Integer> future = executor.submit(callable);
    try {
        System.out.println("准备获取异步任务的结果...");
        Integer result = future.get(); // 获取异步任务的结果,会阻塞等待子线程执行完
        System.out.println("异步任务的结果: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    // 关闭线程池
    executor.shutdown();
}