day22
一、多线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXZUUjJy-1620435124477)(img/image-20210507093823663.png)]
1.1 和多线程有关的几组概念
程序和进程
程序:是一组固定资源的集合,是安装在电脑硬盘上的,是一个静态的概念(比如:安装一个游戏 英雄联盟,就是一个程序)
进程:是一个正在执行的程序,是程序向系统申请资源的独立单位(比如:双击打开 英雄联盟这个程序,那么此程序就会生成一个进程向操作系统申请相关的系统资源:CPU、内存等,没有系统资源就无法正常运行程序)
如何查看进程:
可以使用快捷键打开任务管理器: CTRL + ALT + . 或者 CTRL + SHIFT + ESC
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7qCZ8OWW-1620435124479)(img/image-20210507094725046.png)]
进程和线程
进程:是一个正在执行的程序,是程序是系统申请资源的独立单位。
线程:进程可以包含多个线程,但是至少要有一个线程。一个线程就是一个执行任务。(也就是说一个程序可以同时执行多个任务)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a65OyPTv-1620435124480)(img/image-20210507095212302.png)]
并行和并发
并行:多个CPU或者多核心的情况下,程序的同时执行(这个是物理上的同时执行)
并发:单个CPU的情况下,程序的同时执行(这个是逻辑上的同时执行)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BBL0HmtD-1620435124483)(img/并行和并发.jpg)]
1.2 创建多线程的方式
【1】第一种方式:继承Thread类
1. 声明一个类,并且继承Thread类
2. 在Thread的子类中重写run方法
3. 创建子类对象并通过start方法启动线程
注意事项
1. 启动线程必须要使用start方法,而不是run方法
2. 一个线程只能通过start方法启动一次
3. run方法中一般放的都是比较耗时的操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVaEWhJD-1620435124484)(img/image-20210507102740239.png)]
//1. 声明一个类,并且继承Thread类
public class MyThread extends Thread {
//2. 在Thread的子类中重写run方法
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println(i);
}
}
}
public class Demo2 {
public static void main(String[] args) {
//3. 创建子类对象并通过start方法启动线程
MyThread mt = new MyThread();
mt.start();
}
}
使用匿名内部类创建线程
匿名内部类格式:
new 类名或者接口名(){
方法的重写;
}
本质:类或接口的对象
public class Demo3 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println(i);
}
}
}.start();
}
}
【2】第二种方式:实现Runnable接口
1. 声明一个类实现Runnable接口
2. 重写Runnable接口的run方法
3. 创建一个Runnable接口子类的对象,并将此对象传入Thread的构造方法中 (构造方法:Thread(Runnable target) )
4. 创建Thread的对象,并调用start方法启动线程
注意:要想启动线程,肯定需要使用Thread类的start方法
//1. 声明一个类实现Runnable接口
public class MyRun implements Runnable {
//2. 重写Runnable接口的run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class Demo {
public static void main(String[] args) {
//3. 创建一个Runnable接口子类的对象,并将此对象传入Thread的构造方法中 (构造方法:Thread(Runnable target) )
MyRun mr = new MyRun();
//4. 创建Thread的对象,并调用start方法启动线程
Thread t = new Thread(mr);
t.start();
}
}
使用匿名内部类实现线程
public class Demo2 {
public static void main(String[] args) {
// Runnable r = new Runnable() {
//
// @Override
// public void run() {
// for (int i = 0; i < 10; i++) {
// System.out.println(i);
// }
// }
// };
// Thread t = new Thread(r);
// t.start();
//简化写法:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}).start();
}
}
【3】第三种方式:实现Callable接口
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做
call
的方法。
Callable
接口类似于Runnable
些其实例可能被另一个线程执行的类设计的。但是Runnable
不会返回结果,并且无法抛出经过检查的异常。
分析
1. Thread类有一个构造方法Thread(Runnable target) 可以接收Runnable接口的对象
2. Runnable接口有一个实现类FutureTask(也就说明FutureTask的对象可以传入Thread类的构造方法中)
3. FutureTask有一个构造方法FutureTask(Callable<V> callable) ,可以接收Callable接口的对象
4. Callable接口中有一个call方法类似run方法,可以执行任务。
以上:就是实现多线程的第三种方式Callable接口和Thread类之间的关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNcocbtc-1620435124486)(img/image-20210507110907844.png)]
步骤
1. 声明一个类,实现Callable接口
2. 在类中重写Callable接口的call方法
3. 将Callable接口的实现类对象传递到FutureTask的构造方法中,来创建一个FutureTask对象
4. 因为FutureTask是Runnable接口的实现类,所以我们可以直接将FutureTask对象传递到Thread类的构造方法中
5. 创建Thread类的对象,并通过start方法启动线程
package com.ujiuye.demo01_多线程.第三种方式;
import java.util.concurrent.Callable;
//1. 声明一个类,实现Callable接口
public class MyCall implements Callable<String> {
//2. 在类中重写Callable接口的call方法
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
return "OK";
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3. 将Callable接口的实现类对象传递到FutureTask的构造方法中,来创建一个FutureTask对象
MyCall mc = new MyCall();
FutureTask<String> ft = new FutureTask<>(mc);
//4. 因为FutureTask是Runnable接口的实现类,所以我们可以直接将FutureTask对象传递到Thread类的构造方法中
Thread t = new Thread(ft);
//5. 创建Thread类的对象,并通过start方法启动线程
t.start();
//6. 获取关键信息,也就是call方法的返回值
String s = ft.get();
System.out.println(s);
}
}
1.3 线程调度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MsbWVbto-1620435124486)(img/image-20210507135349987.png)]
1.4 Thread类的常见方法
1.4.1 设置和获取线程的名字
String getName() 返回该线程的名称
void setName(String name) 改变线程名称,使之与参数 name 相同
注意:获取当前线程对象的方法
static Thread currentThread() 返回对当前正在执行的线程对象的引用
/*
Thread的子类可以直接使用getName和setName方法
*/
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.setName("张飞");
mt2.setName("关羽");
mt.start();
mt2.start();
}
}
/*
在Runnable接口的实现类中:可以先通过currentThread()方法获取当前线程对象,再调用getName方法
*/
public class MyRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyRun mr = new MyRun();
Thread t = new Thread(mr, "张飞飞");
Thread t2 = new Thread(mr, "关羽羽");
t.start();
t2.start();
}
}
思考练习:
在main方法中,建立一个for循环,设置并获取主线程的名字
public class Demo3 {
public static void main(String[] args) {
Thread.currentThread().setName("刘备");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
1.4.2 线程的睡眠
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
import java.util.Date;
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "---" + i + "---" + new Date());
}
}
}
public class Demo4 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
/*
输出结果:
Thread-0---0---Fri May 07 14:31:23 CST 2021
Thread-0---1---Fri May 07 14:31:24 CST 2021
Thread-0---2---Fri May 07 14:31:25 CST 2021
Thread-0---3---Fri May 07 14:31:26 CST 2021
Thread-0---4---Fri May 07 14:31:27 CST 2021
Thread-0---5---Fri May 07 14:31:28 CST 2021
Thread-0---6---Fri May 07 14:31:29 CST 2021
Thread-0---7---Fri May 07 14:31:30 CST 2021
Thread-0---8---Fri May 07 14:31:31 CST 2021
Thread-0---9---Fri May 07 14:31:32 CST 2021
*/
1.4.3 线程的优先级
void setPriority(int newPriority) 更改线程的优先级
int getPriority() 返回线程的优先级
注意:
1. 线程的优先级范围为1-10
2. 线程的优先级只是代表了线程被调用的概率。
如果线程优先级高,那么就证明此线程出现的概率较高
如果线程优先级低,那么就证明此线程出现的概率较低
3. 如果优先级不在 MIN_PRIORITY 到 MAX_PRIORITY 范围内,那么会报IllegalArgumentException异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQ5T2R9B-1620435124487)(img/image-20210507145130428.png)]
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println(getName() + "---" + i);
}
}
}
public class Demo5 {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.setName("张飞");
mt2.setName("关羽");
//如果想要设置线程的优先级为最高,那么建议使用常量MAX_PRIORITY,而不是整数10(因为不方便记忆)
mt2.setPriority(Thread.MAX_PRIORITY);
System.out.println(mt.getPriority());
System.out.println(mt2.getPriority());
mt.start();
mt2.start();
}
}
1.4.4 线程的礼让
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
注意:
1. 礼让线程的作用就是避免一个线程过度使用CPU(极端点来说:就是一直使用CPU,而其它线程没有使用的机会)
2. 礼让线程和优先级一样,都是代表了一种概率情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FOdxhwz-1620435124488)(img/yield方法.jpg)]
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++){
System.out.println(getName() + "---" + i);
}
}
}
public class Demo6 {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.setName("张飞");
mt2.setName("关羽");
mt.yield();
mt.start();
mt2.start();
}
}
1.4.5 线程的中断
我们可能会启动多个线程做同一个事情,比如买票!!!,这样抢到票的概率会更高一点。
这多个线程分别去不同的渠道去买票,如果有一个渠道买到票了,那么其它渠道就不需要买票了,就可以关闭其它线程了。
void interrupt() 中断线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUBcmQFf-1620435124489)(img/image-20210507151020952.png)]
1.4.6 线程的守护
void setDaemon(boolean on) 将该线程标记为守护线程或用户线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjnrnVh2-1620435124490)(img/image-20210507153254427.png)]
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println(getName() + "活了:" + i);
}
}
}
public class Demo8 {
public static void main(String[] args) {
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
mt.setName("张飞");
mt2.setName("关羽");
mt.setDaemon(true);
mt2.setDaemon(true);
mt.start();
mt2.start();
Thread.currentThread().setName("刘备");
for(int i = 0; i < 50; i++){
System.out.println(Thread.currentThread().getName() + "活了:" + i);
}
}
}
练习:
目前电影院正在热映《葫芦娃救爷爷》这部电影,但是只剩下来100张票了。这100张票同时被3个商家售卖(淘票票、猫眼电影、美团),请写一个程序,模拟以上买票过程。比如:淘票票卖出了1张票,还剩99张票;猫眼电影卖出了1张票,还剩98张票。 直到卖完为止。
注意:
1. 三个窗口肯定是需要三个线程
2. 一共有100张票,而不是每一个都有100张
3. 使用Thread或者Runnable都可以
package com.ujiuye.demo03_卖票案例;
public class SellTicketsRunnable implements Runnable {
int tickets = 100;
@Override
public void run() {
while(true){
//证明有票可卖
if(tickets > 0) { //因为是抢占式调度,所有三个线程都有可能过来: 淘票票、美团、猫眼电影都在这等
/*
需要明确的知识点:
1. CPU处理的操作都是原子性的
比如:--tickets是分为两步的:
1. tickets先减1
2. 将结果再赋值给tickets
2. Java虚拟机中是抢占式调度
也就说明了有可能在同一时间,这三个线程都到达了CPU的临界点
问题过程分析:
理想中的情况:
一个线程执行完毕,另一个线程再过来
比如:
淘票票过来卖了一张票,剩99
美团过来卖了一张票, 剩98
...
现实中的情况:
三个线程同时都过来了
比如:
还剩最后一张票的时候,三个线程都可以通过if的判断>0,从而进入到if语句中
(三个线程都是拿着tickets=1进来的)
淘票票过来卖了1张票:
tickets先减1 0
...
中间有很大的空档
(万一就是这个空档被美团抢到了)
tickets先减1 -1
...
中间有很大的空档
(万一就是这个空档被猫眼电影抢到了)
tickets先减1 -2
然后赋值给tickets并输出-2
...
然后赋值给tickets并输出-1
...
然后赋值给tickets并输出0
*/
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
}else{
break;
}
}
}
}
1.5 线程安全问题
1.5.1 出现线程安全问题的环境
1. 多线程环境
2. 有共享的数据
3. 多个线程同时操作了共享的数据
1.5.2 如何解决线程安全问题
只需要将出现问题的环境给解决了(解决其中一个就可以)就可以解决线程安全问题
1. 多线程环境 【无法解决】
2. 有共享的数据 【无法解决】
3. 多个线程同时操作了共享的数据 【可以解决】
解决的方式就是:让多个线程一个一个的去访问共享的数据,而不是同时去访问共享的数据
1.5.3 解决方案
同步代码块和同步方法,都可以保证让线程一个一个的去执行共享的数据。
原理:就是在一个线程访问数据的时候,给数据先加一把锁,等操作完这个数据之后,再将这个锁给放开。锁放开之后,其它的线程又可以去争抢了,然后一直这么循环下去。
理解:火车上的卫生间。
使用同步代码块
synchronized(锁){
可能出现线程安全问题的代码;
}
锁是谁? 锁可以是任何对象,但是必须要保证是同一把锁。
使用普通同步方法
修饰符 synchronized 返回值类型 方法名(参数列表) {
可能出现线程安全问题的代码;
}
锁是谁? this
使用静态同步方法
修饰符 static synchronized 返回值类型 方法名(参数列表) {
可能出现线程安全问题的代码;
}
锁是谁? 类名.class
public class SellTicketsRunnable implements Runnable {
static int tickets = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
test();
//使用同步代码块
// synchronized (obj){
// //证明有票可卖
// if(tickets > 0) {
//
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
// }else{
// break;
// }
// }
}
}
//普通的同步方法
// public synchronized void test() {
//
// if(tickets > 0) {
//
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
// }
// }
//使用静态的同步方法
public static synchronized void test() {
if(tickets > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
}
}
}
}
//普通的同步方法
// public synchronized void test() {
//
// if(tickets > 0) {
//
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + “卖出了1张票,还剩” + --tickets + “张票”);
// }
// }
//使用静态的同步方法
public static synchronized void test() {
if(tickets > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
}
}
}