多线程
1. 多线程概述
1.1 什么是进程?什么是线程?
进程: 正在运行的程序(软件)就是一个独立的进程。(可以看做是现实生活当中的公司)
线程: 是一个程序内部的一条执行流程。(可以看做是公司当中的某个员工)
注意: 线程是属于进程的,一个进程中可以同时运行多个线程
1.2 多线程是什么?
多线程: 在单个程序中同时运行多个线程完成不同的工作。
1.3 并发、并行
并发: 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行: 在同一个时刻上,同时有多个线程在被CPU调度执行
1.4 生命周期
Java线程的状态
- Java总共定义了6种状态
- 6种状态都定义在Thread类的内部枚举类中。
NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。
这几种状态之间切换关系如下图所示
2. 多线程的创建★
2.1 线程创建方式一 :继承Thread类
实现步骤:
1.定义一个子类 继承Thread类 ,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(线程启动后,会自动执行run方法中的代码)
注意:直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。
代码演示如下:
MyThread类:
public class MyThread extends Thread{
private String name;//线程名
public MyThread(String name) {
this.name = name;
}
@Override
public void run() { //线程任务
//线程任务,循环10次
for (int i = 1; i <= 10; i++) {
//Thread.currentThread().getName():获取当前线程的名字
//System.out.println("在"+Thread.currentThread().getName()+" 线程中执行"+i);
System.out.println("线程"+name+"运行了"+(i+1)+"次");
}
}
}
Test类:
public class Test {
public static void main(String[] args) {
// 当我们点击 运行 的时候 就开启了一个线程 去执行当前main方法里面写的代码 都是由上至下
System.out.println("主线程"+Thread.currentThread().getName());
// 在main线程中开启一个新的线程
MyThread myThread = new MyThread("A");
myThread.start();
//主线程
for (int i = 1; i <= 10; i++) {
System.out.println("在"+Thread.currentThread().getName()+"中正在执行:"+i);
}
}
}
运行结果截图:
2.2 线程创建方式二:实现Runnable接口
实现步骤:
1.创建一个实现类 实现 线程任务接口 Runnable接口 重写run方法
2.创建Runnable接口实现类对象
3.将线程任务对象教给线程对象 创建Thread类对象的同时 传递线程任务对象
注意:线程创建方式
1.普通对象创建
2.匿名内部类创建
3.lambda表达式创建
4.调用线程对象的start()方法启动线程
代码实现如下:
MyRunnable类:(线程任务类)
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了"+i+"次");
}
}
}
Test类:
public class Test {
public static void main(String[] args) {
//创建 Runable接口实现类对象 MyRunnable对象。
MyRunnable myRunnable = new MyRunnable();
// 将线程任务对象交给线程对象 创建Thread类对象的同时 传递线程任务对象
Thread thread1 = new Thread(myRunnable);
//开启线程
thread1.start();
//内部类方式创建线程
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行了" + (i + 1) + "次");
}
}
});
thread2.start();
//lambda表达式创建
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行了" + (i + 1) + "次");
}
});
thread3.start();
}
}
运行结果截图:(让每个线程都睡眠了100毫秒)
2.3 线程创建方式三:实现Callable接口
实现步骤:
1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Thread的start()方法启动线程(启动后会自动执行call方法)
等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
6.调用FutrueTask对的get()方法获取返回结果
代码演示如下:
MyCallable类:(Callable接口的实现类)
public class MyCallable implements Callable<Integer> {
private Integer number;//定义了成员变量
public MyCallable(Integer number){//怎么把接收到number传递到 下面call方法中
this.number = number;//传过来的值 给了 成员变量
}
// 求一个数的绝对值
@Override
public Integer call() throws Exception {
System.out.println("当前在:"+Thread.currentThread().getName()+"完成绝对值的获取");
return Math.abs(number); //使用到了成员变量
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(Thread.currentThread().getName()+"中执行1");
// 在main线程中开启一个新的线程。
// 创建 Callable实现类对象
MyCallable mc = new MyCallable(-10);
//创建 处理用于接收线程任务返回值类对象 FutureTask 传递线程任务
FutureTask<Integer> task = new FutureTask<>(mc);
// 创建线程对象 传入 处理返回值的线程任务
new Thread(task).start();//线程对象
// 返回值怎么处理 ? tack处理返回值
System.out.println("新线程的返回值是:"+task.get());
System.out.println("在"+Thread.currentThread().getName()+"中执行2");
}
}
执行结果截图:
2.4三种创建方式的优缺点
| 优点 | 缺点 | |
|---|---|---|
| 继承Thread类方式 | 编码简单 | 存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展 |
| Runnable接口方式 | 任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。 | 需要多一个Runnable对象。 |
| Callable接口方式 | 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果 | 编码复杂一点。 |
3. Thread的常用方法★
4.线程安全
4.1什么是线程安全问题?
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
4.2线程安全出现的原因?
- 存在多个线程在同时执行
- 多个线程同时访问一个共享资源
- 存在修改该共享资源的情况
4.3用程序模拟线程安全问题
代码演示如下:
账户类:
public class Account {
private String cardId; // 卡号
private double money; // 余额。
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
// 小明 小红同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
取钱类:
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
// 取钱(小明,小红)
acc.drawMoney(1000);
}
}
测试类
public class ThreadTest {
public static void main(String[] args) {
// 1、创建一个账户对象,代表两个人的共享账户。
Account acc = new Account("ICBC-110", 1000);
// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
new DrawThread(acc, "小明").start(); // 小明
new DrawThread(acc, "小红").start(); // 小红
}
}
运行结果截图:
5.线程同步★
5.1认识线程同步
- 线程同步是解决线程安全问题的方案
5.2 线程同步思想
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
5.3 线程同步的解决方案
5.3.1同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
写法:
synchronized(锁对象){
//...访问共享数据的代码...
}
同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
锁对象的使用规范
1.建议使用共享资源作为锁对象
2.对于实例方法建议使用this作为锁对象
3.对于静态方法建议使用字节码(类名.class)对象作为锁对象
代码演示如下:
// 小明 小红线程同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
// this正好代表共享资源!
synchronized (this) {
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
}
5.3.2 同步方法
作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
写法:
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步方法底层原理:
1.同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
2.如果方法是实例方法:同步方法默认用this作为的锁对象。
3.如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
是同步代码块好还是同步方法好一点?
范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
可读性:同步方法更好。
代码演示如下:
// 同步方法
public synchronized void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
5.3.3 Lock锁
Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下
1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码
lk.lock(); // 加锁
//...中间是被锁住的代码...
lk.unlock(); // 解锁
代码演示如下:
// 创建了一个锁对象
private final Lock lk = new ReentrantLock();
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
try {
lk.lock(); // 加锁
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock(); // 解锁
}
}
}
6.线程通信
6.1 什么是线程通信?
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
6.2 线程通信的常见模型(生产者与消费者模型)
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据
注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!
案例: 有3个厨师(生产者线程),两个顾客(消费者线程)。
案例分析:
1.先确定在这个案例中,什么是共享数据?
答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。
2.再确定有那几条线程?哪个是生产者,哪个是消费者?
答:厨师是生产者线程,3条生产者线程;
顾客是消费者线程,2条消费者线程
3.什么时候将哪一个线程设置为什么状态
生产者线程(厨师)放包子:
1)先判断是否有包子
2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
3)有包子时,不做包子了,直接唤醒别人、然后让自己等待
消费者线程(顾客)吃包子:
1)先判断是否有包子
2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待
代码如下:
public class Desk {
/*
* 定义桌子类
* 定义一个桌子类
* */
//定义一个存储包子的集合(共享资源)
private List<String> list = new ArrayList<>();
//厨师生产包子(生产者)
public synchronized void put(){
//判断桌子上是否有包子
try {
//获取当前厨师的名字
String name = Thread.currentThread().getName();
if(list.size() == 0){
//如果没有包子,制作包子
list.add(name+"做的包子");
System.out.println("厨师:"+name+"正在做包子.....");
//模拟包子制作时间
Thread.sleep(1000);
//唤醒吃货线程
this.notifyAll();
//线程等待
this.wait();
}else {
//线程等待
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void get(){
try {
if(list.size() == 1){
//获取当前吃货的名称
String name = Thread.currentThread().getName();
//模拟吃包子
System.out.println("吃货:"+name+"正在吃"+list.get(0));
list.clear();
Thread.sleep(1500);
//唤醒厨师
this.notifyAll();
//吃货休息
this.wait();
}else {
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
//开始准备吃包子
System.out.println("进入庆丰包子铺 坐下来 点餐");
//创建共享资源
Desk desk = new Desk();
new Thread(()->{
while (true){
desk.put();
}
},"张三大厨").start();
new Thread(()->{
while (true){
desk.put();
}
},"李四大厨").start();
new Thread(()->{
while (true){
desk.put();
}
},"王五大厨").start();
new Thread(()->{
while (true){
desk.get();
}
},"小帅吃货").start();
new Thread(()->{
while (true){
desk.get();
}
},"小美吃货").start();
}
}
代码运行结果:
7.线程池
7.1什么是线程池?
- 线程池就是一个可以复用线程的技术。
7.2 如何创建线程池?
7.2.1方式一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象
代码如下:
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);
7.2.2方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
7.2.3线程池的注意事项
- 临时线程什么时候创建?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么时候会开始拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
7.3线程池处理Runnable任务
创建好线程池之后,接下来我们就可以使用线程池执行任务了。线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。下面的execute方法可以用来执行Runnable任务。
代码如下:
先准备一个线程任务类
public class CuoZao implements Runnable{
//搓澡任务
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+"师傅正在给客人搓澡=====盐搓---奶搓---醋搓--");
//模拟搓澡时间
try {
//5秒搓一个
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。
public static void main(String[] args) throws InterruptedException {
//先构建一个,线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3,//核心线程数量
5,//最大线程数量 = 核心线程数量 + 临时线程数量
8,//临时存活时间
TimeUnit.SECONDS,//超过核心现场后 如果有线程超过8秒中没有被使用 就销毁掉
new ArrayBlockingQueue<>(4),//指定任务队列 任务阻塞队列 阻塞长度是4
Executors.defaultThreadFactory(),//用户创建线程对象的工程对象 固定代码
new ThreadPoolExecutor.CallerRunsPolicy());//任务拒绝策略 四个 我选取最后 忙不过来 找外援
//线程池 草堂
//核心线程数量 老板招聘的 三个搓澡师傅
//执行搓澡任务
CuoZao cz = new CuoZao();
//来一个顾客 搓一个顾客
//接客
pool.execute(cz);//核心
pool.execute(cz);//核心
pool.execute(cz);//核心
//3个客人
pool.execute(cz);//第四个客人 先等待了 核心线程为他服务
pool.execute(cz);//第五个客人
pool.execute(cz);//第六个客人
pool.execute(cz);//第七个客人
pool.execute(cz);//第八个客人 触发了 招聘临时工 阻塞队列4 一旦超出阻塞队列 就增派人手
pool.execute(cz);//第九个客人 阻塞队列4 超出阻塞队列两个 增派两个人手
// 第九个客人 已经有五个搓澡师傅 已经达到 最大线程数量
pool.execute(cz);//第十个客人 阻塞队列满了 超出阻塞队列的 用两个人手 但是还少一个
// 这个时候拒绝策略 -- 增派人手 main来处理。。。
Thread.sleep(20000);//时间过了十一秒 没有新的任务 肯定有线程没有处理任务的 这种任务就会销毁
System.out.println("至少空闲了12秒 已经有被销毁的线程了...销毁之后 ");
pool.execute(cz);
pool.execute(cz);
pool.execute(cz);
pool.execute(cz);
pool.execute(cz);
pool.shutdown();//都搓完了 把 澡堂关闭
// pool.shutdownNow();//里面关闭 没搓完的任务回到队列中
}
代码运行结果截图:
7.3.1使用ExecutorService线程池对象的常用方法
void execute(Runnable command)
7.3.2新任务拒绝策略
7.4线程池处理Callable任务
callable任务相对于Runnable任务来说,就是多了一个返回值。
执行Callable任务需要用到下面的submit方法
先准备一个Callable线程任务 模拟迅雷下载任务
import java.util.concurrent.Callable;
public class Download implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+"正在下载.....");
Thread.sleep(1000);
return "资源下载完毕";
}
}
再准备一个测试类,在测试类中创建线程池,并执行callable任务。
public class XunLei {
public static void main(String[] args) throws Exception{
//创建一个线程池 表示 迅雷
ExecutorService pool = new ThreadPoolExecutor(
2,
5,
8,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 执行提交 callable任务
Future<String> f1 = pool.submit(new Download());
Future<String> f2 = pool.submit(new Download());
Future<String> f3 = pool.submit(new Download());
Future<String> f4 = pool.submit(new Download());
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
7.5 Executors工具类实现线程池
Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。如下图所示
代码演示:
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建一个线程池 传递一个参数
ExecutorService pool = Executors.newFixedThreadPool(3);
// 执行提交 callable任务
Future<String> f1 = pool.submit(new Download());
Future<String> f2 = pool.submit(new Download());
Future<String> f3 = pool.submit(new Download());
Future<String> f4 = pool.submit(new Download());
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
//可以关闭线程池
// pool.shutdown();// 在关闭
// pool.shutdownNow();//不搓了
}
}