# 线程
# 进程
在操作系统中,一个运行中的任务通常对应一个进程。
进程通常包含以下三个特征:
- 独立性:进程是系统中的独立单位,具有独立的资源、私有的空间
- 动态性:进程具有自己的生命周期
- 并发性:多个进程可以并发执行而不会相互影响
# 多进程
多进程就是计算机同时执行多个进程(软件)。
现代操作系统均支持多线程并发运行,用户可以随意开启多个软件,并让它们同时运行在计算机之上。
对于 CPU 而言,它在同一时间只能执行一个程序。因此 CPU 会在多个进程之间轮换执行,从而达到宏观上同时运行的效果。
# 多线程
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
特点
- 线程是进程的组成部分
- 进程中必须有一个主线程
- 进程中可以有很多个线程
- 当进程被创建,主线程也就被创建了
- 线程的执行是抢占式的
- 与进程相比,线程之间的分隔程度更小,它们共享进程的状态
线程与进程的相同点
线程之于进程,就像进程之于操作系统。
- 线程在进程中是独立的
- 线程具有生命周期
- 线程可以并发执行
- 进程可以并发执行
多线程的优势
- 线程会共享进程的状态,因此线程具有更好的性能
- 线程会共享进程的状态,因此线程更容易实现相互通信
# Thread 类
JAVA 用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。
# 构造方法
Thread Thread(Runnable target, String name)
- 传入实现了 Runnable 接口的对象
- 传入线程的名字
# 类方法
Thread currentThread()
获取当前的线程
# 实例方法
void run() {
if (target != null) {
target.run();
}
}
2
3
4
5
线程所执行的"任务",为线程指定任务一般有两种做法:
- 继承 Thread 类,重写
run()
方法 - 传入实现了 Runnable 接口,实现了
run()
方法的类的对象
void start()
启动线程
不要 直接调用
run()
方法
String getName()
获取线程的名字
boolean isAlive()
线程是否启动
提示
当线程处于就绪、运行、堵塞时,返回 true 当线程处于新建、死亡时,返回 false
# 线程的创建和启动
# 继承 Thread 类创建线程类
- 定义 Thread 的子类,重写
run()
方法 - 创建该类的对象
- 调用对象的
start()
方法,启动线程
// 定义 Thread 的子类,重写 run() 方法
class 类名 extends Thread{
@Override
public void run() {
···
}
}
}
// 创建该类的对象
类名 对象名 = new 类名();
// 调用对象的 `start()` 方法,启动线程
对象名.start()
2
3
4
5
6
7
8
9
10
11
12
public class FirstThread extends Thread{
private int i;
@Override
public void run() {
for ( ; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
}
public class Test {
public static void main(String[] args) {
new FirstThread().start();
new FirstThread().start();
}
}
// 输出结果
Thread-1 0
Thread-1 1
Thread-0 0
Thread-1 2
Thread-0 1
Thread-1 3
Thread-0 2
Thread-1 4
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 实现 Runnable 接口创建线程类
- 定义类,实现 Runnable 接口,重写
run()
方法 - 实例化接口实现类
- 以接口实现类的对象作为 Thread 的参数创建 Thread 的对象
- 调用对象的
start()
方法,启动线程
// 定义类,实现 Runnable 接口,重写 run() 方法
class 类名 implements Runnable{
@Override
public void run() {
···
}
}
}
// 实例化接口实现类
类名 对象名1 = new 类名();
// 以接口实现类的对象作为 Thread 的参数创建 Thread 的对象
Thread 对象名2 = new Thread(对象名1);
// 调用对象的 `start()` 方法,启动线程
对象名2.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SecondThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class Test {
public static void main(String[] args) {
SecondThread secondThread = new SecondThread();
new Thread(secondThread).start();
new Thread(secondThread).start();
}
}
// 输出结果
Thread-0 72
Thread-1 8
Thread-0 73
Thread-0 74
Thread-1 9
Thread-1 10
Thread-1 11
Thread-1 12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 线程的生命周期
# 新建
如果仅仅是新建一个线程对象,它和普通 JAVA 一样,由虚拟机分配空间、初始化。此时线程对象没有任何动态特征,其线程执行体也不会被执行。
# 就绪
使用 start()
可以激活线程对象,虚拟机会为其创建方法调用栈和程序计数器,为线程的运行做好准备,线程进入就绪状态。就绪状态的线程只是可以运行了,并不会立刻开始运行。线程何时能够抢到 CPU 的执行权,线程才能进入运行状态。
# 运行
如果处于就绪状态的线程抢到了 CPU 的执行权,则线程会进入运行状态。
程序开始运行后,它不可能始终处于运行状态,它会在运行时被其它线程抢走 CPU 的执行权,并重新进入就绪状态。
# 堵塞
但发生以下情况时,线程将会进入堵塞状态
- 调用
sleep()
方法 - 调用了堵塞式的 IO 方法
- 试图获得一个同步监视器,但该同步监视器正在被其它线程调用
- 在等待通知
- 调用了线程的
suspend()
方法 当线程的堵塞接触后,线程会重新进入就绪状态,等待进入运行状态。
# 死亡
线程会以三种方式结束,进入死亡状态
- 线程执行体执行完毕
- 抛出未捕获的异常或错误
- 调用
stop()
方法结束线程
# 控制线程
# join()
Thread 提供了 join()
方法,它能够让一个线程等待另外一个线程完成后再继续进行。
如果在线程中调用了另一个线程的 join()
方法,则线程将被堵塞,直至另一个线程执行完毕。
未使用 join()
方法
public class RelaxThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("休息了" + i + "下");
}
}
}
public class RunThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("走了" + i + "步");
if (i == 20) {
RelaxThread relaxThread = new RelaxThread();
relaxThread.start();
}
}
}
}
public static void main(String[] args) {
RunThread runThread = new RunThread();
runThread.start();
}
// 输出结果
走了77步
走了78步
休息了4下
休息了5下
休息了6下
走了79步
休息了7下
休息了8下
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
使用 join()
方法
public class RelaxThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("休息了" + i + "下");
}
}
}
public class RunThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("走了" + i + "步");
if (i == 20) {
RelaxThread relaxThread = new RelaxThread();
relaxThread.start();
relaxThread.join();
}
}
}
}
public static void main(String[] args) {
RunThread runThread = new RunThread();
runThread.start();
}
// 输出结果
走了18步
走了19步
走了20步
休息了0下
休息了1下
休息了2下
休息了3下
休息了4下
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# setDaemon()
有一种进程,它运行于后台,为其它进程提供服务,这种进程被称为“后台进程”。
后台进程有一个特征:如果所有的前台进程死亡,后台进程也将自动死亡。
调用 Thread 的 setDaemon()
方法,该方法需要一个 Boolean 类型的参数,填入 true ,即可将指定的进程设置为后台进程。
需要注意的是,当前台线程死亡以后,后台线程并非立即死亡,其接收指令到做出响应也需要一定的时间。
public class BackEndThread extends Thread{
@Override
public void run() {
while(true) {
System.out.println("后台");
}
}
}
public class TestThread extends Thread{
@Override
public void run() {
BackEndThread backEndThread = new BackEndThread();
backEndThread.setDaemon(true);
backEndThread.start();
for (int i = 0; i < 99; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# sleep()
将当前进程设置为堵塞状态。
Thread.sleep(time);
当线程被 “sleep” 之后,在其睡眠时间段内,它都不会获得执行的机会。
# yield()
将当前进程设置为就绪状态。
Thread.yield()
当进程被 yield()
设置为就绪状态后,它便可以开始和其它进程一起争夺 CPU 的执行权,如果抢到便可以再次进入执行状态。
# 线程优先级
提示
默认优先级 NORM_PRIORITY:5 最大优先级 MAX_PRIORITY:10 最小优先级 MIN_PRIORITY:1
每个线程都具有优先级,在争夺 CPU 的执行权时,优先级高的线程可以获得更多的执行机会。
每个线程线程默认的优先级与创建它的父线程的优先级相同。Thread 类提供了 setPriority(num)
和 getPriority()
方法,用于设置和获取指定线程的优先级。
需要注意的是,优先级高的线程可以获得更多的执行机会,优先被执行,而不是一定先执行。
# 线程同步
# 为什么要线程同步?
当多个线程同时访问一个属性时,往往会因为并发访问导致属性的状态出现混乱。 会出现问题的三个条件:
- 多线程环境
- 有共享属性
- 多个线程操作共享属性
public class Account implements Runnable {
private int balance = 1000;
@Override
public void run() {
while (balance >= 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= 10;
System.out.println("还剩" + balance);
}
}
}
public static void main(String[] args) {
Account account = new Account();
Thread thread1 = new Thread(account);
Thread thread2 = new Thread(account);
thread1.start();
thread2.start();
}
// 输出
还剩50
还剩40
还剩30
还剩20
还剩10
还剩0
还剩-10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 同步代码块
JAVA 引入了同步监视器来解决同步问题,其语法格式如下:
synchronized (对象) {
代码体;
}
2
3
- synchronized 为关键字,用于修饰代码块
- obj 为同步监视器,可以为任意对象
- 同一时刻只能有一个线程执行代码块
提示
- 任何时刻只能有一个线程可以获得对同步监视器的锁定
- 当线程锁定同步监视器时,线程便可以执行代码块
- 当同步监视器被锁定时,其它试图访问代码块的线程将被堵塞
- 当线程的代码块执行完成,会自动释放对同步监视器的锁定
public class Account implements Runnable {
private int balance = 1000;
private Object object = new Object();
@Override
public void run() {
synchronized (object) {
while (balance >= 10) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
> balance -= 10;
System.out.println("还剩" + balance);
}
}
}
}
public class Test {
public static void main(String[] args) {
Account account = new Account();
Thread thread1 = new Thread(account);
Thread thread2 = new Thread(account);
thread1.start();
thread2.start();
}
}
// 输出结果
还剩70
还剩60
还剩50
还剩40
还剩30
还剩20
还剩10
还剩0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 同步方法
可以使用 synchronized 修饰方法,使方法成为同步方法。
TIP
同步监视器为对象或类
synchronized 返回值类型 方法名 {
···
}
2
3
public class Account implements Runnable {
private int balance = 1000;
@Override
public synchronized void run() {
while (balance >= 10) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= 10;
System.out.println("还剩" + balance);
}
}
}
public class Test {
public static void main(String[] args) {
Account account = new Account();
Thread thread1 = new Thread(account);
Thread thread2 = new Thread(account);
thread1.start();
thread2.start();
}
}
// 输出结果
还剩70
还剩60
还剩50
还剩40
还剩30
还剩20
还剩10
还剩0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 同步锁
Lock 和 synchronized 一样都可以用于设置线程同步,但与 synchronized 相比,Lock 提供了更多的方法和更灵活的同步控制。
Lock 和 ReadWriteLock 是 JAVA 提供的两个接口,ReentrantLock 和 ReentrantReadWriteLock 分别是它们的实现类。
语法
// 首先获得Lock的lock对象
Lock lock = ···
// 通过lock对象的lock()方法加锁
lock.lock()
// 需要线程同步的代码
···
// 通过lock对象的unlock()方法解锁
lock.unlock()
2
3
4
5
6
7
8
9
10
11
为了保证 lock 锁始终被解开,应该使用 try--catch
写法,并将 unlock()
放置到 finally 中
// 首先获得Lock的lock对象
Lock lock = ···
try
{
// 通过lock对象的lock()方法加锁
lock.lock()
// 需要线程同步的代码
···
}
finally
{
// 通过lock对象的unlock()方法解锁
lock.unlock()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 死锁
当两个线程相互等待对方释放同步监视器时,就会发生死锁。具体来讲:
- 当前线程拥有其它线程需要的资源
- 当前线程等待其它线程的资源 JAVA 并没有检测,也没有措施处理死锁问题,一旦出现死锁,程序并不会发生错误或异常,线程将处于堵塞状态无法继续。
死锁实例
public class DeadLock {
// 用于被线程"抢占"的资源
private static final Object left = new Object();
private static final Object right = new Object();
// 先拿left,再拿right的方法
public static void leftRight() {
// 得到left锁
synchronized (left) {
// 得到right锁
synchronized (right) {
System.out.println("我先左,我再右");
}
}
}
// 先拿right,再拿left的方法
public static void rightLeft() {
// 得到right锁
synchronized (right) {
// 得到left锁
synchronized (left) {
System.out.println("我先右,我再左");
}
}
}
}
// Left线程类
public class Left extends Thread {
@Override
public synchronized void run() {
while (true) {
DeadLock.leftRight();
}
}
}
// right线程类
public class Right extends Thread {
@Override
public synchronized void run() {
while (true) {
DeadLock.rightLeft();
}
}
}
// 执行两个线程
public class Test {
public static void main(String[] args) {
Thread left = new Left();
Thread right = new Right();
left.start();
right.start();
}
}
// 输出结果
我先左,我再右
我先左,我再右
我先左,我再右
我先左,我再右
我先左,我再右
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
程序开始后不久,线程就会进入堵塞状态而无法继续。
其原因是,在程序中的某一刻:
- right 线程占据 right 对象,并期望获得 left 对象
- left 线程占据 left 对象,并期望获得 right 对象
两个线程相互僵持,导致了死锁。
避免死锁
避免死锁有以下几种方法:
- 避免多次锁定:避免同一个线程锁定多个同步监视器
- 固定加锁的顺序:如果有多个线程需要对多个同步监视器进行锁定,应该保证它们以相同的顺序加锁
提示
例如:
所有线程都以 对象1、对象2、···、对象n
的顺序对多个对象加锁,从而避免了死锁
- 使用定时锁:通过
tryLock()
方法,当等待超时后将自动解锁