4.多线程带来的风险(线程安全)
4.1观察线程不安全
java
public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下
Thread thread1=new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread thread2=new Thread(()->{
for(int i=0;i<50000;i++) {
count++;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
尝试下这个代码,我们的预期结果为100000,但是结果似乎并不是
此处的count++操作其实在cpu视角来看,是3个指令
1)把内存中的数据读取到CPU的寄存器里 load
2)把CPU寄存器里的数据+1 add
3)把寄存器的值写回到内存中 save
注:这里的load,add,save只是一种表述,由于不同架构里的cpu有不同的指令集,不同的指令集里都有不同的指令,针对这三个操作不同的cpu里队友的指令的名称肯定是不同的
cpu在调度执行线程的时候,说不上啥时候就会把线程给切走了(抢占式执行,随机调度)
指令是cpu执行的最基本单位,要调度至少把当前执行完,不会执行一般调度走
由于count++是三个指令,可能会出现cpu执行了其中的1个指令或者2个指令或者3个指令调度走的情况,都有可能且无法预测
基于上面的情况,两个线程同时对count++进行++就容易出现bug



还有很多种情况(上面三张图是其中一部分)
线程安全问题
某个代码在多线程环境下会出现bug
原因如下:
1.线程在操作系统随机调度,抢占式执行(根本原因)
2.多线程同时修改同一个变量
3.修改操作,不是"原子"的
4.内存可见性问题
5.指令重排序
不可分割的最小单位
CPU视角----一条指令就是CPU上的不可分割的最小单位
CPU在进行调度切换线程的时候,势必会确保执行完毕一条完整的指令
取指令,解析指令,执行指令,一定是把当前指令执行完毕才能调度走,不存在执行半个指令这样的情况
Java String 不可变对象
有几个好处
1)算出来的hashcode是固定的
2)方便在常量池中缓存
3)避免线程安全问题
String的不可变是如何实现的------------>因为String没有提供各种set系列方法,所以不能修改
只提供了get里面的属性,不能set
4.2线程安全问题的解决办法
解决线程安全问题最主要的办法就是把"非原子"的修改变成"原子"
Java中提供了synchronized关键词来完成加锁操作
synchronized是个关键字,不是函数,后面的()并非是参数,而是需要指定一个"锁对象"
通过锁对象来进行后续的判定,这里()里的对象可以指定任何的对象
()里面的对象可以指定任何的对象
{}里面的代码就是要打包到一起的代码,也可以放任意的其他的代码,包括调用别的方法啥的,只要是合法的java代码都是可以的
java
private static int count=0;
private static Object Locker=new Object();
public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下
Thread thread1=new Thread(() -> {
synchronized(Locker) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
Thread thread2=new Thread(()->{
synchronized(Locker) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
由于t1和t2都是针对Locker对象加锁
t1先加锁的,t1就加锁成功,于是t1就继续执行{}里面的代码
t2后加锁的,发现locker对象已经被别人先锁了,说明已经被占用用了,所以t2只能阻塞等待
又因为t1的unlock一定是在save之后,确保了t2执行load的时候,t1已经save
(这两者的++操作不会穿插执行了,也就不会相互覆盖掉对方的结果了)
本质上是把随机的并发执行过程,强制变成了串行,从而解决了刚才的线程安全问题
上述的代码有效的前提是两个线程都加锁了并且都是针对同一个对象加锁
此时给这两个线程尝试加上不同的锁,各自都能够加上,彼此之间不会有任何的影响和干预,此时这两线程的++操作又是随机调度的并发执行了
锁对象的作用就是用来区分两个线程,多个线程是否针对"同一个对象"加锁
如果针对同一个对象加锁,就会出现阻塞(锁竞争/锁冲突)
不是针对同一个对象加锁,此时不会出现阻塞,两个线程仍然是随机调度的并发执行
锁对象填写哪个对象不重要,重要的是多个线程是否是同一个对象
此时的synchronized是JVM提供的功能,synchronized底层实现就是JVM中通过C++代码实现的
进一步的也就是依靠操作系统提供的api实现的加锁,操作系统的api是来自于CPU上支持的特殊的指令来实现的

之前提到过StringBuilder和StringBuffer,说StringBuffer是线程安全的,不准确
更准确的说法是StringBuffer的相关操作都带有synchronized关键字
而StringBuilder没有带synchronized关键字
即使是StringBuffer,带有加锁操作,如果使用不当,也是可能会存在线程安全问题
4.3Synchronized使用示例
1)修饰代码块:明确指定锁哪个对象
a.锁任意对象
b.锁当前对象(必须是非静态对象)

相当于(synchronized在void前面)

c.锁的是静态方法,想当于锁的是类的对象

相当于

编写的java代码本身是.java文件
通过javac编译成class文件,JVM运行的时候把.class文件加载到内存中形成.class文件对应的类对象
2)可重复入
synchronize同步块对同一个线程是可以重入的,不会出现自己把自己锁死的情况
死锁是一个非常严重的bug
例如:
Synchronized(counter){
counter.add();
}
public void add(){
synchronized (this){
count++;
}
}
里面的synchronized想要拿到锁需要外面的synchronized释放锁
外面的synchronized要释放锁需要执行到}
要想执行到}就需要执行完add
但是add还在阻塞
但是Java为了减少程序员写出死锁的概率
引入了特殊的机制,解决上述死锁问题
"可重复入锁"
4.4volatile关键字
线程安全第四个原因
内存可见性,引起的线程安全问题
java
public class demo1 {
private static Object Locker1=new Object();
private static Object Locker2=new Object();
private static int num=0;
public static void main(String[] args) {
Thread thread1=new Thread(()-> {
while(num==0){
}
});
Thread thread2=new Thread(()->{
Scanner scan=new Scanner(System.in);
System.out.println("请输入一个整数:");
num=scan.nextInt();
});
thread1.start();
thread2.start();
}
}
这个示例代码展示了两个线程
线程thread1的目的是持续循环,条件是num==0;
线程thread2的目的是改变num的值,,使得thread1跳出循环
但是实际上代码并没有去实现,循环一直没有跳出

这种情况就是属于bug,属于线程安全问题("内存可见性问题")
内存可见性问题本质上是编译器/JVM对代码进行优化的时候优化出了bug.
如果代码是单线程的,编译器/JVM,代码的优化一般是非常准确的,优化之后不会出现问题
如果代码是多线程的,编译器/JVM的代码优化,就可能出现误判(编译器/JVM的bug)
导致不该优化的地方,也给优化了,于是就造成了内存可见性问题
while(num==0){
}
这个代码会循环非常多次,每次循环都要执行一个n==0这样的判定
1)从内存读取数据到寄存器中(读取内存相比之下这个操作就非常慢)
2)通过类似于cmp指令,比较寄存器和0的值(这个指令的执行速度非常快)
此时JVM执行这个代码段时候发现每次循环1)操作开销非常大,而每次执行1)操作结果都一样
并且JVM根本没有意识到用户可能在未来会修改n,于是JVM就做了个大胆的操作
直接把1)这个操作给优化掉了------每次循环不会重新读取内存中的数据,而是直接读取寄存器/chache中的数据(缓存的结果)
当JVM做出上述决定之后,意味着循环的开销大幅降低,但是用户修改n的值,内存中的n已经改变了,但是由于t1线程每次循环不会真的读内存,所以感受不到n的改变
所以内存中n的改变,对于线程t1来说是"不可见的"
这样就引起了bug---"内存的可见性问题"
当我们加入sleep之后,刚才对于读取n内存数据的优化操作就不再进行了
因为和读取内存相比,sleep开销更大,远远超过读取内存
就算把读取内存操作优化掉也没有意义

如果代码中循环里没有sleep又希望代码能够没有bug的正确运行
volatile关键字----修饰一个变量,提示编译器这个变量是"易变"的
编译器进行上述优化的提示,是编译器认为的,针对现在变量的频繁读取结果是都是固定的
此时编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据

引入volatile的时候,编译器生成这个代码的时候,就会给变量的读取操作附近生成一些特殊的指令,成为"内存屏障",后续JVM执行到这些特殊指令就知道了,不能进行上述优化了
编译器的开发者知道这个场景中可能出现误判,于是就把权限交给了程序员,让程序员能够部分的干预到优化的进行,让程序员显示的提醒编译器,这里别优化
网络上关于内存可见性问题的相关资料,与这里说的有些偏差
网上的资料会引入两个概念
1)工作内存(指的是CPU上的寄存器,缓存,写缓冲区等),这个储存区进行接下来的运算和逻辑判断
2)主内存(main memory,实际上就是内存)
整个java程序持有这个主内存,每个java程序又会有自己的一份工作内存
向上述例子里的变量count本身是在主内存中,在t1和t2线程工作的过程中就会把主内存的数据拷贝到工作内存
t2如果修改了count,先修改工作内存,然后再写回到主内存
t1读取到count的时候,则是从主内存加载到工作内存,接下来的判定都是依照工作内存的值来进行判定的
此时t2修改了主内存,对t1的工作内存未产生影响,从而出现了上述内存可见性的问题
java语言本身就是想让程序员不必太关心底层硬件设备的细节和差异,cpu的结构持续发生变化
所以用workmemory 来表示
volatile只是解决内存可见性问题,不能解决原子性问题
如果两个线程针对同一个变量进行修改(count++),volatile无能为力
volatile也可以解决指令重排序问题
禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
4.5 wailt()/notify()
由于线程之间是抢占式执行的,因此线程之间的执行的先后顺序难以预制
但是实际开发中有时候我们希望和力的协调多个线程之间的执行先后顺序
完成协调组要涉及到三个方法
wait()/wait(long timeout):让当前线程进入等待状态
notify()/notifyAll():唤醒在当前对象上等待的进程
注意:wait,notify,notifyAll都是Object方法,任意的object对象都可以用来wait和notify
wait和notify也是为了解决"线程饿死"问题
1.wait()方法
1)是当前执行代码进行等待(把线程放到等待队列中)
2)释放当前的锁(需要给wait加锁
3)满足一定条件时被唤醒,尝试重新获取这个锁
wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait结束等待的条件:
其他线程调用该对象的notify方法
wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
其他线程调用该等待线程的interrupted方法导致wait抛出InterruptedException异常
wait中会进行一个操作,就是针对object对象先进行解锁,所以使用wait务必要放到synchronized代码块里,必须先加上锁才能谈"解锁"
这样在执行的object.wait()之后就一直等待,那么程序肯定不能这么一直等待下去,这个时候就需要使用另外一个方法唤醒notify();
2.notify()方法
在notify中也需要确保先加上锁才能执行
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其 它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
更多的时候是唤醒一个线程,确保执行过程是有序的,避免无需的锁竞争
3notifyAll()
notify方法只是唤醒某⼀个等待线程. 使用notifyAll方法可以⼀次唤醒所有的等待线程.
如果对方没有线程wait或者只有一个线程wait但是另一个线程notify多次会怎么样
答:不会怎么样,notify通知的时候如果无人wait不会有任何副作用
案例:创建三个线程,分别打印A,B,C,通过线程的打印顺序,先打印A,让B,最后C
java
public class demo5 {
public static Object Locker1=new Object();
public static Object Locker2=new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA=new Thread(()->{
System.out.println("A");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Locker1){
Locker1.notify();
}
});
Thread threadB=new Thread(()->{
synchronized (Locker1){
try {
Locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Locker2){
Locker2.notify();;
}
});
Thread threadC=new Thread(()->{
synchronized (Locker2){
try {
Locker2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
});
threadA.start();
threadB.start();
threadC.start();
threadA.join();;
threadB.join();;
threadC.join();
}
}
结果:
5.多线程代码案例
案例一:单例模式
单例模式是一种设计模式
设计模式类似于"棋谱"
固定的套路,针对于一些特定的场景给出一些比较好的解决方法
有的程序员代码能力比较松弛,而设计模式恰好就补全了这一点
只要按照模式来写代码,就可以使得代码不会太差(保证代码的下限)
设计模式就是针对编写代码的"软性约束"
框架就是编写代码过程中的"硬性约束"
开发的时候,有时候希望一个进程中不应该存在多个实例(对象)
此时就应该使用单例模式(限制某个类,只能有唯一的实例)
比如数据库
DataSource dataSorece=new MysqlDataSource()
一般来说一个程序中只有一个数据库,对应的MySQL服务器只有一份,此时DataSource这个类没必要创建出多个实例
此时使用单例模式描述DataSourse就可以避免不小心创建多个实例了
单例模式的分类
1)饿汉模式
java
private static singleton instance=new singleton();
private singleton(){} //饿汉模式:创建类的时候就创建实例,并且将实例加载到内存中
public singleton getInstance(){
return instance;
}
static修饰instance成员变量就是"类变量"
类成员的初始化就是在Singleton这个类被加载的时候(程序启动的时候)
现在先近似的认为相等
用private修饰这个构造方法,意味着在类的外面,就无法调用构造方法,就无法创建实例
(可以通过别的方法,反射拿到构造方法,调用反射api,或者序列反序列化也能打破上述单例模式)
每次创建实例都是需要使用getInstance这个方法来获取实例,此时这个类就是单例
单例模式的前提是"一个进程中"
如果有多个java进程,每个进程都可以有一个实例
2)懒汉模式
计算机中的懒是褒义词,谈到蓝意思是效率会更高
推迟了创建实例的时机,第一次使用的时候才会创建实例
举个例子:有一个编辑器,打开一个非常大(1G)的文本文档
1)一启动就把所有文本内容都读取到内存中,然后显示在界面上(饿汉模式)
2)启动之后只加载一小部分的数据(一个屏幕能显示的最大数据),随着用户的翻页操作,再按需的加载剩下的内容(懒汉模式)
java
private static singleton instance=null;//懒汉模式 这里的懒不是真的懒,而是效率更高,创建类的时候不会创建实例,当第一次时候的时候才会创建实例
private singleton(){}; //但是,当判断条件和修改会造成线程安全问题
public singleton getInstance(){
if(instance==null)
instance= new singleton();
return instance;
}
首次调用getInstance的时候,此时引用为空,就会进入if分支从而创建实例,此后再次调用getInstanace不会创建新的实例,直接返回
但是多线程环境下此时会存在一定的线程安全问题,比如
instance=new singleton()涉及数据的修改,外面有一个if条件判断
先判断后修改这种代码模式属于典型的线程不安全,判定和修改之间可能涉及线程的切换
饿汉模式中如果只是针对数据的读取是没有问题的,所以饿汉模式不存在线程安全问题
因此此处需要加锁
java
private static singleton instance=null; //懒汉模式进阶,考虑到 修改和判断导致的线程的不安全,增加了锁,使锁的内部具有原子性
private singleton(){}; //但是,反复地进入锁会增加阻塞,只有第一次需要进入锁,无关的阻塞会导致在阻塞期间,值被修改
public singleton getInstance(){
synchronized (this){
if(instance==null);
instance=new singleton();
return instance;
}
}
外部增加一个锁,让if和new操作打包成一个整体,此处if判定和new操作就是一个"原子"
加锁虽然可以解决了线程安全问题,但是枷锁同样也带来了阻塞的问题,如果上述的代码已经new完了,if分支再也进不去了,单纯的就是读取操作,此时getInstance不加锁也是线程安全的,没有比较加锁了
而当前的写法,只要调用了getInstance,都会触发加锁操作,虽然没有线程安全问题,但是也会因为加锁产生阻塞,影响到性能,所以需要对代码做进一步的改进
java
private static singleton instance=null;//懒汉模式 双重if判定 加锁和解锁本身就是个开销大的事情,懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续不需要加锁
private singleton(){ //但是无法避免内存可见性问题以及指令重排序问题,所有需要使用volatile来避免着两个线程安全问题
}
private static Object Locker1=new Object();
public singleton getInstance(){
if(instance==null){
synchronized (this){
if(instance==null){
instance=new singleton();
}
}
}
return instance;
}
此时使用双重if判定来判断此时是否需要加锁
外部的if判断是否需要加锁,内部if判断是否需要new类的实例,虽然判断条件一样,但是目的是不一样的
创建的局部变量处于JVM内存的"栈"区域中
new出来的变量,处于JVM内存中的"堆"区域中
整个JVM进程中堆只有一份,线程间大家共同
栈,则每个线程都有自己独立的栈,因为变量的共享式常态,所以容易触发多个线程修改同一个变量---->引出了线程安全问题
上述代码仍然存在线程安全问题,存在指令重排序问题
instance = new singleton();此处代码存在线程不安全
这句代码涉及三步,1.创建内存空间 2.调用该类的构造方法 3.将这个实力赋值给对应的引用对象
但是2,和3的执行顺序在编译器的优化下也许并不一定会以123的顺序执行,也许是以132,在3后触发线程切换,也许值并未初始化,值全是0,一旦使用任何的值后果都不堪设想
因此造成内存的重排序问题导致线程不安全
所以volatile的作用可以解决此处的指令重排序问题
编译器发现了Instance是易失的,围绕这个变量会非常克制,不会在读取变量上克制也会在修改变量的优化上进行克制
多线程懒汉模式完全版
java
private static volatile singleton instance=null;
private singleton(){};
public singleton getInstance(){
if(instance==null){
synchronized (this){
if(instance==null){
instance=new singleton();//有三步,1.分配内存空间 2.执行构造方法 3.内存空间的地址赋值给指定变量 如果不适用volatile可能会导致指令重排序问题
}
}
}
return instance;
因此可以写成这样,使得线程更加安全
3)阻塞队列
阻塞队列在普通队列(先进先出队列)的基础上做了扩充
1)线程安全
2)具有阻塞特性
a)如果队列为空,进行出队列操作,此时会出现阻塞,一直阻塞到其他的线程往队列里添加元素为止
b)如果队列为满,进入入队列操作,此时也会出现阻塞,一直阻塞到其他线程从队列取走元素为止
标准库中原有的对垒Queue和其子类都是默认线程不安全的
基于阻塞队列最大的应用场景就是实现"生产者消费者模型"
这个模型也是日常开发中常见的编程手法
当下的后端开发常见的结构"""分布式系统"
不是一台服务器解决所有问题,而是分成多个服务器,服务器之间相互调用

上述的服务器的之间的通信过程中,使用生产者消费者模型就是非常常见的做法
使用生产者消费者模型主要有连个好处
1)服务器之间的"解耦合"
模块之间的关联程度/影响程度
希望见到的是"低耦合",服务器之间的关联程度降低
引入生产者消费者模型之后,结构就成了下列模样

A只和队列通信,B也只和队列通信
A不知道B的存在,代码中没有B的影子
B不知道A的存在,代码中没有A的影子
看起来AB之间是解耦合了,但是A和队列,B和队列引入的新的耦合
消息队列是成熟稳定的产品,代码不会频繁修改,代码是稳定的,A和队列,B和队列之间的交互,逻辑基本写一次就固定下来了
通常谈到的"阻塞队列"是代码中的一个数据结构,但是由于这个东西好用,会把这样的数据结构单独封装成一个服务器,并且在单独的服务器上进行部署
此时这样的阻塞队列就有了一个新的名字"消息队列"(Message Queue,MQ)
2)通过中间的阻塞队列可以起到"削峰填谷"的效果,在遇到请求量激增突发的情况下,可以有效的保护下游服务器不会被请求冲垮

问题:
1)为什么一个服务器收到的请求更多就可能会挂?(可能会崩溃)
一台服务器就是一台电脑,上面就提供了一些硬件资源(包括但不限于CPU,内存,硬盘,网络带宽),就算机器的配置再好,硬件资源也是有限的
服务器每次收到一个请求,处理这个请求的过程就需要执行一系列的代码,在执行这些代码的过程中就会消耗一定的资源(CPU,内存,硬盘,网络带宽)
这些请求消耗的总的硬件资源的量,超过了机器能提供的上线,那么此时,机器就会出现问题(卡死,程序直接崩溃等等)
2)在请求激增的时候 A为啥不会挂,队列为啥不会挂,反而是B容易挂?
A的角色是一个"网关"服务器,收到客户端的请求,再把请求转发给其他服务器,这样的服务器里面的代码做的工作比较简单(单纯的数据转发),消耗的硬件资源通常更少
处理一个请求,消耗的资源更少,同样的配置下,就能支持更多的请求处理
同理,队列其实也是比较简单的程序,单位请求消耗的硬件资源也是比较少的
B这个服务器是真正干活的服务器,要完成一系列的业务逻辑
这一系列的工作,代码量非常庞大,消耗的时间很多,消耗的系统硬件资源也是更多的
因此,MySQL这样的数据库,处理每个请求的时候做的工作就是比较多的,消耗的硬件资源也是比较多的,因此Mysql也是后端系统中最容易挂的部分
对应的,像Redis这样的内存数据库,处理请求做的工作远远少于mysql做的工作,消耗的资源更少,Redis就比Mysql不容易挂掉
总结:生产者模型的好处
1)解耦合
2)削峰填谷
代价:
1)需要更多的机器,来部署消息队列(小,机器硬件不值钱)
2)A和B之间的通行延时会变的更长
对于要求A和B之间的调用,来要求响应时间比较短就不太合适了
阻塞队列
在Java标准库也提供线程的封装BlockingQueue
并且有多种实现方式,比如基于数组实现,或者基于链表实现,或者带有优先级的(基于堆)
后面的参数是容量---最多能容纳多少元素

Queue的一些操作,offerpoll这些在BlockingQueue中同样也能使用,但是缺点是不能阻塞
BlockingQueue提供了另外两种专属方法
put入队列
take出队列
这两种是可以阻塞的,阻塞队列没有提供阻塞版本的获取队首元素的操作
由于put和take可能产生阻塞,这样的阻塞又会被interrupt方法唤醒
java
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class practice {
public volatile static int i=0; // 保证了内存的可见性
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue=new ArrayBlockingQueue(10);
Thread thread1 =new Thread(()->{
while(true){
try {
queue.put(i);
System.out.println("放入元素"+i);
i++;
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2=new Thread(()->{
while(true){
try {
queue.take();
System.out.println("拿走元素"+i);
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
阻塞队列的使用
queue.put()放元素
queue.take()取元素
模仿BlockingQueue来实现自己的BlockingQueue,
实现内部的take()和put() 方法
java
public class simulateBlockingQueue {//模拟阻塞队列的实现
private volatile static int[] ele=null;
private volatile static int size=0;
private volatile static int head=0;
private volatile static int tail=0;
private volatile static int capacity=0;
public simulateBlockingQueue(int i){
this.capacity=i;
ele=new int[capacity];
}
public void put(int i) throws InterruptedException {//放入数据
synchronized (this){
while(size==capacity){//while处理比if更好,多次判断,同时避免被interrupt虚假唤醒
this.wait();//如果大于容量的话就等待,直到减少之后再唤醒此处等待
}
ele[tail]=i;
size++;
tail++;
if(tail>=capacity){
tail=0;
}
synchronized(this){
this.notify();
}
}
}
public int take() throws InterruptedException {//获取数据
synchronized (this){
while(this.size==0){
this.wait();//这里先返回,等会来处理
}
int ret=ele[head];
head++;
if(head>=capacity){
head=0;
}
size--;
this.notify();
return ret;
}
}
}