Java并发编程学习总结(一)


Java并发编程学习总结(一)

进程和线程的概念

进程和线程是操作系统中的两个重要概念。

进程(Process)是计算机中正在运行的程序的实例。每个进程都有自己的内存空间、执行状态和系统资源。进程可以独立运行,相互之间不会干扰。一个进程可以包含多个线程。

线程(Thread)是进程中的一个执行单元。一个进程可以同时拥有多个线程,这些线程共享进程的内存空间和系统资源。线程可以看作是轻量级的进程,它们可以并发执行,提高程序的效率。

与进程相比,线程的创建、销毁和切换开销较小,因此线程适合用于处理需要并发执行的任务。多线程编程可以充分利用多核处理器的性能,并提高程序的响应速度。

总结起来,进程是程序的一次执行过程,而线程是进程中的一个执行单元。进程之间相互独立,而线程之间共享进程的资源。

1. 为什么说本质上实现线程的方式只有一种

​ 首先我们熟知的通过创建线程执行任务的手段有大概有4种

  1. 继承Thread类,重写run方法,执行start方法
  2. 实现Runnable接口,重写run方法,通过传入到Thread的构造方法,最后执行start方法
  3. 实现Callable接口,重写call方法,通过传入到Thread的构造方法,最后执行start方法
  4. 通过创建线程池的方式实现

// 第一种方式
ThreadStudy threadStudy = new ThreadStudy();
threadStudy.start();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 第二种方式
Runnable runnable = new RunnableImpl();
new Thread(runnable).start();

// 第三种方式
Callable<Integer> callable = new CallableImpl();
FutureTask<Integer> future = new FutureTask<>(callable);
new Thread(future).start();
try {
System.err.println("future impl return: "+future.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}

// 第四种方式
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(() -> {
System.err.println("thread pool impl");
});

但其实实现线程本质上只有一种方式,那就是构造Thread类。

首先,无论是实现Runnable接口还是实现Callable接口,最终都需要构造Thread类并执行start方法来执行具体的任务

其次,通过线程池的方式实现执行任务,其底层代码中也是通过DefaultThreadFactory工厂类创建线程的,工厂类中又是通过构造Thread类来创建线程,所以说其实创建线程的方式只有一种,就是构造Thread类,Thread类实例中的start方法的执行逻辑是首先判断是否有Runnable、Callable等实例赋值给target属性对象,有的话则执行target实例的run方法,否则执行Thread类实例重写的run方法。

2. 实现Runnable究竟比继承Thread实现线程好在哪里

  1. Java中只支持单继承,而继承Thread类之后,就无法再去继承其他来,导致降低了类的可扩展性
  2. 实现Runnable接口重写run方法的方式,使类的职责更加清晰,专注于执行任务的内容,与Thread类进行了解耦,Thread类主要负责线程的执行和一些属性的设置
  3. 某些情况下提升性能,使用继承Thread的方式在需要重复执行任务时,需要多次创建继承了Thread类的实例来执行,而如果才用Runnable的方式,则可以将Runable的实例丢入到线程池中即可

3. 如何正确的停止线程

首先,我们来看几种停止线程的错误方法。比如 stop(),suspend() 和 resume(),这些方法已经被 Java 直接标记为 @Deprecated。如果再调用这些方法,IDE 会友好地提示,我们不应该再使用它们了。但为什么它们不能使用了呢?是因为 stop() 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。

而对于 suspend() 和 resume() 而言,它们的问题在于如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的。

Java希望程序间相互协作,通知。于是Java推荐采用interrupt标志位的方式来通知线程,线程内可以根据此标志位自助的选择何时停止线程(可以立即停止线程,可以一段时间后再停止线程,可以立即停止线程)。 需要注意,执行中的线程如果处于sleep,wait方法的阻塞状态时将线程interrupt标志位设置为true时,线程是会监听到中断信号并抛出InterruptedException异常并恢复终端标志位,此时正确的处理方式有两种,一种是捕获异常并再次中断线程,使后续的业务逻辑依旧能正常的接收到中断请求从而做出处理,另一种是将InterruptedException抛出到方法签名中,使调用方做出相应的处理。

因此在线程run方法中使用sleep() 方法时需要注意正确处理InterruptedException,避免生吞异常。

4. 为什么说使用volatile标志位停止的方法是错误的

我们说一个方法是正确的,则不仅仅满足于某一种情况,而应该是在所有情形下依旧正确。使用volatile关键字在生产者-消费者模式下,如果使用了类似ArrayBlockingQueue等阻塞队列使线程处于阻塞状态时,使用volatile标记了的变量虽然被改变了,但是线程处于阻塞状态,无法感知到,因此无法正确停止线程,而使用Interrupt的方式则可以被感知,这就是为什么说volatile标志位停止线程的方式也是错误的。

5. 线程是如何在六种状态之间切换的

线程的六种状态;

  1. New(新创建)
  2. Runnable(可运行)
  3. Blocked(被阻塞)
  4. Waiting(等待)
  5. Timed Waiting(计时等待)
  6. Terminated(被终止)

下图中展示了线程状态切换的条件和流程,线程创建但未执行start()方法时,线程处于New状态,执行start方法后线程处于Runnable状态,在操作系统中可能为两种状态(尚未被分配cpu时间片时的Ready状态和正在执行中的Runnable状态)。 当处于Runnable状态的线程在某些情况下可能会进入到Blocked、Waiting、Timed Waiting状态),线程run方法执行完毕或者异常终止后会进入到Terminnated状态。

img

6. wait/notify/notifyAll 方法的使用注意事项

  1. 为什么 wait 必须在 synchronized 保护的同步代码中使用

wait方法的源码注释中写到:必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁,否则会抛出IllegalMonitorStateException异常。 这是因为如果没有加以同步锁,则while循环中的条件和wait方法不能构成原子操作,此时很容易出现问题,比如底下的give方法和take方法,若是没有被synchronized代码块包裹,则可能存在buffer.isEmpty方法执行完之后,cpu时间片轮转到give方法执行notify方法,notify方法执行完之后再回到执行wait方法,之后若是没有再执行give方法,则take方法会一直处于阻塞状态。

public void give(String data) {
synchronized (this) {
buffer.add(data);
notify();
}
}

public String take() throws InterruptedException {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}

7. 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中

  1. 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
  2. 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
  3. wait/notify/notifyAll 方法主要用于线程之间的协调与通信。这些方法需要在同步代码块或同步方法中使用,并依赖于对象的监视器(Monitor)。wait 方法使当前线程进入等待状态,直到其他线程调用相同对象上的 notify 或 notifyAll 方法来唤醒它。notify 方法唤醒等待该对象监视器的一个线程,而 notifyAll 方法则唤醒所有等待该对象监视器的线程。

sleep 方法则是使当前线程暂停执行一段时间。它可以在任何地方使用,不需要依赖于对象的监视器。sleep 方法常用于控制线程的执行速度、模拟耗时操作或定时任务等场景。

由于 wait/notify/notifyAll 方法是基于对象的监视器实现线程之间的协调与通信,而 sleep 方法是针对线程 自身的操作,所以它们被定义在不同的类中。这样设计可以更好地区分它们的功能和用途,使代码更加清晰 和易于理解。

8. sleep方法和wait方法的异同点

相同点:

  1. sleep方法和wait方法都能阻塞线程
  2. sleep方法和wait都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点:

  1. sleep方法是定义在Thread类的静态方法,wait方法是定义在Object类中的实例方法
  2. sleep方法阻塞线程不会释放monitor锁,wait方法会释放
  3. wait方法必须在synchronized标识的代码块或方法中才能使用,sleep方法没有使用要求
  4. 使用场景不同,sleep 方法则是使当前线程暂停执行一段时间。它可以在任何地方使用,不需要依赖于对象的监视器。sleep 方法常用于控制线程的执行速度、模拟耗时操作或定时任务等场景

9. 有哪几种实现生产者消费者模式的方法?


文章作者: ring2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ring2 !
  目录