多线程
一、进程和线程
1.1进程
进程:程序是静止的,只有真正运行时的程序,才被称为进程。
特点:
- 单核CPU在任何时间点上。
- 只能运行一个进程。
- 宏观并行、微观串行。
1.2线程
线程:又称轻量级进程(Light Weight Process)。
- 程序中的一个顺序控制流程,同时也是CPU的基本调度单位。
- 进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程。
比如:
- 迅雷是一个进程,当中的多个下载任务即是多个线程。
- Java虚拟机是一个进程,默认包含主线程(main),通过代码创建多个独立线程,与main并发执行。
任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间。
- 运行数据:
堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
1.3创建线程
线程是对象–线程类–创建线程
创建有4种方式:
本质上是一种–就implements Runtable
1.3.1extends Thread
创建:
public class TestThread extends Thread{
// 必须重写run方法
@Override
public void run() {
for (int i = 1; i <+ 100; i++) {
System.out.println(Thread.currentThread().getName() +"--"+ i);
}
}
}
使用:
public static void main(String[] args) {
TestThread t1 = new TestThread();
TestThread t2 = new TestThread();
t1.start();
t2.start();
}
1.3.2implements Runnable
最常用的 – 适合共享数据
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() +"--"+ i);
}
}
}
public class TestRun {
public static void main(String[] args) {
MyThread tr = new MyThread();
Thread t1 = new Thread(tr);
Thread t2 = new Thread(tr);
t1.start();
t2.start();
}
}
案例:
定义一个集合,里面存的是1-100,用两个线程随机输出集合中的十个元素,输出完就删除这个元素,不考虑线程安全的问题,用实现接口和继承两种方式 实现,最后的要求是输出剩余元素
1.extends Thread:
public class ExT extends Thread{
ArrayList<Integer> list = new ArrayList<>();
public ExT(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int index = (int)(Math.random()*list.size()); // 随机生成集合范围里的下标
// 打印信息
System.out.println(Thread.currentThread().getName() +"-删除的元素为:-"+ list.get(index));
// 删除对用元素
list.remove(index);
}
}
}
public static void main(String[] args) throws InterruptedException {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
list.add(i);
}
System.out.println(list);
ExT e1 = new ExT(list);
ExT e2 = new ExT(list);
e1.start();
e2.start();
Thread.sleep(500);
System.out.println(list);
}
2.Implements Runtable:
public class ImRun implements Runnable{
ArrayList<Integer> list = new ArrayList<>();
{
for (int i = 1; i <= 100; i++) {
list.add(i);
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int index = (int)(Math.random()*list.size()); // 随机生成集合范围里的下标
// 打印信息
System.out.println(Thread.currentThread().getName() +"-删除的元素为:-"+ list.get(index));
// 删除对用元素
list.remove(index);
}
}
}
public static void main(String[] args) throws InterruptedException {
ImRun i = new ImRun();
Thread t1 = new Thread(i);
Thread t2 = new Thread(i);
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(i.list);
}
这种是会存在异常(当):
1.3.3Callable接口
1.3.3.1Callable
public interface Callable< V >{
public V call() throws Exception;
}
案例:
计算任务,一个包含了2万个整数的数组,
分拆了多个线程来进行并行计算,最后汇总出计算的结果。
public class CalcCall implements Callable<Integer>{
int[] nums;
public CalcCall() {}
public CalcCall(int[] nums) {
super();
this.nums = nums;
}
public Integer call(){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
public class TestCall {
public static void main(String[] args) throws Exception{
int[] num = new int[20000];
for (int i = 0; i < num.length; i++) {
num[i] = i + 1;
}
int count = 4;
int sum = 0;
List<FutureTask<Integer>> fts = new ArrayList<FutureTask<Integer>>();
for (int i = 0; i < count; i++) {
int[] subNums = new int[num.length/count];
for (int j = 0; j < subNums.length; j++) {
subNums[j] = num[j+num.length/count*i];
}
CalcCall c = new CalcCall(subNums);
// 转Runnable
FutureTask<Integer> f = new FutureTask<Integer>(c);
Thread t = new Thread(f);
t.start();
fts.add(f);
}
for (int i = 0; i < fts.size(); i++) {
sum += fts.get(i).get();
}
System.out.println("总和为: " + sum);
}
}
Runnable接口和Callable接口的区别:
- Callable接口中call方法有返回值,Runnable接口中run方法没有返回值。
- Callable接口中call方法有声明异常,Runnable接口中run方法没有异常。
1.3.3.2Future
- Future接口表示将要执行完任务的结果。
- get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
- 常用于把CalcCall转换为Runnable
- 相当于一个中介
CalcCall c = new CalcCall(subNums);
FutureTask<Integer> f = new FutureTask<Integer>(c);
1.3.4线程池*
1.3.4.1为什么需要线程池?
- 如果有非常的多的任务需要多线程来完成,且每个线程执行时间不会太长,这样频繁的创建和销毁线程。
- 频繁创建和销毁线程会比较耗性能。有了线程池就不要创建更多的线程来完成任务,因为线程可以重用。
1.3.4.2线程池原理
线程池用维护者一个队列,队列中保存着处于等待(空闲)状态的线程。不用每次都创建新的线程。
1.3.4.3线程池API
常用的线程池接口和类(所在包java.util.concurrent)。
Executor:线程池的顶级接口。
ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。
Executors工厂类:通过此类可以获得一个线程池。
方法名 | 描述 |
---|---|
newFixedThreadPool(int nThreads) | 获取固定数量的线程池。参数:指定线程池中线程的数量。 |
newCachedThreadPool() | 获得动态数量的线程池,如不够则创建新的,无上限。 |
newSingleThreadExecutor() | 创建单个线程的线程池,只有一个线程。 |
newScheduledThreadPool() | 创建固定大小的线程池,可以延迟或定时执行任务。 |
4 种线程池
// SingleThreadExecutor 单个线程的线程池
// FixedThreadPool 固定大小的线程池
// CachedThreadPool 带缓存的线程池
// ScheduledThreadPool 定时调度的线程池
单个线程的线程池:
ExecutorService pool = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特点
是核心线程=最大线程=1
队列无限大
OOM:
固定大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//带缓存的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点:核心线程为0,最大线程无界
队列的特点是同步队列,不做任何缓存
定时调度的线程池
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
//pool.execute(new MyTask(888));
//pool.schedule(new MyTask(8888), 2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new MyTask(888), 2, 3, TimeUnit.SECONDS);
pool.shutdown();
核心线程数由我们自己固定
最大线程无界
// execute源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
如果运行的线程少于corePoolSize,请尝试
以给定的命令作为第一个线程,启动一个新线程
任务。对addWorker的调用会自动检查运行状态和
workerCount,从而防止会增加
当它不应该时,返回false。
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
如果任务可以成功排队,那么我们仍然需要
再次检查我们是否应该添加一个线程
(因为自上次检查以来,已有的已死亡)或
自进入此方法以来,池已关闭。所以我们
重新检查状态,必要时回滚排队
已停止,如果没有线程,则启动新线程
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
如果我们无法将任务排队,那么我们会尝试添加一个新的
螺纹。如果失败了,我们知道我们已经关闭或饱和了
所以拒绝这个任务。
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
关闭线程:
shutDown(): void shutdown();
执行完正在执行的任务就关闭
shutDownNow(): List shutdownNow();
立即关闭,把没有执行完的任务打包作为集合返回给调用者
案例
计算任务,一个包含了2万个整数的数组,
分拆了多个线程来进行并行计算,最后汇总出计算的结果。
public static void main(String[] args) throws InterruptedException, ExecutionException {
int[] nums = new int[20000];
for (int i = 1; i <= 20000; i++) {
nums[i-1] = i;
}
// 使用线程池 确定个数
ExecutorService es = Executors.newFixedThreadPool(4);
Callable<Integer> c1 = new Callable<Integer>() {
int sum;
public Integer call() throws Exception {
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
};
FutureTask<Integer> f1 = new FutureTask<>(c1);
// 提交
es.submit(f1);
System.out.println(f1.get());
// 关闭线程池
es.shutdown();
}
1.4线程的调度
常用方法:
方法名 | 说明 |
---|---|
public static void sleep(long millis) | 当前线程主动休眠 millis 毫秒。 |
public static void yield() | 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。 |
public final void join() | 允许其他线程加入到当前线程中。 |
public void setPriority(int) | 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。 |
public void setDaemon(boolean) | 设置为守护线程线程有两类:用户线程(前台线程)、守护线程(后台线程) |
1.4.1setPriority()设置优先级
只是一个概率问题
取值是在1-10,默认是5
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public static void main(String[] args) {
TestThread t1 = new TestThread("线程1");
TestThread t2 = new TestThread("线程2");
t1.setPriority(Thread.MAX_PRIORITY); // 也可以是 1-10的整数
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
1.4.2更改线程名
-
getName()、setName()方法
th.setName();
-
继承的方式:
构造:
public MyThread(String name){
super(name);
}
实现接口的方式
Thread th = new Thread(r,name);
public MyThread(String name){
super(name);
}
实现接口的方式
Thread th = new Thread(r,name);
public class TestThread extends Thread{
public TestThread(String name) {
super(name);
}
// 必须重写run方法
@Override
public void run() {
for (int i = 1; i <+ 100; i++) {
System.out.println(Thread.currentThread().getName() +"--"+ i);
}
}
}
public static void main(String[] args) {
TestThread t1 = new TestThread("线程1");
TestThread t2 = new TestThread("线程2");
// t1.setName("线程1");
// t2.setName("线程2");
t1.start();
t2.start();
1.4.3休眠sleep
线程的休眠
释放cpu,但是不释放锁
练习:
编写一个抽取学员回答问题的程序,要求倒数三秒后输出被抽中的学员姓名
i. 采用数组存储6个学员姓名 String[]
ii. 生成0-5之间的随机数,确定被抽中的学员 start,end
iii. 在屏幕每隔一秒,依次输出3,2,1,然后输出学员姓名
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
String[] students = {"aa","bb","cc","dd","ee","ff"};
public void run() {
for (int i = 1; i <= 3; i++) {
Random random = new Random();
int num = random.nextInt(5);
System.out.println("被抽中的学生第" +i+"位学生是:"+students[num]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
for (int i = 3; i >= 1; i--) {
System.out.println(i);
Thread.sleep(1000);
}
t1.start();
}
1.4.4join
加入:必须优先把加入的线程任务执行完才会执行其余线程
案例:
医院每天放20个特需号和50个普通号,特需号看一个人需要200ms,普通号需要100ms,特需号优先高于普通号(5-8),如果每天普通号看到第10个号的时候,要求必须特需号看完后再继续看普通号,用多线程模拟该过程。
public class PuTong extends Thread{
Thread t;
public PuTong(Thread t) {
super();
this.t = t;
}
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println(Thread.currentThread().getName()+"普通号"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i==10) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class TeXuHao extends Thread{
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName()+"特需号"+i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TeXuHao t = new TeXuHao();
PuTong p = new PuTong(t);
// 设置优先级
p.setPriority(5);
t.setPriority(8);
p.start();
t.start();
}
运行结果:
1.4.5中断
本质是发送一个信号量,只是改变一个信号值
1.5线程的生命周期
线程状态(基本):新建、就绪、运行、终止。
二、线程安全
- 需求:A线程将“Hello”存入数组;B线程将“World”存入数组。
- 线程不安全:
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
2.1同步代码块
语法:
synchronized(临界资源对象){ //对临界资源对象加锁
//代码(原子操作)
}
- 每个对象都有一个互斥锁标记,用来分配给线程的。
- 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
- [线程退出同步代码块时,会释放相应的互斥锁标记。](
2.2同步方法
语法:
synchronized 返回值类型 方法名称(形参列表){ //对当前对象(this)加锁
// 代码(原子操作)
}
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
- 线程退出同步方法时,会释放相应的互斥锁标记。
- [如果方式是静态,锁是类名.class。](
2.3同步规则
只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
JDK中线程安全的类:
- StringBuffer
- Vector
- Hashtable
以上类中的公开方法,均为synchonized修饰的同步方法。
三、死锁
- 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
- 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
案例:
public class Dead implements Runnable{
Integer a;
Integer b;
public Dead(Integer a, Integer b) {
super();
this.a = a;
this.b = b;
}
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "--获取了" + a);
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "--获取了" + b);
}
}
}
}
public class TestDead {
public static void main(String[] args) {
// 利用了Integer缓冲数组
new Thread(new Dead(1,2)).start();
new Thread(new Dead(2,1)).start();
}
}
四、ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中*****填充的变量**属于当前线程***,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量
4.1案例:
有一千个时间,用多个线程输出。要求是一个线程里面只输出一个时间。
4.1.1版本1
自己创建1000个线程来输出
// 线程类
public class TimeTask implements Runnable { //传一个i,线程帮你输出对应的时间
private int i;
public TimeTask(int i) {
this.i = i;
}
public void run() {
String dateStr = DateUtil.format(i);
System.out.println(Thread.currentThread().getName()+"--"+dateStr);
}
}
//格式化日期的
public class DateUtil {
public static String format(int i) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(i*1000));
}
}
// 测试类 -- 生成1000个任务创建线程来完成
public class TestTime {
public static void main(String[] args) {
for(int i=1;i<=1000;i++) {
new Thread(new TimeTask(i)).start();
}
}
}
分析:
1.线程太多,浪费
2.SimpleDateFormat太多,对象浪费
4.1.2版本2
使用线程池解决线程多的问题,同时解决SimpleDateFormat的问题
// 测试类 - 使用线程池
public class TestTime {
public static void main(String[] args) {
//线程池的大小设置为多少?
ExecutorService pool = Executors.newFixedThreadPool(10);
for(int i=1;i<=1000;i++) {
pool.execute(new TimeTask(i));
}
}
}
// 格式化日期
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(int i) {
return sdf.format(new Date(i*1000));
}
}
分析:
会有相同的日期出现–SimpleDateFormat这个类是线程不安全的
一个类如果没有成员变量,那么这个类就是线程安全的。
4.1.3版本3
解决共用同一个SimpleDateFormat的线程安全问题
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(int i) {
String str;
synchronized (DateUtil.class) {
str = sdf.format(new Date(i * 1000));
}
return str;
}
}
分析:
效率不行,每一个要过来格式化日期的线程都要在format方法这儿排队
4.1.4版本4
最终的改进
使用ThreadLocal解决线程隔离的问题—一个线程一个SimpleDateFormat
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> th = new ThreadLocal<SimpleDateFormat>() {
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String format(int i) {
//需要用到SimpleDateFormat--从th中取出来
SimpleDateFormat sdf = th.get();//从ThreadLocal对象取出SimpleDateFormat
//sdf是线程隔离的,意味着每个线程有自己的sdf,不是所有线程共用
String str = sdf.format(new Date(i * 1000));
return str;
}
}
4.2源码分析
Thread类
ThreadLocal类
ThreadLocalMap类
// SimpleDateFormat这个对象是通过get方法获取的
// ThreadLocal类
//get
public T get() {
// 获取当前的线程
Thread t = Thread.currentThread();
// 根据当前的线程去获取到ThreadLocalMap
// 这是一个ThreadLocal的内部类, map类型
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();// 这里只会在这个线程第一次调用get方法的时候去执行
}
// getMap
ThreadLocalMap getMap(Thread t) { //
// t是线程,返回的是当前线程里面的一个成员变量 -- ThreadLocalMap
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();//调用初始化值的方法--子类的方法
//new 的那一个SimpleDateFormat放到value里
Thread t = Thread.currentThread();//获取到当前线程
ThreadLocalMap map = getMap(t);//获取map
if (map != null)
map.set(this, value);//修改当前map的值
else
createMap(t, value);//创建一个ThreadLocalMap的对象
return value;
}
void createMap(Thread t, T firstValue) {
//底层实现--ThreadLocalMap---以ThreadLocal对象为键,值是存的对象
//意味着每个线程都维护了一个ThreadLocalMap的集合,这个集合键都是ThreadLocal对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//Thread类
threadLocals 是一个ThreadLocalMap的一个集合
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
五、生产者消费者模型
引入:
需求:模拟银行存钱、取钱
// 银行卡 - 实体类 - 共用的
public class Card {
private int money;
public Card(int money) {
super();
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void save() {
while(money >= 2000) {
System.out.println("卡里有钱了,等等在存吧");
try {
wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setMoney(getMoney() + 1000);
System.out.println("存钱成功,当前余额为:" + money);
this.notify(); // 唤醒锁
}
public void take() {
while(money <= 0) {
System.out.println("卡里没钱了,等等再来取钱吧");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setMoney(getMoney() - 1000);
System.out.println("取钱成功,当前余额为: " + money);
this.notify(); // 唤醒锁
}
}
// 存钱的线程
public class BF implements Runnable{
Card card;
public BF(Card card) {
super();
this.card = card;
}
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (card) {
System.out.println(Thread.currentThread().getName() + "第" + i + "次存钱");
card.save();
}
}
}
}
// 取钱的线程
public class GF implements Runnable{
Card card;
public GF(Card card) {
super();
this.card = card;
}
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (card) {
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱");
card.take();
}
}
}
}
// 测试类
public class Teat {
public static void main(String[] args) {
Card card = new Card(0);
BF b1 = new BF(card);
BF b2 = new BF(card);
GF g1 = new GF(card);
GF g2 = new GF(card);
new Thread(b1,"男1").start();
new Thread(b2,"男2").start();
new Thread(g1,"女1").start();
new Thread(g2,"女2").start();
}
}
wait():由锁来调用
这个方法是Object类的,锁可能是任意的对象,因此这个方法放到Object这个类里面是合适的。
这个方法的意思是当前线程释放掉占有的锁,加入锁的等待队列。
要想出来重新抢锁必须有其他的线程去唤醒他。—notify()随机唤醒一个线程
notifyAll()唤醒所有线程
死等现象:
线程调用了wait()方法并且没有传参数 → 自己醒不过来,只能调用notify()方法来唤醒无限等待状态
无限等待状态 和 睡眠状态 称为 冻结状态.
解决方法
1唤醒所有等待 – notifyAll()
2.wait(等待时间)
六、仓储模型
案例:
模拟一个蛋糕店,有两个厨师,一个消费者
每个厨师每两秒生产一个蛋糕,消费者每秒钟消费一个蛋糕,蛋糕店里有一个橱柜,橱柜里能
存放6个蛋糕,模拟生产过程,要求,橱柜满了就让厨师休息,橱柜空了,就让消费者休息。
sleep不释放锁,意味着sleep方法不会影响到橱柜满还是不满
// 蛋糕类
public class Cake {
private String brand;
private String dateTime;
public Cake() {
}
public Cake(String brand, String dateTime) {
super();
this.brand = brand;
this.dateTime = dateTime;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getDateTime() {
return dateTime;
}
public void setDateTime(String dateTime) {
this.dateTime = dateTime;
}
@Override
public String toString() {
return brand + "--" + dateTime ;
}
}
// 橱柜 - 仓库
public class CakeFactory {
LinkedList<Cake> cakes = new LinkedList<>(); // 蛋糕容器
int maxCapacity = 6; // 橱柜最大容量
int currentCapacity = 0;
public CakeFactory() {
}
public synchronized void cook(Cake cake) {
while (currentCapacity >= maxCapacity) {
System.out.println("橱柜满了,等等在做蛋糕吧");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
currentCapacity++;
cakes.add(cake);
System.out.println("橱柜当前蛋糕数为:" + currentCapacity);
try {
Thread.sleep(2000); // 生产一个休息2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notifyAll();// 唤醒
}
public synchronized void take() {
while (currentCapacity == 0) {
System.out.println("蛋糕卖完了,等等在卖吧");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
currentCapacity--;
cakes.removeFirst();
System.out.println("橱柜当前蛋糕数为:" + currentCapacity);
try {
Thread.sleep(1000); // 消费一个蛋糕休息1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notifyAll();
}
}
// 消费者
public class Consumer implements Runnable{
CakeFactory cakeFactory;
public Consumer(CakeFactory cakeFactory) {
super();
this.cakeFactory = cakeFactory;
}
public void run() {
for (int i = 1; i <= 40; i++) {
System.out.println(Thread.currentThread().getName() + "--消费了一个蛋糕");
cakeFactory.take();
}
}
}
// 生产者
public class Producer implements Runnable{
CakeFactory cakeFactory;
public Producer(CakeFactory cakeFactory) {
super();
this.cakeFactory = cakeFactory;
}
public void run() {
for (int i = 1; i <= 20; i++) {
Cake cake = new Cake("华生蛋糕",LocalDateTime.now().toString());
System.out.println(Thread.currentThread().getName() + "--" + cake.getdateTime() + "生产了一个" + cake.getBrand());
cakeFactory.cook(cake);
}
}
}
// 测试类
public class Test {
public static void main(String[] args) {
CakeFactory cakeFactory = new CakeFactory();
new Thread(new Consumer(cakeFactory),"青鸢").start();
new Thread(new Producer(cakeFactory),"鸾").start();
new Thread(new Producer(cakeFactory),"凰").start();
}
}
生产者消费者模型:
把加锁的逻辑放在线程中
仓储模型:
把加锁的逻辑放在仓库中