JavaSE 进阶 (11) 多线程

多线程慨述-初步了解多线程

简单了解多线程

是指从软件或者硬件上实现多个线程并发执行的技术

具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能

图1

在外人眼中,由于你的手速非常快,看起来像是同时执行三个操作,但是在某一个瞬间,其实只做了一件事情

图2

多线程慨述-并发和并行

并发和并行

  1. 并行:在同一时刻,有多个指令在多个CPU上同时执行

三个厨师炒三个菜

图3
  1. 并发:在同一时刻,有多个指令在单个CPU上交替执行

一个厨师炒三个菜

图4

多线程慨述-进程和线程

进程和线程

进程:是正在运行的软件 (就是操作系统中正在运行的一个应用程序)

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
  • 动态性:进程的实质是程序的一次执行过程,进程是动态产生的,动态消亡的
  • 并发性:任何进程都可以同其他进程一起并发执行

线程:是进程中的单个顺序控制流,是一条执行路径 (就是应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾)

  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序

注意事项:

计算机中的程序,天然是 “并发执行”,每个线程必须是完全独立

多线程的实现方式-继承Thread

多线程的实现方案

  • 继承Thread类的方式进行实现
  • 实现Runnable接口的方式进行实现
  • 利用Callable和Future接口方式实现

方案1:继承Thread类

  • 定义一个类MyThread继承Thread类
  • 在MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程
 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
//创建MyThread类继承Thread类,并重写run()方法
public class MyThread extends Thread{
	@Override
	public void run(){
	//代码就是线程在开启之后执行的代码
	for(int i = 0; i < 100; i++){
		System.out.println("线程开启了" + i);
	}
}

//在测试类中进行测试
public class Demo {
	public static void main(String[] args){
		MyThread t1 = new MyThread();
		t1.start();
	}
}

//开启多条线程进行测试,会出现线程之间交替运行的情况,线程之间的运行具有随机性
public static void main(String[] args){
	//创建一个线程对象
	MyThread t1 = new MyThread();
	//创建一个线程对象
	MyThread t2 = new MyThread();
	//开启一条线程
	t1.start();
	//开启第二条线程
	t2.start();
}

T1.start()只是表示准备好了,并不代表执行

多线程的实现方式-两个小问题

两个小问题

继承Thread类,为什么要重写run()方法?

  • 因为run()是用来封装被线程执行的代码

run()方法和start()方法的区别?

  • run():封装线程执行的代码,如果t1.run()直接调用,相当于创建对象,用对象去调用方法,并没有开启线程
  • start():启动线程,然后由JVM调用此线程的run()方法,去执行代码

start()方法的源码

图5

在start()方法源码的 803 行,有一个start0(),点击start0()我们可以看到start0()前有个native的修饰符

Native表示start0()是个本地方法,start0()方法就是跟本地操作系统进行交互,去开启一条线程


线程:->需不需要系统资源(CPU/内存?????)

CPU和内存是谁的?电脑?Java??

我们创建线程:就是问电脑要 “CPU和内存”

其实:Java语言本身是无法操作电脑的。

Java语言是"给C++写个申请"

多线的实现方式-实现Runnable接口

方案2:实现Runnable接口

  • 定义一个类 MyRunnable 实现 Runnable 接口
  • 在 MyRunnable 类中重写 run() 方法
  • 创建 MyRunnable 类的对象
  • 创建 Thread 类的对象,把 MyRunnable 对象作为构造方法的参数
  • 启动线程

MyRunnable中没有start()方法,所以我们需要借助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
26
27
28
29
30
31
32
33
34
35
//实现Runnable接口,并重写run()方法
public class MyRunnable implements Runnable{
	@Override
	public void run(){
		//线程启动后执行的代码
		for(int i = 0; i < 100; i++){
			System.out.println("第二种方式实现多线程" + i);
		}
	}
}

//在测试类中进行测试
public static void main(String[] args){
	//创建了一个参数的对象
	MyRunnable mr = new MyRunnable();
	//创建了一个线程对象,并把参数传递给这个线程
	//在线程启动之后,执行的就是参数里面的run方法
	Thread t1 = new Thread(mr);
	t1.start();
}

//创建两个线程进行测试
public static void main(String[] args){
	//创建了一个参数的对象
	MyRunnable mr = new MyRunnable();
	//创建了一个线程对象,并把参数传递给这个线程
	//在线程启动之后,执行的就是参数里面的run方法
	Thread t1 = new Thread(mr);
	//开启线程
	t1.start();

	MyRunnable mr2 = new MyRunnable();
	Thread t2 = new Thread(mr2);
	t2.start();
}

多线程的实现方式-实现callab1e接口

方案3:实现Callable和Future接口

  • 定义一个类 MyCallable 实现 Callable 接口
  • 在 MyCallable 类中重写 call() 方法
  • 创建 MyCallable 类的对象
  • 把 MyCallable 对象作为构造方法的参数,创建 Future 的实现类 FutureTask 对象
  • 把 FutureTask 对象作为构造方法的参数,创建 Thread 类的对象
  • 启动线程
  • 可以调用 get() 方法,就可以获取线程结束之后的结果

之前的两种方式没有返回值,call() 方法有返回值

 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
//实现Callable接口,并重写call()方法,Callable<>的泛型为返回值的数据类型
public class MyCallable implements Callable<String> {
	@Override
	public String call() throws Exception {
		for(int i = 0; i < 100; i++){
			System.out.println("跟女孩表白" + i);
		}
		//返回值就表示线程运行完毕之后的结果
		return "答应";
	}
}

public class Demo {
	public static void main(String[] args) {
		//线程开启之后需要执行里面的call方法
		MyCallable mc = new MyCallable();
		
		//FutureTask 可以获取线程执行完毕之后的结果,也可以作为参数传递给Thread对象
		FutureTask<String> ft = new FutureTask<>(mc);
		
		//创建线程对象
		Thread t1 = new Thread(ft);
		
		//开启线程
		t1.start();
		
		//获取结果
		String s = ft.get();
		System.out.println(s);
	}
}

Thread t1 = new Thread(mc); 不可以,是因为new Thread()中要传递的是Runnable的实现类,而MyCallable和Runnable没有任何关系

Thread t1 = new Thread(ft); 为什么可以呢?这是因为FutureTask除了实现Future接口,还实现了Runnable接口,FutureTask <>的泛型要跟MyCallable返回值的类型保持一致

图6

ft.get()方法的注意事项

  • 获得线程运行之后的结果
  • 如果线程还没有运行结束,那么get()方法会在这里死等
  • get()方法必须写在start()方法之后
图7

三种实现方式的对比

优点 缺点
实现 Runnable、Callable 接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能直接使用 Thread 类中的方法
继承 Thread 类 编程比较简单,可以直接使用 Thread 类中的方法 扩展性较差,不能再继承其他的类

Runnable、Callable因为没有继承Thread类,所以不能直接使用Thread类中的方法,例如:getName()…

Thread方法-设置获取名字

获取线程的名字

String getName():返回此线程的名称

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class MyThread extends Thread {
	@Override
	public void run(){
		for(int i = 0; i < 100; i++){
			System.out.println(getName() + "@@@" + i);
		}
	}
}

public class Demo {
	//1,线程是有默认名字的,格式:Thread-编号
	public static void main(String[] args){
		MyThread t1 = new MyThread();
		MyThread t2 = new MyThread();
		t1.start();
		t2.start();
	}
}

根据源码可知,线程有默认的名字格式

图8

nextThreadNum() 用来计算编号,初始化的时候为0,下一次执行到这个方法的时候,返回的是return threadInitNumber++


Thread类中设置线程的名字

  1. void setName(String name):将此线程的名称更改为等于参数name
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread {
	@Override
	public void run(){
		for(int i = 0; i < 100; i++){
			System.out.println(getName() + "@@@" + i);
		}
	}
}

public static void main(String[] args) {
	MyThread t1 = new MyThread();
	MyThread t2 = new MyThread();
	
	t1.setName("小蔡");
	t2.setName("小强");
	
	t1.start();
	t2.start();
}

  1. 通过构造方法也可以设置线程名称
图9

此时,报错了,报错的原因:虽然父类 Thread 有带参构造,但是 MyThread 并没有继承,需要通过 super() 调用父类的构造方法

图10

再次尝试,错误消失

图11

Thread方法-获取线程对象

获得当前线程的对象

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

那条线程执行到 currentThread() 方法,那么就把这条线程的对象返回


main 方法也是由 main 线程去调用的,而 main 线程是 JVM 在刚启动时创建的

1
2
3
4
5
6
public class Demo {
	public static void main(String[] args){
		String name = Thread.currentThread().getName();
		System.out.println(name);  //main
	}
}

currentThread的使用场景

对于 Runnable 和 Callable 方式的多线程,由于没有继承 Thread 类,所以没有 getName() 方法,这时候就可以使用 currentThread

图12

先获取当前线程的对象,再调用 getName() 获取名字

Thread方法-sleep

线程休眠

Public static void sleep(long time):让线程休眠指定的时间,单位为毫秒

那条线程执行到了 sleep() 方法,那么就让这条线程在这睡这么长时间

1
2
3
4
5
6
7
public class Demo {
	public static void main(String[] args) throws InterruptedException {
		System.out.println("睡觉前");
		Thread.sLeep(3000);
		System.out.println("睡醒了");
	}
}

如何让我们自己创建的线程睡眠呢?

一个类/接口中的方法没有抛异常,那么他们的子类或者实现类重写的方法,就不能抛异常,必须要自己 try…catch

图13
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void main(String[] args) throws InterruptedException {
    /*System.out.println("睡觉前");
    Thread.sleep(3000);
    System.out.println("睡醒了");*/

    MyRunnable mr = new MyRunnable();

    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);

    t1.start();
    t2.start();
}

Thread方法-线程的优先级

线程调度

多线程的并发运行:

  • 计算机中的 CPU,在任意时刻只能执行一条机器指令。每个线程只有获得 CPU 的使用权才能执行代码。
  • 各个线程轮流获得 CPU 的使用权,分别执行各自的任务。

线程有两种调度模型

  • 分时调度模型:所有线程 轮流 使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机 选择 一个,优先级高的线程获取的 CPU 时间片 相对多 一些

Java 使用的是抢占式调度模型

图14

第一种方式是轮流打,每个人都打一下

第二种方式是随机


线程的优先级

  • Public final void setPriority(int newPriority) 设置线程的优先级
  • Public final int getPriority() 获取线程的优先级
 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 MyCallable implements Callable<string> {
	@Override
	public String call() throws Exception {
		for(int i = 0; i < 100; i++){
			System.out.println(Thread.currentThread().getName() + "---" + i);
		}
		return "线程执行完毕了";
	}
}

public static void main(String[] args){
	MyCallable mc = new MyCallable();
	FutureTask<String> ft = new FutureTask<>(mc);
	
	Thread t1 = new Thread(ft);
	t1.setName("飞机");
	System.out.println(t1.getPriority());  //5
	//t1.start();
	
	MyCallable mc2 = new Mycallable();
	FutureTask<String> ft2 = new FutureTask<>(mc2);
	
	Thread t2 = new Thread(ft2);
	t2.setName("坦克");
	System.out.println(t2.getPriority());  //5
	//t2.start();
}

优先级的范围

图15
1
2
3
4
5
6
7
8
/**...*/
public static final int MIN_PRIORITY = 1; 	// 最小优先级

/**...*/
public static final int NORM_PRIORITY = 5; 	// 默认优先级

/**...*/
public static final int MAX_PRIORITY = 10; 	// 最大优先级

优先级更高,只能代表抢到 cpu 的几率更大,并不代表它一定先执行完

Thread方法-守护线程

后台线程/守护线程

public final void setDaemon(Boolean on):设置当前线程为守护线程 (可以称之为备胎线程,就是为了守护普通线程而存在的,当普通线程执行完毕,守护线程也就没有执行下去的必要了,会停止运行)

场景:QQ 的聊天框界面是一个线程,聊天界面传输文件是一个线程,当我把 QQ 退出时,上面的聊天框和传输文件就不存在了


  1. 下面是两个普通的线程,两个线程交替执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread1 extends Thread {
	@Override
	public void run(){
		for(int i = 0; i < 100; i++){
		System.out.println(getName() + "---" + i);
	}
}

public class MyThread2 extends Thread {
	@Override
	public void run(){
		for(int i = 0; i < 100; i++){
		System.out.println(getName() + "---" + i);
	}
}

public static void main(String[] args) {
	MyThread1 t1 = new MyThread1();
	MyThread2 t2 = new MyThread2();
	t1.setName("女神");
	t2.setName("备胎");
	t1.start();
	t2.start();
}
  1. 设置守护线程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Demo {
	public static void main(String[] args){
		MyThread1 t1 = new MyThread1();
		MyThread2 t2 = new MyThread2();
		
		t1.setName("女神");
		t2.setName("备胎");

		//把第二个线程设置为守护线程
		//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了
		t2.setDaemon(true);
		
		t1.start();
		t2.start();
	}
}

此时,”女神” 线程执行完毕后,“备胎” 线程就没有存在的必要了,当然,守护线程不会立即关闭,还会执行一会

这是因为在普通线程结束后,守护线程在内存中仍占有 cpu,而 cpu 运行速度快,所以还需要再运行一会,再关闭

如果守护线程先打印完,也不能结束,得等着普通线程结束,才能结束

cpu 进行切换的时候,如果切换到守护线程的时候,守护线程已经把活干完了,那 cpu 就在这休息一下,再切换到别的进程


守护线程的意义在于:其他线程停止的时候,该线程自动停止

垃圾回收器GC->线程(守护线程)

线程安全问题-卖票案例实现

案例:卖票

需求:某电影院目前正在上映国产大片,共有 100 张票,而它有 3 个窗口卖票,请设计一个程序模拟该电影院卖票

①定义一个类 Ticket 实现 Runnable 接口,里面定义一个成员变量:private int ticketCount = 100

②在 Ticket 类中重写 run() 方法实现卖票,代码步骤如下

  • A:判断票数大于 0,就卖票,并告知是哪个窗口卖的
  • B:票数要减 1
  • C:卖光之后,线程停止

③定义一个测试类 TicketDemo,里面有 main 方法,代码步骤如下

  • A:创建 Ticket 类的对象
  • B:创建三个 Thread 类的对象,把 Ticket 对像作为构造方法的参数,并给出对应的窗口名称
  • C:启动线程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Ticket implements Runnable {
	//票的数量
	private int ticket = 100;

	@Override
	public void run(){
		while(true){
			if(ticket == 0){
				//卖完了
				break;
			}else{
				ticket--;
				System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket);
			}
		}
	}
}

注意点

1
2
3
4
5
6
7
8
9
public static void main(String[] args){
	Ticket ticket1 = new Ticket();
	Ticket ticket2 = new Ticket();
	Ticket ticket3 = new Ticket();
	
	Thread t1 = new Thread(ticket1);
	Thread t2 = new Thread(ticket2);
	Thread t3 = new Thread(ticket3);
}

Ticket 对象作为参数,不能创建三个,如果每条线程都执行不同的参数,那么 ticket1 有 100 张票,ticket2 有 100 张票,ticket3 有 100 张票,那么就意味着每个线程都有各自的 100 张票,我们要求的是三个线程卖同一个 100 张票

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args){
	Ticket ticket = new Ticket();
	
	Thread t1 = new Thread(ticket);
	Thread t2 = new Thread(ticket);
	Thread t3 = new Thread(ticket);
	
	t1.setName("窗口一");
	t2.setName("窗口二");
	t3.setName("窗口三");
	
	t1.start();
	t2.start();
	t3.start();
}
图16

为什么 ”还剩99张票” 这个输出语句会出现在这里呢?

多线程在执行每一行代码的时候,cpu 的执行权都有可能被别人抢走,窗口一第一次抢到了执行权,但是在打印的时候 (还没有打印,也就是打印之前),cpu 的执行权被窗口二和窗口三抢走了,所以出现窗口二先打印,窗口一抢到了再打印,但是在这种情况下,打印的顺序和数据的安全是没有任何关系的 (在现实中买票,只要不是买两张重复的,在 1~100 之间就可以,具体先买第 1 张,还是先买第 55 张都是不要紧的)

线程安全问题-原因分析

卖票案例的思考

刚才讲解了电影院卖票程序,好像没有什么问题。但是在实际生活中,售票时出票也是需要时间的,所以,在出售一张票的时候,需要一点时间的延迟,接下来我们去修改卖票程序中卖票的动作:每次出票时间 100 毫秒,用 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
public class Ticket implements Runnable {
	//票的数量
	private int ticket = 100;

	@Override
	public void run(){
		while(true){
			if(ticket == 0){
				//卖完了
				break;
			}else{
				try {
					Thread.sleep(100);
				} catch (InterruptedException e){
					e.printstackTrace();
				}
				ticket--;
				System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket);
			}
		}
	}
}

public static void main(String[] args){
	Ticket ticket = new Ticket();
	
	Thread t1 = new Thread(ticket);
	Thread t2 = new Thread(ticket);
	Thread t3 = new Thread(ticket);
	
	t1.setName("窗口一");
	t2.setName("窗口二");
	t3.setName("窗口三");
	
	t1.start();
	t2.start();
	t3.start();
}

卖票出现了问题

  • 相同的票出现了多次 (相当于同一张票卖给了多个人)
  • 出现了负数的票
图17

原因分析

相同的票出现了多次

假设有绿、红、蓝三个线程抢夺 cpu 的执行权

图18

当绿色线程执行到 sleep(),睡眠,此时红色线程抢到 cpu 的执行权

图19

红色线程走到 sleep(),睡眠,蓝色线程抢到 cpu 的执行权,走到 sleep() 睡眠

此时,三个线程都在睡眠

图20

经过 100ms 后,假设绿色线程第一个抢到 cpu 的执行权,经过 ticket–,变为 99,在将要执行打印还未打印的时候, cpu 的执行权又被别人抢走 (多线程在执行每行代码的时候都有可能被别人抢走 cpu 的执行权)

图21

蓝色线程抢到 cpu 的执行权,ticket–,ticket=98,因为这三个线程使用的是同一个变量,所以绿色线程的值也是 98, 在蓝色线程要执行打印的时候,cpu 的执行权被红色线程抢走

图22

同理, 绿色、蓝色、红色线程的值都是 97,此时,无论谁抢到打印语句的执行权,打印的都是 97

图23

出现负数票的情况

图24

绿色线程执行完,变成死亡状态

图25 图26 图27

注意点

在 Java 中,多线程在执行每行代码时都有可能被其他线程抢占 CPU 处理器的执行权,这也是多线程并发执行的一个特点。对于 for 循环而言,它也是 Java 程序执行的一个语句块,每次执行一行代码,然后根据循环条件判断是否继续下一次循环,这个整个过程仍然是存在并发竞争的。

在实际应用中,使用多线程并发执行 for 循环可能会出现一些问题,比如多线程竞争同一个资源时,如果没有进行合适的同步控制,可能会出现数据不一致等问题。因此,Java 提供了一些同步机制,如 synchronized、Lock 等,来保证多线程间的同步和互斥,从而避免出现竞态条件。同时在 Java 8 中也提供了 Stream API,可以方便地进行多线程的并行流处理,避免一些常见的多线程问题。

线程安全问题-同步代码块