十一、线程

一、线程

:::tip

  • 线程是由进程创建的,线程就是进程的实体
  • 线程可以分为单线程 和 多线程
    • 单线程: 同一时刻,只允许执行一个线程;
    • 多线程: 同一时刻,可以执行多个线程;
      • 并发: 同一时刻,多个任务交替执行,单核 CPU 实现多任务并发;
      • 并行: 同一时刻,多个任务同时执行,多核 CPU 可以实现并行;
      • 并行 与 并发 也可能同时存在
        :::

1. 线程使用

:::tip
创建线程的两种方法:

  1. 继承 Thread 类,重写 run 方法;
  2. 实现 Runnable 接口,重写 run 方法;
    :::

:::tip
继承 Thread 类,创建线程:

  • 主线程结束后,子线程任然在执行时,进程不会结束;
  • 调用 run() 方法并不会启动线程,而且会造成主线程阻塞
  • 调用 start() 方法可以启动线程,不会造成线程阻塞,主线程和子线程会同时执行
    :::
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 继承 Thread 类 - 案例
public class Thread01{
public static void main(String[] args) {
Cat cat = new Cat();
// 调用 run() 方法并不会启动线程,而且会造成主线程阻塞
// 调用 start() 方法可以启动线程,不会造成线程阻塞,主线程和子线程会同时执行
cat.start();
}
}

class Cat extends Thread{
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("第"+(i+1) +"次,喵呜~~~");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

:::tip
实现 Runnable 接口,创建线程:

  • 先创建 Thread 类 : Thread thread = new Thread(实现类);
  • 调用 start() 方法,启动线程
    :::
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 实现 Runnable 接口 - 案例
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
Thread thread = new Thread(dog);
thread.start();
}
}

class Dog implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("第" + (i+1)+ "次, 旺财: 汪~~~");
try {
// option + command + t 快捷键, 快速实现 try ... catch
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Thread 和 Runnable 的区别:
:::tip

  1. Thread 和 Runnable 在 Java 底层没有区别,都是调用 start();
  2. 实现 Runnable 接口的方式更加适合多线程共享一个资源的情况,并且可以避免单继承的限制;
    :::

2. 线程终止、线程同步

:::tip

  • 当线程完成任务后,会自动退出;
  • 通过使用 变量 来控制 run 方法退出的方式来停止线程;
  • 使用synchronized同步器来修饰买票的方法(防止出现一张票被两人同时抢到)
    :::

:::tip
线程同步(synchronized):

  • 在多线程编程中,某些数据不允许被多个线程同时访问,此时就使用线程同步访问技术,保证线程在同一时刻最多只能被一个线程访问,以保证线数据完整性;
  • 线程同步: 即 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作;
    • 同步代码块:synchronized(对象){需要被同步的代码};
    • synchronized 还可以放在方法声明中,表示整个方法 为 同步方法;
      :::
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
// 售票系统 - 案例
public class Thread03 {
public static void main(String[] args) {
Ticket1 t = new Ticket1();

Thread r1 = new Thread(t);
Thread r2 = new Thread(t);
Thread r3 = new Thread(t);

r1.start();
r2.start();
r3.start();

}
}

class Ticket1 implements Runnable{
int count = 100;

@Override
public void run() {
sell();
}

//使用synchronized同步器来修饰买票的方法(防止出现一张票被两人同时抢到)
public synchronized void sell(){
while (true) {
if (count > 0) {
System.out.println("售出 1 张票,剩余" + (--count) + "--->" + Thread.currentThread().getName() );
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("已售空");
break;
}
}
}
}

3. 线程常用方法

setName设置线程名称,使之与参数 name 相同
getName返回线程名称
start使该线程开始执行,
run调用线程对象 run 方法,run 方法不会创建新线程
setPriority更改线程优先级,MAX_PRIORITY = 10, MIN_PRIORITY=1,NORM_PRIORITY=5;
getPriority获取线程优先级
sleep在指定的毫秒数内,线程休眠
interrupt中断线程, 线程没有关闭,一般用于中断正在休眠的线程
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
// 线程常用方法 - 案例
public class Thread04 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
Thread thread = new Thread(t);
// 设置线程名称
thread.setName("线程 1");
thread.start();

Thread.sleep(2000);
// 中断线程休眠
thread.interrupt();
}
}

class T implements Runnable{
int count = 0;

@Override
public void run() {
while (true) {
if (count < 3) {
for (int i = 0; i < 10; i++) {
System.out.println("吃了" + (i + 1) + "个包子~ " + Thread.currentThread().getName());
}
System.out.println("休息 10 秒中....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("休眠被中断");
}
++count;
}else {
break;
}
}
}
}

yield线程礼让,让出 CPU让其他线程先执行,因时间不确定,不一定礼让成功
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
27
28
29
30
31
32
// 线程插队 - 案例
public class Thread04 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
Thread t1 = new Thread(t);
t1.setName("线程 1");
t1.start();

for (int i = 0; i < 10; i++) {
System.out.println("吃了" + (i + 1) + "个包子~ " + Thread.currentThread().getName());
Thread.sleep(1000);
if (i == 5){
t1.join(); // 让子线程插队
}
}
}
}

class T implements Runnable{

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("吃了" + (i + 1) + "个包子~ " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("");
}
}
}
}

4.用户线程和守护线程

:::tip

  • 用户线程: 也叫工作线程,当线程的任务执行完 或以 通知的方式结束;
  • 守护线程 : 一般是为工作线程服务的,当所有的用户线程结束时,守护线程自动结束。
    • 常见的守护线程: 垃圾回收机制
    • 设置守护线程: thread.setDaemon(true)
      :::
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
// 守护线程 - 案例
public class Thread05 {
public static void main(String[] args) throws InterruptedException {
R r = new R();
Thread thread = new Thread(r);

// 设置 子线程为守护线程, 当主线程结束后, 子线程也会结束
thread.setDaemon(true);

thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行中~~~");
Thread.sleep(1000);
}
}
}

class R implements Runnable{
@Override
public void run() {
for (;;){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这是一个无限循环~~~");
}
}
}

5.互斥锁

:::tip

  • 互斥锁: 用来保证共享数据操作的完整性;
  • 每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;
  • 关键字 synchronized 来与对象的互斥锁联系;
    • 当某个对象用了 synchronized 修饰时,表示该对象在任一时刻只能由一个线程访问;
  • 同步的局限性:导致程序的执行效率要降低;
  • 同步方法(非静态的)的锁 可以是 this,也可以是其他对象(要求是同一对象);
  • 同步方法(静态的)的锁为当前类本身;
    :::

:::warning
注意事项:

  • 同步方法如果没有使用 static 修饰:默认锁对象为 this;
  • 如果方法使用 static 修饰,默认锁对象为 当前类.class
  • 实现步骤:
    • 分析需要上锁的代码;
    • 选择同步代码块或同步方法;
    • 要求多个线程的锁对象为同一个;
      :::
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
// 互斥锁 - 案例
class Ticket implements Runnable{
static int count = 10;

@Override
public void run() {}

// 同步方法
public synchronized void sell1(){
if (count > 0) {
System.out.println("售出 1 张票,剩余" + (--count) + "--->" + Thread.currentThread().getName());
} else {
System.out.println("已售空");
}
}

public void sell2(){
// 同步代码块(非静态), 锁为 this
synchronized (this) {
if (count > 0) {
System.out.println("售出 1 张票,剩余" + (--count) + "--->" + Thread.currentThread().getName());
} else {
System.out.println("已售空");
}
}
}

public static void sell3(){
// 同步代码块(静态的), 锁为当前类本身
synchronized (Ticket.class) {
if (count > 0) {
System.out.println("售出 1 张票,剩余" + (--count) + "--->" + Thread.currentThread().getName());
} else {
System.out.println("已售空");
}
}
}
}

6.死锁

:::tip
线程死锁:

  • 多个线程都占用了对象的锁资源,但不肯相让,导致了死锁,在编程里一定要避免死锁的发生;
  • 多个线程互相获取了对方想要获取的线程锁,即为死锁
    :::

7. 释放锁

:::tip
释放锁的情形:

  • 当前线程的同步方法,同步代码块执行结束;
  • 当前线程在同步代码块、同步方法中遇到了 break、return;
  • 当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束;
  • 当前线程在同步代码快、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁;

不会释放锁的情形:

  • 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程执行,不会释放锁;
  • 线程执行同步代码块时,其他线程调用类该线程的 suspend() 方法,将该线程挂起,该线程不会释放锁;
    :::

案例

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
// 输入字母 Q ,暂停随机数
public class HomeWork01 {
public static void main(String[] args) {
RandomInt randomInt = new RandomInt();
Keyboard keyboard = new Keyboard(randomInt);
Thread t1 = new Thread(randomInt);
Thread t2 = new Thread(keyboard);
t1.start();
t2.start();
}
}

class RandomInt implements Runnable{
public boolean num = true;
@Override
public void run() {
while (num){
System.out.println((int)(Math.random()*100));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程 1 退出");
}
}

class Keyboard implements Runnable{
public RandomInt r;
Scanner s = new Scanner(System.in);

public Keyboard(RandomInt r) {
this.r = r;
}

@Override
public void run() {
while (true) {
System.out.println("请输入: ");
char print = s.next().toUpperCase().charAt(0);
if (print == 'Q'){
r.num = false;
System.out.println("线程 2 退出");
break;
}
}
}
}
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
// 两个线程分别取钱
public class HomeWork02 {
public static void main(String[] args) {
money money = new money();
Thread t1 = new Thread(money);
t1.setName("线程 1 ");
Thread t2 = new Thread(money);
t2.setName("线程 2 ");
t1.start();
t2.start();
}
}

class money implements Runnable{
private double m = 10000;

public double getM() {
return m;
}

public void setM(double m) {
this.m = m;
}

@Override
public void run() {
while (true){
if (m < 1000){
System.out.println("钱被取完了.");
break;
}
synchronized (this){
setM(getM()-1000);
System.out.println(Thread.currentThread().getName() + "取出了 1000 大洋, 剩余" + getM());
}

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