多线程
多线程
Smith1.实现多线程
1.1了解
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
1.2并发和并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
1.3进程和线程
进程:是正在运行的程序
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行线程:是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
Java提供了几种创建线程的方式
1.4实现多线程方式一:继承Thread类
方法介绍
方法名 说明 void run() 在线程开启后,此方法将被调用执行 void start() 使此线程开始执行,Java虚拟机会调用run方法() 实现步骤
- 定义一个类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/*
线程创建的方式1 继承Thread 类
*/
public class MyThread1 extends Thread{
// 重写 run 方法
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("自定义的线程任务执行了,...."+i);
}
}
}
public class TestMyThread1 {
public static void main(String[] args) {
// 1: 创建线程对象
MyThread1 thread1 = new MyThread1();
// 2: 启动线程 永远是调用 线程对象的 start方法,不要调用run
thread1.start(); // 一旦这个线程开启后,无论线程的任务是否执行完了,main方法都无需等待,可以直接执行下一行代码了
// thread1.run();// 面向对象,调用run方法,不会开启新线程,这个run没有执行完之前,main不能执行下面的代码
// 让main循环输出10次
for (int i = 0; i < 10; i++) {
System.out.println("main....."+i);
}
}
}我们会发现MyThread和main线程在相互抢夺CPU的执行权
(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样)
两个小问题
为什么要重写run()方法?
因为run()是用来封装被线程执行的代码
run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
1.5实现多线程方式二:实现Runnable接口
Thread构造方法
方法名 说明 Thread(Runnable target) 分配一个新的Thread对象 Thread(Runnable target, String name) 分配一个新的Thread对象 实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
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// 这个类本身不算线程类,但是可以被线程对象识别并调用run方法,因此,可以算是线程的 任务类
public class MyRun implements Runnable{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("自定义的runnable接口的实现类的run方法...."+i);
}
}
}
public class Test {
public static void main(String[] args) {
// 1: 创建任务对象
MyRun run = new MyRun();
//run.start();// 由于此时的 run 对象不是 Thread 类型,没有调用 start方法的资格
// 2: 创建线程对象,同时,把 run 对象作为构造方法的参数进行传递
Thread t = new Thread(run);
// 3: 开启线程
t.start(); // start会先调用 Thread的run,Thread的run会对target进行判断,如果不是null,会自动调用target的run
for (int i = 0; i < 10; i++) {
System.out.println("main..."+i);
}
}
}
1 | // 这个类设计一个成员变量,用于接收 调用者创建任务对象的时候,传递的"参数" |
1.6实现多线程方式三: 实现Callable接口【应用】
- 假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。
1 | public void run(){ |
- JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的
1 | public T call(){ |
方法介绍
方法名 说明 V call() 计算结果,如果无法计算结果,则抛出一个异常 | V get() | 如有必要,等待计算完成,然后获取其结果 |
实现步骤
定义一个类MyCallable实现Callable接口
在MyCallable类中重写call()方法
创建MyCallable类的对象
创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
创建Thread类的对象,把FutureTask对象作为构造方法的参数
启动线程
再调用get方法,就可以获取线程结束之后的结果。
1 | public class MyCall implements Callable<Integer> { |
为什么需要 FutureTask?
Thread 构造器只能接受 Runnable,不能直接接受 Callable。而 FutureTask 实现了 Runnable 接口,所以可以 “包装” Callable,让它能被 Thread 执行。FutureTask 只能执行一次
task.get() 的阻塞特性(核心重点):
调用 get() 时,如果子线程还没执行完 call() 方法,主线程(main)会被阻塞(暂停),直到子线程执行完毕并返回结果。
get() 的异常:get() 会抛出 InterruptedException(线程被中断)和 ExecutionException(call() 内部抛出异常),实际开发中需要 try-catch 处理。
执行顺序:
只有当 task 和 task2 两个任务都执行完后,才会打印最后两行结果和 main 的最后一行代码。
- 三种实现方式的对比
- 实现Runnable(方法)、Callable接口(有返回值,有异常)
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
- 实现Runnable(方法)、Callable接口(有返回值,有异常)
1.7设置和获取线程名称【应用】
方法介绍
方法名 说明 void setName(String name) 将此线程的名称更改为等于参数name String getName() 返回此线程的名称 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
29
30
31
32
33public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
1.8线程休眠
相关方法
方法名 说明 static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数 代码演示
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
31public class MyRun implements Runnable{
public void run() {
// 1: 获取正在执行这个任务的线程对象
Thread thread = Thread.currentThread();
// 2: 输出线程名称
System.out.println(thread.getName()+"正在执行自定义的任务....");
// 3: 让线程休眠
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(thread.getName()+"执行自定义的任务结束了----------....");
}
}
public class Test06 {
public static void main(String[] args) {
// 1: 创建一份任务对象和3个线程对象,让3个线程,执行同一份任务;(每个线程都会执行一遍)
MyRun run = new MyRun();
Thread t1 = new Thread(run,"小强");
Thread t2 = new Thread(run,"旺财");
Thread t3 = new Thread(run,"来福");
t1.start();
t2.start();
t3.start();
}
}
1.9线程优先级
线程调度
两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型
随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。
所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
优先级相关方法
方法名 说明 final int getPriority() 返回此线程的优先级 final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-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
26public class Test06_2 {
public static void main(String[] args) {
// 1: 使用lambda的方式,创建 任务对象
Runnable r = ()->{
// 获取当前线程名称
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name+"......"+i);
}
};
// 2: 创建3个线程对象,分别设置不同的 优先级,观察运行效果
Thread t1 = new Thread(r,"最低");
Thread t2 = new Thread(r,"一般");
Thread t3 = new Thread(r,"最高");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
// 3: 启动线程
t1.start();
t2.start();
t3.start();
}
}
1.10守护线程
相关方法
方法名 说明 void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 代码演示
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
27public class Test06_3 {
public static void main(String[] args) {
// 1: 使用lambda的方式,创建 任务对象!!并直接创建3个线程
Thread t1 = new Thread(()->{
String name = Thread.currentThread().getName();
for (int i = 0; i < 50; i++) {
System.out.println(name+"....."+i);
}
},"主公");
Runnable r = ()->{
String name = Thread.currentThread().getName();
for (int i = 0; i < 500; i++) {
System.out.println(name+"....."+i);
}
};
Thread t2 = new Thread(r,"护卫1");
Thread t3 = new Thread(r,"护卫2");
t2.setDaemon(true);
t3.setDaemon(true);
// 3: 启动线程
t1.start();
t2.start();
t3.start();
}
}
2.线程同步
2.1卖票【应用】
案例需求
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
实现步骤
定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
在SellTicket类中重写run()方法实现卖票,代码步骤如下
判断票数大于0,就卖票,并告知是哪个窗口卖的
卖了票之后,总票数要减1
票卖没了,线程停止
定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
创建SellTicket类的对象
创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
启动线程
代码实现
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 Ticket implements Runnable{
private int hao = 100;
public void run() {
// 1: 获取线程的名称
String name = Thread.currentThread().getName();
// 2: 循环卖票,直到座位号为0停止
while (true){
if(hao > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(name+"卖出了: "+ hao +" 号票!");
hao--;
}else {
System.out.println(name+" 已停止售票!");
break;
}
}
}
}
public class Test07 {
public static void main(String[] args) {
// 1: 创建资源对象(任务对象) 只能创建1次对象!!!
Ticket t = new Ticket();
// 2: 创建3个窗口对象,分别卖票
Thread t1 = new Thread(t,"窗口 1 ");
Thread t2 = new Thread(t,"窗口 2 ");
Thread t3 = new Thread(t,"窗口 3 ");
t1.start();
t2.start();
t3.start();
}
}
2.2卖票案例的问题【理解】
卖票出现了问题
- 相同的票出现了多次
- 出现了负数的票
问题产生原因
线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
2.3解决数据安全问题-同步代码块
安全问题出现的条件
是多线程环境
有共享数据
有多条语句操作共享数据
如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
怎么实现呢?
把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
Java提供了同步代码块的方式来解决
同步代码块格式:
1
2
3synchronized(任意对象) {
多条语句操作共享数据的代码
}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/*
这是一个卖票的任务类,同时也是一个资源类,拥有一个成员变量,代表 座位号
*/
public class Ticket implements Runnable {
private int hao = 1000;
public void run() {
// 1: 获取线程的名称
String name = Thread.currentThread().getName();
System.out.println(name + "获取到的this的地址值是:" + this);
// 2: 循环卖票,直到座位号为0停止
while (true) {
synchronized (this) {
if (hao > 0) {
System.out.println(name + "卖出了: " + hao + " 号票!");
hao--;
} else {
System.out.println(name + " 已停止售票!");
break;
}
}
}
}
}
public class Test08 {
public static void main(String[] args) {
// 1: 创建资源对象(任务对象) 只能创建1次对象!!!
Ticket t = new Ticket();
System.out.println(t);
// 2: 创建3个窗口对象,分别卖票
Thread t1 = new Thread(t,"窗口 1 ");
Thread t2 = new Thread(t,"窗口 2 ");
Thread t3 = new Thread(t,"窗口 3 ");
t1.start();
t2.start();
t3.start();
}
}
2.4解决数据安全问题-同步方法
同步方法的格式
同步方法:就是把synchronized关键字加到方法上
1
2
3修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}同步方法的锁对象是什么呢?
this
静态同步方法
同步静态方法:就是把synchronized关键字加到静态方法上
1
2
3修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}同步静态方法的锁对象是什么呢?
类名.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/*
这是一个卖票的任务类,同时也是一个资源类,拥有一个成员变量,代表 座位号
*/
public class Ticket implements Runnable {
private int hao = 100;
public void run() {
// 1: 获取线程的名称
String name = Thread.currentThread().getName();
System.out.println(name + "获取到的this的地址值是:" + this);
// 2: 循环卖票,直到座位号为0停止
while (true) {
boolean res = mp();
if(!res){
break;
}
}
}
private synchronized boolean mp() {
String name = Thread.currentThread().getName();
if (hao > 0) {
System.out.println(name + "卖出了: " + hao + " 号票!");
hao--;
return true;
} else {
System.out.println(name + " 已停止售票!");
return false;
}
}
}
1 | public class Test09 { |
2.5Lock锁【应用】
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 加锁解锁方法
方法名 说明 void lock() 获得锁 void 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/*
这是一个卖票的任务类,同时也是一个资源类,拥有一个成员变量,代表 座位号
*/
public class Ticket implements Runnable {
private int hao = 100;
//final:非常重要!保证锁对象的引用不可变,确保多个线程使用的是同一把锁
//构造函数传入 true,表示创建一个公平锁(Fair Lock)
private final Lock lock = new ReentrantLock(true);
public void run() {
// 1: 获取线程的名称
String name = Thread.currentThread().getName();
System.out.println(name + "获取到的this的地址值是:" + this);
// 2: 循环卖票,直到座位号为0停止
while (true) {
lock.lock();
try {
if (hao > 0) {
System.out.println(name + "卖出了: " + hao + " 号票!");
hao--;
} else {
System.out.println(name + " 已停止售票!");
break;
}
} finally {
lock.unlock();
}
}
}
}
public class Test10 {
public static void main(String[] args) {
// 1: 创建资源对象(任务对象) 只能创建1次对象!!!
Ticket t = new Ticket();
System.out.println(t);
// 2: 创建3个窗口对象,分别卖票
Thread t1 = new Thread(t,"窗口 1 ");
Thread t2 = new Thread(t,"窗口 2 ");
Thread t3 = new Thread(t,"窗口 3 ");
t1.start();
t2.start();
t3.start();
}
}
| 特性 | synchronized |
Lock (ReentrantLock) |
|---|---|---|
| 实现方式 | JVM 层面关键字 | JDK 层面接口实现类 |
| 锁释放 | 自动释放(代码块执行完 / 异常) | 手动释放(必须 unlock()) |
| 公平性 | 只能非公平 | 可选择公平 / 非公平 |
| 灵活性 | 低(自动加锁解锁) | 高(可尝试获取锁、超时、中断等) |
2.6死锁【理解】
概述
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
根据图中所示:线程T1正在持有R1锁,但是T1线程必须要再获取到R2锁才能继续执行,而线程T2正在持有R2锁,但是T2线程必须再获取到R1锁后才能继续执行,两个线程就会处于无限的等待中,即死锁。在程序中的死锁将出现在同步代码块的嵌套中。
什么情况下会产生死锁
- 资源有限
- 同步嵌套
代码演示
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
28public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()->{
while(true){
synchronized (objA){
//线程一
synchronized (objB){
System.out.println("小康同学正在走路");
}
}
}
}).start();
new Thread(()->{
while(true){
synchronized (objB){
//线程二
synchronized (objA){
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}









