0%

Java(28)多线程

多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键——->打开任务管理器,可以查看当前任务的进程:

进程

线程

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 设置线程的优先级

    设置线程优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

      抢占式调度

创建线程类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

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
package com.tipdm.Demo03;

/**
* 创建多线程程序的第一种方式:创建Thread类的子类
* java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
*
* 实现步骤:
* 1. 创建一个Thread类的子类
* 2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
* 3. 创建Thread类的子类对象
* 4. 调用Thread类中的方法start方法,开始新的线程,执行run方法
* void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
* 结果是两个线程并发地运行;当前线程(main)和另一个线程(创建的新线程)。
* 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
* java程序属于抢占式调度,哪个优先级高优先给哪个线程优先执行;同一个优先级,随机选择一个执行。
*/
public class demo2 {
public static void main(String[] args) {
//3. 创建Thread类的子类对象
MyThread myThread = new MyThread();
// 4. 调用Thread类中的方法start方法,开始新的线程,执行run方法
myThread.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}
}

自定义线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tipdm.Demo03;

public class MyThread extends Thread{
public MyThread() {
super();
}

public MyThread(String name) {
super(name);
}

//2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run:" + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
main:0
run:0
run:1
run:2
main:1
run:3
main:2
main:3
main:4
main:5
main:6
...
run:15
run:16
run:17
run:18
run:19

进程已结束,退出代码0

线程名称

获取线程名称

获取线程的名称:

  1. 使用Thread类中的方法getName()

    String getName() 返回该线程的名称。

    默认情况下线程的名称为Thread-0,Thread-1...Thread-N

  2. 可以先获利到当前正在执行的线程,使用线程中的方法getName()获取线程的名称

    static Thread currentThread() 返回对当前正在执行的线程对象的引用

自定义线程类:

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
package com.tipdm.Demo01;

/**
* 获取线程的名称:
* 1. 使用Thread类中的方法getName()
* String getName() 返回该线程的名称。
* 2. 可以先获利到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
* static Thread currentThread() 返回对当前正在执行的线程对象的引用
*/
// 定义一个Thread类的子类
public class MyThread extends Thread{
// 重写run方法

@Override
public void run() {
// // 获取线程名称
// String name = getName();
// System.out.println(name);

// Thread t = Thread.currentThread();
// System.out.println(t);
//
// String name = t.getName();
// System.out.println(name);

System.out.println(Thread.currentThread().getName());
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tipdm.Demo01;

/**
* 线程的名称:
* 主线程:main
* 新线程:Thread-0, Thread-1, Thread-2
*/
public class demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();

new MyThread().start();
new MyThread().start();

System.out.println(Thread.currentThread().getName()); // 主线程名称
}
}
1
2
3
4
5
6
Thread-1
Thread-2
main
Thread-0

进程已结束,退出代码0

设置线程名称

设置线程名称:(了解)

  1. 使用Thread类中的方法setName(名字)

​ void setName(String name) 改变线程名称,使之与参数 name 相同。

  1. 创建一个带参数的构造方法,参数传递线程的名称;

​ 调用父类的带参构造方法,让线程名称传递给父类,让父类(Thread)给子线程起一个名字

​ Thread(String name) 分配新的 Thread 对象

自定义线程类:

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
package com.tipdm.Demo02;

import java.util.TreeMap;

/**
* 设置线程名称:(了解)
* 1. 使用Thread类中的方法setName(名字)
* void setName(String name) 改变线程名称,使之与参数 name 相同。
* 2. 创建一个带参数的构造方法,参数传递线程的名称;
* 调用父类的带参构造方法,让线程名称传递给父类,让父类(Thread)给子线程起一个名字
* Thread(String name) 分配新的 Thread 对象
*/
public class MyThread extends Thread{
public MyThread() {
}

public MyThread(String name) {
super(name);
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.tipdm.Demo02;


public class demo1 {
public static void main(String[] args) {
// 开启多线程
MyThread mt = new MyThread();
mt.setName("小强");
mt.start();

// 开启多线程
new MyThread("旺财").start();
}
}
1
2
3
4
小强
旺财

进程已结束,退出代码0

线程等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tipdm.Demo03;

/**
* public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
*
*/
public class demo1 {
public static void main(String[] args) {
for (int i = 0; i < 60; i++) {
System.out.println(i);
// 使用Thread类的sleep方法,让程序睡眠1s
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
0
1
2
...
58
59

进程已结束,退出代码0

创建多线程的第二种方式

创建多线程程序的第二种方式:实现Runnable接口

java.lang.Runnable

Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

java.lang.Thread类的构造方法

1
2
Thread(Runnable target)     分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。

实现步骤:

  1. 创建一个Runnable接口的实现类

  2. 在实现类中重写Runnable接口的run方法,设置线程任务

  3. 创建一个Runnable接口的实现类对象

  4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象

  5. 调用Thread类中的start方法,开启新的线程执行run方法

实现Runnable接口创建多线程程序的好处:

  1. 避免了单继承的局限性

​ 一个类只能继承一个类(一个人只能由一个亲爹),类继承了Thread类就不能继承其他的类

​ 实现了Runnable接口,还可以继承其他的类,实现其他的接口

  1. 增强了程序的扩展性,降低了程序的耦合性(解耦)

​ 实现了Runnable接口的方式,把设置线程任务和开启新线程任务进行了分离(解耦)

​ 实现类中,重写了run方法:用来设置线程任务

​ 创建Thread类对象,调用start方法:用来开启新线程。

自定义线程类1:

1
2
3
4
5
6
7
8
9
10
11
package com.tipdm.Demo04;
//1. 创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable{
// 2. 在实现类中重写Runnable接口的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}

自定义线程类2:

1
2
3
4
5
6
7
8
9
10
11
package com.tipdm.Demo04;
//1. 创建一个Runnable接口的实现类
public class RunnableImpl2 implements Runnable{
// 2. 在实现类中重写Runnable接口的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("helloWorld!" + i);
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tipdm.Demo04;

public class demo1 {
public static void main(String[] args) {
// 3. 创建一个Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
// Thread t = new Thread(run); // 打印线程名称
Thread t2 = new Thread(new RunnableImpl2()); // 打印线程名称
// 5. 调用Thread类中的start方法,开启新的线程执行run方法
t2.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
main0
main1
helloWorld!0
helloWorld!1
helloWorld!2
...
main16
main17
main18
main19

进程已结束,退出代码0

匿名内部类方式实现线程的创建

匿名内部类方式实现线程的创建

  • 匿名:没有名字
  • 内部类:写在其他类内部的类

匿名内部类作用:简化代码

把子类继承父类,重写父类的方法,创建子类对象合一步完成。

把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:

1
2
3
new 父类/接口(){
重写父类/接口中的方法
}
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
package com.tipdm.Demo05;

public class demo1 {
public static void main(String[] args) {
// 线程的父类是Thread
// new MyThread().start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}.start();

// 线程的接口Runnable
// Runnable r = new RunnableImpl(); // 多态
Runnable r = new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
};
new Thread(r).start();

// 简化接口的方式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread-0-->0
Thread-0-->1
Thread-0-->2
Thread-0-->3
...
Thread-0-->15
Thread-0-->16
Thread-0-->17
Thread-1-->4
...
Thread-2-->16
Thread-2-->17
Thread-2-->18
Thread-2-->19

线程安全

以电影院卖票为例:

image-20211224152531175

线程安全导致的结果:可能会出现同一张电影票被卖给了不同的用户。

实现卖票案例

线程实现类:

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
package com.tipdm.Demo06;

/**
* 实现卖票案例
*/
public class RunnableImpl implements Runnable{
// 定义一个多线程共享的票源
private int ticket = 100;

// 设置线程任务:卖票
@Override
public void run() {
while(true){
// 先判断票是否存在
if(ticket > 0){
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
ticket--;
}else{
break;
}
// 提高线程安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tipdm.Demo06;

/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class demo1Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 调用start方法开启多线程
t0.start();
t1.start();
t2.start();
// Thread-2--->正在卖第4张票
// Thread-1--->正在卖第4张票
// Thread-0--->正在卖第4张票
// 出现线程安全问题,出现重复的票
}
}
1
2
3
4
5
6
...
Thread-0--->正在卖第1张票
Thread-1--->正在卖第1张票
Thread-2--->正在卖第1张票

进程已结束,退出代码0

解决线程安全问题——使用同步代码块

卖票案例出现了线程安全问题

卖出了不存在的票和重复的票

解决线程安全问题的一种方案:使用同步代码块

格式:

1
2
3
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:

  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
package com.tipdm.Demo07;

public class RunnableImpl implements Runnable{
// 定义一个多线程共享的票源
private int ticket = 100;

// 创建一个锁对象
Object obj = new Object();

// 设置线程任务:卖票
@Override
public void run() {
while(true){
// 同步代码块
synchronized (obj){
// 先判断票是否存在
if(ticket > 0){
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
// 提高线程安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else{
break;
}
}
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tipdm.Demo07;

/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class demo1Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
1
2
3
4
5
6
7
8
9
10
...
Thread-2--->正在卖第19张票
Thread-2--->正在卖第18张票
Thread-2--->正在卖第17张票
Thread-1--->正在卖第16张票
Thread-1--->正在卖第15张票
Thread-1--->正在卖第14张票
Thread-1--->正在卖第13张票
...
进程已结束,退出代码0

解决线程安全问题——使用同步方法

解决线程安全问题的第二种方案:使用同步方法

使用步骤:

  1. 把访问了共享数据的代码抽取出来,放到一个方法种

  2. 在方法上添加synchronized修饰符

格式:定义方法的格式

1
2
3
修饰符 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
package com.tipdm.Demo08;

public class RunnableImpl implements Runnable{
// 定义一个多线程共享的票源
private int ticket = 100;

// 设置线程任务:卖票
@Override
public void run() {
System.out.println("this:" + this);
while(ticket>0){
payTicket();
}
}

/**
* 定义一个同步方法
* 同步方法也会把方法内部的代码锁住
* 只让一个线程执行
* 同步方法的锁对象是谁?
* 就算实现类对象 new RunnableImpl()
* 也就是this
*/
public /*synchronized*/ void payTicket(){
synchronized (this){
// 先判断票是否存在
if(ticket > 0){
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
// 提高线程安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tipdm.Demo08;

/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class demo1Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("run:" + run);
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
1
2
3
4
5
6
7
8
9
10
...
Thread-0--->正在卖第71张票
Thread-2--->正在卖第70张票
Thread-2--->正在卖第69张票
Thread-2--->正在卖第68张票
Thread-1--->正在卖第67张票
Thread-1--->正在卖第66张票
...

进程已结束,退出代码0

解决线程安全问题——使用静态同步方法

解决线程安全问题的第二种方案:使用静态同步方法

使用步骤:

  1. 把访问了共享数据的代码抽取出来,放到一个方法中

  2. 在方法上添加synchronized修饰符

格式:定义方法的格式

1
2
3
修饰符 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
package com.tipdm.Demo09;

public class RunnableImpl implements Runnable{
// 定义一个多线程共享的票源
private static int ticket = 100;

// 设置线程任务:卖票
@Override
public void run() {
System.out.println("this:" + this);
while(ticket>0){
payTicketStatic();
}
}

/**
* 静态的同步方法
* 锁对象是谁?
* 不能是this
* this是创建对象之后产生的,静态方法优先于对象
* 静态方法的锁对象是本类的class属性 --> class文件对象(反射)
*/
public static /*synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class) {
// 先判断票是否存在
if (ticket > 0) {
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
// 提高线程安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tipdm.Demo09;

/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class demo1Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("run:" + run);
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
1
2
3
4
5
6
7
8
9
...
Thread-2--->正在卖第20张票
Thread-2--->正在卖第19张票
Thread-2--->正在卖第18张票
Thread-0--->正在卖第17张票
Thread-0--->正在卖第16张票
...

进程已结束,退出代码0

解决线程安全问题——使用Lock锁

解决线程安全问题的第三种方案:使用Lock锁

java.util.concurrent.locks.Lock接口

Lock接口种的方法:

​ void Lock() 获取锁

​ void unlock() 释放锁

java.util.concurrent.locks.ReentrantLock implements Lock接口

使用步骤:

  1. 在成员位置创建一个ReentrantLock对象

  2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁

  3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

线程实现类:

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
package com.tipdm.Demo10;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class RunnableImpl implements Runnable{
// 定义一个多线程共享的票源
private int ticket = 100;

// 1. 在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();

// // 设置线程任务:卖票
// @Override
// public void run() {
// while(ticket > 0){
// l.lock();
// // 票存在,卖票 ticket--
// System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
// // 提高线程安全问题出现的概率,让程序睡眠
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// ticket--;
// l.unlock();
// }
// }


// 设置线程任务:卖票
@Override
public void run() {
while(ticket > 0){
l.lock();
// 提高线程安全问题出现的概率,让程序睡眠
try {
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票");
ticket--;
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
l.unlock(); // 无论程序是否异常都会把锁释放,提高程序效率。
}
}
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tipdm.Demo10;

/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class demo1Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
// 调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
1
2
3
4
Thread-0--->正在卖第2张票
Thread-0--->正在卖第1张票

进程已结束,退出代码0

线程状态

当线程被创建并启动后,它既不是一启动就进入了执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六个线程状态。

image-20211224153510798

image-20211224153454954

Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。

其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待),那么我们通过一个案例加深对该状态的一个理解。

1
2
3
4
5
try{
Thread.sleep(5000);
}catch(Eception e){
e.printStackTrace();
}

Blocked(锁阻塞)

Blocked 状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

image-20211224153950525

Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

等待唤醒案例:线程之间的通信

创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无线等待)

创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子。

注意:

  • 顾客和老板线程必须使用同步代码包裹起来,保证等待和唤醒只能有一个在执行
  • 同步使用的锁对象必须保证唯一
  • 只有锁对象才能调用wait和notify方法

Object类中的方法

  • void wait() 在其他线程调用此对象的notify()方法或 notifyAll()方法前,导致当前线程等待。
  • void notify() 唤醒在此对象监视器上等待的单个线程。 会继续执行wait方法之后的代码
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
package com.tipdm.Demo11;

public class demo1 {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
while(true){
// 告知老板要的包子的种类和数量
System.out.println("告知老板要的包子的种类和数量");

// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
// 进入等待状态,等老板做包子
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("包子真香!!!!!!!!!!");
System.out.println("------------------");
}
}
}.start();

// 创建一个老板线程(生产者)
new Thread(){
@Override
public void run() {
while(true){
// 花了5秒做包子
try {
Thread.sleep(5000);
System.out.println("包子做好了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
// 包子做好了唤醒顾客吃包子
obj.notify();
}
}
}
}.start();
}
}

进入到TimeWaiting(计时等待)有两种方式

  1. 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态

  2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

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
package com.tipdm.Demo11;

public class demo2 {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
while(true){
// 告知老板要的包子的种类和数量
System.out.println("告知老板要的包子的种类和数量");

// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
// 进入等待状态,等老板做包子
try {
obj.wait(5000); // 就算不被唤醒,也将在5s后自动醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("包子真香!!!!!!!!!!");
System.out.println("------------------");
}
}
}.start();
}
}

void notifyAll() 唤醒在此对象监视器上等待的所有线程。 会继续执行wait方法之后的代码

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
package com.tipdm.Demo11;

/**
* void notifyAll() 唤醒在此对象监视器上等待的所有线程。 会继续执行wait方法之后的代码
*/
public class demo3 {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
while(true){
// 告知老板要的包子的种类和数量
System.out.println("顾客1,告知老板要的包子的种类和数量");

// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
// 进入等待状态,等老板做包子
try {
obj.wait(5000); // 就算不被唤醒,也将在5s后自动醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("顾客1,包子真香!!!!!!!!!!");
}
}
}.start();

// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
while(true){
// 告知老板要的包子的种类和数量
System.out.println("顾客2,告知老板要的包子的种类和数量");

// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
// 进入等待状态,等老板做包子
try {
obj.wait(5000); // 就算不被唤醒,也将在5s后自动醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("顾客2,包子真香!!!!!!!!!!");
}
}
}.start();

// 创建一个老板线程(生产者)
new Thread(){
@Override
public void run() {
while(true){
// 花了5秒做包子
try {
Thread.sleep(5000);
System.out.println("包子做好了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
// 包子做好了唤醒顾客吃包子
obj.notifyAll(); // 唤醒所有等待的线程
}
}
}
}.start();
}
}
1
2
3
4
5
6
7
8
9
10
告知老板要的包子的种类和数量
包子做好了!
包子真香!!!!!!!!!!
------------------
告知老板要的包子的种类和数量
包子做好了!
包子真香!!!!!!!!!!
------------------
告知老板要的包子的种类和数量
包子做好了!
-------------本文结束感谢您的阅读-------------