2021年第一个安排,搞一波java并发编程的知识。大概会分几篇来展开,本次首先梳理一下线程的基础知识。
1 什么是线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
程序计数器:是一个内存区域,用来记录线程当前要执行的指令地址,方便线程重新获得CPU之后,定位上次执行的地址。这个计数器只有在执行java代码的时候才有值,执行的是native方法,则计数器为undefined地址。
栈:用于存储该线程的局部变量,以及线程的调用栈帧。
堆:进程中最大的一块内存,被进程中的所有线程共享,进程创建时分配,存放一些对象实例;
方法区:用来存放JVM加载的类,常量及静态变量等信息,也被线程共享。
2 线程创建
java中有三种创建方式:继承Thread类并重写run方法;实现Runnable接口的run方法,使用FutureTask方式。他们各有特点,可以根据具体场景选择合适的创建方式。
2.1 使用继承Thread类的方式
创建线程对象并用start()方法启动线程。
public class ThreadDemo {
public static class MyThread extends Thread{
@Override
public void run(){
System.out.println("by extends Thread");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
好处:
在run方法里获取当前线程,直接使用this就好,不需要使用Thread.currentThread()
方法;
方便传参,直接使用构造方法,set方法。
坏处:
- 任务没有返回值;
- java不支持多线程,继承Thread类之后,无法再继承其他类;
- 任务与线程代码没有分离,当多个线程执行一样的任务时,需要多份任务代码,而Runnable没有这个限制。
2.2 实现Runnable接口
将任务剥离出来,实现Runnable接口,新建Thread对象的时候传入任务实例。
public static class RunnableTask implements Runnable{
@Override
public void run() {
System.out.println("by implements Runnable");
}
}
public static void main(String[] args) {
RunnableTask runnableTask = new RunnableTask();
new Thread(runnableTask).start();
new Thread(runnableTask).start();
}
如上代码所示,Thread提供了接受Runnable参数的构造方法,利用该方法,可以将任务单独抽取出来,且RunnableTask也可以继承其他类,实现其他接口,避免了使用继承方式的弊端。
缺点:
只能使用主线程里声明为final的常量;(但是可以给不同的任务实例,赋予不同的参数)
任务没有返回值。
2.3 FutureTask方式
Runnable解决了任务与线程区分开的问题,但是还是存在没有返回值的问题。
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
return "by FutureTask";
}
}
public static void main(String[] args) {
FutureTask<String> futureTask =new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try{
String result = futureTask.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
这里使用了FutureTask的传CallTask类型的构造方法,实例化一个futureTask之后,使用Thread传入FutureTask的构造方法,创建一个实例。
最后再通过futureTask.get()
获得返回结果。
3 线程的状态切换
3.1 线程的状态
在jdk中,定义了线程的六种状态:
- NEW :一个已经创建的线程,但是还没有调用start方法启动;
- RUNNABLE :正在运行或者正在等待CPU资源;
- BLOCKED : 阻塞状态,当线程准备进入synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。
- WAITING : 调用了**Object.wait()或者Thread.join()或者LockSupport.park()**进入该状态。处于该状态下的线程在等待另一个线程执行一些其余action来将其唤醒。
- TIMED_WAITING : 该状态和上一个状态一样,不过其等待的时间是明确的。
- TERMINATED : 线程执行结束,run方法执行结束表示线程处于消亡状态。
其原版注释如下:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
线程状态切换如下图所示:
3.2 状态切换的函数
3.2.1 通知与等待
- Object.wait():
当一个线程调用了共享变量的wait()方法,那么该调用的线程会被挂起;
直到:
- 其他对象调用了该共享变量的notify()或者notifyAll()方法;
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
以上两种情况发生一种,线程进入运行状态。
如果调用wait()的线程,事先没有获得到该对象的监视器锁,那么会抛IllegalMonitorStateException异常。
通常获得监视器锁的方法是使用synchronized
关键字声明代码段,或者方法。
synchronized(共享变量){
// do something
}
synchronized void test(int a){
// do something
}
值得注意到是,线程从挂起到运行可能不经过上述的情况,定义为虚假唤醒,所以为了防止这种意外的发生造成程序错误,在拿到监视器之后的第一件事是检查该线程被唤醒的条件是否满足,不满足则进入挂起状态:
synchronized(obj){
while(条件不满足){
obj.wait();
}
// do something
}
此外,wait()方法还有一些重载的方法:
- wait(long timeout): 设置了超时返回的界限,如果参数是0,等同于wait();
- wait(long timeout, int nanos): 调用的还是wait(long timeout),在nanos>0的时候,timeout+1,传入wait(long timeout)。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
- notify():
主要作用就是,一个线程调用了共享对象的notify方法之后,会唤醒一个在该共享变量上调用wait()系列方法被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个是随机的。
notifyAll():不同点在于会唤醒所有因该共享变量挂起的线程。
3.2.2 等待线程终止
join()方法,由Thread类提供,无参数,无返回值。
主要的应用场景是,需要等待多个事情完成之后,再继续处理。
public static void main(){
...
threadOne.start();
threadTwo.start();
// 等待子线程完成
threadOne.join();
threadTwo.join();
// do something
}
如上例子,主线程在开启两个子线程之后,调用了threadOne.join(),进入阻塞状态,等待threadOne完成之后,threadOne.join()就会返回,然后继续调用threadTwo.join(),等待第二子线程的完成。实现对多个线程的等待。
值得注意的是:当线程A,调用了线程B的join方法,A进入阻塞状态之后,其他线程C调用了线程A的interrupt()方法中断了线程A时,A会抛出InterruptedException异常而返回。(也就是说,这种情况下,是不可以中断的)
3.2.3 让线程睡眠
Thread类中提供了一个静态方法void sleep(long millis),当一个执行中的线程调用了Thread的sleep方法,调用的线程会暂时让出指定时间的时间执行权,这段时间不参与CPU调度,但是拥有的监视器资源不释放。指定时间后,线程重新进入就绪状态,等待CPU资源。
如果在睡眠期间,其他线程调用了该线程的interrupt方法,中断了该线程,则会在调用sleep()的地方抛出InterruptedException异常。
3.2.4 让出CPU执行权
Thread类中提供了一个静态方法void yield()。
当一个线程调用了yield方法,相当于通知调度器,我已经完成工作,可以让出CPU,进行下一轮调度。
所以调用的效果就是让出CPU使用权,处于就绪状态,不再等待CPU的时间片使用完。
3.2.5 线程中断
线程中断,一种线程间的协作模式,可以设置线程的中断标志,不会直接终止线程的执行,而是由被中断的线程根据中断状态自行处理。
- void interrupt()方法:中断线程,当线程A运行时,B调用了A的interrupt()方法,来设置线程A的中断标志位为true,并立即返回。此时A没有中断,只是被修改了标志位,会继续执行。需要注意的是,当A因为wait系列的函数,join或sleep方法被阻塞挂起时,此时B再调用A的interrupt()方法就会抛出InterruptedException异常而返回。
- boolean isInterrupted()方法:检测当前线程是否被中断。
- boolean Interrupted()方法:检测当前线程是否被中断,与isInterrupted()不同的是,如果发现当前线程被中断,会清楚中断标志。
线程使用Interrupted()退出:
try{
while(!Thread.currentThread().isInterrupted && other){
// do
}
} catch (InterruptedException e){
// 线程在阻塞状态,执行interrupt()
}
finally{
// 释放资源
}
其实就是相当于,当需要线程之间进行中断的时候,被中断的线程要在逻辑上加上对中断标志的判断,以及相关处理。
此时,其他线程调用了该线程的interrupt方法,修改了标志位,该线程就会根据标志位进行相关操作。
3.3 死锁
死锁产生的四个条件:
- 互斥条件:资源同时只能由一个线程占用;
- 请求并持有条件:一个线程已经拥有一个资源,还要申请新的资源;
- 不可剥夺条件:线程在获取资源之后,在使用完之前不能被其他线程抢走;
- 循环等待条件:存在线程之间互相等待的环形链。
避免死锁就是破坏这四个条件,在java中主要的解决方式: 一种是用synchronized,一种是用Lock显式锁实现。
本文参考:《java并发编程之美》