多线程.md

线程的运行机制

在 Java 虚拟机进程中, 执行程序代码的任务是由线程来完成的, 每个线程都有一个独立的程序计数器和方法调用栈(method invocationb stack)

  • 程序计数器 也称为PC寄存器, 当线程执行一个方法时, 程序计数器只想方法区中下一条要执行的字节码指令
  • 方法调用栈, 简称方法栈, 用来跟踪线程运行中一系列的方法调用过程, 栈中的元素称为栈帧, 每当线程调用一个方法时, 就会向方法栈压入一个新帧, 桢用来存储方法的参数, 局部变量和运算过程

栈帧由三部分组成

  • 局部变量去: 存放局部变量和方法参数
  • 操作数栈: 是线程的工作区, 是用来存放运算过程中临时生成数据
  • 栈数据区: 为线程执行指令提供相关的信息, 包括: 如何定位到堆区和方法去的特定数据, 如何正常退出方法或者异常中断方法

每当java命令启动一个java虚拟机进程, java 虚拟机就会创建一个主线程, 该线程从程序入口 main() 开始执行

实现线程的两种方式

继承 Thread

Thread 类是 java.lang 包中的一个类, 从这个类实例化一种对象代表线程, 程序启动一个新线程需要建立 Thread 实例. Thread 类中常用的两种构造方法

  • public Thread() 创建一个新的线程对象
  • public Thread(String threadName) 创建一个名称为 threadName 线程的对象

继承 Thread 类创建一个新的线程语法

1
2
3
4
5
6
public class ThreadTest extends Thread{

public void run() {

}
}

完成线程真正功能的代码放在类的 run() 方法, 当一个类继承 Thread 类后, 就可以在该类中覆盖 run() 方法, 将实现该线程功能的代码写入 run 方法中, 然后同时调用 Thread 类中的 start() 方法执行线程

Thread 对象需要一个任务来执行, 任务是指线程在启动时执行的工作, 该工作的功能代码被写在 run() 方法中

如果 start() 方法调用一个已经启动的线程, 系统将抛出 IllegalThreadStateException 异常

当执行一个线程程序时, 就会自动产生一个线程, 主方法正式在这个线程上运行时, 当不再启动其他线程时, 该程序就为单线程程序, 如在本章以前的程序都是单线程程序, 主方法线程启动由 Java 虚拟机负责, 程序员负责启动自己的线程

1
2
3
public static void main(String[] args) {
new ThreadTest().start();
}

继承 Thread 类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTest extends Thread {

private int count = 10;

public void run() {

while(true) {
System.out.println(count + "");

if (--count == 0) {
return;
}
}
}

public static void main(String[] args) {
new ThreadTest().start();
}
}

通常在 run() 方法中使用无限循环的形式, 使得线程一直进行下去, 所以要指定一个跳出循环的条件

main() 中, 使线程执行需要调用 Thread 类中的 start() 方法, 线程永远不会启动, 在主方法没有调用 start() 方法之前, Thread 对象只是一个实例, 而不是真正的线程

实现 Runnable 接口

如果需要使用其他类(非 Thread 类), 而且还要使当前类实现多线程, 那么可以通过 Runnable 接口来实现

1
public class Thread extends Object implements Runnable

实质上 Thread 类实现了 Runnable 接口, 其中 run() 方法正是对 Runnable 接口中的 run() 方法具体实现

实现 Runnable 接口的程序会创建一个 Thread 对象, 并将 Runnable 对象与 Thread 对象相关联, Thread 类中有一下两个构造方法

  • public Thread(Runnable target)
  • public Thread(Runnable target, String name)

这两个构造方法的参数都存在 Runnable 实例, 使用以上构造方法就可以将 Runnable 实例与 Thread 实例相关联

使用 Runnable 接口启动新的线程步骤

  1. 建立 Runnable 对象
  2. 使用参数为 Runnable 对象的构造方法创建 Thread 实例
  3. 调用 start() 方法启动线程
1
2
3
4
graph LR 

A(Runnable 对象) --> B(Thread 对象)
B --> C(启动线程, 执行 run 方法中的代码)

通过 Runnable 接口创建线程时程序员首先需要一个便携 Runnable 接口的类, 然后实例化该类的对象, 这样就建立了 Runnable 对象; 接下来使用相应的构造方法创建 Thread 实例; 最后使用该类实例调用 Thread 类中的 start() 方法启动线程

线程最引人注目的部分应该是与 Swing 相结合创建 GUI 程序

启动一个新线程, 不是直接调用 Thread 子类对象的 run() 方法, 而是调用 Thread 类中的 start() 方法, Thread 类的 start() 方法产生一个新的线程, 该线程运行 Thread 字类中的 run() 方法

线程的生命周期

线程具有生命周期, 其中包含 7 种状态 , 分别为 出生状态, 就绪状态, 运行状态, 等待状态, 休眠状态, 阻塞状态和死亡状态. 出生状态就是线程被创建时处于的状态, 在用户使用该线程实例调用 start() 方法之前线程都处于出生状态, 当用户调用 start() 方法后, 线程处于就绪状态(又称为可执行状态), 当线程得到系统资源后就进入了运行状态

一旦线程进入可执行状态, 它会在就绪与运行状态下转换, 同时也有可能进入等待, 休眠, 阻塞死亡状态. 当处于运行状态下线程调用 Thread 类中的 wait() 方法时, 线程便进入等待状态, 进入等待状态的线程必须调用 Thread 类中的 wait() 方法时, 该线程便进入等待状态, 进入等待状态的线程必须调用 Thread 类中的 notify() 方法才能被唤醒, 而 notifyAll() 方法才能被唤醒, 而 notifyAll() 方法是将所有处于等待状态下的线程唤醒, 当线程调用 Thread 类中的 sleep() 方法时, 则会进入休眠状态. 如果一个线程在运行状态下发出输入/输出请求, 该线程将进入阻塞状态, 在其等待输入输出结束时县城进入就绪状态, 对于阻塞的线程来说, 即使系统资源空闲, 线程依然不能回到运行状态, 当线程的 run() 方法执行完毕时, 线程进入死亡状态

1
2
3
4
5
6
7
8
9
10
graph TB
A(出生状态) --> |t.start|B(就绪状态)
B --> |时间片结束|C(执行状态)
B --> |得到系统资源|C
C --> |t.wait|D(等待状态)
D --> |t.notify 或 t.notifyAll|B
C --> |t.sleep|E(休眠)
C --> F(阻塞)
C --> G(死亡)

虽然多线程看起来像同时执行, 但事实上在同一个时间点上只有一个线程被执行, 只是线程之间切换比较快, 所以才会使人产生线程使同时进行的假象, 在 windows 操作系统中, 系统会为每个线程分配一个小段 CPU 时间片, 一旦 CPU 时间片结束就会将线程换为下一个线程, 即使该线程没有结束

线程处于就绪状态的集中方法

  • 调用 sleep() 方法
  • 调用 wait() 方法
  • 等待输入/输出完成

当线程处于就绪状态后, 可以用以下几种方法使线程再次进入运行状态

  • 线程调用 notify() 方法
  • 线程调用 notifyAll() 方法
  • 线程调用 interrupt() 方法
  • 线程的休眠时间结束
  • 输入/输出结束

操作线程的方法

使线程从某一种状态切换到另一种状态

线程的休眠

一种能控制线程行为的方法是调用 sleep() 方法, sleep() 方法需要一个参数用于指定该线程休眠的时间, 该时间以毫秒为单位, 它通常在 run() 方法内循环中使用

1
2
3
4
5
6
// sleep 语法
try {
Thread.sleep(2000);
} catch(InterruptedException e) {
e.printStackTrace();
}

上述代码会使线程在2秒内不会进入就绪状态, 由于 sleep() 方法执行有可能抛出InterruptedException 异常, 所以将 sleep() 方法调用放在 try-catch 块中, 虽然使用了 sleep() 方法的线程在一段时间内会醒来, 但是并不保证它醒来后进入运行状态, 只能保证它进入就绪状态

1
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
public class SleepMethodTest {

public SleepMethodTest() {

Thread t = new Thread(new Runnable(){

public void run() {
while(true) {

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("线程休眠结束");
}
}
});

t.start();
}

public static void main(String[] args) {
new SleepMethodTest();
}
}

线程的加入

如果当前某程序为多线程程序, 加入存在一个线程 A, 现在需要插入线程 B, 并要求线程 B先执行完毕, 然后在继续执行线程 A, 此时可以使用 Thread 类中的 join() 方法来完成

运行的线程可以调用另一个线程的 join() 方法, 当前运行的线程将转到阻塞状态, 直至另一个线程结束运行, 它才会恢复运行

线程恢复运行, 确切的意思是指线程从阻塞状态转到就绪状态, 在这个状态就能获得运行机会

当某个线程使用 join() 方法加入到另外一个线程时, 另一个线程会等待该线程执行完毕后在继续执行

1
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

class ThreadA extends Thread {

@Override
public void run() {
System.out.println("子线程执行完毕");
}
}

public class ThreadJoinTest {

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {

try {
ThreadA tA = new ThreadA();
tA.start();
tA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("主线程执行完毕");
}
}
}

线程的中断

以往有些时候会使用 stop() 方法停止线程, 但 JDK 已经废除了 stop() 方法, 不建议使用 stop() 方法来停止一个线程的执行, 现在提倡在 run() 方法中使用无限循环的形式, 然后使用一个布尔型标记控制循环的停止

1
2
3
4
5
6
7
8
9
10
11
12
private boolean isContinue = false;

@Override
public void run() {
while(true) {
if (isContinue) break;
}
}

public void setContinue() {
this.isContinue = true;
}

如果线程是因为使用了 sleep()wait() 方法进入了就绪状态, 可以使用 Thread 类中 interrupt() 方法使线程离开 run() 方法, 同时线程结束, 但程序会抛出 InterruptedException 异常, 用户可以在处理该异常时完成线程的中断业务处理, 如终止 while 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
while (true) {

try {
Thread.sleep(3999);
} catch (InterruptedException e) {
System.out.println("线程被中断");
e.printStackTrace();
break;
}
}
}
});

thread.start();
thread.interrupt();

线程的让步

当执行了 Thread.yield() 的静态方法, 如果此时具有相同或者更高优先级的其他线程处于就绪状态, yield 方法可以把当前运行的线程放到可运行池中并使另一个线程运行, 如果没有相同优先级的可运行的线程, yield() 什么都不做

1
2
3
4
5
6
public void run() {
for(int a = 0; a < 20; a++) {
log.append(currentThread().getName() + "a" + a);
yield();
}
}

sleep() 方法和 yield() 方法都是给 Thread 类的静态方法, 都会使当前处于运行状态的线程放弃 CPU, 把运行机会让给别的线程, 两者区别:

  • sleep() 方法会给其他线程运行的机会, 不考虑其他线程的优先级, 一次会给较低优先级的线程, yield() 方法只会给相同优先级或者更高优先级的线程一个运行机会
  • 先线程执行了 sleep(long milis) 方法后, 将转到阻塞状态, 参数 millis 指定线程会面的时间; 当线程执行了 yield() 方法后, 将转到就绪状态
  • sleep() 方法声明抛出 InterruptedException 异常; 而 yield 方法没有声明抛出任何异常
  • sleep() 方法比 yield() 方法具有更好的移植性, 不能依靠yield()方法来提高程序的并发性能, 对于大多数程序来说, yield() 方法唯一用户是在测试期间认为的提高程序的并发性能, 以帮助发现一些隐藏的错误

线程的优先级

每个线程都具有各自的优先级, 线程的优先级可以表明在程序中该线程的重要性, 如果有很多线程处于就绪状态, 系统会根据优先级来决定首先使哪个线程进入运行状态. 但这并不意味着一优先级的线程得不到运行, 而只是它运行概率比较小, 如垃圾回收线程的优先级就比较低

Thread 类中包含的成员变量代表了线程的某些优先级, 如 Thread.MIN_PRIORITY (常数1), Thread.MAX_PRIORITY (常数10), Thread.NORM_PRIORITY (常数5). 其中每个线程优先级都在 Thread.MIN_PRIORITY - Thread.AX_PRIORITY 之间, 在默认情况家优先级都是 Thread.NORM_PRIORITY, 每个新产生的线程都继承了父线程的优先级

在多任务操作系统中, 每个线程都会得到一小段 CPU 时间片运行, 在时间结束时, 将轮换另一个线程进入运行状态, 这时系统会选择与当前线程优先级相同的线程予以运行. 系统始终选择与当前线程优先级相同的线程予以支持, 系统始终选择就绪状态下优先级较高的线程进入运行状态

1
2
3
4
5
graph LR;

A(优先级5) --> B[线程A] --> C[线程B]
D(优先级4) --> E[线程C]
F(优先级3) --> G[线程D]

优先级5的线程A会首先得到 CPU 时间段; 当该时间结束后, 轮换到与线程A相同优先级线程B; 当线程B的运行时间结束后, 会继续轮换到线程A, 直到线程A与线程B都执行完毕, 才会轮换到线程C, 当线程C结束后, 才会轮换到线程D

1
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
64
65
66
67
68
69
70
71
public class PriotityTest {

Thread threadA;
Thread threadB;
Thread threadC;
Thread threadD;
Thread threadE;
Thread threadF;
Thread threadG;

public PriotityTest() {
threadA = new Thread(new MyThread("threadA"));
threadB = new Thread(new MyThread("threadB"));
threadC = new Thread(new MyThread("threadC"));
threadD = new Thread(new MyThread("threadD"));
threadE = new Thread(new MyThread("threadE"));
threadF = new Thread(new MyThread("threadF"));
threadG = new Thread(new MyThread("threadG"));

setProtity("threadA", 5, threadA);
setProtity("threadB", 5, threadB);
setProtity("threadC", 4, threadC);
setProtity("threadE", 4, threadE);
setProtity("threadF", 4, threadF);
setProtity("threadG", 3, threadG);

}


public static void setProtity(String threadName, int priority, Thread t) {
t.setPriority(priority); // 设置线程优先级
t.setName(threadName); // 设置线程启动名称
t.start();
}

public static void main(String[] args) {
new PriotityTest();
}

}


class MyThread implements Runnable {

int count = 0;

String threadName;

public MyThread(String threadName) {
this.threadName = threadName;
}

@Override
public void run() {
while (true) {
count += 1;

try {
if (count >= 100) {
break;
} else {
System.out.println(threadName + " Count: " + count);
Thread.sleep(10);
}
} catch (InterruptedException e) {
System.out.println(threadName + " 线程退出");
e.printStackTrace();
}
}
}
}

线程同步

在单线程程序中, 每次只能做一件事, 后面的事情需要等待前面的事情完成后才可以进行, 但是如果使用多线程程序, 就会发生两个线程抢占资源的问题, 如果两个人同时说话, 两个人同时过同一个独木桥等. 所以在多线程编程中防止这些资源访问冲突, Java 提供了线程同步的机制来防止资源冲突

线程安全

在实际开发中, 使用多线程程序的情况有很多, 如银行排号系统, 火车站售票系统等, 这种多线程的程序通常会发生问题. 以火车站售票为例, 在代码中判断当前票数是否大于0, 如果大于0则执行将该票出售给乘客, 但当两个线程同时访问这段代码时(加入这时只剩下一张票), 第一个线程将票售出, 于此同时第二个线程也已经执行完成判断是否有票的操作, 并得出票数大于0的结论, 于是他也执行出票操作, 这样就会产生负数

所以在编写多线程程序时, 应该考虑线程安全问题, 实际上线程安全问题源于两个线程同时存取单一对象的数据

1
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
// 资源共享冲突
public class ThreadSafeErrTest implements Runnable {

int num = 10;

@Override
public void run() {
while(true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("tickets: " + num--);
}
}
}

public static void main(String[] args) {
ThreadSafeErrTest tErrTest = new ThreadSafeErrTest();

Thread tA = new Thread(tErrTest);
Thread tB = new Thread(tErrTest);
Thread tC = new Thread(tErrTest);
Thread tD = new Thread(tErrTest);
Thread tE = new Thread(tErrTest);

tA.start();
tB.start();
tC.start();
tD.start();
tE.start();
}
}

线程同步机制

解决多线程资源冲突问题的方法都是采用给定时间只允许一个线程访问共享资源, 这时就需要给共享资源上一道锁.

同步块

在 Java 中提供了同步机制, 可以有效防止资源冲突, 同步机制使用 synchronized 关键字

1
synchronized(Object) {}
同步方法

同步方法在方法前面修饰 synchronized 关键字的方法

1
synchronized void f() {}

当某个对象调用了同步方法时, 该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行, 必须将每个能访问共享资源的方法修饰为 synchronized, 否则就会报错

1
public synchronized void doit() {}