-
三高:高可用、高性能、高并发
-
在操作系统中运行程序就是进程,如看视频, 一个进程可以有多个线程,
如视频中同时播放声音、播放图像、显示字幕 -
Process:进程 Thread:进程
-
进程是资源分配的基本单位、线程是CPU调度的基本单位
区别 进程 线程 根本区别 作为资源分配的基本单位 作为CPU调度和执行的基本单位 开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小 所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行 分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除了CPU外,不会为线程分配内存(线程所使用的资源时它所属的进程的资源),线程组只能共享资源 包含关系 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程中不是一条线的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程 -
很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。
如果是模拟出来的多线程,即一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,
因为切换的很快,所以就有同时执行的错觉 -
在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、主线程
-
main()称之为主线程,为系统的入口点,用于执行整个程序
-
在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的
-
对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制
-
线程会带来额外的开销,如cpu调度时间、并发控制开销
-
每个线程在自己的工作内存交互、加载和存储,主内存控制不当会造成数据不一致
-
进程:正在运行的程序
- 进程是系统进程资源分配和调用的基本单位
- 每一个进程都有他自己的内存空间和系统资源
-
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
-
创建线程的三种方法
-
1.继承Thread类,Thread是一个线程类
- 要少用继承多用实现,因为java中类只能多继承,后期如果继承Thread的类需要继承其他类,就需要大量的重构代码
- 实现方式:
- 1.1. 定义一个Thread类的子类
- 1.2. 在子类中重写Thread类的run()方法
- 1.3. 然后创建Thread类的对象
- 1.4 启动线程
- Thread的子类中并不是所有的代码都需要被线程执行,为了区分哪些代码需要被线程执行,java就提供了run()方法
- 直接调用子类的run方法并不会启动线程,应该调用子类的start()方法
- start()方法可以让此线程开始执行,并且是由java虚拟机调用此线程的run()方法的
- 重点:
- Thread子类为什么要重写start()方法?
- 因为run()方法是用来封装被线程执行的代码
- run()方法和start()方法的区别
- run():封装线程执行的代码,如果直接调用run()方法,相当于调用一个普通方法
- start():启动线程,然后由JVM调用此线程的run()方法
- Thread子类为什么要重写start()方法?
- Thread类中设置和获取线程名称的方法
- void setName(String name):将此线程的名称更改为name,默认名称为Thread-i, i = 0, 1, 2…
- String getName():返回此线程的名称
- Thread类中: static Thread currentThread():返回当前正在执行的线程对象
- Thread.currentThread().getName(): 获取当前正在执行的线程的名称
-
2.实现Runnable接口
- 2.1. 定义一个类MyRunnable实现Runnable接口
- 2.2. 在MyRunnable类中重写run()方法
- 2.3. 创建MyRunnable类的对象
- 2.4. 创建Thread类的对象,把MyRunnable对象作为构造方法的参数传入Thread对象
- 2.5. 启动线程
public class MyThread1 implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class MainThread1 { public static void main(String[] args) { MyThread1 myThread1 = new MyThread1(); // 这里是把myThread1作为1个公共的资源 new Thread(myThread1, "线程A").start(); new Thread(myThread1, "线程B").start(); new Thread(myThread1, "线程C").start(); } } -
相比继承Thread类,实现Runnable接口的好处:
- 避免了Java单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,
较好的体现了面向对象的设计思想
-
3.实现Callable接口, 重写call()方法
- 属于JUC高并发领域
-
-
如果想要执行线程,就必须调用start()方法,将线程加入到调度器中,此时线程不一定立即执行,
具体是有系统安排调度分配执行,直接调用run()方法不是开启多线程,而是普通方法
- 线程分为用户线程 和 守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕,如后台记录操作日志、监控内存使用等
- 线程默认是用户线程
- 线程调度有两种调度模型
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,
优先级高的线程获取的CPU时间片相对多一些
- Java使用的是抢占式调度模型
- 假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,
才可以执行执行指令,所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的 - Thread类中设置和获取线程优先级的方法
- public final int getPriority():返回此线程的优先级
- public final void setPriority(int newPriority):更改此线程的优先级
- 线程默认优先级是5, main线程的优先级也是5
- 线程优先级从1 - 10,均为整数
- 线程优先级高,仅仅代表线程获取到CPU时间片的几率高,但并不是一定会优先执行,
只有在次数较多或者多次运行的时候才能体现出优先级高,线程就优先执行
- 我们在使用Runnable接口,实现多线程的时候,启动必须借助Thread对象,那么这个Thread对象就叫做代理对象
- 静态代理 和 动态代理的区别
- 静态代理的代理类是我们已经写好的,可以直接拿来用,静态代理中真实类 和 代理类需要实现相同的接口
- 动态代理的代理类是在运行过程中动态构建出来的
- Lambda表达式主要用于简化匿名内部类,它属于函数式编程的概念
- 内部类是随着外部类的使用而加载,如果外部类不使用,内部类就不会加载,不会随着程序的加载而加载,也就不会编译
- 静态内部类放在类中、局部内部类放在方法体中、匿名内部类放在方法参数中
- 使用Lambda表达式时:要求接口中只能有一个抽象方法
- 扩展
- lambda 表达式引用方法
- 有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以可以利用 lambda表达式的接口快速指向一个已经被实现的方法
- 语法:方法归属者::方法名 静态方法的归属者为类名,普通方法归属者为对象
public class Exe1 { public static void main(String[] args) { ReturnoneParam lambda1 = a -> doubleNum(a); System.out.println(lambda1.method(3)); //lambda2 引用了已经实现的 doubleNum 方法 ReturnoneParam lambda2 = Exe1::doubleNum; System.out.println(lambda2.method(3)); Exe1 exe = new Exe1(); //lambda4 引用了已经实现的 addTwo 方法 ReturnoneParam lambda4 = exe::addTwo; System.out.println(lambda4.method(2)); } public static int doubleNum(int a) { return a * 2; } public int addTwo(int a) { return a + 2; } }
- lambda 表达式引用方法
方法名 | 说明
----------------------------------|--------------------------------
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join() | 等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
void setDaemon(boolean on) | 将此线程标记位守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
-
静态方法
- static Thread currentThread():返回当前正在执行的线程对象
- static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数
-
非静态方法
-
setName(String name):将此线程的名称更改为name,默认名称为Thread-i, i = 0, 1, 2…
-
getName():获取当前正在执行的线程的名称
-
setPriority(int newPriority):更改此线程的优先级
-
getPriority():返回此线程的优先级
线程优先级的设定建议在start()调用前 -
run():封装线程执行的代码,如果直接调用run()方法,相当于调用一个普通方法
-
start():启动线程,然后由JVM调用此线程的run()方法
-
void join():插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
myThread1.start(); try { myThread1.join(); } catch (InterruptedException e) { e.printStackTrace(); } myThread2.start(); myThread3.start();- void setDaemon(boolean on):将此线程标记位守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hAdjf4d-1632891916549)(…/NoteImg/线程生命周期.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noDhFOy2-1632891916553)(…/NoteImg/线程生命周期1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mhwcWDHB-1632891916561)(…/NoteImg/线程生命周期2.png)]
- 线程进入就绪状态的方法:
- 1.线程调用start()方法
- 2.阻塞解除(同步解除、调用notify唤醒线程、wait等待时间到了、join执行完了、sleep休眠时间到了、IO流阻塞借宿)
- 3.调用static yield()方法
- 4.jvm本身将cpu从本地线程切换到其他线程
- 线程进入阻塞状态的四种原因:
- 1.调用static sleep()方法
- 2.调用wait()方法:
- 3.调用join()方法:插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
- 4.I/O
- 线程进入死亡状态:
- 1.代码执行完了,正常结束
- 2.这个线程被强制截止:stop()、destroy() 但是这两个方法不推荐使用,已经过时了,不安全
-
1.线程停止:stop()、destroy()
- jdk不推荐使用这两个方法
- 解决方案:提供一个boolean类型的终止变量,当这个变量置为false时,则线程终止运行
class MyThread implements Runnable { private boolean flag = true; @Override public void run() { while (true) { System.out.println("线程正在运行"); } } public void stop() { this.flag = false; } } -
2.static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数
- sleep()存在异常InterruptedException, 由于sleep()存在异常,
但是run()方法不可以throws线上抛出异常,所以如果在run()方法体内调用sleep()方法,只能用try catch包裹 - sleep()调用后线程进入阻塞状态
- sleep()时间达到后,线程进入就绪状态
- 每一个对象都有一个锁,sleep不会释放锁,sleep会占据资源,使其他线程也不能使用资源,直到时间结束后,才可以继续运行
- 使用方法:Thread.sleep(millis)
- sleep()存在异常InterruptedException, 由于sleep()存在异常,
-
3.礼让:static void yield()
- 让当前正在执行的线程暂停,注意不是阻塞线程,而是将线程从运行状态转入就绪状态
- 礼让后,其他线程是可以继续使用资源的
- 使用方法:Thread.yield()
- 使用yield()后,该线程重回调度队列,与其它线程公平竞争
- 礼让不是每次都一定成功,礼让有可能失败
-
4.插队:join()
- 插队,等待这个线程死亡(如果某个线程调用了这个方法,
那么其它的线程必须等待这个线程执行完毕才有机会执行),此时其它的线程阻塞
- 插队,等待这个线程死亡(如果某个线程调用了这个方法,
-
5.等待:wait()
- wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”) - notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;
notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。 - wait(long timeout)让当前线程处于“等待(阻塞)状态”,
“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,
当前线程被唤醒(进入“就绪状态”)。
- wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
-
6.Thread.activeCount():当前正在活动的线程数
-
7.boolean isAlive():判断线程是否还活着,即线程是否还未终止
-
8.setName(): 给线程取一个名字
-
9.getName(): 获取线程名字
-
10.currentThread():获取当前正在运行的线程对象
- NEW:尚未启动的线程处于此状态
- new Thread(),即处于新生状态的线程
- RUNNABLE:在Java虚拟机中执行的线程处于此状态
- 处于就绪状态 或 运行状态的线程
- BLOCKED:被阻塞,等待监视器锁定的线程处于此状态
- wait()、 I/O
- WAITING:正在等待另一个线程执行特定动作的线程处于此状态
- wait(),with no timeout
- join()
- TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- static sleep(long millis)
- join(long millis)
- TERMINATED:已经退出的线程处于此状态
- 处于死亡状态
- 1.代码执行完了,正常结束
- 2.这个线程被强制截止:stop()、destroy() 但是这两个方法不推荐使用,已经过时了,不安全
- 需求:某电影院目前正在上映国产大片,共有100张票,而它有三个窗口,请设计一个程序模拟该电影院买票
- 1.定义一个类SellTicket实现Runnable接口,里边定义一个成员变量:private int tickets = 10;
- 2.在SellTicket类中重写run()方法实现买票
- A:判断票数大于0, 就卖票,并告知是哪个窗口卖的
- B:买了票之后,总票数-1
- C:票没有了,也可能有人来问,所以这里用死循环让卖票的动作一致执行
public class SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
-
- 如果没出售一张票都需要一定的时间延迟,那么假设每次出票时间100毫秒,用sleep()方法实现
public class SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
* 这样会出现票的重复卖出、卖出负数票的情况
-
使用同步代码块解决上述出现的线程问题
- 判断多线程程序出现数据安全问题的标准
- 1.是否是多线程环境
- 2.是否有共享数据
- 3.是否有多条语句操作共享数据
-
由于我们上述的案例满足了这三个要求,所以会出现安全问题
-
只有三条都满足了,才会出现安全问题
-
如何解决多线程安全问题?
- 基本思想:让程序没有安全问题的环境
-
如何实现?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方法来解决
-
1.同步代码块:
- 锁住操作共享数据的多条语句,可以使用同步代码块实现
- 基本语法:
synchronized(任意对象){
多条语句操作共享数据的代码
} - synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
- 用的对象不能是局部变量,因为我们要求每次用的锁是同一把锁
-
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都回去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
-
2.同步方法:就是把synchronized关键字加到方法上
- 基本语法:
修饰符 synchronized 返回值类型 方法名(方法参数) {
多条语句操作共享数据的代码
} - 同步方法的锁是:this
- 基本语法:
-
3.同步静态方法:就是把synchronized关键字加到静态方法上
- 基本语法:
修饰符 static synchronized 返回值类型 方法名(方法参数) {
多条语句操作共享数据的代码
} - 同步静态方法的锁是:类的字节码文件,即类名.class
- 基本语法:
-
线程安全的类:StringBuffer、Vector、Hashtable
-
一般来说:多线程中使用StringBuffer,但是Vector和Hashtable在多线程中也不使用,
- java.util.Collections类下:static List synchronizedList(List list):返回指定列表支持的同步(线程安全)列表
- set 和 map同理
- java.util.concurrent.CopyOnWriteArrayList;
- CopyonWriteArrayList securityList = new CopyOnWriteArrayList<>();// 得到一个线程安全的ArrayList,内部加了锁
-
优点:
- 同一进程的多个线程共享同一块存储空间时,加锁,保证了数据的正确性
-
缺点:
-
- 一个线程持有锁会导致其它所有需要此锁的线程挂起
-
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
-
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
-
-
synchronized 方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,
每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,
方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。 -
由于我们可以通过 private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,
这套机制就是synchronized关键字,它包括两种用法:synchronized方法和 synchronized块。 -
同步块可以粒度更小的锁定资源
-
可重入锁:如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立即成功,
并且会将这个锁的计数值加1,
而当线程退出同步代码块时,计数器会递减,并且当计数值等于0时,就会释放锁。如果没有可重入锁的支持,
在第二次企图获得锁时将会进入死锁状态 -
可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,而其他的线程是不可以的
-
synchronized 和 ReentrantLock 都是可重入锁。
-
可重入锁的意义之一在于防止死锁。
-
可重入锁的实现原理:
- 通过为每个锁关联一个请求计数器和一个占有它的线程。
当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1
如果同一个线程再次请求这个锁,计数器将递增;
每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放
- 通过为每个锁关联一个请求计数器和一个占有它的线程。
-
自定义不可重入锁:
public class LockTest {
private MyLock myLock = new MyLock();
public void a() throws InterruptedException {
myLock.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true
doSomething();// isLocked = true --> myLock.lock() --> wait() 于是本线程就一直处于阻塞状态,无法解除
myLock.unlock();
}
public void doSomething() throws InterruptedException {
myLock.lock();
System.out.println("测试。。。。。");
myLock.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
lockTest.a();
}
}
class MyLock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}
- 自定义可重入锁:
public class LockTest1 {
private MyLock1 myLock1 = new MyLock1();
public void a() throws InterruptedException {
myLock1.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true
doSomething();// isLocked = true --> myLock.lock() --> wait() 于是本线程就一直处于阻塞状态,无法解除
myLock1.unlock();
}
public void doSomething() throws InterruptedException {
myLock1.lock();
System.out.println("测试。。。。。");
myLock1.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest1 lockTest1 = new LockTest1();
lockTest1.a();
}
}
class MyLock1{
private boolean isLocked = false;
private Thread lockedBy = null;
private int holdCount = 0;
public synchronized void lock() throws InterruptedException {
Thread thread = Thread.currentThread();
while (isLocked && lockedBy != thread) {
wait();
}
isLocked = true;
lockedBy = thread;
holdCount++;
}
public synchronized void unlock() {
if (Thread.currentThread() == lockedBy) {
holdCount--;
if (holdCount == 0) {
isLocked = false;
notify();
lockedBy = null;
}
}
}
}
- synchronized是可重入锁
- ReentrantLock是可重入锁
- 之前说的synchronized中套synchronized只是可能会发生死锁
-
根据锁是否沿用:
- 可重入锁
- 公平锁
- 不公平锁
- 不可重入锁
- 可重入锁
-
悲观锁:synchronized是独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁
-
乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
- 比如更新数据,如果失败了就不断重试,直到成功为止
- 现实中的秒杀,有一个version
-
Compare and Swap:比较并交换
-
乐观锁的实现:
- 有三个值:当前的内存值Version、旧的预期值A、可以更新的值B。
- 操作:先获取到内存当中的内存值Version,再将内存值Version和原值A作比较,如果相等就可以修改B并返回true,
否则,什么都不能做,并返回false
-
CAS是硬件级别的操作,(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率要比加锁操作高
-
CAS是一组原子操作,不会被外部打断
-
但是存在AVA的问题:如果变量Version初次读取的时候也是A,并且在准备赋值的时候检查到它仍然是A,
那能说明它的值没有被其它线程修改过了吗?如果子啊这段期间曾经被改为B,然后又改回A,那CAS操作就会误认为它从来没有被修改过
- 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,
在哪里释放了锁,为了更清晰的表达如何 加锁 和 释放锁,JDK1.5以后提供了一个新的锁对象Lock - Lock类是一个接口
- Lock的实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
- 其中的2个方法:
- void lock():获得锁
- void unlock():释放锁
- ReentrantLock():无参构造方法
- ReentrantLock类实现了Lock接口
public class SellTicket implements Runnable{
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 通过lock获得锁
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}finally {
// 通过lock释放锁
lock.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
生产者消费者
-
生产者消费者模式是一个十分经典的多线程协作的模式
-
所谓生产者消费者问题,实际上主要包含了两类线程:
- 一类是生产者线程,用于生产数据
- 一类是消费者线程,用于消费数据
-
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
- 生产者生产数据之后直接放置公共数据区中,并不需要关心消费者的行为
- 消费者只需要从共享数据区中去获得数据,并不需要关心生产者的行为
生产者 ---------> 共享数据区域 <-------- 消费者
-
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用功能。
这几个方法在Object类中,Object类的等待和唤醒方法方法名 说明 void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法 void notify() 唤醒正在等待对象监视的单个线程 void notifyAll() 唤醒正在等待对象监视的所有线程 -
生产者消费者案例中包含的类:
- 奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
- 生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
- 消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
- 测试类(BoxDemo): 里边有main()方法,main()方法中的代码步骤如下:
- 1.创建奶箱对象,这是共享数据区域
- 2.创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
- 3.创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
- 4.创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
- 5.启动线程
-
生产者消费者问题是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖、互为条件
- 对于生产者,没有生产产品之前,需要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续产生新产品以供消费
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized可以阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(即通信)
-
线程通信
- 解决方式1:并发协作模型 “生产者/消费者模式” --> 管程法
- 生产者:负责生产数据的模块(这里的模块可能是:方法、对象、线程、进程)
- 消费者:负责处理数据的模块(这里的模块可能是:方法、对象、进程、线程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个"缓冲区";
生产者将生产好的数据放入"缓冲区",消费者从"缓冲区"拿要处理的数据 - 管程法:使用共享内存(缓冲区)的方式结局生产者并发协作,缓冲区看起来像一个管道,因此叫做管程法
public class Main { public static void main(String[] args) { Container container = new Container(); new Producer(container).start(); new Consumer(container).start(); } } class Producer extends Thread { private Container container; private int produceDadaCount = 0; public Producer(Container container) { this.container = container; } @Override public void run() { while (true) { synchronized (container) { container.push(new Data(++produceDadaCount)); System.out.println("正在生产第" + produceDadaCount + "个数据"); } } } } class Consumer extends Thread { private Container container; public Consumer(Container container) { this.container = container; } @Override public void run() { while (true) { synchronized (container) { System.out.println("正在消费生产的第" + container.pop().getId() + "个数据"); } } } } class Container { private Data[] dataArr = new Data[10]; private int count = 0; public void push(Data data) { if (count == dataArr.length) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 先在当前位置存,然后索引先后移动一位,所以count指向的是下一个可以存储数据的位置 dataArr[count++] = data; notifyAll(); // 存在生产了,可以唤醒消费者 } public Data pop() { if (count == 0) { try { wait(); // 此时线程阻塞 ,当生产者通知消费时,才能解除阻塞 } catch (InterruptedException e) { e.printStackTrace(); } } // 由于count指向的是下一个可以存储数据的位置,所以count应当先减1,再取值 Data data = dataArr[--count]; notifyAll();// 存在空间了,可以通知生产者生产 return data; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } } class Data { private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } public Data(int id) { this.id = id; } }- 解决方式2:并发协作模型 “生产者/消费者模式” --> 信号灯法
- 信号灯法:借助标志位
public class Main { public static void main(String[] args) { Data data = new Data(); new Producer(data).start(); new Consumer(data).start(); } } class Producer extends Thread{ private Data data; public Producer(Data data) { this.data = data; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i % 2 == 0) { data.setResource("偶数"); }else { data.setResource("奇数"); } } } } class Consumer extends Thread{ private Data data; public Consumer(Data data) { this.data = data; } @Override public void run() { for (int i = 0; i < 20; i++) { data.getResource(); } } } class Data { private String resource; private boolean signal = true; public synchronized String getResource() { // 1.(共享区域无资源时)消费者无法获取数据时 // 生产者要生产资源,消费者等待 if (signal) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 2.(共享区域有资源时)消费者可以消费 System.out.println("消费者消费了" + resource); notifyAll(); signal = true; return resource; } public synchronized void setResource(String resource) { // 1.(共享区域有资源时)生产者无法生产数据 // 消费者要消费数据,生产者等待 if (!signal) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 当signal = true时,生产者可以生产 // 2.(共享区域无资源时)生产者可以生产数据 this.resource = resource; System.out.println("生产者生产了" + resource); notifyAll(); signal = false; } }
- 解决方式1:并发协作模型 “生产者/消费者模式” --> 管程法
-
方法:
方法名 作用 void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,与sleep()不同,wait()会释放锁 void wait(long timeout) 指定等待的毫秒数 void notify() 唤醒一个处于等待状态的线程 void notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 -
以上四个方法军事java.lang.Object类中的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常
-
wait()和sleep():
- 调用wait()后,会释放锁,即该线程处于阻塞状态,但是他并不占用资源
- 调用sleep()后,该线程处于阻塞状态,但是它依旧占用资源,他所占用的资源在sleep()时间未到之前,不能被其它线程使用
- 作用:可以指定在固定的时间点,执行代码
- 通过Timer和Timetask,我们可以实现定时启动某个线程
- java.util.Timer:类似闹钟的功能,本身实现的就是一个线程
- java.util.TimerTask:一个抽象类,该类实现了Runnable接口,所以该类具备多线程功能
- Timer:
- 线程调度任务 以供将来在后台线程中执行的功能,任务可以安排依次执行,或者定期重复执行
public class MyTimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
// 执行安排,1秒后执行线程new MyTask()线程,之后每隔200毫秒执行一次
// timer.schedule(new MyTask(), 1000);
// 执行安排,1秒后执行new MyTask()线程,之后每隔200毫秒执行一次
// timer.schedule(new MyTask(), 1000, 200);
// 执行安排, 指定时间第一次执行new MyTask()线程,之后每隔200毫秒执行一次
Calendar cal = new GregorianCalendar(2021, 12, 21, 21, 21, 21);
timer.schedule(new MyTask(), new Date(5000), 200);
}
}
class MyTask extends TimerTask {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("放空大脑,休息一会儿");
}
System.out.println("end............");
}
}
任务定时调度框架QUARTZ
- 该框架有四大部分组成:
- Scheduler:调度器,控制所有的调度
- Trigger:触发条件,采用DSL模式
- JobDetail:需要处理的JOB
- Job: 执行逻辑
- QUARTZ已经集成到Spring中
- DSL:Domain-specific language领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,
即领域专用语言,声明式编程:- 1.Method Chaining 方法链 Fluent Style流畅风格, builder模式构建器
- 2.Nested Function 嵌套函数
- 3.Lambda expressions/Closures
- 4.Functional Sequence
- volatile:易变、可变:说的是变量在主内存和工作内存出现不一致的情况
- 只要变量用volatile修饰,对变量的修改就会立刻写入到主内存当中
- volatile保证线程间变量的可见性,简单地说就是当线程A对变量x进行了修改之后,
在线程A后面执行的其他线程能看到变量x的变动,更详细地说是要符合以下两个规则:- 线程对变量进行修改之后,要立刻回写到主内存
- 线程对变量读取的时候,要从主内存中读,而不是缓存
- volatile不能保证原子性,但可以避免指令重排
- 1)通过在总线加LOCK#锁的方式
- 2)通过缓存一致性协议
- 这2种方式都是硬件层面上提供的方式。
- 概念:多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能进行,
而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有"两个以上对象的锁"时,
就可能会发生死锁 - 过多的同步可能造成相互不释放资源,从而相互等待,一般发生于同步中持有多个对象的锁,进而发生死锁
- 解决死锁方案:不要在同一个代码块中同时持有多个对象锁
- 在多线程环境下,对外存在一个对象
- 单例模式
- 1.构造器私有化 --> 避免外部new构造器
- 2.内部提供私有的静态属性 --> 存储对象的地址(这里如果直接new对象了,我们就称之为饿汉式,如果没有new对象就称之为懒汉式)
- 3.提供公共的静态方法 --> 获取属性
- 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,
因为局部变量只有线程自己才能看见,不会影响其它线程 - ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。
说白了,ThreadLocal就是想在多线程的环境下去保证成员变量的安全,常用方法:get/set/initialValue - JDK建议ThreadLocal定义为private static
- ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,身份信息等,
这样一个线程的所有调用到的方法都可以非常方便地访问这些资源 - 每个线程在ThreadLocal中都会有自己的一片区域,在每个线程中通过threadLocal.set(保存的对象)时,
修改的只是ThreadLocal中自己的那个线程的那片区域中的存内容 - ThreadLocal中的两种初始值设置方式是设置所有的线程在ThreadLocal中存储的初始值
- ThreadLocal不会继承上下文环境的数据,也就是说A线程中创建了B线程,那么B线程在ThreadLocal中的初始值为最开始初始化的值,
即使之前A线程该变量自身在ThreadLocal存储的值,B线程在ThreadLocal中的初始值并不会受影响
public class ThreadLocalTest {
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Integer initialValue() {
return 200;
}
};
private static ThreadLocal threadLocal1 = ThreadLocal.withInitial(()->300);
public static void main(String[] args) {
// 300
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
threadLocal.set(100);
// 400
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
new Thread(new MyRun()).start();
}
public static class MyRun implements Runnable {
@Override
public void run() {
// threadLocal.set(400);
// 400
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get()); // 200
}
}
}
- ThreadLocal上下文:
- threadLocal.get()在构造器中时,哪里调用就属于哪里,要找线程体,注意new Thread(new MyThread())是属于调用线程的
- threadLocal.get()在run方法内,取出的值是该线程自身的
- InheritableThreadLocal: 是ThreadLocal的一个子类
- 它会继承上下文环境的数据,也就是说A线程中创建了B线程,那么B线程在ThreadLocal中的初始值就和A一样
- 只是初次的时候会拷贝,但并不是共享
public class InheritableThreadLocalTest {
private static ThreadLocal threadLocal = new InheritableThreadLocal();
public static void main(String[] args) {
// null
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.set(100);
// 100
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
new Thread(()->{
// 100
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.set(200);
// 200
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
}
}
- 问题总结:
- 什么是进程,什么是线程,两者的关系
- 什么是多线程
- 调度方式以及知道的调度算法
- 什么时候会产生多线程并发问题?
- wait(),notify(), sleep(), yield(), join()
- destroy(), stop(),代替方案
- 什么是生产者消费者问题,有哪两种方式实现
- 什么是死锁
- 解释一下Lock锁
锁的分类介绍



