Java 并发编程-线程基础
1. 线程相关概念
1.1 线程和进程的关系
-
从构成关系上看,线程是进程中的一个实体,一个进程中至少有一个线程,线程本身不能独立存在。
-
从程序角度来看,进程是代码在数据集合上的一次运行活动,线程是进程的一个执行路径。
-
从操作系统资源分配上看,进程是系统进行资源分配和调度的基本单位,线程是CPU分配的基本单位。
-
从通信方式上看, 进程通信有管道、有名管道、信号量、消息队列、信号、共享内存、套接字方式,线程有锁机制、信号量机制、信号机制。线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
-
从资源访问的角度上看,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。如下图所示
1.2 线程上下文切换
-
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
-
JVM 使用程序计数器来记住下一条指令的执行地址,是线程私有的
-
线程上下文频繁切换会影响性能,所以不是线程越多越好。这也是为什么在cpu核心数量只有一个的系统上,并发的效果甚至不如串行。
1.3 线程优先级
-
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需 要多或者少分配一些处理器资源的线程属性。
-
Java 中 Thread 对象有一个整形变量 priority 来控制优先级,可以通过 setPriority() 来设置这个值, 但是 Java 中线程优先级的设置效果不明显,操作系统可能会忽略这个线程优先级的设置.
1.4 线程状态
1.4.1 操作系统线程状态(五种)
状态名称 | 说明 |
---|---|
初始状态 | 仅在语言层面创建了线程对象,还未与操作系统线程关联 |
可运行状态 | 指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行 |
运行状态 | 指获取了CPU时间片运行中的状态 |
阻塞状态 | 如果调用了阻塞API,这时线程实际不会用到CPU,会导致上下文切换,进入阻塞状态。等操作完毕,由操作系统唤醒阻塞的线程,转换至可运行状态 |
终止状态 | 表示线程已经执行完毕,生命周期已经结束,不会转化为其他状态 |
(图片源自网络)
1.4.2 Java线程状态(六种)
根据 Thread.State 枚举,分为六种状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start() 方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪、运行、阻塞三种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知和中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
Java线程状态变迁
(图片来自《Java并发编程的艺术》)
1.5 线程死锁
- 四个必要条件:互斥条件,请求并持有条件,不可剥夺条件,环路等待条件。
- 避免死锁只需要破环至少一个必要条件,目前只有请求并持有和环路等待条件可以被破环。
- 资源申请的有序性原则可以避免死锁。
2.创建线程的三种方法
2.1 方法一:继承 Thread ,重写 run 方法
public class ThreadTest1 {
// 继承 Thread 并重写 run 方法
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("t1 run");
}
}
public static void main(String[] args) {
// 创建线程
MyThread thread = new MyThread();
// 启动线程
thread.start();
}
}
由于Java 不支持多继承,继承了 Thread 类就不能再继承其他类。并且任务和代码没有分离,当多个线程执行一样的任务时需要多份代码。
而实现 Runnable 接口的方法解决了上面的问题 。
2.2 方法二:实现 Runnable接口
public void method2() {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
// jdk8 之后可以用lambda 表达式简化上面的代码
Runnable runnable1 = () -> {
log.debug("running");
};
Thread t = new Thread(runnable,"t");
t.start();
}
RunableTask 可以继承其他类。并且两个线程共用了一个 task 代码逻辑。
但是,方法一和方法二都没有返回值,下面看方法三使用 FutureTask。
2.3 方法三:Callable + FutrueTask
public class ThreadTest3 {
// 创建任务类,类似 Runnable
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
return "call";
}
}
public static void main(String[] args) throws InterruptedException {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// 启动线程
new Thread(futureTask).start();
try {
// 等待任务执行完毕,返回结果
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 方法一和方法二的比较
方法一 继承 Thread
-
优点
- 在 run() 方法内获取当前线程直接使用 this 就可以,无须使用 Thread.currentThread() 方法。
- 使用继承的好处是方便传参,你可以在子类里面添加成员变量,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量
-
缺点
- Java 不支持多继承,如果继承了 Thread 类那么就不能再继承其他类。
- 任务和代码没有分离,当多个线程执行一样的任务时需要多份代码,而 Runnable 则没有这个限制 。
- 任务没有返回值
方法二 实现 Runnable 接口
- 优点
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活,Java 中使用组合一般要优于使用继承
“有些时候,通过‘合成’技术用现有的类来构造新类。而继承是最少见的一种做法。”
“应提醒自己防止继承的滥用。”(《Java编程思想》)
- 缺点
- 没有返回值
3. 开启线程:start
3.1 start 和 run 的区别
- start() 是开启一个线程,run() 是这个线程要执行的方法
- 调用 start() 后这时此线程是处于就绪状态,并没有运行,真正运行取决于操作系统
- 能不能直接调用 run() 方法呢? 可以,但是执行这个 run() 方法的线程就是调用该方法的线程,而不是新创建一个线程去执行。
public void testStartRun() {
Thread t = new Thread(){
@Override
public void run() {
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
/*
t.run(); //5秒后打印hello
*/
t.start(); //立刻打印hello
System.out.println("hello");
}
4. 线程等待与通知
4.1 等待 wait
- 当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
(1)其他线程调用了该对象的 notify 或者 notifyAll 方法
(2)其他线程调用了该线程的 interrupt 方法,该线程抛出 InterruptedException 异常返回 - 调用 wait 方法的线程必须要先获得该对象的监视器锁,否则抛出 IllegalMonitorStateException 异常。
- 当前线程调用共享变量的 wait() 方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的
4.2 通知 notify / notifyAll
- notify() 方法会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的进程;
- notifyAll() 方法则会唤醒所有在该共享变量上调用 wait 系列方法后被挂起的进程;
- 被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁之后才可以返回。
- 只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify() 方法,否则会抛出IllegalMonitorStateException 异常
4.3 虚假唤醒
- 一个线程可以从挂起状态变为可运行状态正常情况下为四种,其他情况被唤醒就是虚假唤醒
(1)该线程被其他线程调用notify() 进行通知,
(2)该线程被其他线程调用notifyAll() 方法进行通知,
(3)该线程被其他线程中断
(4)等待超时 - 虚假唤醒在实践中发生的概率很小,一般在循环中调用 wait() 方法来进行防范
5.等待线程终止 join
join 作用:
-
插入线程中,被插入的线程进入阻塞状态需要等待插入线程运行结束。
-
可以指定一个 long 值,规定等待的时间
下面是一个使用 join 来实现同步的例子
同步与异步:
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
//方法外声明一个变量
int r = 0;
public void testJoin() throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
try {
sleep(2000);
r = 10; //子线程修改 r 变量
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//开启子线程
t.start();
//如果去掉下面这行,打印结果为 r = 0
t.join(); //主线程需要等待t线程结束才能继续运行
log.debug("r={}",r); // r = 10
}
6. 线程睡眠 sleep
6.1 sleep
- sleep 会让当前线程从 Running 进入 Timed Waiting 状态 (阻塞)
- 其他线程可以使用 interrupt() 打断正在睡眠的线程,sleep() 会抛出
InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
6.2 sleep 和 wait 的区别
- sleep() 是 Thread 类的静态方法;wait() 是 Object 类的方法,任何对象实例都能调用
- sleep() 不会释放锁,也不需要占用锁;wait() 会释放锁,但前提是当前线程占有锁,即 wait()只能用于同步代码块中。
- sleep() 会暂停线程休眠指定的时间后自动恢复,调用 wait() 的线程会进入等待队列,直到被 notify()/notifyAll() 唤醒去争夺锁。
- 都可以被 interrupt 方法打断
7. 线程CPU让权 yield
7.1 yield
- 调用 yield 会让当前线程从 Runing 进入 Runnable 状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停。
- 具体实现依赖于操作系统的任务调度器。
7.2 sleep 和 yield 的区别
- sleep 后线程处于 Timed Waiting 状态,cpu 不会分配时间片给这个线程
- yield 后线程处于Runnable 状态,cpu可能分配时间片给它
8. 线程中断
8.1 void interrupt()
- 中断线程,如果该线程因为调用wait(),join(),sleep()而被阻塞挂起,抛出
InterruptedException
异常。否则,会将中断状态设置为 true。
8.2 boolean isInterrupted() - 检测当前线程是否被中断,如果是返回 true, 否则返回false。
- 不会清除中断标志。
8.3 boolean interrupted() - 检测当前线程是否被中断,如果是返回 true, 否则返回false。
- 与isInterrupted不同,会清除中断标记。
- 另外与isInterrupted不同在于,interrupted是static方法,内部是获取当前调用线程的中断标志而不是调用interrupted方法的实例对象的中断标志
public void testInterrupt() throws InterruptedException {
Thread t = new Thread() {
@Override
public void run() {
while (true) {
//用 isInterrupted() 方法测试这个线程是否被中断
boolean interrupted = isInterrupted();
if (interrupted) {
log.debug("被打断,退出循环");
break;
}
}
}
};
t.start();
//执行中断
t.interrupt();
log.debug("打断标记:{}",t.isInterrupted());
}
/**输出结果
13:36:44.373 [Thread-0] DEBUG c.MethodTest - 被打断,退出循环
13:36:44.373 [main] DEBUG c.MethodTest - 打断标记:true
**/
9. 用户线程和守护线程
用户线程:平时我们创建的线程一般都属于用户线程
守护线程:即 Daemon thread,是个服务线程,准确地来说就是服务其他的线程,比如垃圾回收器就是一种守护线程,只要其他非守护线程运行结束了,则守护线程会强制结束
10. 其他补充
10.1 JDK 线程指令
JDK 提供了专门指令
- jps 命令查看所有Java进程
- jstack
查看某个Java 进程的所有线程状态 - jconsole 图形界面来查看某个 Java 进程中线程的运行情况