多线程

2021/3/4 Java

# 1 基本概念与术语

# 1.1 进程与线程

进程是操作系统分配资源的基本单位。线程是处理器任务调度和执行的基本单位。一个进程包含一个或多个线程。

进程是相互独立的,每个进程都有独立的代码和数据空间(程序上下文),进程的切换会有较大的开销。同一进程的线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换开销小。

一个进程崩溃后,一般不会影响到其他进程。一个线程崩溃可能会导致整个进程死掉。

举例:WPS是一个进程,我们在打字的同时,WPS可以进行拼写检查,打字跟拼写检查可以理解为两个线程。

# 1.2 并行与并发

串行:在同一时刻,有一个任务单个CPU依次执行

并行:在同一时刻,有多个任务多个CPU同时执行

并发:在同一时刻,有多个任务单个CPU交替执行

# 1.3 线程调度

# 1.3.1 线程并发执行

计算机中的CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务。

# 1.3.2 线程调度模型

分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间片。 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选取一个,优先级高的线程获取CPU的时间片相对多一些。

# 1.4 可重入锁

JVM允许同一个线程重复获取同一个锁,被同一个线程反复获取的锁,叫做可重入锁。

# 1.5 死锁

线程死锁,是由于两个或更多的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。

死锁是因为锁的嵌套产生的,所以避免死锁的根本就是要避免锁的嵌套。多线程获取锁的顺序要一致。

# 2 创建线程的方式

# 2.1 继承Thread类

继承Thread类,并重写run方法。

# 2.2 实现Runnable接口

实现Runnable接口,并重写run方法。

# 2.3 实现Callable接口

实现Callable接口,并重写call方法。

call方法有返回值的,可以抛出异常。Callable接口支持泛型。实现Callable接口的派生类需要结合FutureTask一起使用。

JDK5新增的。

  • Future可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask是Future接口唯一的实现类
  • FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

# 2.4 线程池

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。

JDK5新增的。

ExecutorService接口 + Executors工具类

// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);

// 提交任务
executorService.execute(task1);
executorService.submit(task2);
1
2
3
4
5
6

# 3 线程的生命周期

1、当new Thread时,线程处于新建状态;

2、当调用start方法时或sleep方法时间到/阻塞结束,线程处于就绪状态;

3、当线程抢到CPU的执行权时,线程处于运行状态;

4、当线程执行到sleep方法或其他阻塞方法时,线程处于阻塞状态;

5、当run方法结束时,线程处于死亡状态。

# 4 线程的安全问题

解决线程之间共享数据的安全问题有以下3种方式。

# 4.1 使用volatile

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

public volatile boolean myVar = true;
1

# 4.2 使用synchronized

同步代码块

synchronized(lock){
    // 业务逻辑
}
1
2
3

同步方法

public synchronized void myMethod(){
	// 业务逻辑
}
1
2
3

# 4.3 使用ReentrantLock

Lock,JDK5新增的

private final Lock lock = new ReentrantLock();

try{
    lock.lock();
    // 业务逻辑
}
finally{
    lock.unlock();
}
1
2
3
4
5
6
7
8
9

# 4.4 几种方式比较

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。

ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

# 5 线程通信的应用

生产者消费者模式,也叫等待唤醒机制,是一个非常经典的多线程协作模式

一个线程负责生产数据,放到共享区域,然后通知另一个线程去消耗数据。

  1. synchronized + wait + notify 实现多线程协调

    public synchronized void send() throws InterruptedException {
    	if(条件){
            // 业务逻辑
    		notify();
    	}
    	else {
    		wait();
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  2. Lock + Condition 实现多线程协调

    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    
    public void send() throws InterruptedException {
        try{
            lock.lock();
            if(条件){
                // 业务逻辑
                condition.signal(); // 相当notify
            }
            else {
                condition.await(); // 相当于wait
            }
        }
        finally{
            lock.unlock();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的

# 6 扩展知识

# sleep 和 wait 的区别

相同点:

  • 调用sleep或wait,线程都会进入阻塞状态。

不同点:

  1. 声明位置不同:Thread类中声明sleep,Object类中声明wait;
  2. 调用位置不同:sleep在任何地方都可以使用,wait只能用在同步代码块或者同步方法中;
  3. 关于是否释放同步监听器:如果两个方法都用在同步代码块或者同步方法中,sleep不会释放,wait会释放。

# wait、notify、notifyAll

  1. 三个方法都是定义在Object类中;

  2. 三个方法只能使用在同步代码块或同步方法中;

  3. 三个方法的调用者必须是同步代码块或同步方法中的同步监听器;

  4. 一旦调用wait方法,线程就会进入阻塞状态,释放同步监听器;

  5. 一旦调用notify方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级最高的那个;

  6. 一旦调用notifyAll方法,就会唤醒所有被wait的线程。

# 参考资料

多线程 - 廖雪峰的官网网站 (opens new window)

Java -Java 学习- Java 基础到高级-宋红康-零基础自学Java-尚硅谷 (opens new window)

Java中Atomic类的使用分析 (opens new window)

锁机制

锁升级

偏向锁、轻量级锁、重量级锁

AQS是什么?AQS如何实现可重入锁?

可重入锁

可重入锁,也称递归锁。

可以重复获取相同的锁。
https://blog.csdn.net/w8y56f/article/details/89554060

如果一个线程在执行一个持有锁的方法,在这个方法中调用另一个持有相同锁的方法,则该线程可以直接调用,而无需重新获取锁。
1
2
3
4
5
6