学习目标:
1. 学习目标:
- 什么是并发与并行
- 什么是进程与线程
- 线程创建
- 线程生命周期
- 线程安全问题
- 什么是线程安全问题
- 线程安全问题解决方案
- 线程死锁
- 死锁必要条件
- 如何避免死锁
- 线程通讯
一、多线程基础
1、什么是并发与并行
并行
:指两个或多个事件在同一时刻发生(同时发生)。并发
:指两个或多个事件在同一个时间段内发生。
2、什么是进程、线程
进程
- 进程是正在运行的程序的实例。
- 进程是线程的容器,即一个进程中可以开启多个线程。
- 比如打开一个浏览器、打开一个word等操作,都会创建进程。
线程
- 线程是进程内部的一个独立执行单元;
- 一个进程可以同时并发运行多个线程;
- 比如进程可以理解为医院,线程是挂号、就诊、缴费、拿药等业务活动
注意: 多线程:多个线程并发执行。
3. 如何创建线程
java
中创建线程的四种方式:
- 1. 继承 `Thread` 类
- 2. 实现 `Runnable` 接口
- 3. 实现 `Callable` 接口(与`Future`结合使用)
- 4. `线程池`方式
注意:
- 在java中,每次程序运行至少启动2个线程。一个是`main线程`,一个是`垃圾收集线程`。
3.1 继承Thread
类
- 相关类和接口:
Thread
代码实现:
// 第一步:创建自定义线程类
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i<10; i++){
System.out.println("mythread线程正在执行:"+new Date().getTime());
}
}
}
// 第二步:创建测试类
public class ThreadCreateDemo {
public static void main(String[] args){
//1.创建自定义线程
MyThread thread = new MyThread();
thread.start();
//2.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
}
}
3.2 实现Runnable
接口
- 相关类和接口:
Runnable
代码实现:
// 第一步:创建自定义类实现Runnable接口
class MyRunable implements Runnable {
public void run() {
for (int i=0; i<10; i++){
System.out.println("MyRunnable线程正在执行:"+new Date().getTime());
}
}
}
// 第二步:创建测试类
public class ThreadCreateDemo {
public static void main(String[] args){
//1.创建自定义线程
Thread thread = new Thread(new MyRunable());
thread.start();
//2.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
}
}
3.3 实现Callable
接口
- 相关类和接口:
FutureTask
、Callable<T>
、Future
FutureTask
介绍:
-
Callable
需要使用FutureTask
类帮助执行,FutureTask
类结构如下:
-
Future
接口:
- 判断任务是否完成:
isDone()
- 能够中断任务:
cancel()
- 能够获取任务执行结果:
get()
代码实现:
// 第一步:创建自定义类实现Callable接口
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
System.out.println("MyCallable正在执行:" + new Date().getTime());
}
return "MyCallable执行完毕!";
}
}
// 第二步:创建测试类
public class CallableDemo01 {
public static void main(String[] args) {
//1.使用Executors创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池
//2.通过线程池执行线程
for (int i = 0; i < 10; i++) {
executorService.execute(new MyRunnable());
}
//3.主线程循环打印
for (int i = 0; i < 10; i++) {
System.out.println("main主线程正在执行:" + new Date().getTime());
}
// 4. 关闭线程池
executorService.shutdown();
}
}
案例说明
:(Future
接口)
- 1. `isDone()`:可以判断是否该线程是否正在执行
- 2. `cancel( boolean )`:可以取消正在执行的线程。
- boolean = 'true'时 -- 线程`不再执行`,`get()`方法不可用。
- boolean = 'false'时 - 线程`继续执行`,`get()`方法不可用
- 3. `get()`:当线程正常执行结束后由返回值。
- 但如果线程被取消,则不能获取,并出现异常:`java.util.concurrent.CancellationException`
3.4 线程池 - Executor
-
线程池线类关系图
-
Executor
接口:声明了execute(Runnable runnable)方法,执行任务代码 -
ExecutorService
接口:继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等 -
AbstractExecutorService
抽象类: 实现ExecutorService接口,基本实现ExecutorService中声明的所有方法 -
ScheduledExecutorService
接口:继承ExecutorService接口,声明定时执行任务方法 -
ThreadPoolExecutor
类: 继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法 -
ScheduledThreadPoolExecutor
类:继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法 -
Executors
类:提供快速创建线程池的方法
代码实现:
// 第一步:创建自定义类实现Runnable接口
class MyRunable implements Runnable {
public void run() {
for (int i=0; i<10; i++){
System.out.println("MyRunnable线程正在执行:"+new Date().getTime());
}
}
}
// 第二步:创建测试类
public class ThreadCreateDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.使用Executors创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池
//2.通过线程池执行线程
executorService.execute(new MyRunable());
//3.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
executorService.shutdown();
}
}
3.5 小结:
实现接口
和继承Thread类
比较:
- 1. 接口更适合多个相同的程序代码的线程去共享同一个资源。
- 2. 接口可以避免java中的`单继承的局限性`。
- 3. 接口代码可以被多个线程共享,代码和线程独立。
- 4. 线程池只能放入实现`Runable`或`Callable`接口的线程,不能直接放入继承Thread的类。
推荐实现接口。
Runnable
和Callable
接口比较:
相同点:
- 1. 两者都是接口;
- 2. 两者都可用来编写多线程程序;
- 3. 两者都需要调用Thread.start()启动线程;
不同点:
- 1. 实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果;
- 2. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常;
- 3. 实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能
注意点
- Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,
此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
二. 线程生命周期
三、多线程安全问题
1. 什么是线程安全?
- 如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。
问题演示:
public class TicketDemo {
public static void main(String[] args){
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "窗口1");
Thread thread2 = new Thread(ticket, "窗口2");
Thread thread3 = new Thread(ticket, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
private static class Ticket implements Runnable {
private int ticktNum = 100;
public void run() {
while(true){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
} else {
break;
}
}
}
}
}
-
运行结果:
出现票数为负数的情况
、卖同一张票的情况
-
产生负数的原因
- 当`线程A`,在判断'ticktNum > 0'后进入后续代码,并获取得到票数'ticktNum'的数量为'1'。
此时`线程B`也通过了'ticktNum > 0'判断条件,也进入了后续的代码块。此时,`线程A`进行卖票操作之后,
`线程B`再获取票数'ticktNum'的数量为'0',也进行了卖票操作。因此出现了票数为'-1'的现象。
卖相同票的原因
:
- 当`线程A`进入程序后,获取了票数'ticktNum'的数量为'10';
与此同时`线程B`也进入了程序,也获取了票数'ticktNum'的数量为'10';
然后`线程A`再进行卖票操作,之后`线程B`也进行卖票操作。因此卖出了同一张票
总结:
- 多个线程在操作共享的数据;
- 操作共享数据的线程代码有多条;
- 多个线程对共享数据有写操作;
2. 线程同步
-
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
-
Java引入了7种线程同步机制:
- 1) `同步代码块`(synchronized)
- 2) `同步方法`(synchronized)
- 3) `同步锁`(ReenreantLock)
- 4) 特殊域变量(volatile)
- 5) 局部变量(ThreadLocal)
- 6) 阻塞队列(LinkedBlockingQueue)
- 7) 原子变量(Atomic*)
2.1 同步代码块:
public void run() {
while(true){
synchronized (obj){ // 对obj对象进行加锁操作,其他对象不可操作。
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
}
2.2 同步方法:
public synchronized void run() {
...
}
2.3 同步锁(ReenreantLock)
- 同步锁:
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
ReenreantLock
public void lock(); //加同步锁。
public void unlock(); //释放同步锁。
具体代码:
public void run() {
lock.lock(); //加锁
// 需要同步的代码块...
lock.unlock(); //放锁
}
2.4 小结
Synchronized
和Lock
区别:
- synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),
Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,
线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
四、线程死锁
1. 什么是死锁?
- 多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——
死锁
。 死锁
:指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
2. 死锁产生的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
- 互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。
2.2 死锁示例代码:
class DeadLock implements Runnable {
private static Object obj1 = new Object();//定义成静态变量,使线程可以共享实例
private static Object obj2 = new Object();//定义成静态变量,使线程可以共享实例
public int flag = 0;
public void run() {
if(flag == 0){
System.out.println("flag:"+flag);
synchronized (obj1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("flag:"+flag);
}
}
}
if(flag == 1){
System.out.println("flag:"+flag);
synchronized (obj2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("flag:"+flag);
}
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args){
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock2.flag = 1;
Thread thread1 = new Thread(deadLock1);
Thread thread2 = new Thread(deadLock2);
thread1.start();
thread2.start();
}
}
3. 死锁处理
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
- 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
- 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
银行家算法
五、多线程通讯问题
网址:https://blog.csdn.net/First_Bal/article/details/107571935