多线程
1.了解线程
1.1多线程的概念
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
1.2并行和并发
-
并行:在同一时刻,有多个指令在多个CPU上同时执行。
-
并发:在同一时刻,有多个指令在单个CPU上交替执行。
- PS: 并行和并发都可以在同一时刻执行多个指令,不同的是,并行针对的是多个CPU,并发针对的是一个CPU
1.3进程和线程
-
进程:是正在运行的程序
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行 -
线程:是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
2.多线程实现
2.1实现方式一:继承Thread类
-
方法介绍
方法名 说明 void run() 在线程开启后,此方法将被调用执行 void start() 使此线程开始执行,Java虚拟机会调用run()方法
-
实现步骤
-
定义一个类MyThread继承Thread类
-
在MyThread类中重写run()方法
-
创建MyThread类的对象
-
启动线程
-
-
代码演示
public class MyThread extends Thread { @Override public void run() { for(int i=0; i<100; i++) { System.out.println(i); } } } public class MyThreadDemo { public static void main(String[] args) { MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); // my1.run(); // my2.run(); //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法 my1.start(); my2.start(); } }
-
两个小问题
-
为什么要重写run()方法?
因为run()方法是用来封装被线程执行的代码
-
run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法,建议使用
区别详解
如图所示:
run(): 主线程先调用子线程的 run 方法,等到 run 方法执行完毕,才继续执行剩余代码
start(): 主线程 和 子线程 交替执行
-
2.2实现方式二:实现Runnable接口(常用)
-
Thread构造方法
方法名 说明 Thread(Runnable target) 分配一个新的Thread对象 Thread(Runnable target, String name) 分配一个新的Thread对象 -
实现步骤
- 定义一个类MyRunnable实现Runnable
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
-
代码演示
public class MyRunnable implements Runnable { @Override public void run() { for(int i=0; i<100; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } } public class MyRunnableDemo { public static void main(String[] args) { //创建MyRunnable类的对象 MyRunnable my = new MyRunnable(); //创建Thread类的对象,把MyRunnable对象作为构造方法的参数 //Thread(Runnable target) // Thread t1 = new Thread(my); // Thread t2 = new Thread(my); //Thread(Runnable target, String name) Thread t1 = new Thread(my,"坦克"); Thread t2 = new Thread(my,"飞机"); //启动线程 t1.start(); t2.start(); } }
-
PS:用实现Runnable接口的方式创建多线程经常采用Lambda形式(jdk1.8新特性)
2.3实现方式三: 实现Callable接口(了解)
-
方法介绍
方法名 说明 Executors.newFixedThreadPool(int nThreads) 创建一个服务,参数为线程的数量 submit(Callable task) 提交任务并执行 V get() 如有必要,等待计算完成,然后获取其结果 shutdown() 关闭服务 -
实现步骤
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法,需要抛出异常
- 创建MyCallable类的对象
- 使用Executors.newFixedThreadPool()方法创建服务,传入线程数量
- 调用submit()方法提交执行
- 调用get方法,就可以获取线程结束之后的结果。
-
代码演示
//线程创建方式三: 实现 callable 接口 public class TestCallable implements Callable<Boolean> { private String url; //网络图片地址 private String name; //保存的文件名 public TestCallable(String url,String name){ this.url = url; this.name = name; } @Override public Boolean call(){ WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url,name); System.out.println("下载了文件名为:"+ name); return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { TestCallable t1 = new TestCallable("https://img.ivsky.com/img/tupian/li/202007/25/richu-003.jpg","1.jpg"); TestCallable t2 = new TestCallable("https://img.ivsky.com/img/tupian/li/202007/25/richu-003.jpg","2.jpg"); TestCallable t3 = new TestCallable("https://img.ivsky.com/img/tupian/li/202007/25/richu-003.jpg","3.jpg"); //创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(3); //提交执行 Future<Boolean> r1 = ser.submit(t1); Future<Boolean> r2 = ser.submit(t2); Future<Boolean> r3 = ser.submit(t3); //获取结果 boolean rs1 = r1.get(); boolean rs2 = r2.get(); boolean rs3 = r3.get(); System.out.println(rs1); System.out.println(rs2); System.out.println(rs3); //关闭服务 ser.shutdown(); } } //下载器 class WebDownloader{ //下载方法 public void downloader(String url,String name){ try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,downloader方法出现异常"); } } }
-
三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
- 实现Runnable、Callable接口
3.线程状态
3.1线程状态一览图
3.2线程方法
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程优先级 |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待该线程终止,相当于插队 |
static void yield() | 暂停当前正在执行的线程 |
void interrupt() | 中断线程,别用这个 |
boolean isAlive | 测试线程是否处于活动状态 |
void setName(String name) | 将此线程的名称更改为等于参数name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
3.3停止线程
-
不推荐使用JDK提供的stop()、destroy()方法[已废弃]
-
建议使用一个标志位进行终止变量,当flag=false,则终止线程运行
-
代码示例
//测试 stop
//1.建议线程正常停止----->利用次数,不建议死循环
//2. 建议使用标志位 ------> 设置一个标准位
//3. 不要使用 stop 或者 destroy 等 过时 或 JDK不建议使用的方法
public class TestStop implements Runnable {
//1. 设置一个标识位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("run....Thread" + i++);
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
if(i == 900){
//调用 stop 方法 切换标志位,让线程停止
testStop.stop();
System.out.println("线程该停止了");
}
}
}
}
3.4线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入到就绪状态
- sleep可以模拟网络延时,倒计时等
- 每个对象都有一把锁,sleep不会释放锁
- 代码示例
//模拟网络延时: 放大问题的发生性
public class TestSleep implements Runnable{
//票数
private int ticketNums = 10;
@Override
public void run() {
while(true){
if(ticketNums <= 0){
break;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---->拿到了第"+ ticketNums-- + "票");
}
}
public static void main(String[] args) {
TestSleep ticket = new TestSleep();
new Thread(ticket, "小明").start();
new Thread(ticket, "老师").start();
new Thread(ticket, "黄牛党").start();
}
}
3.5线程礼让
-
礼让线程,让当前正在执行的线程暂停,但不阻塞
-
将线程从运行状态转为就绪状态
-
让CPU重新调度,礼让不一定成功,看CPU心情
-
代码示例
//测试礼让线程
//礼让不一定成功,看 CPU 心情
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield(); //礼让
System.out.println(Thread.currentThread().getName() + "线程停止执行");
}
}
3.6 线程插队
-
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
-
可以想象为插队
-
代码示例
//测试 join 方法 想象为插队
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程vip来了" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动我们的线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 500; i++) {
if (i == 200) {
thread.join(); //插队
}
System.out.println("main" + i);
}
}
}
3.7线程优先级
-
线程调度
-
两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
-
Java使用的是抢占式调度模型
-
随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
-
-
优先级相关方法
方法名 说明 final int getPriority() 返回此线程的优先级 final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
-
代码演示
public class MyCallable implements Callable
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
//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("坦克");
t2.setPriority(1);
//System.out.println(t2.getPriority());//5
t2.start();
}
}
#### 3.8守护线程
- 相关方法
| 方法名 | 说明 |
| -------------------------- | ------------------------------------------------------------ |
| void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
- 代码演示
```java
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; 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 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();
}
}
4.线程同步
-
并发:同一个对象被多个线程同时操作
- 生活实例:上万人同时抢100张票、同一账户在两家不同的银行同时取钱
-
线程同步
- 现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个一个来
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用
- 解决办法:队列 + 锁
-
由于同一进程的多个线程共享同一块储存空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
4.1同步方法
- 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法 和 synchronized 块
- 同步方法 : public synchronized void method(int args){}
- synchronized方法控制对 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能得到这个锁,继续执行
- 缺陷:若将一个大的方法申明为 synchronized 将会影响效率
- 代码示例
//不安全的买票
//线程不安全, 有负数
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"苦逼的我").start();
new Thread(station, "牛逼的你们").start();
new Thread(station, "可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
private boolean flag = true; //外部停止方式
@Override
public void run() {
//买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// synchronized 同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(1000);
//买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}
4.2 同步块
-
同步块:synchronized(Obj) {}
-
Obj 称之为 同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是 class [反射机制]
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
-
弊端
- 方法里面有读写操作时,只有需要修改的内容才需要锁,锁的太多,浪费资源
-
代码示例
//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(1000, "结婚基金");
Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");
you.start();
girlFriend.start();
}
}
//账户
class Account {
int money; //余额
String name; //卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行
class Drawing extends Thread {
//账户
private Account account;
//取了多少钱
private int drawingMoney;
//现在手里有多少钱
private int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
// synchronized 默认锁的是this,就是当前方法对应的类对象
// synchronized(obj){} 同步块 可以锁任何对象
@Override
public void run() {
//锁的对象就是变化的量,需要增删改查的对象
synchronized (account){
// 判断有没有钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
//sleep可以放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
// this.getName() 等价于 Thread.currentThread().getName()
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
}
5.死锁 & Lock(锁)
5.1 死锁
-
死锁
- 多个线程各自占用一些共享资源,并且互相等待其他线程占用的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
-
死锁避免方法
- 产生死锁的四个必要条件:S
-
互斥条件:一个资源每次只能被一个进程使用
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
上面列出了死锁的四个必要条件,我们只要想办法破除其中的任意一个或多个,就可以避免死锁产生
-
代码示例
//死锁:多个线程互相抱着对方需要的资源,形成僵持 public class DeadLock { public static void main(String[] args) { Makeup g1 = new Makeup(0,"灰姑娘"); Makeup g2 = new Makeup(1,"白雪公主"); g1.start(); g2.start(); } } //口红 class Lipstick{ } //镜子 class Mirror{ } class Makeup extends Thread{ //需要的资源只有一份 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choices; //选择 String girlName; //使用化妆品的人 public Makeup(int choices, String girlName){ this.choices = choices; this.girlName = girlName; } @Override public void run() { //化妆 try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } private void makeup() throws InterruptedException { if(choices == 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 + "获得口红的锁"); } } } }
5.2Lock(锁)
-
Lock(锁)
-
synchornized 与 Lock(锁) 的对比
-
代码示例
public class TestLock { public static void main(String[] args) { TestLock2 testLock2 = new TestLock2(); new Thread(testLock2).start(); new Thread(testLock2).start(); new Thread(testLock2).start(); } } class TestLock2 implements Runnable { int tickNums = 10; //定义 lock 锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); //加锁 if (tickNums > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(tickNums--); } else { break; } }finally { // 解锁 lock.unlock(); } } } }
6.线程通信
6.1线程通信及分析
6.2线程通信方法
6.3解决方式一(管程法)
-
示例代码
//测试:生产者消费者模型 ---> 利用缓冲区解决:管程法 //生产者、消费者、产品、缓冲区 public class TestPC { public static void main(String[] args) { SynContainer container = new SynContainer(); new Productor(container).start(); new Consumer(container).start(); } } //生产者 class Productor extends Thread{ SynContainer container; public Productor(SynContainer container){ this.container = container; } //生产 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("生产了" + i + "只鸡"); container.push(new Chicken(i)); } } } //消费者 class Consumer extends Thread{ SynContainer container; public Consumer(SynContainer container){ this.container = container; } //消费 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了--->" + container.pop().id + "只鸡"); } } } //产品 class Chicken{ int id; //产品编号 public Chicken(int id) { this.id = id; } } //缓冲区 class SynContainer{ //需要一个容器大小 Chicken[] chickens = new Chicken[10]; //容器计数器 int count = 0; //生产者 放入产品 public synchronized void push(Chicken chicken){ //如果容器满了,就需要等待消费者消费 if(count == chickens.length){ //通知消费者消费,生产者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果没有满,我们就需要丢入产品 chickens[count] = chicken; count++; //可以通知消费者消费了 this.notifyAll(); } //消费者消费产品 public synchronized Chicken pop(){ //判断能否消费 if(count == 0){ //等待生产者生产,消费者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //count != 0 可以消费 count--; Chicken chicken = chickens[count]; // 吃完了,通知生产者生产 this.notifyAll(); return chicken; } }
6.4解决方式二(红绿灯法)
-
示例代码
//测试生产者消费者问题2:信号灯法,标志位解决 public class TestPC2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Watcher(tv).start(); } } //生产者---> 演员 class Player extends Thread { TV tv; public Player(TV tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i % 2 == 0) { this.tv.play("快乐大本营播放中...."); } else { this.tv.play("抖音:记录美好生活"); } } } } //消费者---> 观众 class Watcher extends Thread { TV tv; public Watcher(TV tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { this.tv.watch(); } } } //产品---> 节目 class TV { //演员表演,观众等待 T //观众观看,演员等待 F String voice; //表演的节目 boolean flag = true; //表演 public synchronized void play(String voice) { if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员表演了:" + voice); //通知观众观看 this.notify(); //通知唤醒 this.voice = voice; this.flag = !this.flag; } //观看 public synchronized void watch() { if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观看了:" + voice); //通知演员表演 this.notifyAll(); this.flag = !this.flag; } }
7.扩展之线程池
7.1线程池
7.2 使用
7.3代码示例
//测试线程池
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool 参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
说明:本篇博客整理的笔记来源于狂神和黑马