其中线程池是博主写大创时自学的,考试不考。问题不大
Thread Sum
实现多线程
什么是线程:线程是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
多线程的实现方式:
方式1: 继承Tread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
两个问题:
- 为什么要重写run()方法?
- 因为run()是用来封装被线程执行的代码
- run()方法和start()方法的区别?
- run():封装线程执行代码,直接调用,相当于普通方法的调用
- start():启动线程,然后由JVM调用此线程run()方法
public class MyThreadDemo{
public static void main(String[] args){
//创建MYThread类对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.run();
my2.run();
/*
my1执行完后my2才执行
*/
my1.start();//启动线程,调用run方法
my2.start();
/*
my1和my2两个线程同时在跑
*/
}
}
public class MyThread extends Thread{
@Override
public void run(){
for(int i = 0; i < 100; i++){
sout(i);
}
}
}
设置和获取线程的名称
Thread类中设置和获取线程名称的方法
- 设置:
- void setName(String name): 将此线程的名称更改为等于参数name
- 通过带參方法设置名称:在MyThread中通过写无參构造和有參构造方法
super(name)
实现。因为带參方法在Thread()中的Thread(String name)
,要在自己写的类中写有參构造方法用super传值去调用父类(Thread)中的有參构造方法
- 获取:
- String getName(): 返回此线程的名称
- static Thread currentThread():返回对当前正在执行的线程对象的引用```sout(Thread.currentThread().getName());````
没有设置名称直接获取:
- 设置线程时,默认构造方法会给线程默认名称为“Thread-i”(i为第i个线程)
public class MyThreadDemo{
public static void main(String[] args){
//创建MYThread类对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("飞机");
MyThread my2 = new MyThread("高铁");
my1.start();//启动线程,调用run方法
my2.start();
/*
my1和my2两个线程同时在跑
*/
//sout(Thread.currentThread().getName());
}
}
public class MyThread extends Thread{
@Override
public void run(){
for(int i = 0; i < 100; i++){
sout(getName() + " " + i);
}
}
//设置无參构造方法和有參构造方法
public MyThread(){}
public MyThread(String name){
super(name);
}
}
多线程的实现方式(方法二)
相比继承Thread类,实现Runnable接口的好处
- 避免了Java单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想
声明一个实现Runnable接口的类。那个类然后实现了run方法。
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法。在MyRunnable类中不能直接使用Thread中的getName()/setName()方法,因为它没有继承Thread。要实现Thread中的方法用Thread.currentThread()来引出
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
public class MyRunnableDemo{
public static void main(String[] args){
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
//Thread(Runnable target)
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
//Thread(Runnable target,String name)
Thread t1 = new Thread(my,"飞机");
Thread t2 = new Thread(my,"高铁");
//启动线程
t1.start();
t2.start();
}
}
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 100;i++){
sout(Thread.currentThread.get.Name() + " " + i);
}
}
线程调度
线程有两种调度模型
- 分时调度模型:所有线程流程使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先相同,那么会随机选择一个,优先级高的线程获取CPU时间片相对多一些
Java使用的是抢占式调度模型
多线程程序的执行是随机的
Thread类中设置和获取线程优先级的方法
public final int getPriority()
:返回此线程的优先级public final void setPriority(int newPriority)
:更改此线程的优先级
Priority有范围:
- max:10
- min:1
- 默认值:5
线程优先级高,仅仅表示获取CPU时间片的几率高,而不是 每一次都跑在前面
线程控制
Method’s Name | Statement |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 插队。等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时(主线程执行完毕后),Java模拟机将退出(需要一定时间,不是马上停止) |
static void yeild() | 暂停当前正在执行的线程,并重新竞争一下优先级执行其他线程 |
boolean isAlive() | 测试当前线程是否处于活动状态 |
setPrority(int newPriority) | 更改线程的优先级 |
线程五个状态
线程停止
不建议使用JDK自带的已经过时了的stop
或者destroy
方法
使用一个标志位进行终止变量,在run方法中进行判断:当flag = false
,则线程停止
public class TestStep implements Runnable{
private boolean flag = true;
@Override
public void run(){
while(flag){
//running code
}
}
//对外提供方法更改标识
public void stop(){
this.flag = false;
}
}
线程中断
interrupt
区分三个方法,
方法 | 描述 | static? |
---|---|---|
public static boolean interrupted | 测试当前线程是否已经中断。调用该方法后,线程的中断状态会被重置。此方法内部调用的是私有的Thread.currentThread().isInterrupted(true); 带true 参数的isInterrupted 方法意思是返回当前线程的中断状态,然后reset当前线程的中断状态 | Yes |
public boolean isInterrupted() | 测试指定线程是否已经中断。线程的中断状态不受该方法的影响。内部调用的私有的参数为false 的isInterrupted(false) 方法 | No |
public void interrupt() | 中断线程,将中断状态设为true | No |
线程休眠
Thread.sleep(long millis)
,指定当前线程阻塞的毫秒数
- 异常:存在异常
InterruptedException
- 当达到线程sleep的时间后,线程进入就绪状态
- 可模拟网络延时,倒计时等
- sleep不会释放锁
线程等待与唤醒
如果没有在synchronized
方法内调用wait()
或者nodify()
的话,会抛出**IllegalMonitorStateException
**
线程名.wait()
- 让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
notify()/notiyfyAll()
- 是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
wait(long timeout)
让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()
方法或notifyAll()
方法,或者超过指定的时间量”,当前线程被唤醒**(进入“就绪状态”**)。
//main(主线程)
synchronized(t1) {
try {
t1.start();
t1.wait();
} catch(exception e){
e.printStackTrace();
}
}
//在 t1 线程中唤醒主线程
synchronized (this) {//这里的 this 为 t1
this.notify();
}
注:
-
synchronized(t1)锁定t1(获得t1的监视器)
-
synchronized(t1)这里的锁定了t1,那么wait需用t1.wait()(释放掉t1)
-
因为wait需释放锁,所以必须在
synchronized
中使用(没有锁定则么可以释放?没有锁时使用会抛出异常IllegalMonitorStateException
(正在等待的对象没有锁)) -
notify
也要在synchronized
中使用,应该指定对象,t1. notify()
,通知t1对象的等待池里的线程使一个线程进入锁定池,然后与锁定池中的线程争夺锁。那么为什么要在synchronized使用呢? t1. notify()需要通知一个等待池中的线程,那么这时我们必须得获得t1的监视器(需要使用synchronized),才能对其操作,t1. notify()程序只是知道要对t1操作,但是是否可以操作与是否可以获得t1锁关联的监视器有关。 -
synchronized()
,wait()
,notify()
对象一致性 -
在while循环里而不是if语句下使用wait(防止虚假唤醒spurious wakeup)
不是使调用线程等待,而是当前执行 wait 的线程等待
- 在主线程中调用
t1.wait(3000)
,主线程等待t1通过notify()唤醒 或 notifyAll()唤醒,或超过3000ms延时;然后才被唤醒。
class ThreadA extends Thread{
public ThreadA(String name) {
super(name);
}
public void run() {
synchronized (this) {
try {
Thread.sleep(1000); // 使当前线阻塞 1 s,确保主程序的 t1.wait(); 执行之后再执行 notify()
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" call notify()");
// 唤醒当前的wait线程
this.notify();
}
}
}
public class WaitTest {
public static void main(String[] args) {
ThreadA t1 = new ThreadA("t1");
synchronized(t1) {
try {
// 启动“线程t1”
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start();
// 主线程等待t1通过notify()唤醒。
System.out.println(Thread.currentThread().getName()+" wait()");
t1.wait(); // 不是使t1线程等待,而是当前执行wait的线程等待
System.out.println(Thread.currentThread().getName()+" continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
main start t1
main wait()
t1 call notify()
main continue
Sleep与Wait对比
sleep()
和wait()
方法都是Java中造成线程阻塞的方法
sleep()和wait()方法的阻塞线程的场景
①sleep()
实现线程阻塞的方法,我们称之为“线程睡眠”,方式是超时等待,怎么理解?就是sleep()通过传入“睡眠时间”作为方法的参数,时间一到就从“睡眠”中“醒来”;
②**wait()
方法实现线程阻塞的方法**,我们称之为“线程等待”,方式有两种:
1)和sleep()方法一样,通过传入“睡眠时间”作为参数,时间到了就“醒了”;
2)不传入时间,进行一次“无限期的等待”,只用通过notify()方法来“唤醒”。
sleep()和wait()的区别
- sleep()和wait()方法的区别之一,就是实现线程阻塞的方式不一样。
- 是否释放同步锁
①sleep()释放CPU执行权,但不释放同步锁;
②wait()释放CPU执行权,也释放同步锁,使得其他线程可以使用同步控制块或者方法。
以上,就是sleep()和wait()方法的两个关键性区别。
线程礼让
Thread.yield()
-
礼让线程:使当前正在执行的线程暂停,但不阻塞。
-
将线程从运行状态转为就绪状态
-
让CPU重新调度,但礼让不一定成功,还得看从就绪状态进入运行状态阶段
线程强制执行
线程名.join()
-
Join合并线程,让这个线程执行完成后,再执行其他线程。
也就是说,t.join()方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING 状态,直到线程t完成,此线程再继续;
通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
public static void main(String[] args) { Thread thread1 = new Thread(new JoinTester01("One")); Thread thread2 = new Thread(new JoinTester01("Two")); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("Main thread is finished"); }
会让其他线程(调用者)中等待
tread0
执行完 -
在其他线程中调用
线程名.join()
表示让线程名
该线程插队执行 -
有异常,要捕获或者抛出
InterruptException
Try…Catch包裹
方法名 | 是否要放到Try…Catch内 | Static? |
---|---|---|
sleep | Yes | Yes |
yield | No | Yes |
join | Yes | No |
wait | Yes | No |
观测线程状态
Tread.State
线程状态:线程可以处于以下状态之一:
-
NEW
尚未启动的线程处于此状态
-
RUNNABLE
在 Java 虚拟机中执行的线程处于此状态
-
BLOCKED
被阻塞等待监视器锁定的线程处于此状态
-
WAITING
正在等待另一个线程执行特点动作的线程处于此状态
-
TIMED_WAITTING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。例如:调用Sleep后,线程会处于这个状态。
-
TERMINATED
已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态
观察状态的方法:Thread.State state = Thread.getState();
Thread.State state = Thread.getState();
System.out.println(state);
线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定哪个线程来执行。高优先级线程有更大的概率被分配到执行权。
线程的优先级用数字表示,范围从1~10
- 最低:1
- 最高:10
- 默认:5
- 如果超出 [1,10] 这个范围,会抛出异常
获取优先级的方法:getPriority()
改变优先级的方法::setPriority(int xxx)
守护线程 daemon
线程名.setDaemon(Boolean on)
,默认为false即用户线程
线程分为用户线程和守护线程
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Thread thread = new Thread();
thread.setDaemon(true);
thread.start();
主线程执行完后,守护线程也会退出
Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
volatile
特性:可将变量变得线程安全
-
保证可见性,不保证原子性
-
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去
-
这个写会操作会导致其他线程中的volatile变量缓存无效
-
-
禁止指令重排
- 执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
volatile
不适用的场景
-
不适合复合操作,
inc++
不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到30000。public class Test { public volatile inc = 0; public void increase(){ inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i = 0; i < 10; i++){ new Thread(){ public void run(){ for (int j = 0; j < 1000; j++){ test.increase(); } } }.start(); } while(Thread.activeCount() > 2 )//确保前面线程执行完成,在IDEA中要>2, 其余为>1 Thread.yield(); System.out.println(test.inc); } }
-
解决方法:使用
synchronized
public inc = 0; public synchronized void increase(){ inc++; }
-
采用
Lock
public inc = 0; Lock lock = new ReentrantLock(); public synchronized void increase(){ lock.lock(); try{ inc++; } finally{ lock.unlock; } }
volatile的原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
线程同步
案例:卖票
需求:卖100张票,有3个窗口同时卖票。设计程序模拟
思路:
- 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int ticket = 100;
- 在SellTicket类中重写run()方法实现卖票,代码步骤如下
- 判断票数大于0,就卖票,并告知是哪个窗口卖的
- 卖完之后,总票数减1
- 票卖完后,还会有人来问有没有票,用死循环让程序一直走下去
- 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下:
- 创建SellTicket类对象
- 创建三个Thread类的对象,把SellTicket对象作为构造方法的函数,并给出对应的窗口名称
- 启动线程
public class SellTicketDemo{
public static void main(String[] args){
//创建一个SellTicket对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应窗口的名称
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
//启动线程
st1.start();
st2.start();
st3.start();
}
}
public SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run(){
//判断票数大于0,就卖票,并告知是哪个窗口卖的
//卖完之后,总票数减1
//票卖完后,还会有人来问有没有票,用死循环让程序一直走下去
while(1){
if(ticket > 0){
//通过sleep方法来模拟出票时间
Thread.sleep(100);
sout(Thread.currentThread().getName() + "正在出第" + tickets + "张票");
ticket--;
}
}
}
}
卖票案例的思考
每次卖票的时候,要有100ms的延迟。用sleep()
实现
线程安全问题
为什么出现问题?(判断多线程程序是否会有数据安全问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
卖票程序满足以上三条
如何解决?
- 基本思想:让程序没有安全问题的环境
解决方法:
- 把多条语句操作共享数据锁起来让任一时刻只能有一个线程操作数据
- Java提供同步代码块的方法
保证访问正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用完后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁-释放锁会导致比较多的 上下文切换 和 调度延时 引起性能问题
- 如果一个优先级高的线程等待一个低优先级的线程释放锁,会导致优先级倒置,引起性能问题。
同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
锁的对象一定要会发生变化的对象
- 格式:
synchronized(任意对象){
多条语句操作共享数据的代码
}
- synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
public class SellTicket implements Runnable{
private Object obj = new Object();//在方法外面定义,确保只有一个锁
synchronized(obj){
statement...
}
}
同步方法
同步方法:把synchronized关键字加到方法上
-
理解:
- synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
-
方法里需要修改的内容的代码部分才需要加锁
-
格式:
修饰符吗 synchronized 返回值类型 方法名(方法参数){}
-
同步方法的锁的对象,是
this
对象,相当于synchronized(this){ } -
同步静态方法:
static synchronized
,相当于synchronized(类名.class)
private synchronized void sellTicket(){
statement...
}
线程安全的类 JUC
- StringBuffer:线程安全的可变字符序列(相当于StringBuilder <-运行更快)
- vector:实现了list集合,实现了可扩展的数组(相当于ArrayList <-运行更快)
- HashTable:实现了Map接口,将键映射到值。任何非null对象都可以用作键或者值(建议使用HashMap代替HashTable)
死锁
多线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的清醒。某一个同步块同时拥有”两个以上对象的锁“时,可能会引发死锁。
Lock锁
为了更清晰表达若何加锁和释放锁,设定了Lock。为一个接口。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法:
ReentrantLock():
创建一个ReentrantLock的实例
添加锁和加锁位置:
private Lock lc = new ReentrantLock();
public void m(){
lc.lock();
try{
//保证线程安全的代码
} finally{
lc.unlock();
//如果同步代码有异常,要将unlock()写入finally中
}
}
@Override
public void run(){
try{
lc.lock();
...多线程执行语句...
}finally{
lc.unlock();
}
}
synchronized 和 lock 的对比
-
Lock是显式锁(手动开启和手动关闭,要记得关闭)
-
synchronized是隐式锁,出了作用域自动释放
-
使用优先顺序:
Lock > 同步代码块 > 同步方法
生产者消费者模式
概述:生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
生产者消费者问题,实际上主要是包含了两类线程:
- 一类是生产者线程,用于生产数据
- 一类是消费者线程,用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
- 生产者生产数据之后直接放在共享数据区中,并不需要关心消费者的行为
- 消费者只需要从共享数据中去获取数据,并不需要关心生产者的行为
等待与唤醒,方法在Object类中Object类的等待和唤醒方法:
Method’s name | Statement |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,要用wait()就得synchronized修饰 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
生产者消费者案例
生产者消费者案例中包含的类:
- 奶箱类(Box): 定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
- 生产者类(Producer): 实现Runnable接口,重写run()方法,调用存储牛奶的操作
- 消费者类(Customer):实现RUnnable接口,重写run()方法,调用获取牛奶的操作
- 测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
- 创建奶箱对象,这是共享数据区域
- 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
- 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛菜的操作
- 创建两个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
- 启动线程
public class BoxDemo{
public static void main(String[] args){
//创建奶箱对象
Box b = new Box();
//创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Producer p = new Producer(b);
//创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
Customer c = new Customer(b);
//创建两个线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//启动线程
t1.start();
t2.start();
}
}
public class Box{
//定义一个成员变量,表示第x瓶奶
private int milk;
//定义一个成员变量,表示奶箱的状态
private boolean state = false;
//提供存储牛奶和获取牛奶的操作
public synchronized void put(int milk){
//如果有牛奶,等待消费
if(state){
try{
wait();
} catch(InterruptedException e){
e.printStackTrace();
}
}
//如果没有牛奶,就生产牛奶
this.milk = milk;
sout("送奶工将第" + this.milk + "瓶奶放入奶箱");
//生产完毕之后,修改奶箱状态
state = true;
//唤醒其他等待的线程
notifyAll();
}
public synchronized void get(){
//如果没有牛奶,等待生产
if(!state){
try{
wait();
} catch(InterruptedException e){
e.printStackTrace();
}
}
//如果有牛奶,就消费牛奶
sout("用户拿到第" + this.milk + "瓶奶");
//消费完毕之后,修改奶箱状态
state = false;
//唤醒其他等待的线程
notifyAll();
}
}
public class Producer implements Runnable{
private Box b;
public class Producer(Box b){
this.b = b;
}
@Override
public void run(){
for(int i = 1; i <= 5; i++){
b.put(i);//生产者生产了五瓶奶
}
}
}
public class Customer implements Runnable{
private Box b;
public Customer(Box b){
this.b = b;
}
@Override
public void run(){
while(1){
b.get();
}
}
}
线程池
总结
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池
线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
创建线程池:
- 创建线程池的一个类:
Executor
- 我们创建时,一般使用它的子类:
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
从图中,我们可以看出:
- 线程池中的
corePoolSize
就是线程池中的核心线程数量- 核心线程,在没有用的时候,也不会被回收。
maximumPoolSize
就是线程池中可以容纳的最大线程的数量keepAliveTime
,就是线程池中除了核心线程之外的其他的最长可以保留的时间- 因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,
util
是计算这个时间的一个单位
workQueue
,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory
,就是创建线程的线程工厂handler
,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
最大承载 = workQueue.capacity()
+ maximumPoolSize
线程池的执行流程:
由图可见,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
handler的四种拒绝策略:
AbortPolicy
:默认的抛异常,不出席那个*(不执行新任务,直接抛出异常,提示线程池已满)*DisCardPolicy
:直接扔掉*(不执行新任务,也不抛出异常)*DisCardOldSetPolicy
:作为下一个执行*(消息队列中的第一个任务替换为当前新进来的任务执行)*CallerRunsPolicy
:马上执行*(直接调用execute来执行当前任务)*
四种常见的线程池:
CachedThreadPool
:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。SecudleThreadPool
:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。SingleThreadPool
:只有一条线程来执行任务,适用于有顺序的任务的应用场景FixedThreadPool
:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
入门
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();//单一线程
Executors.newSingleThreadExecutor();//单一线程
Executors.newFixedThreadPool(5);//固定的
Executors.newCachedThreadPool();//可变的
try{
//使用线程池创建线程
for(int i = 0; i < 10; i++){
threadPool.execute(() ->{
System.out.println(Thread.currentThread().getName());
});
}
} catch(Exception e){
e.printStackTrace();
} finally {
//finally保证程序走完才关
//程序跑完要关闭线程池
threadPool.shutdown();
}
}
自定义线程池
每一个线程池的创建都调用了ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize
核心线程池大小maximumPoolSize
就是线程池中可以容纳的最大线程的数量keepAliveTime
超时了没人调用会被回收,指的是非核心线程util
超时的单位workQueue
,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory
,创建线程的线程工厂,一般不懂handler
,拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
银行排队例子
两个核心线程
三个非核心线程
等待区3个人
代码:
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try{
//最大承载为 3 + 5 = 8个线程
//使用线程池创建线程
for(int i = 0; i < 10; i++){
threadPool.execute(() ->{
System.out.println(Thread.currentThread().getName());
});
}
} catch(Exception e){
e.printStackTrace();
} finally {
//finally保证程序走完才关
//程序跑完要关闭线程池
threadPool.shutdown();
}
}
Thread 1
Thread 方法总结
主要分清楚是静态方法还是非静态方法
非静态方法👇
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
静态方法👇
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
课一总结
4个概念:Atomicity、Visibility、Order of execution、Critical Code
两种创建多线程的方法:
继承Thread
实现Runnable
Thread类的两种构造器
- 无参
- 带参(Runnable)
要求:
- 理解Thread是什么
- 理解Threads用来干嘛
- 学习如何创建Threads
- 学习用Java操作Thread
sleep()
- Interrupting threads
线程状态
Java中线程的状态分为6种。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。阻塞(BLOCKED):表示线程阻塞于锁。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
Process & JVM
是什么?
- You can download a file (takes a few minutes)
- You can use a word processor at same time.
- (it only need to listen for your keystrokes)
JVM (Java Virtual Machine) is a process
- 默认只会用一个Process来运行
- Java中的多线程是伪并行处理或异步行为行为Within a Java application you work with several threads to achieve pseudo parallel processing or asynchronous behaviour. 伪并行处理或异步行为
Java程序的运行都是从Main方法中开始的
Process 和 Thread的区别
process:
- Process和其他的process是isolated的
- process之间不能直接的共享数据
- process所利用的资源,例如memory和CPU time,都是在操作系统中的
thread:a thread is a “lightweight process”.
- Each has its own call stack
- Can access shared data of other threads in the same process.
- Every thread has its own memory cache.
- If a thread reads shared data, it stores this data in its own memory cache. A
thread can re‐read the shared data
Key Concepts
Atomicity
定义:不能被打断的操作,具有原子性(An operation is said atomic when it cannot be interrupted.)
性质:一旦开始必须执行完
- 例如声明变量。
int i = 5
Visibility
This occurs when a thread must watch the actions of another thread
例如:
- 终结一个线程
- 设置一个值
Order of execution
正常程序:按照代码顺序执行
并发编码(concurrent programming):execution的顺序是不保证的
Critical code
A part of code that must only be executed by a single thread at one time
例如:
- Writing to a file
Thread Class
是什么:可执行类。The Thread class is responsible for executing your stuff in a thread.
- 要执行的代码放到
run()
- 管理需要运行的代码
两种给Thread设置run()
的方法
- Passing your class (with a run() method) into a new Thread object
- Extending the Thread class
开始运行Thread
通过方法:start()
两种创建多线程的方法
实现Runnable接口
implement Runnable
一定要重写实现run()
方法,否则会报错
报错为:Class 'RunnableError' must either be declared abstract or implement abstract method 'run()' in 'Runnable'
Main方法中使用
Thread t = new Thread(RunnableClass);
继承Thread类
步骤:
- 继承Thread类
extends Thread
- 重写
run()
方法,在run()
方法中加入代码 - 在main方法中创建实例
start()
Main方法中使用
MyThread t = new MyThread();
两种方法的比较
第一种方法要比第二种方法好
- 继承Thread后不能继承其他类
- 继承Runnable接口后还可以继承其他类
Thread 两种构造器
-
无参构造器
new Thread();
-
带Runnable的构造器
r = new MyRunnable(); new Thread(r);
Thread 2
课二总结
要求:
- 线程的
sleep()
- 让sleep的线程苏醒
目的:
- 让代码在合适的时间执行
- 管理资源
sleep 睡眠
要使用try...catch...
代码包裹,报错为:InterruptedException
Thread.sleep(long millis);
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
参数:milliseconds,一秒为1000
使用对象:Thread
注意:只能使当前的线程休眠,不能选择某一个线程休眠
作用:
- free up CPU time
yield 礼让
转让CPU的使用权:yield()
- 可以让其他线程获的CPU使用的优先权
- 正在运行的线程暂停
![image-20210913225500912](https://i-blog.csdnimg.cn/blog_migrate/1d67dd2395160dee65b9f2c30542e749.png)
![image-20210913225626096](https://i-blog.csdnimg.cn/blog_migrate/0b0d5848f6e626598bd373d42912538f.png)
Interrupt 中断
为什么线程会被Interrupt?
- 当其他线程需要资源时
- 当调用
interrupt()
时
如果那个线程B在执行一个低级可中断阻塞方法,例如
Thread.sleep()
、Thread.join()
或Object.wait()
,那么它将取消阻塞并抛出InterruptedException
。否则,interrupt()
只是设置线程B的中断状态。
详解:Sleep()
和interrupt()
线程A正在使用
sleep()
暂停着:Thread.sleep(100000);
如果要取消他的等待状态,可以在正在执行的线程里(比如这里是B)调用a.interrupt();
令线程A放弃睡眠操作(这里a是线程A对应到的Thread实例)执行
interrupt()
时,并不需要获取Thread实例的锁定。任何线程在任何时刻,都可以调用其他线程
interrupt()
。当sleep中的线程被调用
interrupt()
时,就会放弃暂停的状态.并抛InterruptedException
.丢出异常的,是A线程.
两种情况:
- 先睡眠后打断,则直接打断睡眠,并且清除停止状态值,使之变成false,抛出
InterruptedException
- 先打断后睡眠,则直接不睡眠,抛出
InterruptedException
InterruptedException
抛出时候:在线程被interrupted时抛出,并执行了sleep()
作用:可捕获该异常并进行响应的处理
Interrupt Flag
public static boolean interrupted
每一个线程都有一个boolean值变量表示线程是否被中断(interrupt)
每个线程都有一个与线程是否已中断的相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false,中断后变为true
Interrupt Method
public void interrupt()
中断线程,将中断状态(boolean interrupted
)设为true,执行完后会抛出InterruptedException
注意:
-
线程的阻塞方法(Blocking Methods,如
Thread.sleep()
):在执行前会先检测该线程是否为interrupted的状态。如果是interrupted,则会先返回 -
当对一个阻塞的线程使用该方法时,不会有任何效果。
使用场景
- 线程执行太久,如线程被锁
- 打断睡眠,注意不是唤醒所以不推荐该方法
sleep时使用
- 先睡眠后打断,则直接打断睡眠,并且清除停止状态值,使之变成false:
- 先打断后睡眠,直接不睡眠
- 抛出
InterruptedException
wait时使用
-
打断wait状态
-
抛出
InterruptedException
非堵塞状态使用
- 线程正常执行
- flag 变成 true
Thread类中的三个Interrupt
区分三个方法,很tm的像
方法 | 描述 |
---|---|
public static boolean interrupted | 测试当前线程是否已经中断。调用该方法后,线程的中断状态会被重置。此方法内部调用的是私有的Thread.currentThread().isInterrupted(true); 带true 参数的isInterrupted 方法意思是返回当前线程的中断状态,然后reset当前线程的中断状态 |
public boolean isInterrupted() | 测试指定线程是否已经中断。线程的中断状态不受该方法的影响。内部调用的私有的参数为false 的isInterrupted(false) 方法 |
public void interrupt() | 中断线程,将中断状态设为true |
wait等待
使当前(executing)线程进入等待状态,直到有其他线程中调用notify()
或者notifyAll()
方法对其进行唤醒。
- 方法应该被拥有这个对象的监视器的线程调用
- 如果当前对象不拥有Monitor,则会报错
IllegalMonitorStateException
- 被Interrupt,会报错
InterruptedException
notify¬ifyAll
wait()
, notify()
, notifyAll()
只能在有Lock的对象或者在同步方法中使用
- 如果在其他地方使用,会抛出
IllegalMonitorStateException
线程状态
线程为Blocked的原因
- sleep()
- wait()
- join()
使用interrupt()
可以将线程从blocked状态拉出
join 插队
作用:等待另一个线程死亡,使用该方法的线程会停止直到另一个线程死亡
join()方法的作用,是等待这个线程结束;
也就是说,t.join()方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING 状态,直到线程t完成,此线程再继续;
通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
**异常:**当调用此方法的线程被interrupt时,会报InterruptedException
Thread 3
学习目标:
- 理解
interrupts
如何实现 - 理解当使用
interrupt
时为什么以及什么时候抛出异常 - 理解如何terminate一个线程
- 理解线程安全问题,并学会如何避免
总结已经学的方法
Name | Statement | Static? |
---|---|---|
Thread.sleep() | Pause,一段指定时间 | yes |
myThread.join() | Pause,直到myThread 运行完毕 | no |
myThread.interrupt() | 将interrupt的信号发送给myThread ;改变myThread中interrupt status变为true | no |
JVM线程数
Java Virtual Machine 是一个进程Process
- 一个Java程序默认只会用一个线程运行
- Java 中的多线程是pseudo parallel processing 或者 asynchronous behaviour
interrupt总结
非静态方法
**是什么:**中断是对线程的指示,它应该停止正在做的事情并做其他事情。
- 可以通过编程决定中断后去做什么
- 通过
线程名.interrupt()
使名为线程名
的线程对象被打断 - 不会使线程进入终结状态
异常何时抛出:
如果线程处于**sleep
,join
,waiting
另一个线程**,对其使用interrupt()
会抛出异常InterruptedException
- 随后,interrupted status会被清除
- 可以让被interrupt的线程接着完成其任务
Mechanism:
- 使用内部的标志去标识是否处于interrupt状态,称为interrupt status
- 调用
interrupt()
方法实际是改变了interrupt status的状态为true
Thread.interrupted
可以让线程自己检查是否处于interrupt状态- 该方法是static的
- 使用该方法后,interrupt的状态会自动清除
- 查询方法:
isInterrupt()
,非静态
Thread.interrupted()
&线程名.isInterrupt()
比较
name | statement | static? |
---|---|---|
Thread.interrupted() | 将interrupt flag设为true,并且清除线程的一切状态(如sleep,wait) | yes |
线程名.isInterrupt() | 检查线程名 线程对象是否被阻塞,例如if(thread2.isInterrupted() == true) | no |
System.exit()
参数输入 | 代表含义 |
---|---|
System.exit(-1) | 异常中断 |
System.exit(0) | 运行完退出 |
interrupt异常处置方式
要处理异常而不是什么都不做
-
接住后再抛出
try { ……} catch (FileNotFoundException e) { System.err.println("FileNotFoundException: " + e.getMessage()); throw new SampleException(e); }
-
直接进行处理
catch (IOException e) { System.err.println("Caught IOException: " + e.getMessage()); }
接住异常后再执行一次myThread.interrupt()
确保其运行正常
public void run(){
while(!Thread.currentThread().isInterrupted(){
// Doing some heavy operations
try {
Thread.sleep(5000);
}
catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
}
线程是否存活
非静态方法
myThread.isAlive()
- true表示线程还在运行
- 只要运行了还没有死亡都算
Alive
- 只要运行了还没有死亡都算
用法:
if(myThread.isAlive()){
// Check something
//myThread.interrupt();
}
不常用,主要用join()
结束线程
有三种方法使线程结束
- 当
run()
中代码运行完 - 守护线程:如果创建其的线程灭亡,那对应的守护线程也将死亡
- 运行时接受到interrupt signal
- 不执行Run方法里面内容直接退出
- 退出sleep、wait状态接着工作
没有命令使Thread直接死亡
自然死亡
可以添加pleaseFinish
的Boolean
型变量。
- 添加判断,如果判断到其为
true
,这跳过Run中全部的代码
class MyThread extends Thread{
private volatile boolean pleaseFinish = false;
public void run(){
//Periodically check for pleaseFinish to be
// set to true
}
public void finishThreadPlease(){
pleaseFinish = true;
}
}
注意:
- 如果线程没有抢到执行权,设定停止Flag将无任何影响
- 处于sleep状态
- wait状态
- 被锁
- 使用
interrupt()
方法使其提早结束sleep
或者wait
状态 - 记得使用volatile
守护线程 Daemon
myThread.setDaemon(false)
- 当父线程运行完后,
myThread
线程仍继续运行
myThread.setDaemon(true)
- 当父线程运行完后,
myThread
线程也被迫结束
Interrupt
- 可以使用
interrupt
来终结一个线程 - 必须捕获异常
InterruptedException
,再结束 thread.interrupt()
不会自动终结一个线程,需要在代码中自己终结- 不适用于在有wait或者sleep的run中
public void run(){
for (int i = 0; i < importantInfo.length; i++){
try {
Thread.sleep(4000);
}
catch (InterruptedException e){
return;
}
System.out.println(importantInfo[i]);
}
}
重申异常抛出时机:
- 只在
sleep()
和wait()
中会自动抛出 - 应该常态化将检查中断作为程序检查点已到达
设定判断使中断变成结束点
if (Thread.currentThread.isInterrupted()){
// cleanup and stop execution
return;
}
例子:
public class StopThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new DemoThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
private static class DemoThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
if (Thread.currentThread().isInterrupted())
return;
System.out.println("i is " + i);
}
}
}
Please Stop
使用于有wait和sleep的方法中
例子:
public class StoppableTask extends Thread{
private volatile boolean pleaseStop;
public void run(){
while(!pleaseStop){
...
}
}
public void tellMeStop(){
pleaseStop = true;
}
}
csdn例子:
public class StopThread {
private static volatile boolean isStopped = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new DemoThread();
thread.start();
Thread.sleep(1000);
// thread.interrupt();
isStopped = true;
}
private static class DemoThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
if (isStopped)
return;
System.out.println("i is " + i);
}
}
}
}
再讲Interrupt
为了使得线程安全,可以在catch中再次interrupt来运行
public void run()
{ while(!Thread.currentThread().isInterrupted()
{
// Doing some heavy operations
try {
Thread.sleep(5000);
}
catch(InterruptedException e)
{
Thread.currentThread().interrupt();
}
}
}
Interrupt状态的恢复
抛出InterruptedException
的方法,例如sleep会中断当前的运行并且立马返回
sleep方法会在被interrupt后清除掉interrupt的状态
线程安全
为什么会有线程安全问题?
看另一个笔记
Volatile
保持变量的安全
public volatile int counter = 0
原理:一个变量一次只会发给一个线程使用,使用其具备原子性的操作,例如load, store, read, write
坑点:
对于数组来说:如果声明了volatile int[] arr
- 其中
arr[5]
不安全
复合操作不能使用
- 例如
i++
应用举例:使线程停下
public class StoppableTask extends Thread {
private volatile boolean pleaseStop;
public void run() {
while (!pleaseStop)
{ // do some stuff... } }
public void tellMeToStop()
{ pleaseStop = true; }
}
不必使用Volatile的时刻
- 已经被
final
修饰 - 当只有一个线程
- 复合运算
- 有同步方法了
Critical Sections
被多个线程使用的access的代码称为:critical sections
Monitor
支持两个用处:
- 同步方法,锁
wait()
和notify()
wait和notify
属于Object类下,任何一个对象都可以作为一个Monitor
每一个对象都有着两个方法
- 每一个对象都有锁。锁确保一次只能有一个线程执行使用或执行该对象。因此每一个对象都有 wait set
因为每一个对象都有两个方法、锁、wait set,所以称之为monitor
wait和notify都要放在synchronized方法中执行
“entry” and “wait” set
一堆正在等待被notify的Thread的集合
- 处于该状态的集合,需要等待nodify来唤醒
图示:每一次只能有一个线程被唤醒
entry set
拥有相关Monitor的线程会首先进入entry set,如果没有其他的线程在等待或者持有Monitor,当前线程则会拿取Monitor继续执行Monitor的代码
拥有该Monitor对象释放条件:当当前线程已经执行完
wait set
当一个拥有Monitor的线程在Monitor内部执行了wait()
,则释放Monitor并去到Wait Set
此时如果有notify()
唤醒,则该线程会继续持有Monitor并执行代码直至再次释放,释放的两种可能
- 执行完代码
- 再次
wait()
注:被唤醒的线程需要再次抢夺Monitor而非直接拥有,
对象一致性
wait, notify, synchronized 需确保其对象一样
Notify entry set还是wait set?
- 如果拥有Monitor的线程在执行完代码前没有notify,则唤醒的是在entry set中的线程
- 如果执行了 notify,则entry set 的线程要与wait set中的线程竞争Monitor
如果一个线程当前拥有Monitor可以执行,它只能执行wait()
方法
生产者消费者模型
中间变量为数组的
- 标记最后的位置,使用单独变量
private int last;
- 判断已满方法:
public boolean isFull() { return(last == buf.length); }
- 判断为空方法:
public boolean isEmpty() { return (last == 0); }
- 取出元素后去掉数组内数据方法:
System.arraycopy(buf, 1, buf, 0, --last);
public class Buffer {
private char[] buf; // buffer storage
private int last; // last occupied position
public Buffer(int sz) {
buf = new char[sz];
last = 0;
}
public boolean isFull() { return(last == buf.length); }
public boolean isEmpty() { return (last == 0); }
public synchronized void put(char c) {
while (isFull()) {
try {
wait();
}
catch(InterruptedException e) { }
}
buf[last++] = c;
notify(); //to the buffer
}
public synchronized char get(){
while (isEmpty()) {
try {
wait();
}
catch(InterruptedException e) { }
}
char c = buf[0];
System.arraycopy(buf, 1, buf, 0, --last);
notify();
return c;
}
}
public class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer b) { buffer = b; }
public void run() {
for (int i=0; i<250; i++) {
System.out.println(buffer.get());
}
}
}
public class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer b) { buffer = b; }
public void run() {
for (int i=0; i<250; i++) {
buffer.put((char)('A' + (i%26)));
}
}
}