赞
踩
Java怎么实现平台无关性
class文件保存的就是翻译成的二进制字节码,也就是Java类文件的中的属性,方法,以及类中的常量信息都会被分别存储在class文件中,当然还会添加一个公有的静态常量属性 .class,这个属性记录的类的相关信息,及类型信息即class的一个实例。有了这个class文件就可以进一步调用,使用java 命令。
Javap:jdk自带的反编译器,可以查看Java编译器生成的字节码。
Java怎么实现平台无关性
class文件保存的就是翻译成的二进制字节码,也就是Java类文件的中的属性,方法,以及类中的常量信息都会被分别存储在class文件中,当然还会添加一个公有的静态常量属性 .class,这个属性记录的类的相关信息,及类型信息即class的一个实例。有了这个class文件就可以进一步调用,使用java 命令。
Javap:jdk自带的反编译器,可以查看Java编译器生成的字节码。
JVM如何加载.class文件
package com.muke.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author CHENBT
* @date 2019/08/04
* getDeclaredMethod() 方法可以获取到类中所有的方法属性,但是继承的方法是拿不到的
* getMethod()方法只能拿到非private的方法,也可以拿到集成的方法
*
*/
public class ReflactSimple {
public static void main(String[] args) {
try {
Class clazz = Class.forName("com.muke.reflect.Rocket");
/* Method method[] = clazz.getDeclaredMethods();
Constructor constructor = clazz.getConstructor();
for(Method m : method){
System.out.println(m.getName());
m.invoke(r,String.class);
}*/
/* Rocket r = (Rocket)clazz.newInstance();
Field f = clazz.getDeclaredField("name");
f.set(r,"cbt");
Method m = clazz.getDeclaredMethod("testHello");
m.setAccessible(true);
String s = (String)m.invoke(r);
System.out.println(s);
Method m1 = clazz.getMethod("testHello1");
m1.invoke(r);*/
Constructor c = clazz.getConstructor(String.class);
Rocket r = (Rocket)c.newInstance("cbt");
Method m = clazz.getMethod("testHello1",int.class);
m.invoke(r,4);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
ClassLoader
从编译到执行的过程
ClassLoader:ClassLoader主要工作在Class装载的加载阶段,主要作用是从系统外部获取Class二进制数据流。他是Java的核心组件,所有的Class都由ClassLoader进行加载,ClassLoader负责将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作。
ClassLoader的种类:
自定义ClassLoader的实现:
关键函数:
寻找Class文件,读进二进制流,并且对读取到的流做处理,进而返回一个Class对象。
定义类,函数接受的参数是以一个字节数组的形式传入进来的,byte数组就是接受字节码的。之后就可以对类进行定义返回。
示例代码:
package com.muke.reflect;
import java.io.*;
/**
* @author CHENBT
* @date 2019/08/27
*/
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName ;
public MyClassLoader(String path, String classLoaderName){
this.classLoaderName = classLoaderName;
this.path = path;
}
//用于查找类文件
@Override
public Class findClass(String name){
byte[] b = loadClassData(name);
return defineClass(name, b,0,b.length);
}
// 用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try{
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while((i = in.read()) != -1){
out.write(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return out.toByteArray();
}
}
类加载器的双亲委派机制
首先自底向上检查时候有类加载器已经加载过这个类,然后在自顶向下尝试加载类。
为什么要使用双亲委派机制来加载类
类的加载方式
隐式加载 :new (可以得到类的对象,支持不同参数类型的构造形式)
显式加载:loadClass,forName等(必须通过newInstance方法才能得到类的对象)
loadClass和forName的区别
元空间(MetaSpace)和永久代(PermGen)均是方法的实现,只是实现不同。方法区是指JVM的一种规范。在JDK7之后的永久代当中的字符串常量池已经被移动到了Java堆中,并且在JDK7之后使用元空间替代了永久代。其中区别是元空间使用本地内存,而永久代使用的是JVM内存。
JVM三大性能调优参数:
-Xms:堆的初始值
-Xmx:堆能达到的最大值
-Xss:每个线程虚拟机栈(堆栈)的大小,影响并发线程数的大小
Java内存模型中堆和栈的区别 -内存分配策略
静态存储:编译时确定每个数据目标在运行时需要的存储空间需求
栈式存储:数据区需求在编译期未知,运行时模块入口前确定。规定在运行中进入一个程序模块的时候必须知道该程序模块所需要的数据区的大小才能分配器内存。
堆式存储:编译时和运行时模块入口都无法确定,动态分配
对象被判定为垃圾的标准
没有其他对象锁引用
两种方法:
通过判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,被引用个+1,完成引用则-1
任何引用计数为0的对象实例可以被当做垃圾收集
优点:执行效率高,程序受执行影响较小
缺点:无法检测循环引用的情况,导致内存泄漏
通过判断对象的引用链是否可达来决定对象是否被回收
可以作为GC ROOT的对象
虚拟机栈中引用的对象(栈帧中本地变量表中引用的对象);
方法区中常量引用的对象
方法区中类静态属性引用的对象
本地方法栈中JNI(native方法)引用的对象
活跃线程的引用对象
垃圾回收算法
标记-清除算法
标记:从根集合开始扫描,对存活的兑现进行标记。
清除:从堆内存从头到尾进行线性遍历,回收不可达的对象
复制算法:
将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将存货的对象复制到另一面,然后再把已经使用过的内存一次性全部清理掉。
标记整理算法:
将可回收的对象标记后,将存活的对象移动到一端,然后清理掉端边界以外的内存。
避免内存的不连续性,适用于对象存活率较高的场景。
分代收集算法:
年轻代:尽可能块速的收集到那些生命周期短的对象Eden区和两个Survivor区
新创建的对象是要被放到Eden区的,如果对象较大,是直接被放到Survivor区的。From和To区也不是固定的,是随着垃圾回收的进行相互转换。
对象如何晋升到老年代
老年代:存放生命周期较长的对象
使用标记清除算法和标记整理算法
Serial收集器(新):(-XX:+UseSerialGC,复制算法)
单线程收集,进行垃圾收集时必须暂停所有的工作线程。
简单高效,Client模式的年轻代默认收集器
ParNew收集器(新):(-XX:+UseParNewGC,复制算法)
多线程收集器,其余的行为,特点和Serial收集器一样
单核执行效率不如Serial,在多核执行才有优势
目前只有这个收集器能和CMS收集器配合工作
Parallel Scavenge收集器(新):(-XX:+UseParallalGC,复制算法)
吞吐量=运营用户代码时间/(运行用户代码时间+垃圾收集时间)
这个算法更关注吞吐量,在多核执行下才有优势,Server模式下默认的年轻代收集器
SerialOld收集器(老):(-XX:UseSerialOldGC,标记整理算法)
单线程收集,使用垃圾收集时,必须暂停所有的工作线程,简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(老):(-XX:+UseParallelOldGC,标记整理算法)
多线程,吞吐量优先
CMS收集器(老):(-XX:+UseConcMarkSweepGC,标记清除算法)
垃圾回收线程几乎能与工作线程同时工作
G1收集器(-XX:+UseG1GC,复制+标记整理算法)
GarbageFirst收集器的特点:
引用
软引用可以用来实现高速缓存,因为软引用在内存不足时会被回收,内存足够时是不会被回收的,所以虽然在缓存里,但是不会出现内存溢出问题。
进程是自愿分配的最小单位,线程的CPU调度的最小单位。
对于Java虚拟机中运行程序的每个对象来说都有两个池,锁池和等待池,而这两个池又与Object的wait,notify和notifyAll方法以及synchronized有关。
锁池:假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B,C想要调用这个对象的某个synchronized方法(或者块),由于B,C线程在进入这个对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程A所占用,此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池:假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线层不会去竞争该对象的锁。
当调用Thread.yield()方法时,会给线程调度器一个当前线程愿意让出CPU使用权的暗示,但是线程调度器可能会忽略这个暗示。
同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号中的实例对象。
同步非静态方法锁市当前对象的实例对象。
同步代码块(synchronized(类.class)),锁是小括号中的类对象(Class对象)。
同步静态方法
对象在内存中的布局:对象头,实例数据和对其填充。
对象头
一般而言,synchronized使用的锁对象是存储在Java对象头里的,其主要结构是由Mark Word和Class Metadata Address组成,其中Class Metadata Address是对象指向他的类的元数据的,JVM通过这个指针确定该对象是那个类的数据。而Mark Word则用于存储对象自身的运行时数据,他是实现轻量级锁和偏向锁的关键。Mark Word默认存储的是对象的hashCode,分代年龄,锁类型,锁标志位信息等。由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word被设计成为一个非固定的数据结构,以便存储更多有效的数据。他会根据对象本身的状态,复用自己的存储空间。其中轻量级锁和偏向锁是Java6后对synchronized锁进行优化后新增加的。
Monitor
Java每个对象天生自带了一把看不见的锁,他叫做内部所或者Monitor锁,也叫做管程或者监视器锁,也可以描述为一种同步机制。
主要看一下重量级锁,也就是重量级锁。其中指针指向的是Monitor对象的起始地址,每个对象都存在着个Monitor与之关联,对象与其Monitor之间的关系有存在多种实现方式,如Monitor可以和对象一起创建销毁,或当前线程试图获取对象锁时自动生成。但当Monitor被一个线程持有后,它便处于锁定状态。在Java虚拟机即Hotspot中monitor是由ObjectMonitor实现的。位于Hotspot虚拟机原码即objectMonitor.hpp文件里,是通过c++实现。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; 指向持有ObjectMonitor对象的线程
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
WaitSet和EntryList就是保存ObjectWaiter对象列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面,_owner 指向持有ObjectMonitor对象的线程,当多个线程同时访问同一块同步代码的时候,首先会进入到EntryList集合里面,当线程获取到对象的Monitor对象后,就进入到Object区域,并把Monitor中的_owner设置为当前线程,同时Monitor中的计数器_count就会+1 ,若线程调用wait方法就会释放当前线程持有的Monitor,_owner就会被恢复成null,_count -1,同时该线程即Object实例就会被放入WaitSet集合中等到唤醒。若当前线程执行完毕也会释放Monitor锁,并复位对应变量的值,以便其他线程继续获取Monitor锁。
图中可以看出,synchronized的开始是monitorenter指令,结束是monitorexit指令。
synchronized代码块:如果一个线程先于当前线程持有了ObjectMonitor的持有权,那么当前线程将会被阻塞在这里,直到持有该锁的线程执行完毕,即MonitorExit指令被执行,持有线程将释放Monitor锁,并设置计数器为0,其他线程将有机会持有Monitor。为了保证在方法异常完成时,monitorenter和monitorexit依然可以配对执行,便器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,他的目的就是用来执行monitorexit指令,在字节码中看出多了一个monitorexit指令。
synchronized方法:方法级的同步是隐式的,即无需通过字节码指令来控制,在synchronized方法中有个ACC_SYNCHRONIZED标志,用来区分一个方法是否是同步方法,当方法调用时调用指令会检查ACC_SYNCHRONIZED访问标志是否被访问,执行线程将会持有Monitor,然后执行方法,该方法不困是正常完成还是非正常完成都会释放Monitor,在方法执行期间,该线程持有了Monitor,其他任何线程都无法在获得同一个monitor,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理时,那么这个线程持有的monitor在异常抛出到方法外部时释放。
为什么会对synchronized嗤之以鼻?
自旋锁与自适应自旋锁
自旋锁
由于每个线程的执行时间是不固定的,所以很难合理的设置PreBlockSpin,这时候就出现了自适应自旋锁。
自适应自旋锁
锁消除
更彻底的优化
锁粗化
原则上在加锁的时候,尽可能将同步块的作用范围限制到尽量小的范围,即只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量进可能变小,在存在锁同步竞争中,也可以使得等待锁的线程尽可能早的拿到锁,大部分的情况是可以完成的。但是如果存在一连串操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的。那即使没有线程竞争,频繁的进行互斥锁操作也会有不必要性能操作。此时的操作就是通过加锁范围,避免反复加锁和解锁。
synchronized的四种状态
锁膨胀方向:无锁à偏向锁à轻量级锁à重量级锁
无锁就是并没有加入任何锁,此时的目标共享数据是没有被任何一条线程给占用的。
减少统一线程获取锁的代价
核心思想:如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变成了偏向锁结构,当该线程在此请求锁时,无需在做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
不适合锁竞争比较激烈的多线程场合。
轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,偏向锁就会升级成轻量级锁。
使用场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
轻量级锁的加锁过程:
解锁过程:
锁的内存语义
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主存中;
当线程获取锁时,Java内存模型就会把该线程对应的本地内存置为无效, 从而使得被监视器保护的临界区代码必须从主存中读取共享变量。
ReentrantLock(重入锁)
ReentrantLock公平性的设置
ReentrantLock fairLock = new ReentrantLock(true);这样的锁的创建方式就是公平锁,这里所谓的公平就是在竞争过程中,将锁赋予等待时间最长的线程,公平性是减少线程饥饿的办法。获取锁的顺序是按调用lock方法的循序。而非公平锁是每个线程抢占锁的顺序不一定,随机抢占,synchronized就是非公平锁。
什么是Java内存模型中的happens-before
Java内存模型
Java内存模型(Java Memory Model),本身是一种抽象的概念,并不真实存在,它描述的是一种规范或者格则,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成实例数组对象的元素)的访问方式。
jvm运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),用于存储线程私有的数据。而Java内存模型规定所有变量都存储在主内存中,主内存是共享数据区域,所有线程都可以访问,但线程对变量的操作即读取,赋值等必须在工作内存中进行。首先将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量写回到主内存中,不能直接操作主内存中的变量。那工作内存中存储者主内存中变量的副本拷贝,工作内存是每个线程的私有区域。因此不同的线程间无法访问彼此的工作内存,线程的通信也就是传值必须通过主内存来完成。
JMM主内存
JMM工作内存
JMM与Java内存区域划分的是不同的 概念层次
主内存与工作内存的数据存储类型及操作方式的归纳
指令重排序
在执行程序的时候为了提高效率,处理器和编译器常常会对指令进行重排序,但是不能随意重排序。需要满足两个条件:
即如果无法通过happens-before原则推导的,才能进行指令的重排序。jvm内部的实现通常是通过依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证,也就是实现了各种happens-before规则。与此同时,更多的复杂度在于需要尽量确保各种编译器,各种体系结构的处理器能够提供一致的行为。
A操作的结果必须对B操作可见,则A与B存在happens-before关系,happens-before原则是他们是否存在竞争,线程是否安全的主要依据。依靠这个原则我们就能解决在并发环境下两个操作之间存在冲突的问题。
i = 1 ; //线程A执行
j = i ; //线程B执行
线程A happens-before线程B 操作, 线程B在执行后,j = 1 一定是成立的。如果他们之间不存在happens-before,那么j就不一定等于1。
happens-before八大原则
如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,那么JVM可以对这两个操作进行重排序;如果操作A happens-before 操作B,那么操作A 在内存上所做的操作对操作B都是可见的。
volatile
jvm提供的轻量级同步机制
volatile修饰的共享变量在多线程环境下的写操作不能够保证线程安全
volatile是怎样实现立即可见的?
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile值时,JMM会把该线程对应的工作内存置为无效,那么该线程只能从主内存中读取变量。
volatile如何禁止重拍优化
内存屏障(Memory Barrier),这是一个CPU指令,起作用有两个,一是保证特定操作的执行顺序,由于编译器和处理器都能执行指令的重拍优化,如果在指令这里插入一条memory barrier,那就告诉编译器和处理器,不管什么指令,都不能和这条指令重排序,就是不能打乱他们的顺序。通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。二是保证某些变量的可变性,即利用该特性实现volatile内存可见性,强制刷出CPU的各种缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
以上单利写法,通过使用synchronized代码块试图解决多线程请求单利时带来的重复创建单利对象的隐患。以上代码在多线程情况下依然还是有隐患的。原因在于某一个线程执行到第一次检测的时候,读到的instance不为null时,instance是实例对象可能还没完全初始化,因为instance = new Singleton();有一下三个步骤:首先为对象分配内存空间,其次初始化对象,然后就是设置instance指向刚分配的内存地址。此时instance不为空,这是时候就有可能发生重排序。将第三步提前,第二部后移,这是因为步骤2和步骤三不存在数据依赖的关系,而且无论排前还是排后执行程序的结果在单线程中并没有改变,因此这种重排序优化是允许的。但是指令重排只能保证串行语义的一致性,即单线程的一致性,并不会关心多线程的一致性。因此当一条线程访问instance不为null的时候,对象未必完成初始化,这就造成线程的安全问题。
解决方法:
使用volatile去禁止instance去进行指令重排序。
synchronized这种独占锁属于悲观锁,悲观锁始终假定会发生并发冲突,因此会屏蔽一切可能会违反数据完整性的操作。除此之外还有乐观锁,他假设不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试。cas就是乐观锁。
一种高效实现线程安全的方法
CAS思想
缺点
利用Executors创建不同的线程池满足不同的场景的要求
Error和Exception的区别
从概念角度解释Java的异常处理机制
Exception
RuntimeException:运行时的异常,不可预知的,如数组下标越界访问空指针等,但是这种异常在编写程序时应当避免。
非RuntimeException
出RuntimeException以外的异常,从编译器角度讲是必须处理的异常,如果不处理,程序就不能编译通过。如IOException,SQLException等。
从责任角度看
常见的Error以及Exception
RuntimeException
非RuntimeException
Error
put()方法的逻辑:
JUC包的主要分类
executo
executor,即线程执行器,就是一个任务的执行和调度的框架,次外,在tools下还有和executor相关的Executors,用于创建ExecutorService,ScheduledExecutorsService,ThreadFactory和Callable对象。
locks
在Java5以前协调共享对象的访问时可以使用的调机制只有synchronized和volatile,在之后便出现了JUC的locks了,locks里引入了显式锁,方便对线程间的共享资源进行更细粒度的锁控制。Condition是Lock对象创建的,一个Lock对象可以创建多个Condition对象,主要用于将线程的等待唤醒即wait,notify,notifyAll对象化。不管是Lock还是Condition,都是就要AQS实现的。而AQS的底层是调用LockSupport.unpark和LockSupport.park()方法去实现线程的阻塞和唤醒的。
ReentrantReadWriteLock,可重入的读写锁,写写排斥,写读排斥,读读共享。
atomic
这个当中指一个操作是不可中断的。在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程锁干扰。所以所谓原子类就是具有原子操作特征的类。这些类的底层是借用CAS操作实现的。
tools
事件指的就是CountDownLatch里面的countDown()方法CyclicBarrier
其他线程调用完countDown()方法还是会继续执行的,并不是调用了countDown()方法后就执行完成了,是可以执行你之后的操作了。通过调用countDown()方法也就说我这个线程对主没有影响了,具体主线程是否可以执行就看其他线程对你有没有影响了。在图中,主线程引用了CountDownLatch之后,就进入了等待状态,此时CountDownLatch里面有个类似count的变量,一开始初始化为一个整数(就是事件的个数),每当一个子线程调用countDown()之后这里的count就-1,直到所有的子线程都调用了countDown()之后,即count编程0之后,线程才可以重新恢复到执行的状态。
等待其他线程,会阻塞自己的当前线程,所有的线程都必须同时到达了栅栏才能继续执行。也可以是所有线程到达栅栏处就触发另外一个预先设置执行的线程。
和CountDownLatch一样,CyclicBarrier里面也有一个计数器,如图中,每一个线程调用一次await()计数器就会-1,且在这些线程调用await()方法是计数器不为0,这些线程也会被阻塞。当TA线程在所有线程到达栅栏时才会跟着其他线程一起执行。与CountDownLatch不同之处在于CountDownLatch在子线程执行完countDown方法后子线程是可以继续执行的,但是CyclicBarrier是阻塞了子线程,等到所有线程都执行完之后才一起执行。
通过aquire去获取一个许可,如果没有,就等待,一旦利用这个资源执行完业务逻辑之后,线程就会调用release方法释放一个许可出来。
它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。Exchanger会产生一个同步点,一个线程先执行到达同步点就会被阻塞,直到另外一个线程也进入到同步点,那当两个都到了同步点后就开始换数据。线程方法中调用Exchanger.exchange方法就是所谓的同步点了。Exchanger只能用于两个线程交换数据。将交换的数据放在了String girl = exchanger.exchange("我其实暗恋你很久了。。。");里面,执行后就交换了。
collections
BlockingQueue:提供可阻塞的入队和出队操作。如果队列满了,出队操作将阻塞,如果队列空了,出队操作将阻塞。
主要用户生产者消费者模式,在多线程场景时,生产者线程在队列尾部添加元素,消费者线程在队列头部添加元素,而通过这样的方式能够达到生产和消费进行个例的目的。
BlockingQueue
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。