目录
哥几个来学线程啦~~
🌲一、wait()方法 与 notify()方法
为了能够协调地调度多个线程之间的执行顺序,我们可以使用wait()方法 与 notify()方法。
方法 | 说明 |
wait() | 在没有通知之前,死等 |
wait(long mills) | 等待 mills ms或者收到通知,再获取锁 |
notify() | 唤醒对应锁的随机一个阻塞等待线程 |
notifyAll() | 唤醒所有对应锁阻塞等待的线程 |
🍎1.wait()方法
wait()方法是Object类里的方法,如何类都可以调用wait()方法。
wait()方法的作用:
使当前线程从调用处终端并且释放锁转入到等待队列,直到notify或者notifyAll的通知才能从等待队列转入到锁池队列,如果没有收到通知,会一直死等。
wait(long time)方法的作用:
相比起wait多了一个等待的时间time,如果经过time(毫秒)时间后没有收到notify或者notifyAll的通知,自动从等待队列转入锁池队列。
wait做的事情:
- 使当前执行代码的线程进行等待(把线程放入等待队列中)
- 释放当前锁(可以给其他线程获取)
- 满足一定条件(被唤醒 / 到达时间)后重新尝试获取该锁
注意:wait方法要搭配synchronized方法使用,如果脱离了synchronized,会抛出异常!!!
使用示栗🌰:
public class Demo15 {
public static void main(String[] args) throws InterruptedException/* 1 */ {
Object o = new Object();//2
synchronized (o) {//3
System.out.println("等待中");
o.wait();//4
System.out.println("等待结束");
}
}
}
代码说明:
1.使用wait()方法会抛出阻塞异常
2.由于wait()方法是Object类里的方法,如何类都可以调用wait()方法,我们这里可以创建一个Object类对象或者其他对象来调用wait()方法
3.wait要与synchronized搭配使用,synchronized()里必须传入一个锁对象
4.调用对象o的wait()方法
🍅2.notify()方法
notify()方法是Object类里的方法,如何类都可以调用notify()方法。
notify()方法的作用:
唤醒等待的线程。随机从等待队列中通知一个持有相同锁的一个线程,如果没有持有相同锁的wait线程,那么指令忽略无效。
notify做的事情:
- notify是用来通知那些可能等待该对象的对象锁的其他线程,对它们发出通知,使他们重新获取该对象的对象锁
- 如果有多个线程等待,则从线程调度器随机挑选一个呈wait状态的线程(并不是按照先来后到)
- 在notify()方法之后,当前线程并不会马上释放该对象锁,而是要等到notify()方法的线程将程序执行完,也就是说退出同步代码块之后才会释放对象锁
注意:notify方法要搭配synchronized方法使用。
使用示栗🌰:
class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {//使用循环,目的是wait()循环执行
try {
System.out.println(Thread.currentThread().getName() + "的wait开始");
locker.wait();
System.out.println(Thread.currentThread().getName() + "的wait结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify开始");
locker.notify();
System.out.println("notify结束");
}
}
}
public class Demo16 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();//下面两个实现了Runnable接口的类要使用同一个锁对象
WaitTask waitTask = new WaitTask(locker);
NotifyTask notifyTask = new NotifyTask(locker);
Thread waitThread1 = new Thread(waitTask, "waitThread1");
Thread notifyThread = new Thread(notifyTask);
waitThread1.start();
Thread.sleep(1000);//等待1秒再唤醒
notifyThread.start();
}
}
从执行结果中不难看出,当waitThread线程等待了之后,需要notifyThread线程将其唤醒。唤醒了之后run()方法里的代码进入了第二次循环,但是第二次wait开始之后,并没有notify将其唤醒,因此waitThread线程会死等。
🍓3.notifyAll()方法
notifyAll()方法也是Object类里的方法,如何类都可以调用notifyAll()方法。
notifyAll()方法的作用:
notifyAll()方法可以一次性唤醒所有线程。通知所有处于等待队列中且持有相同锁的线程,让这些线程转入锁池队列。如果没有持有相同锁的wait线程那么指令忽略无效。
注意:notifyAll方法要搭配synchronized方法使用,
使用示栗🌰:
class WaitTask implements Runnable {
//代码不变
}
class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify开始");
locker.notifyAll();//把notify改成notifyAll
System.out.println("notify结束");
}
}
}
public class Demo16 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();//下面两个实现了Runnable接口的类要使用同一个锁对象
WaitTask waitTask = new WaitTask(locker);
NotifyTask notifyTask = new NotifyTask(locker);
Thread waitThread1 = new Thread(waitTask, "waitThread1");
Thread waitThread2 = new Thread(waitTask, "waitThread2");
Thread waitThread3 = new Thread(waitTask, "waitThread3");
Thread notifyThread = new Thread(notifyTask);
waitThread1.start();
waitThread2.start();
waitThread3.start();
Thread.sleep(1000);//等待1秒再唤醒
notifyThread.start();
}
}
使用notify只唤醒一个线程
使用notifyAll唤醒所有线程
注意:虽然是同时唤醒了三个线程,但是这三个线程并不是同时执行的,而是有先后的,是要去进行锁竞争的。
🍒4.wait()方法 和 sleep()方法 的区别
🍕1.两者都可以让线程停止执行一段时间。
🍔2.wait()方法是用于线程之间通信的,让线程进入WAITING状态。而sleep()方法则是让线程阻塞一段时间,进入TIME_WAITING状态。
🍟3.wait()方法需要搭配synchronized使用,而sleep()方法不用。
🌭4.wait()方法是Object类的方法,而sleep()方法是Thread类的静态方法。
🌳二、多线程案例
🥝1.单例模式
单例模式通俗来说就是一个类只能创建一个对象,它能保证式能保证某个类在程序中只存在唯一的一份实例,而不会创建出多个实例。它是借助Java语法,来使代码不能new多个对象来实现的。
实现单例模式又可以分成饿汉模式和懒汉模式。
🌺1.1饿汉模式
饿汉模式就是在程序启动的时候立马创建一个实例对象。
饿汉模式的弊端:
假设我有一个10G的文件,如果我一打开这个文件,就要立马读取这10G的文件到我的内存中,这时候消耗的资源就非常多了,速度也非常慢了~~
饿汉模式的实现:
class Singleton {
private static Singleton instance = new Singleton();
//在成员变量instance前加static关键字,使它成为静态成员变量,
//静态成员变量会在类加载的时候进行初始化,也就是在程序启动的时候进行初始化
//首次创建类对象、访问类的静态成员(变量或方法)会引发类加载
//静态成员在类中只存在一份
private Singleton() {
//将构造函数设置为私有,使它在类外不能被调用
}
public static Singleton getInstance() {
return instance;
//使类外函数能够调用instance对象
}
}
由于在这种模式下在程序启动的时候就会加载实例,因此多线程在获取该对象的时候只是读取该对象,并没有涉及到创建该对象,因此饿汉模式是线程安全的。
🌻1.2懒汉模式(单线程版)
懒汉模式就是在类加载的时候不创建对象,等到第一次使用的时候才创建。
比如我有一个10G的文件,我在使用它的时候并不全部读取,而是要用到哪些资源读取哪些资源。
懒汉模式(单线程版)的实现:
class Singleton {
private static Singleton instance = null;
private Singleton() {
//用private修饰构造函数,使它不能在其他函数中调用
}
public static Singleton getInstance() {//如果第一次调用这个函数,那就创建一个实例
if (instance == null) {//如果还没创建,那就创建一个
instance = new Singleton();
}
return instance;
}
}
🌼1.3懒汉模式(多线程版)
上面这个懒汉模式单线程版的是线程不安全的,如果多个线程同时调用getInstance()方法,那么就可能会导致创建多个instance对象。
要使它变成线程安全只需要对这个getInstance方法加锁:
public synchronized static Singleton getInstance() {//如果第一次调用这个函数,那就创建一个实例
//加synchronized关键字,使之成为同步代码块,保证new操作是一个原子操作
if (instance == null) {//如果还没创建,那就创建一个
instance = new Singleton();
}
return instance;
}
只加synchronized关键字就足够了吗?不这还永远不够,我的梦想是写出完美的代码呀!!!
我们在加锁的基础上1.给instance添加volatile关键字 2.在添加一层if语句:
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
//用private修饰构造函数,使它不能在其他函数中调用
}
public static Singleton getInstance() {//如果第一次调用这个函数,那就创建一个实例
if (instance == null) {
//加synchronized关键字,使之成为同步代码块
synchronized (Singleton.class) {
if (instance == null) {//如果还没创建,那就创建一个
instance = new Singleton();
}
}
}
return instance;
}
}
🍇加volatile关键字的原因:
假设volatile关键字是为了保证内存可见性,避免在读取instance的时候出现错误。
🍍加多一层if语句的原因:
减少加锁 / 解锁的操作,减小资源开销。
首先我们要明白一点,就是加锁 / 解锁是一件开销比较大的操作,而懒汉模式线程不安全只发生在第一次创建对象的时候,而后续使用就不用再创建实例了。
它创建实例的过程是这样的:
假设有三个线程都调用了getInstance()方法
- 在第一次创建对象的时候进入第一层if
判断,发现instance为null,那么这三个线程都会执行if语句内的代码
- 于是这三个线程开始竞争同一把锁
假设是线程1率先拿到锁,那么它就会再判定一次instance是否为null,如果为null,就把这个对象创建出来。
- 当线程1释放锁了之后,线程2、线程3也开始尝试获得锁,获得锁之后发现instance对象被创建出来了之后,就不再创建对象了。
- 而后续的线程再调用getInstance方法的时候,在第一层if语句就停下来了,就不会再进行加锁 / 解锁的操作了。
🍏2.阻塞队列
🍷2.1什么是阻塞队列
阻塞队列是一种特殊的队列,也遵守先进先出的原则。
阻塞队列是一种线程安全的数据结构,他有以下特点:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素
- 当队列空的时候,继续出队列就会阻塞,直到有其他线程往队列中插入元素
阻塞队列的一个经典应用场景就是“生产者消费者模型”。
🍹2.2生产者消费者模式
生产者消费者模式就是利用一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接联系,而是通过阻塞队列来进行通讯。所以生产者在生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中取。
没使用阻塞队列的情况:
如果生产者和消费者直接通信,那么耦合度和风险就非常高了:如果我们要加入消费者D,那么就要改变生产者的代码。且如果生产者奔溃了,那么消费者A、B、C都得玩完~~
使用阻塞队列的情况:
生产者与消费者提供阻塞队列进行通信,如果我们要加入消费者D,那么就让它直接从阻塞队列读取数据,这样生产者就不用改变代码了。且如果生产者崩溃了,由于消费者与生产者并不直接联系,因此消费者不受牵连,还可以从阻塞队列中获得数据,直到阻塞队列为空。
使用阻塞队列的优势:
🍕1.阻塞队列相当于一个缓冲区,平衡了生产者与消费者的处理能力,起到“削锋填谷”的作用。
🍔2.阻塞队列可以使生产者与消费者解耦,使代码高内聚,低耦合。(高内聚:相关的代码,分门别类,放在一块儿。低内聚:相关的代码东一块儿西一块儿。高耦合:代码与代码之间牵一发而动全身。低耦合:修改一个代码,对另一个代码的影响小。)
🍸2.3标准库中的阻塞队列
Java中内置了阻塞队列——BlockingQueue
- BlockingQueue是一个接口,真正实现的类使LinkedBlockingQueue。
- put方法用于阻塞队列的入队列,如果阻塞队列满了,则会阻塞等待。take方法用于阻塞队列的出队列,如果阻塞队列为空,也会阻塞等待。
- BlockingQueue也有offer、poll、peek等方法,但这些方法不带有阻塞队列特性。
利用BlockingQueue实现生产者消费者模式:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Demo19 {
private static BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
public static void main(String[] args) throws InterruptedException {
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();//拿出元素
System.out.println(Thread.currentThread().getName() + "拿到了value:" + value);
} catch (InterruptedException e) {//使用take方法会抛出阻塞异常
e.printStackTrace();
}
}
}, "消费者");
customer.start();//启动消费者
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int value = random.nextInt(1000);
blockingQueue.put(value);//放入元素
System.out.println(Thread.currentThread().getName() + "放入了value" + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
}
🍻2.4实现阻塞队列
public class MyBlockingQueue {
private int[] values = new int[1000];
private int size = 0;
private int head = 0;
private int tail = 0;
synchronized public void put(int value) throws InterruptedException {
while (size == values.length) {
this.wait();
}
values[tail] = value;
tail++;
if (tail == values.length) {//由于实现的是循环队列,所以要在tail上溢的时候置为0
tail = 0;
}
size++;//元素个数增加一个
this.notifyAll();
}
synchronized public int take() throws InterruptedException {
int ret;
while (size == 0) {
this.wait();
}
ret = values[head];
head++;
if (head == values.length) {//由于实现的是循环队列,所以要在head上溢的时候置为0
head = 0;
}
size--;//元素个数减少一个
this.notifyAll();
return ret;
}
}
为什么在判定size是否为空或为满的时候要用while语句呢?
其实这个问题官方文档给了我们答案:
大致意思就是:线程可以在没有被通知、中断或超时的情况下唤醒,这就是所谓的虚假唤醒。wait()方法是可能被其他方法打断的,比如interrupt()方法,此时wait()方法等待的条件还未成熟就被提前唤醒,代码就可能不符合预期~~
官方文档给的解决方案:
🥦3.定时器
🚙3.1什么是定时器
定时器就相当于一个闹钟,到达指定时间后就执行某个指定的任务。
🚓3.2标准库中的定时器
- 标准库中提供了一个Timer类,Timer类的核心方法为 schedule。
- schedule 包含了两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)。
使用示栗🌰:
import java.util.Timer;
import java.util.TimerTask;
public class Demo20 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("三秒后执行该语句");
}
}, 3000);
}
}
🚗3.3实现定时器
定时器不仅能够管理一个任务,还能管理多个任务,我们还需要让这些任务根据执行时间先后执行,因此我们需要用到优先级阻塞队列:
import java.util.concurrent.PriorityBlockingQueue;
public class Timer {
class Task implements Comparable<Task> {
private Runnable command;
private long time;
public Task(Runnable command, long time) {
this.command = command;
this.time = System.currentTimeMillis() + time;
//System.currentTimeMillis()是当前时间,time是绝对时间
}
public void run() {
command.run();
}
@Override
public int compareTo(Task o) {
return (int) (time - o.time);
}
}
private PriorityBlockingQueue<Task> blockingQueue = new PriorityBlockingQueue<>();
private Object mailBox = new Object();
//mailBox存在的意义是避免worker线程忙等
class Worker extends Thread {
@Override
public void run() {
while (true) {
try {
Task task = blockingQueue.take();
if (task.time > System.currentTimeMillis()) {
//如果任务的执行时间比当前时间要大,证明还未到执行时间
//需要把该任务重新放入优先级阻塞队列中
blockingQueue.put(task);
synchronized (mailBox) {
mailBox.wait(task.time - System.currentTimeMillis());
//让线程等待一会儿,避免大量执行while循环造成忙等的情况
}
} else {
//执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public Timer() {
Worker worker = new Worker();
worker.start();//启动线程
}
public void schedule(Runnable command, long time) {
Task task = new Task(command, time);
blockingQueue.put(task);
//将任务放入
synchronized (mailBox) {
mailBox.notifyAll();
}
}
public static void main(String[] args) {
Timer timer = new Timer();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("我是一个平平无奇的任务");
timer.schedule(this, 1000);
}
};
timer.schedule(runnable, 1000);
}
}
🍀4.线程池
⚽4.1什么是线程池
线程池做的事情就是提前把线程准备好,创建线程不是从系统申请,而是直接从线程池里拿,如果线程不用了就放回线程池里。
线程池的好处:
虽然线程比进程更加轻量,但是如果频繁地创建销毁,那么开销也是不容忽视的。那么使用线程池就能减少创建销毁线程的频率,从而提高效率,减少开销。
🍕1.降低资源消耗。通过重复利用已创建的线程降低线程创建销毁造成的消耗。
🍔2.提高相应速度。当任务到达时,任务可以不需要等待线程的创建就能执行。
🍟3.提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
为什么从线程池里拿线程比创建线程更高效?
原因是从线程池里拿线程属于用户态操作,而创建线程需要在用户态和内核态之间切换,真正创建的时候属于内核态操作。用户态操作,时间是可控的。而内核态操作,时间就不可控了。
⚾4.2标准库中的线程池
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池
- newSingleThreadExecutor: 创建只包含单个线程的线程池
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令,是进阶版的 Timer
示栗🌰:
- ExecutorService 表示一个线程池实例。
- Executors 是一个工厂类,能够创建出几种不同风格的线程池。
- ExecutorService 的 submit 方法能够向线程池中提交若干个任务。
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
这里是借助工厂模式来创建对象,而不是用new。就是借助一些其他方法(一般是静态方法)来协助我们创建对象。
🏀4.3实现线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
//阻塞队列用来存放任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void sumbit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int n) {
//创建n个线程
for (int i = 0; i < n; i++) {
Thread thread = new Thread(() -> {
try {
while (true) {
//while循环,不断取任务
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//启动线程
thread.start();
}
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int number = i;//这里需要创建一个变量去接收i(变量捕获)
myThreadPool.sumbit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + number);
}
});
}
}
}
如果你看到这里,恭喜你已经学完了多线程初阶了,接下来还有多线程进阶等着大家哦~~