1.线程相关概念

  • 程序:是为了完成特定任务、用某种语言编写的一组指令的集合。

  • 进程:

    • 进程是程序的一次执行过程,或者说是正在运行的一个程序,比如我们使用QQ,就启动了一个进程,操作系统会为该进程分配内存空间。是一个动态过程,有产生、存在、消亡的过程。
  • 线程:

    • 概念:
      • 线程是由进程创建出来的,是进程的一个实体
      • 一个进程可以拥有多个线程,比如迅雷同时下载多个文件,QQ打开多个聊天窗口,可将这些看为进程里的线程
    • 单线程:同一时刻,只允许执行一个线程
    • 多线程:同一时刻,可以执行多个线程
    • 并发:同一时刻,多个任务交替执行,造成一种“貌似同时进行”的错觉,单核cpu实现的多任务就是并发,就像人一心多用做好几件事,就是并发。
    • 并行:同一时刻,多个任务同时进行,只有多核CPU才可以实现并行,正在并行中的CPU也可以单独实现并发,两者可同时存在。

2.线程基本使用

2.1 创建线程的两种方式

在Java中线程的使用有两种方法:

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

2.2 线程创建方式1-继承Thread类

案例:开启一个线程,该线程每隔一秒,在控制台输出“喵喵,我是小猫咪”,当输出80次时结束该线程

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
public class Thread01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.start();
}
}

class Cat extends Thread {
private int count = 0;

@Override
public void run() {

while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("喵喵,我是小猫咪" + (++count));

if (count == 80) {
break;
}
}


}
}

注意:Java 中实现真正的多线程是 start 中的 start0() 方法,run() 方法只是一个普通的方法。start0() 是本地方法,是 JVM 调用, 底层是用 c/c++实现的,真正实现多线程的效果, 是 start0(),而不是 run。

img

2.3 线程创建方式2-实现Runnable接口

  1. Java是单继承,所以在某些情况下一个类可能已经继承了了某个父类,这时就没办法再用Thread类方法来创建线程。
  2. 所以Java设计者提供了另一个方式来创建线程,就是实现Runnable接口来创建线程。

案例:开启一个线程,该线程每隔一秒,在控制台输出“小狗汪汪叫:旺旺旺~”,当输出10次时结束该线程

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
/**
* 通过实现Runnable接口来开发线程
*
* @auther xiaochen
* @create 2022-07-08 14:55
*/
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();

//创建了Thread对象,把dog对象放入Thread
Thread thread = new Thread(dog);
thread.start();
}

}


class Dog implements Runnable {

int count = 0;

@Override
public void run() {//普通方法

while (true) {
System.out.println("小狗汪汪叫:旺旺旺~" + (++count) + Thread.currentThread().getName());

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

e.printStackTrace();
}
if (count == 10){
break;
}
}
}
}

2.4 继承Thread类和实现Runnable的区别

  1. 从Java的设计上来看,通过继承Thread类或者实现Runnable接口来创建线程本质上没有区别,并且Thread类本身就实现了Runnable接口。
  2. 实现Runnable接口的方式更适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。

2.5案例:

编辑模拟三个窗口售票100张分别使用继承Thread类和实现Runnable接口来实现,并分析有什么问题。

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package edu.ysu.ticket;

/**
* @auther xiaochen
* @create 2022-07-08 15:37
* 使用多线程,模拟三个窗口同时售票
*/
public class SellTicket {
public static void main(String[] args) {
// SellTicket01 sellTicket01 = new SellTicket01();
// SellTicket01 sellTicket02 = new SellTicket01();
// SellTicket01 sellTicket03 = new SellTicket01();
//
//
// sellTicket01.start();
// sellTicket02.start();
// sellTicket03.start();


SellTicket02 sellTicket02 = new SellTicket02();

Thread thread01 = new Thread(sellTicket02);
Thread thread02 = new Thread(sellTicket02);
Thread thread03 = new Thread(sellTicket02);


thread01.start();
thread02.start();
thread03.start();


}

}


class SellTicket01 extends Thread {
//让多个线程共享Num
private static int ticketNum = 100;

@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
break;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}

}
}

class SellTicket02 implements Runnable {
//让多个线程共享Num
private int ticketNum = 100;

@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
break;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}

}
}

3.线程终止

3.1 基本说明

  1. 当线程完成任务后,会自动退出。
  2. 也可以通过使用变量的方式来控制run方法退出的方式来停止线程,即通知方式

3.2 案例:

启动一个线程T,在main线程中停止T线程

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
package edu.ysu.exit_;

/**
* @auther xiaochen
* @create 2022-07-08 15:53
*/
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
t1.start();


//希望main线程去控制t1线程的终止,必须可以修改loop
//让t1 退出run方法,从而终止t1线程 - > 通知方式

System.out.println("主线程休眠10s");

Thread.sleep(10 * 1000);
t1.setLoop(false);
}
}

class T extends Thread {

private int count = 0;

//设置一个控制变量
private boolean loop = true;


//为loop变量添加set方法来保证主线程可以对该变量进行修改
public void setLoop(boolean loop) {
this.loop = loop;
}

@Override
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {


}

System.out.println("线程T运行中。。。。。。" + (++count));
}
}
}

4.线程常用方法

4.1 常用方法第一组

  1. setName //设置线程名称,使之与参数name相同
  2. getName //返回该线程的名称
  3. start //使该线程开始执行,Java虚拟机底层调用该线程的start0方法
  4. run //调用线程对象的run方法
  5. setPriority //更改线程的优先级
  6. getPriority //获取线程的优先级
  7. sleep //在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

4.2 注意事项和细节

  1. start方法的底层才会创建新的线程调用run,而run方法只是一个简单的方法调用,不会启动新线程
  2. 线程优先级的范围
  3. interrupt,中断线程,但是没有结束线程,所以一般用于中断正在休眠的线程
  4. 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ThreadMethod1 {
public static void main(String[] args) throws InterruptedException {

ThreadDemo1 td1 = new ThreadDemo1();
//给线程起名字
td1.setName("刘亦菲");
//给线程设置优先级
td1.setPriority(Thread.MIN_PRIORITY);
//启动子线程
td1.start();
//测试优先级
System.out.println("默认优先级:" + Thread.currentThread().getPriority());

//interrupt测试
Thread.sleep(3000);
//程序执行到这里就会中断td1线程的休眠
td1.interrupt();

}
}

class ThreadDemo1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "正在吃包子");

}


try {
System.out.println(Thread.currentThread().getName() + "正在休眠");
Thread.sleep(20000);
} catch (InterruptedException e) {
//当该线程指定到一个interrupt方法时,就会catch一个异常
//InterruptedException 是指捕获到一个中断异常
System.out.println(Thread.currentThread().getName() + "被中断了");
}
}
}

4.3 常用方法第二组

  1. yield:线程礼让。让出CPU,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。yield是将线程从运行态,转为就绪态,然后与其他线程重新竞争。
  2. join:线程插队。插队的线程一旦插队成功,则会先执行完插入线程的所有任务。

image-20220709081348581

案例:

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
package edu.ysu.method;

/**
* @auther xiaochen
* @create 2022-07-09 8:18
*/
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
T2 t2 = new T2();
t2.start();


for (int i = 0; i <=20 ; i++) {
Thread.sleep(1000);
System.out.println("主线程 吃了" + i+ "个汉堡");
if(i == 5){
System.out.println("主线程让子线程先吃");
//join线程插队
//相当于让t2线程先执行完毕
// t2.join();

//yield线程礼让
t2.yield();
}
}
}

}

class T2 extends Thread {
@Override
public void run() {
for (int i = 0; i <= 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程 吃了" + i+ "个汉堡");
}
}
}

4.4守护线程

用户线程和守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。
  2. 守护线程:一般视为工作线程服务的,当所有工作线程结束,守护线程自动结束
  3. 常见的守护线程:垃圾回收机制

案例:

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
package edu.ysu.method;

/**
* @auther xiaochen
* @create 2022-07-09 8:18
*/
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();



//如果我们希望当主线程结束,子线程可以自动结束
//只需将子线程设置为守护线程即可
myDaemonThread.setDaemon(true);


myDaemonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("老板在办公室摸鱼");
Thread.sleep(1000);
}
System.out.println("老板出来视察,我结束摸鱼,认真工作(假装)");
}

}

class MyDaemonThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我在愉快地摸鱼");
}
}
}

5.线程的生命周期

5.1 JDK 中用 Thread.State 枚举表示了线程的几种状态

image-20220709084024884

5.2 线程状态转换图

Runnable状态只是代表可以运行了,但是否运行取决于线程调度器。

5.3 案例

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
package edu.ysu.state_;

/**
* @auther xiaochen
* @create 2022-07-09 9:03
*/
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
System.out.println(t.getName() + "状态为:" + t.getState());
t.start();

//只要线程状态不是终止状态,就打印线程t的状态
while (Thread.State.TERMINATED != t.getState()) {
System.out.println(t.getName() + "状态为:" + t.getState());
Thread.sleep(5000);
}


System.out.println(t.getName() + "状态为:" + t.getState());

}
}


class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println("Hello" + i);

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


break;
}
}
}

6. 线程同步

6.1 Synchronized

线程同步机制

  1. 在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
  2. 线程同步:即当有一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作,其他县城才能对该线程地址进行操作。

线程同步具体的实现方法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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package edu.ysu.synchronized_;

/**
* @auther xiaochen
* @create 2022-07-08 15:37
* 使用多线程,模拟三个窗口同时售票
*/
public class SellTicket {
public static void main(String[] args) {

SellTicket03 sellTicket02 = new SellTicket03();

Thread thread01 = new Thread(sellTicket02);
Thread thread02 = new Thread(sellTicket02);
Thread thread03 = new Thread(sellTicket02);


thread01.start();
thread02.start();
thread03.start();


}

}


//实现接口方式,使用synchronized实现线程同步
class SellTicket03 implements Runnable {
//让多个线程共享ticketNum
private int ticketNum = 100;

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

}


public synchronized void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}
}
}

同步原理分析

image-20220709095517528

三个线程争夺一把锁(对象锁),谁抢到谁就执行run方法中的代码,其余线程会被挡在外面,执行完毕后释放锁,然后三个线程继续争夺。

6.2 互斥锁

基本介绍:

  1. Java语言中引入了对象互斥锁的概念,来保证共享数据操作的完整性。
  2. 每个对象都对应一个可以被称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
  3. 关键字synchronized来与对象互斥锁联系。当某个对象用synchronized修饰时,表名该对象在任一时刻只能由一个线程访问。
  4. 同步的局限性:导致程序的执行效率会降低(相当于多个车排队经过一个收费站)
  5. 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一对象)。
  6. 同步方法(静态的)的锁为当前类本身。
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
72
73
74
75
76
77
78
79
//1. public synchronized void sell() {} 就是一个同步方法,此时的锁在this对象

public synchronized void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}
}
//2. 也可以在代码块上写synchronized ,同步代码块,互斥锁还是在this对象
public void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法
synchronized (this) {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}
}
}



//3. 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一对象)。

Object object = new Object();
public void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法
synchronized (object) {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束。。。。。。");
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("窗口:" + Thread.currentThread().getName() + "售出一张票" +
"剩余票数:" + (ticketNum--));

}
}
}
//4.同步方法(静态的)的锁为当前类本身。
//m1的锁是加在SellTicket03这个类上的
public synchronized static void m1(){}

public static void m2(){
synchronized(SellTicket03.class){
System.out.println("m2");
}
}



注意事项和细节:

  1. 如果同步方法没有使用static修饰:默认锁对象是this
  2. 如果同步方法使用static进行修饰:默认锁对象是当前类.class
  3. 实现的步骤
    1. 先分析需要进行上锁的代码
    2. 选择同步代码块或者同步方法,建议同步代码块,因为这样可以使得上锁的代码更少,效率相对更高一些。
    3. 要求多个线程的锁对象为同一个即可。

6.3 线程死锁

基本介绍

多个线程都互相占用了对方的锁资源,但不肯相让,导致了死锁,编程时要避免死锁的发生。

案例:

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
public class DeadLock_ {
public static void main(String[] args) {
//模拟死锁现象
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A线程");
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B线程");
A.start();
B.start();
}
}


//线程
class DeadLockDemo extends Thread {
static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用static
static Object o2 = new Object();
boolean flag;

public DeadLockDemo(boolean flag) {//构造器
this.flag = flag;
}

@Override
public void run() {

//下面业务逻辑的分析
//1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
//2. 如果线程A 得不到 o2 对象锁,就会Blocked
//3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
//4. 如果线程B 得不到 o1 对象锁,就会Blocked
if (flag) {
synchronized (o1) {//对象互斥锁, 下面就是同步代码
System.out.println(Thread.currentThread().getName() + " 进入1");
synchronized (o2) { // 这里获得li对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入2");
}

}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入3");
synchronized (o1) { // 这里获得li对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入4");
}
}
}
}
}

运行结果:

image-20220709104944597

两线程死锁,程序卡死。

6.4 释放锁

会释放锁的操作

  1. 当前线程的同步方法、同步代码块执行结束
    • 案例:上厕所,上完厕所出来
  2. 当前线程在同步代码块、同步方法中遇到break、return
    • 案例:没有正常的完事,经理叫他修改bug,不得已中断上厕所
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致异常结束
    • 没有正常的完事,发现忘带纸,不得已出来
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
    • 案例:没有正常完事,觉得需要酝酿一下,先出来,等会再进去

不会释放锁的操作

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁。
    • 案例:上厕所,太困了,坐马桶上睡一会
  2. 线程执行同步代码块或同步方法时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。
    • 提示:应该尽量避免suspend()和resume()来控制线程,方法不在推荐使用