多线程学习笔记
多线程的简单学习,本节内容主要学习多目标:
- 线程与进程区别
- 线程的创建方式(常用的两种)
- 线程的状态(5种)
- 线程方法(sleep、yield、join、wait、notify、notifyAll)
- 线程优先级
- 线程同步
- 并发 (线程不安全)
- 同步(synchronized & lock)
- 死锁
- 线程协作(通信)
- 管程法
- 信号灯法
- 线程池
1、 线程VS 进程
在了解进程和线程之前,先来看一张图片:
程序:程序是指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念。
进程:进程则是执行程序的一次执行过程,譬如在电脑端运行的QQ、音乐、游戏等都是一个个的进程
- 进程依赖于程序的运行而存在,进程是动态的,程序是静态的;
- 进程是操作系统进行资源分配和调度的一个独立单位(CPU除外,线程是处理器任务调度和执行的基本单位);
- 每个进程拥有独立的地址空间,地址空间包括代码区、数据区和堆栈区,进程之间的地址空间是隔离的,互不影响
线程: 一个进程中可以包含一个或者多个线程,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),同一个进程中的多个线程共享该进程所拥有的全部资源。
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
进程与线程区别:
- 包含关系: 一个进程中包含一个或者多个线程,同一进程中的多个线程共享进程的内存资源;
- 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 影响性:进程之间独立运行,一个进程的结束不会影响其他进程的正常运行;一个进程中的多线程情况,如果其中一个线程崩掉可能会导致这个进程被操作系统终止;
- 资源应用:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
2、多线程
2.1 概念
单线程: 在单线程的Java
程序中, 从main函数开始执行,一旦程序中出现循环处理、等待IO输入等操作时,程序需要等待操作结束之后继续运行,在这个等待的过程中CPU可能处于空闲的状态,从而利用率较低。
多线程: 顾名思义,多个线程同时运行,当一个线程因为某种原因处于阻塞状态时,CPU转而去执行其他线程,提高CPU的利用率,具体表述请看下图:
2.2 线程的创建方式
-
创建类A 继承Thread 类, 重写run方法
-
创建类B 实现Runnable 接口, 重写run方法
-
创建类C 实现Callable接口
- 创建类A 继承Thread 类, 重写run方法
package com.kevin.mutithread.demo1;
/**
* @Author Kevin
* @Date 16:37 2022/7/28
* @Description TODO
* 多线程的 实现方法1:继承Thread类,重写run方法来创建线程
*/
public class ThreadDemo extends Thread{
public static void main(String[] args) {
// 创建线程类的对象
ThreadDemo thread = new ThreadDemo();
// 启动线程
thread.start();
// 对象方法的普通调用
// thread.run();
// main线程的执行体
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "在执行main方法.." + i);
}
}
// 重写run方法
@Override
public void run() {
// 线程体需要做的事情
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "正在进行遍历.." + i);
}
}
}
执行结果可以看出,main线程和创建出的thread线程在交替执行;
当将代码中的thread.start()方法改成thread.run()时,结果如下:
可以发现,线程的调用为:创建线程对象,调用对象的start()方法开启线程,使其处于就绪状态,start方法会自动去调用线程类的run方法去执行;如果通过对象直接调用run()方法,则为对象的普通调用,没有多线程的影子。
- 创建类B实现Runnable接口,实现run方法
package com.kevin.mutithread.demo1;
/**
* @Author Kevin
* @Date 18:09 2022/7/28
* @Description
多线程的实现方法2:
* 创建类继承Runnable接口,实现run方法
* 创建实现类对象,将其作为参数传递给new Thread(实现类对象)
*/
public class RunnableDemo implements Runnable{
public static void main(String[] args) {
// 1. 创建线程对象
RunnableDemo runnableDemo = new RunnableDemo();
// 2. 启动线程
Thread thread = new Thread(runnableDemo);
thread.start();
// 3. main线程的执行体
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "执行main线程体..." + i);
}
}
@Override
public void run() {
// 线程体
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "执行Runnable的实现方法.." + i);
}
}
}
执行结果:
继承Thread类的方法:
- 子类继承Thread类具备多线程能力
启动线程:子类对象.start()
不建议使用,,避免OOP单继承局限性
实现Runnable接口:
- 实现接口Runnable具有多线程能力
启动线程:传入目标对象+Thread对象.start()
推荐使用该方法,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
- 实现callable接口
package com.kevin.mutithread.demo1;
import java.util.concurrent.*;
/**
* @Author Kevin
* @Date 20:19 2022/7/28
* @Description
* 第三种创建方式:实现callable接口
*/
public class CallableDemo implements Callable<Boolean> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建3个对象
CallableDemo demo1 = new CallableDemo();
CallableDemo demo2 = new CallableDemo();
CallableDemo demo3 = new CallableDemo();
// 创建执行服务 创建一个线程池,可容纳3个线程
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> res1 = executorService.submit(demo1);
Future<Boolean> res2 = executorService.submit(demo1);
Future<Boolean> res3 = executorService.submit(demo1);
// 获取结果
Boolean b1 = res1.get();
Boolean b2 = res2.get();
Boolean b3 = res3.get();
}
@Override
public Boolean call() throws Exception {
// 需要实现的线程体
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行..." + i);
}
return true;
}
}
执行结果:
Callable接口的实现,可以自定义返回值类型;该创建线程的方式,了解即可。
3、线程状态
线程从创建到销毁也是有“生命周期” 的,线程的生命周期包含5个状态
-
创建状态 :通过Thread t = new Thread()语句来创建线程,线程对象一旦创建就进入到了新生状态;
-
就绪状态: 创建好的线程,调用start()方法之后,进入就绪状态,但不意味着立即调度执行,要看CPU的心情;
-
运行状态: CPU调度执行该线程进入运行状态,此时才真正执行线程体的代码块;
-
阻塞状态: 当调用sleep, wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待CPU重新调度执行;
-
死亡状态:线程中断或者结束被销毁,一旦进入死亡状态就不能再次启动。
Thread类中常用的方法
方法 | 说明 |
---|---|
setPriority(int newPriority) |
更改线程的优先级 |
static void sleep(long millis) |
在指定的毫秒数内让当前正在执行的线程体休眠 |
void join() |
等待该线程终止 |
static void yield() |
暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() |
中断线程(不建议使用) |
boolean isAlive() |
测试线程是否处于活动状态 |
3.1 线程停止 (stop)
- 不推荐使用
JDK
提供的stop() 、destroy()方法。(已废弃) - 推荐线程自己停止下来(使用一个标志位进行终止变量,当flag=false,则终止线程运行)
demo演示:
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 21:38 2022/7/28
* @Description
* 演示线程的停止方式,通过标志位进行停止
*/
public class StopDemo implements Runnable {
// 定义线程停止的标志位
static boolean flag = true;
public static void main(String[] args) {
StopDemo stopDemo = new StopDemo();
Thread thread = new Thread(stopDemo, "子线程");
thread.start();
// 主线程main,循环执行,当i执行到20的时候,让子线程终止
for (int i = 0; i < 50; i++) {
if(i == 20){
flag = false;
System.out.println("子线程该停止了...");
}
System.out.println(Thread.currentThread().getName()+ "一直在执行..." + i);
}
}
@Override
public void run() {
int i = 0;
// 线程体执行内容放入由标志位控制的循环体中,
// 当标志位不满足条件时,直接退出,结束线程
while(flag){
System.out.println(Thread.currentThread().getName() + "一直在执行.." + i++);
}
}
}
3.2 线程休眠(sleep)
- sleep (时间)指定当前线程阻塞的毫秒数;
- sleep存在异常
InterruptedException
; - sleep时间达到后线程进入就绪状态;
- 每一个对象都有一个锁,sleep不会释放锁。
sleep休眠模拟倒计时:
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 21:48 2022/7/28
* @Description
* 线程休眠模拟倒计时
*/
public class SleepDemo implements Runnable{
public static void main(String[] args) {
// 创建线程对象
SleepDemo sleepDemo = new SleepDemo();
Thread thread = new Thread(sleepDemo);
thread.start();
}
@Override
public void run() {
// 从10开始倒计时
System.out.println("倒计时开始....");
for (int i = 10; i >=0 ; i--) {
try {
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新年快乐!!!");
}
}
3.3 线程的礼让(yield)
Demo演示:
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 21:55 2022/7/28
* @Description
* 线程的礼让: 创建两个线程,当执行到礼让时,正在执行的线程处于就绪状态,就绪线程开始抢夺cpu资源
*/
public class YieldDemo implements Runnable {
public static void main(String[] args) {
YieldDemo yieldDemo = new YieldDemo();
// 礼让的前提是有多个线程
Thread thread1 = new Thread(yieldDemo, "线程--1--");
Thread thread2 = new Thread(yieldDemo, "线程--2--");
// 线程启动
thread1.start();
thread2.start();
}
@Override
public void run() {
// sleep方法可以放大并发的可能性
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行...");
Thread.yield(); // 线程礼让
System.out.println(Thread.currentThread().getName() + "线程礼让...");
}
}
从执行结果来看:当线程1执行到yield时,让线程2开始执行,线程2执行到yield时,进行礼让,随后线程1抢到了CPU的执行权限,执行结束。。
3.4 线程的强制执行(join)
- join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以想象成插队,线程加塞去执行
Demo演示
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 22:06 2022/7/28
* @Description
* 线程的join 加塞执行,CPU 停止当前正在执行的线程,转而去先执行加塞的线程
*/
public class JoinDemo implements Runnable {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象
JoinDemo joinDemo = new JoinDemo();
Thread thread = new Thread(joinDemo);
// main线程限制性,当i为20的时候启动加塞线程
// 必须先启动线程,再调用线程的join方法
for (int i = 0; i < 50; i++) {
if (i == 20){
thread.start();
thread.join();
}
System.out.println(Thread.currentThread().getName() + "正在执行.." + i);
}
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("加塞的线程来执行了.." + i);
}
}
}
可以看出,当main线程执行到i==20
的时候,子线程开始加塞执行,执行结束之后,main线程继续执行
此代码的演示有点类似于单线程的执行,在执行加塞的子线程时,main线程处于等待的状态。
3.5 线程状态观测(state)
线程5种状态对应的 标识可以通过Thread.State来判断:
- NEW: 尚未启动的线程处于此状态
- RUNNABLE: 在Java虚拟机中执行的线程处于此状态
- BLOCKED: 被阻塞等待监视器锁定的线程处于此状态
- WAITING: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TIMED_WAITING: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED: 已退出的线程处于此状态
demo演示:
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 9:18 2022/7/31
* @Description
* 观察线程的状态
*/
public class StateDemo {
public static void main(String[] args) {
// lambda表达式方式创建Thread
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
// 线程sleep休眠
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("============");
});
// 先观察线程thread目前的状态
Thread.State state = thread.getState();
System.out.println("线程创建之后未启动,此时的状态是:" + state);
// 启动线程,再观察状态
thread.start();
state = thread.getState();
System.out.println("线程已经启动了,此时的状态是:" + state);
int i= 0;
while (state != Thread.State.TERMINATED) {
//只要线程不终止就一直输出状态
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新线程状态
state = thread.getState();
System.out.println("此时线程的状态是:" + state + i++);
}
}
}
执行结果:
3.6 线程优先级 (priority)
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调用哪个线程来执行。
-
线程的优先级用数字表示,范围从1~10
Thread.MIN_PRIORITY =1; // 最小优先级 Thread.MAX_PRIORITY =10; // 最大优先级 Thread.NORM_PRIORITY = 5; // 默认优先级(正常)
-
使用以下方式改变或获取优先级
thread.getPriority() // 获取优先级 thread.setPriority(int num) // 设置优先级
demo演示:
package com.kevin.mutithread.demo2;
/**
* @Author Kevin
* @Date 9:34 2022/7/31
* @Description
* 线程的优先级
*/
public class PriorityDemo {
public static void main(String[] args) {
//主线程为默认优先级 5
System.out.println(Thread.currentThread().getName() + "的优先级是-->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
//先设置优先级再启动,二者的顺序不能颠倒,
// 是对每个线程设置各自的优先级,线程必须先设置优先级之后再启动
t1.start();//默认优先级
t2.setPriority(1);
t2.start();
t3.setPriority(3);
t3.start();
//最高优先级 10
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
t5.setPriority(8);
t5.start();
}
}
class MyPriority implements Runnable {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的优先级是--->" + Thread.currentThread().getPriority());
}
}
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用,这都是看CPU的心情及采用的调度算法
4、线程同步
4.1 并发
并发实际上就是多个线程同时操作于同一个对象作,这里强调三个点:
- 多个线程(如果这个银行卡只有你一个人用显然也不会发生问题)
- 同时操作(大家都在操作这个卡的时候,就会发生并发问题)。
- 同一个对象(如果每个人操作自己的银行卡显然不会发生并发)
- 并发三要素:多个线程、同时操作、同一个对象。
多个线程同时操作于同一个对象,比如上万人同时抢100张票。一旦发生并发就有可能会产生数据不准确的问题,这又被称为线程不安全。
4.2 并发不安全案例
- 不安全案例1: 抢票(多个线程同时进行抢票)
package com.kevin.mutithread.demo3;
/**
* @Author Kevin
* @Date 9:53 2022/7/31
* @Description
* 多个线程同时进行抢票
*/
public class BuyTicketDemo{
public static void main(String[] args) {
// 创建线程对象
BuyTicket buyTicket = new BuyTicket();
// 创建三个线程并启动
new Thread(buyTicket, "学生").start();
new Thread(buyTicket, "军人").start();
new Thread(buyTicket, "黄牛党").start();
}
// 创建线程类,实现runnable接口,重写 run方法
static class BuyTicket implements Runnable{
// 定义票源
int ticketCount = 10;
// 定义线程停止标志位, 默认为true
boolean flag = true;
@Override
public void run() {
// 多线程的执行体 抢票,循环一直抢票
while (flag){
buyTicket();
}
}
private void buyTicket() {
//判断是否有票可买
if(ticketCount <= 0){
System.out.println("票已经售罄,手速慢了...");
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 否则,直接进行抢票,票源减少
System.out.println(Thread.currentThread().getName() + "抢到了票-->" + ticketCount--);
}
}
}
抢票结果出现了重复的票或者负数时,均表示不安全:
-
不安全案例2:账户取钱
两个人同时操作一个账户 进行取钱
package com.kevin.mutithread.demo3; /** * @Author Kevin * @Date 10:09 2022/7/31 * @Description * 账户取钱问题,多个人同时操作同一个账户进行取钱 */ public class DrawingMoneyDemo { public static void main(String[] args) { Account account = new Account("老婆本儿", 60); Drawing myself = new Drawing(account, 30, "myself"); Drawing girlfriend = new Drawing(account, 40, "girlfriend"); myself.start(); girlfriend.start(); } // 创建取款线程类 static class Drawing extends Thread{ // 取哪个账户的钱 private Account account; // 取多少钱 private int drawingMoney; // 现在手里多少钱 private int moneyInHand; // 构造方法 public Drawing(Account account, int drawingMoney, String threadName){ // 给创建的thread重命名 super(threadName); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { // 线程执行体 取钱 // 卡内的钱还够取不 if(account.leftMoney < this.drawingMoney){ System.out.println("卡内余额为:" + account.leftMoney +"\n" + Thread.currentThread().getName() + "需要取钱:" + this.drawingMoney +"余额不足,无法取钱..."); return; } // 模拟延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 取钱, 卡内余额减少 account.leftMoney -= this.drawingMoney; // 手中前多了 this.moneyInHand += this.drawingMoney; //这里this.getName()等价于Thread.currentThread().getName() System.out.println(this.getName() + "取钱成功,手里的钱为:" + this.moneyInHand); System.out.println(account.accountName + "余额为:" + account.leftMoney); } } // 创建账户类 static class Account{ // 账户名字 String accountName; // 账户余额 double leftMoney; // 构造方法 public Account(String accountName, double leftMoney) { this.accountName = accountName; this.leftMoney = leftMoney; } } }
两个线程同时操作同一账户取钱,账户余额出现了负数,线程不安全:
4.3 线程同步
上面两种情况都是因为多线程同时操作同一个对象(多个线程之间没有进行信息同步),导致出现了线程不安全的状况,处理这一问题就需要使用线程同步;线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。保证线程安全可以通过“队列”和“锁”来完成。
线程同步实现的两个条件:
- 等待池形成队列
- 资源上锁。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
凡事有利就有弊,锁机制存在的问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
类中的成员变量可以通过private关键字进行访问权限的限制,并提供get和set方法进行访问,因此可以针对修改的方法进行保护,即synchronized方法和synchronized代码块。
- 同步方法: public synchronized void method(int args){ }
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized 将会影响效率
同步方法弊端
方法里面需要修改的内容才需要锁,锁的太多,浪费资源
- 同步代码块synchronized (Obj ){ }
Obj称之为同步监视器Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码;
- 第二个线程访问,发现同步监视器被锁定,无法访问;
- 第一个线程访问完毕,解锁同步监视器;
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
在买票案例中,进行的修改:
package com.kevin.mutithread.demo3;
/**
* @Author Kevin
* @Date 9:53 2022/7/31
* @Description
* 多个线程同时进行抢票
*/
public class BuyTicketDemo{
public static void main(String[] args) {
// 创建线程对象
BuyTicket buyTicket = new BuyTicket();
// 创建三个线程并启动
new Thread(buyTicket, "学生").start();
new Thread(buyTicket, "军人").start();
new Thread(buyTicket, "黄牛党").start();
}
// 创建线程类,实现runnable接口,重写 run方法
static class BuyTicket implements Runnable{
// 定义票源
int ticketCount = 10;
// 定义线程停止标志位, 默认为true
boolean flag = true;
@Override
public void run() {
// 多线程的执行体 抢票,循环一直抢票
while (flag){
buyTicket();
}
}
// 买票中同步分方法是buyTicket,该方法在一直对共享变量ticketCount进行操作。
// 对该方法使用synchronized
private synchronized void buyTicket() {
//判断是否有票可买
if(ticketCount <= 0){
System.out.println("票已经售罄, " +Thread.currentThread().getName() + "手速慢了...");
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 否则,直接进行抢票,票源减少
System.out.println(Thread.currentThread().getName() + "抢到了票-->" + ticketCount--);
}
}
}
加上synchronized关键字之后,执行结果正常
取钱案例的优化:取钱时,多个线程操作的同一个account账户,需要对account账户进行synchronized:
package com.kevin.mutithread.demo3;
/**
* @Author Kevin
* @Date 10:09 2022/7/31
* @Description
* 账户取钱问题,多个人同时操作同一个账户进行取钱
*/
public class DrawingMoneyDemo {
public static void main(String[] args) {
Account account = new Account("老婆本儿", 60);
Drawing myself = new Drawing(account, 30, "myself");
Drawing girlfriend = new Drawing(account, 40, "girlfriend");
myself.start();
girlfriend.start();
}
// 创建取款线程类
static class Drawing extends Thread{
// 取哪个账户的钱
private Account account;
// 取多少钱
private int drawingMoney;
// 现在手里多少钱
private int moneyInHand;
// 构造方法
public Drawing(Account account, int drawingMoney, String threadName){
// 给创建的thread重命名
super(threadName);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
// 需要加锁的对象就是被修改的变化的对象account
synchronized (account){
// 卡内的钱还够取不
if(account.leftMoney < this.drawingMoney){
System.out.println("卡内余额为:" + account.leftMoney +"\n" + Thread.currentThread().getName() + "需要取钱:" + this.drawingMoney +"余额不足,无法取钱...");
return;
}
// 模拟延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 取钱, 卡内余额减少
account.leftMoney -= this.drawingMoney;
// 手中前多了
this.moneyInHand += this.drawingMoney;
//这里this.getName()等价于Thread.currentThread().getName()
System.out.println(this.getName() + "取钱成功,手里的钱为:" + this.moneyInHand);
System.out.println(account.accountName + "余额为:" + account.leftMoney);
}
}
}
// 创建账户类
static class Account{
// 账户名字
String accountName;
// 账户余额
double leftMoney;
// 构造方法
public Account(String accountName, double leftMoney) {
this.accountName = accountName;
this.leftMoney = leftMoney;
}
}
}
执行结果:
4.4 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
简言之:线程A和B都各自持有一把锁,又同时需要对方的被锁的资源才能继续向下执行,可是AB线程谁都不愿意先释放锁,造成僵局。
demo演示:
package com.kevin.mutithread.demo3;
/**
* @Author Kevin
* @Date 10:55 2022/7/31
* @Description
* 死锁
*/
public class DeadLockDemo {
public static void main(String[] args) {
Makeup g1 = new Makeup(0, "灰姑娘");
Makeup g2 = new Makeup(1, "白雪公主");
g1.start();
g2.start();
}
//口红类
static class Lipstick {
}
//镜子
static class Mirror {
}
// 线程类 化妆
static class Makeup extends Thread {
//需要的资源只有一份,用static来保证
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//使用化妆品的人
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆方法,互相持有对方的锁,需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) {
//一秒钟后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) {
//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000);
synchronized (lipstick) {
//一秒钟后想获得镜子
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
}
两者都想要对方的锁资源,又不愿意释放自己手中的锁,造成死锁:
解决方法:在使用锁资源的时候,不能抱着两把锁:一个锁资源内不能包含另外一把锁
//化妆方法,互相持有对方的锁,需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror) {
//一秒钟后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
} else {
synchronized (mirror) {
//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000);
}
// 把另外一把锁资源方到外面
synchronized (lipstick) {
//一秒钟后想获得镜子
System.out.println(this.girlName + "获得口红的锁");
}
}
}
执行结果正常:
总结:
产生死锁的四个必要条件:
-
互斥条件:一个资源每次只能被一个进程使用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
以上四个必要条件,只要想办法破其中的任意一个或多个条件就可以避免死锁发生
4.5 锁(Lock)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
定义锁:private final ReentrantLock lock = new ReentrantLock();
加锁: lock.lock()
释放锁: lock.unlock()
买票demo演示:
package com.kevin.mutithread.demo3;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author Kevin
* @Date 11:10 2022/7/31
* @Description
* 买票案例演示lock
*/
public class LockDemo {
public static void main(String[] args) {
// 创建线程对象
BuyTicketDemo.BuyTicket buyTicket = new BuyTicketDemo.BuyTicket();
// 创建三个线程并启动
new Thread(buyTicket, "学生").start();
new Thread(buyTicket, "军人").start();
new Thread(buyTicket, "黄牛党").start();
}
// 创建线程类,实现runnable接口,重写 run方法
static class BuyTicket implements Runnable{
// 定义票源
int ticketCount = 10;
// 定义线程停止标志位, 默认为true
boolean flag = true;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
// 多线程的执行体 抢票,循环一直抢票
while (flag){
buyTicket();
}
}
// 使用lock来进行控制
private void buyTicket() {
try{
// 先上锁, 在try的语句块里面进行上锁,
lock.lock();
//判断是否有票可买
if(ticketCount <= 0){
System.out.println("票已经售罄, " +Thread.currentThread().getName() + "手速慢了...");
flag = false;
return;
}
//模拟网络延时
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 否则,直接进行抢票,票源减少
System.out.println(Thread.currentThread().getName() + "抢到了票-->" + ticketCount--);
}finally {
// 因为最后无论如何都要释放锁,将其放入finally中
lock.unlock();
}
}
}
}
结果:
4.6 synchronized VS lock
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
5、线程协作
5.1 线程通信
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
问题分析:
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费;
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费;
- 在生产者消费者问题中,仅有synchronized是不够的;synchronized可阻止并发更新同一个共享资源,实现了同步;synchronized不能用来实现不同线程之间的消息传递(通信)
线程之间通信常用方法:(均是Object方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常)
方法名 | 作用 |
---|---|
wait() |
表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) |
指定等待的毫秒数 |
notify() |
唤醒一个处于等待状态的线程 |
notifyAll() |
唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
5.1.1 管程法
- 生产者:只负责生产数据并发送给缓冲区的模块
- 消费者:只负责从缓冲区取出数据并处理的模块
- 缓冲区: 生产者和消费者之间的联系”中介“, 存放交换信息的缓冲区
demo演示:
package com.kevin.mutithread.demo4;
/**
* @Author Kevin
* @Date 17:48 2022/7/31
* @Description
* 管程法 Producer/consumer
*/
public class PCDemo {
public static void main(String[] args) {
// 创建缓冲区对象
SynContainer synContainer = new SynContainer();
// 创建线程类并启动
new Producer(synContainer).start();
new Consumer(synContainer).start();
}
// 生产者类,负责生产消息
static class Producer extends Thread{
SynContainer container;
// 构造器
public Producer(SynContainer container){
this.container = container;
}
// 重写run方法
@Override
public void run() {
// 生产者只负责生产方法并放入缓冲区
for (int i = 0; i < 100; i++) {
container.putMessage(new Product(i+1));
}
}
}
// 消费者类, 负责从缓冲区取数据
static class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
// 重写run方法
@Override
public void run() {
// 消费者只负责消费消息
for (int i = 0; i < 100; i++) {
container.getMessage();
}
}
}
// 生产的东西类
static class Product extends Thread{
// 生产数据的编号
int productId;
public Product(int productId){
this.productId = productId;
}
}
// 缓冲区类
static class SynContainer{
// 缓冲区大小的容器来存放生产出的products
Product[] productsList = new Product[5];
// 容器计数器
int count = 0;
// 生产者将生产的商品放入容器中
public synchronized void putMessage(Product product){
// 如果容器满了,放不进去,需要等待先消费
if(count == productsList.length){
// 需要先等待消费者消费掉消息
try {
this.wait(); // wait会释放对象的锁资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 否则 直接放进去
productsList[count++] = product;
System.out.println("生产者生产了第" + product.productId + "个产品...");
// 产品已经放入了缓冲区,通知消费者可以消费了
this.notifyAll();
}
// 消费者从缓冲区取数据消费
public synchronized Product getMessage(){
// 如果缓冲区没有数据,需要先等生产者放入数据
if(count == 0){
// 需要先等待数据放入
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 直接取出数据消费
if(count > 0){
count--; // 数据个数和索引之间相差1, 先减一再取出
Product product = productsList[count];
System.out.println("消费者从缓冲区取出了第" + product.productId + "个数据进行消费处理...");
// 处理完之后,通知生产者,可以放数据了
this.notifyAll();
return product;
}else{
// 如果缓冲区根本没有数据,就直接等待生产者放入
this.notifyAll();
throw new RuntimeException("缓冲区压根儿没有内容可以消费,生产者麻利儿生产...");
}
}
}
}
缓冲区大小设置为了5,在生产前5个消息时,可以成果放入到缓冲区中,随后便开始了消费和生产的协同进行
5.1.2 信号灯法
借助标志位,完成线程间的通信
- 生产者:负责生产数据的模块(这里的模块可能是:方法、对象、线程、进程)
- 消费者:负责处理数据的模块(这里的模块可能是:方法、对象、线程、进程)
- 缓冲对象:生产者消费者使用同一资源,他们之间有个标志位,类似于信号灯的作用,通过信号灯控制生产者和消费者的循环使用
信号灯法中,缓冲区只有一个位置,即可以理解为生产者生产了一个消息,消费者消费一个消息,二者交替进行
demo演示:
package com.kevin.mutithread.demo4;
/**
* @Author Kevin
* @Date 18:32 2022/7/31
* @Description
* 信号灯法: 生产者消费者交替进行,犹如交通信号灯一般,生产-消费交替进行
*/
public class PCDemo2 {
public static void main(String[] args) {
Message message = new Message();
new Producer(message).start();
new Consumer(message).start();
}
// 生产者负责生产消息
static class Producer extends Thread{
Message message;
public Producer(Message message){
this.message = message;
}
//重写run方法
@Override
public void run() {
// 生产者生产消息
for (int i = 0; i < 50; i++) {
this.message.producerMessage(i+1);
}
}
}
// 消费者负责消费消息
static class Consumer extends Thread{
Message message;
public Consumer(Message message){
this.message = message;
}
//重写run方法
@Override
public void run() {
// 消费者消费消息
for (int i = 0; i < 50; i++) {
this.message.consumerMessage();
}
}
}
// 消息处理
static class Message{
// message编号
int messageId;
// 标志位,生产者生产True, 消费者消费false
boolean flag = true;
// 生产
public synchronized void producerMessage(int messageId){
// 如果flag为false则表示 消费者正在消费,生产者需要等待
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产者生产
System.out.println("生产者生产了第" + messageId + "个消息...");
// 生产完之后便可通知消费者消费,标志位改变
this.notifyAll();
this.flag = !this.flag; // 更新标志位
this.messageId = messageId; // 更新消息编号
}
//消费
public synchronized void consumerMessage(){
// 如果flag标志位true 表示需要等待生产者生产
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费消息
System.out.println("消费者消费了第" + messageId + "个消息...");
// 同样通知生产者生产,标志位改变
this.notifyAll();
this.flag = !this.flag;
}
}
}
生产者和消费通过信号标志位,交替执行:
5.2 线程池
- 线程池应用背景
线程的频繁创建和销毁对CPU的资源也是一种消耗,如果能做到对线程的循环利用便能有效的提高资源利用率;想想一班公交车从始发地到终点站一趟便完成这次的使命,如果完成之后直接把该公交车销毁,重新生产新的公交车,岂不是资源巨大浪费嘛,只需要多辆公交车放到公交车站循环使用就好了,需要的时候发车,不需要的时候就停靠在公交车站。
线程的循环使用也是同样的道理。提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
- 好处:
提高响应速度(减少了创建新线程的时间);降低资源消耗(重复利用线程池中线程,不需要每次都创建);便于线程管理(…)
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池的使用
- JDK 5.0起提供了线程池相关APl: ExecutorService和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
线程池的demo演示:
创建一个容量为3的线程池,并创建10个线程任务执行,执行结果应为线程池中的三个线程循环执行这10个任务
package com.kevin.mutithread.demo4;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Author Kevin
* @Date 19:05 2022/7/31
* @Description TODO
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
//创建服务,创建线程池
//newFixedThreadPool 参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(3);
//创建10个线程任务
for (int i = 0; i < 10; i++) {
service.execute(new MyThread());
}
//关闭连接
service.shutdown();
}
static class MyThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在执行任务....");
}
}
}
执行结果:
至此,多线程的内容已经全部完毕!