Java内置锁的核心原理
Java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或者阻塞,直到线程A释放这个锁,如果A线程不释放这个锁,那么B线程将永远等待下去。
Java中每个对象都可以用做锁,这些锁成为内置锁。线程进入同步代码块或方法时会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法
线程安全问题
什么是线程安全呢?当多个线程并发访问某个Java对象(Object)时,不管系统如何调度这些线程,也不管这些线程将如何交替操作,这个Object都能表现出一致的、正确的行为,那么对这个Object的操作是线程安全的。如果这个Object表现出不一致的、错误的行为,那么对这个Object的操作不是线程安全的,发生了线程的安全问题。
自增运算不是线程安全的
1.线程安全小实验
为了说清楚问题,这里先提供一下以上实验的代码:10条线程并行运行,对一个共享数据进行自增运算,每条线程自增运算1000次,具体的代码如下:
public class NotSafePlus{
private Integer amount = 0;
//自增
public void selfPlus(){
amount++;
}
public Integer getAmount(){
return amount;
}
}
以上的测试不安全的累加器NotSafePlus
的测试用例,大致如下:
public class PlusTest{
final int MAX_TREAD = 10;
final int MAX_TURN = 1000;
CountDownLatch latch = new CountDownLatch(MAX_TREAD);
/**
* 测试用例:测试不安全的累加器
*/
@org.junit.Test
public void testNotSafePlus() throws InterruptedException{
NotSafePlus counter = new NotSafePlus();
Runnable runnable = () -> {
for(int i = 0; i < MAX_TURM; i++){
counter.selfPlus();
}
latch.countDown();
};
for(int i = 0; i < MAX_TREAD; i++){
new Thread(runnable).start();
}
latch.await();
Print.tcfo("理论结果:" + MAX_TURN * MAX_TREAD);
Print.tcfo("实际结果:" + counter.getAmount());
Print.tcfo("差距是:" + (MAX_TURN * MAX_TREAD - counter.getAmount()));
}
}
运行程序,输出的结果是:
[main|PlusTest.testNotSafePlus]:理论结果:10000
[main|PlusTest.testNotSafePlus]:实际结果:2949
[main|PlusTest.testNotSafePlus]:差距是:7051
通过结果可以看出:总计自增 10000 次,结果少了 2994 多次,差距在 30%左右。当然,这只是一次结果,每一次运行,差距都是不同的,大家可以动手运行体验一下。总之,从结果可以看出,对 NotSafePlus
的 amount 成员的“++”运算在多线程并发执行场景下出现了不一致的、错误的行为,自增运算符“++”不是线程安全的。 以上代码中,为了获得 10 个线程的结果,主线程通过 CountDownLatch
(倒数闩)工具类,进行了并发线程的等待。
CountDownLatch
(倒数闩)是一个非常实用的等待多线程并发的工具类。调用线程可以在倒数闩上进行等待,一直等待倒数闩的次数减少到 0,才继续往下执行。每一个被等线程执行完成之后,进行一次倒数。所有的被等线程执行完成之后,倒数闩的次数减少到 0,调用线程可以往下执行,从而达到一个并发等待的效果。
在使用 CountDownLatch
时,先创建了一个 CountDownLatch
实例,设置其倒数的总数(例子中值为 10),这表示等待 10 个线程执行完成。主线程通过调用 latch.await()
在倒数闩实例上执行等待,等到 latch 实例的倒数到 0,才能继续执行。
2.原因分析:自增运算不是线程安全的
为什么自增运算符不是线程安全的呢?实际上,一个自增运算符是一个复合操作,至少包括三个JVM
指令:“内存取值”“寄存器增加 1”“存值到内存”。这三个指令在 JVM
内部是独立进行的,中间完全可能会出现多个线程并发进行。 比如在 amount=100 时,假设有三个线程读同一时间取 amount 值,读到的都是 100,增加 1后结果为 101,三个线程都将结果存入到 amount 的内存, amount 的结果是 101,而不是 103。
而三个 JVM
指令“内存取值”“寄存器增加 1”“存值到内存”是不可以再分的,这三个操作具备原子性,是线程安全的,也叫原子操作。两个或者两个以上的原子操作合在一起进行操作,就不在具备原子性。比如先读后写,那么就有可能在读之后,这个变量被修改过,写入后就出现了数据不一致的情况。
1.2 临界区资源与临界区代码段
Java 工程师在进行代码开发时,常常倾向于认为代码会以线性的、串行的方式执行,常常容易忽视了多个线程并行执行,这就会导致意想不到的结果。 前面的线程安全小实验,展示了在多个线程操作相同资源(如变量、数组或者对象)时候,就可能出现线程安全问题。一般来说,只在多个线程对这个资源进行写操作的时候才会出现问题,如果是简单的读操作,不改变资源的话,显然是不会出现问题的。 临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。 在并发情况下,临界区资源是受保护的对象。临界区代码段(Critical Section)是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。临界区代码段(Critical Section)的进入和退出,具体如图所示。
竟态条件(Race Conditions)是可能在由于在访问Critical Section时没有互斥的访问而导致的特殊情况。如果多个线程在Critical Section的并发执行结果,可能因为代码的执行顺序不同而出现不同的结果,我们就说这时候在临界区出现了竞态条件问题。
前面的线程安全小实验的代码中,amount为临界区资源,selfPlus()
可以理解为临界区代码段(Critical Section),具体如下:
public class NotSafePlus{
private Integer amount = 0; //临界区资源
//临界区代码段(Critical Section)
public void selfPlus(){
amount++;
}
}
当多个线程访问临界区selfPlus()
方法时,就会出现竞态条件(Race Condition)的问题。更标准的说,当两个或多个线程竞争同一个资源时,对资源的访问顺序就变得非常关键。
为了避免竞态条件的问题,我们必须保证Critical Section操作必须具备排他性。这就意味着一个线程进入Critical Section执行时,其他线程不能进入Critical Section执行。
在Java中,我们可以使用synchronized关键字同步代码块,对Critical Section进行排他性保护,示意代码如下:
synchronized(synObject){
//critical section
}
在Java中,使用synchronized关键字,还可以使用Lock显式锁实例,或者使用原子变量(Atomic Variables)。对Critical Section 进行排他性保护。Lock显式锁、原子变量将在后续介绍,本章首先介绍synchronized关键字。
synchronized关键字
Java中,线程同步使用最多的方法是——使用synchronized关键字。每个Java对象都隐含有一把锁,这里称之为Java内置锁(或者对象锁、隐式锁)。使用synchronized(syncObject
)调用相当于获取syncObject
的内置锁,所以,可以使用内置锁对Critical Section进行排他性保护。
synchronized 同步方法
synchronized关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为了同步方法,具体的例子如下
//同步方法
public synchronized void selfPlus(){
amount++;
}
关键字 synchronized 的位置处于同步方法的返回类型之前。回到前面的线程安全小实验,现在使用 synchronized 关键字对 Critical Section 其进行保护,代码如下:
public class SafePlus{
private Integer amount = 0;
public void selfPlus(){
synchronized (this){
amount++;
}
}
public Integer getAmount(){
return amount;
}
}
再次运行测试用例程序,累加 10000 此之后,最终的结果不再有偏差,与预期的结果(10000)是相同的。 在方法声明中设置 synchronized 同步关键字,保证了其方法的代码执行流程是排他性的。任何时间只允许一条线程进入同步方法(临界区代码段),如果其他线程都需要执行同一个方法,那么对不起,其他的线程只能等待和排队。
synchronized 同步块
对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件(Race Conditions)的问题。但是对于较大的临界区代码段,我们为了执行效率,最好将同步方法分为小的临界区代码段。我们通过下面这个例子来具体讲述:
public class TwoPlus{
private int sum1 = 0;
private int sum2 = 0;
//同步方法
public synchronized void plus(int val1, int val2){
//临界区代码段
this.sum1 += val1;
this.sum2 += val2;
}
}
在以上代码中,临界区代码段包含了对两个临界区资源的操作,这两个临界区资源分别为sum1、 sum2。使用 synchronized 对 plus(int val1, int val2)进行同步保护之后,进入临界区代码段的线程拥有 sum1、 sum2 的操作权,并且是全部占用。一旦线程进入,当线程在操作 sum1 而没有操作 sum2 时,也将 sum2 的操作权白白占用,其他的线程由于没有进入临界区而只能看着 sum2被闲置而不能去执行操作。 所以,将 synchronized 加在方法上,如果其保护的临界区代码段包含的临界区资源(要求是相互独立的)多于一个,会造成临界区资源的闲置等待,这就会影响临界区代码段的吞吐量。为了提升吞吐量,可以将 synchronized 关键字放在函数体内,同步一个代码块。 synchronized 同步块的写法是:
在synchronized 同步块后边的括号中,是一个 syncObject
对象,代表着进入临界区代码段需要获取 syncObject
对象的监视锁,或者说将 syncObject
对象监视锁做为临界区代码段的同步锁。由于每一个 Java 对象都有一把监视锁(Monitor),所以任何 Java 对象都能作为 synchronized 的同步锁。 单个线程在 synchronized 同步块后边同步锁后,方能进入临界区代码段;反过来说,当一条线程获得 syncObject
对象的监视锁,其他线程就只能等待。 使用 synchronized 同步块对上面的 TwoPlus
类进行吞吐量的提升改造,具体的代码如下:
public class TwoPlus{
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1);//同步锁一
private Integer sum2Lock = new Integer(2);//同步锁二
public void plus(int val1, int val2){
//同步块1
synchronized(this.sum1Lock){
this.sum1 += val1;
}
//同步块2
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
改造之后,对两个独立的临界区资源 sum1 、 sum2
的加法操作可以并发执行了,在某一个时刻,不同的线程可以对 sum1 、 sum2
的同时进行加法操作,提升了 plus( )方法的吞吐量。
在 TwoPlus
代码中,由于同步块 1、同步块 2 保护着两个独立的临界区代码段,需要两把不同的 syncObject
对象锁,所以, TwoPlus
代码新加了 sum1Lock
、 sum2Lock
两个新的成员属性。这两个属性没有参与业务处理, TwoPlus
仅仅利用了 sum1Lock 、 sum2Lock
的内置锁功能。 synchronized 方法和 synchronized 同步块,有啥区别呢?总体来说, synchronized 方法是一种粗粒度的并发控制,某一时刻只能有一条线程执行该 synchronized 方法;而 synchronized 代码块则是一种细粒度的并发控制,处于 synchronized 块之外的其他代码,是可以被多条线程并发访问的。在一个方法中,并不一定所有代码都是临界区代码段,可能只有几行代码会涉及到线程同步问题。所以 synchronized 代码块比 synchronized 方法更加细粒度地控制了多条线程的同步访问。 synchronized 方法和 synchronized 代码块有什么联系呢?在 Java 的内部实现上, synchronized方法实际上等同于用一个 synchronized 代码块,这个代码块包含了同步方法中的所有语句,然后在 synchronized 代码块的括号中传入 this 关键字,使用 this 对象锁作为进入临界区的同步锁。
例如,下面两种实现多线程同步的plus方法版本,编译成JVM
内部字节码后,结果是一样的。 版本一,使用 synchronized 代码块进行方法内部全部代码的保护,具体代码如下:
public void plus(){
synchronized(this){//进行方法内部全部代码的保护
amount++;
}
}
版本二, synchronized 方法进行方法内部全部代码的保护,具体代码如下:
public synchronized void plus(){
amount++;
}
综上所述,synchronized方法的同步锁实质上是使用了this对象锁,这样就免去了手工设置同步锁的工作。而使用synchronized代码块,则需要手工设置同步锁。
静态方法的同步调用
在 Java 世界里一切皆对象。 Java 有两种对象: Object 实例对象和 Class 对象。每个类的运行时的类型信息,用 Class 对象表示的,它包含了与类名称、 继承关系、字段、方法有关的信息。JVM
将一个类加载入自己的方法区内存时,都会为其创建一个 Class 对象,对于一个类来说其Class 对象也是唯一的。 Class 类没有公共的构造方法, Class 对象是在类加载的时候由 Java 虚拟机调用类加载器中的defineClass
方法自动构造的,因此不能显式地声明一个 Class 对象。 所有的类都是在第一次使用时,被动态加载到 JVM
中(懒加载),其各个类都是在必需时才加载的。这一点与许多传统语言(如 C++)都不同,动态加载使能的行为,使得类加载器首先检查这个类的 Class 对象是否已经被加载。如果尚未加载,类加载器会根据类的全限定名查找.class文件,验证后加载到 JVM
的方法区内存,并构造其对应的 Class 对象。 普通的 synchronized 实例方法,其同步锁是当前对象 this 的监视锁。那么,如果某个synchronized 方法是 static 静态方法,而不是普通的对象实例方法,其同步锁又是什么呢?下面展示一个使用 synchronized 关键字修饰 static 静态方法的例子,具体如下:
public class SafeStaticMethodPlus{
private static Integer amount = 0;
public static synchronized void selfPlus(){
amount++;
}
public Integer getAmount(){
return amount;
}
}
大家都知道,静态方法属于Class实例而不是单个Object实例,在静态方法内部是不可以访问Object实例的this引用(也叫指针、句柄)。所以,修饰static静态方法synchronized关键字就没有办法获得Object实例的this对象的监视锁。
实际上,使用synchronized关键字修饰static静态方法时,synchronized的同步锁并不是普通Object对象的监视锁(Monitor),而是类所对应的Class对象的监视锁。
为了以示区分,这里将Object对象的监视锁(Monitor)叫做对象锁,将Class对象的监视锁(Monitor)叫做类锁。当synchronized关键字修饰static静态方法时,同步锁为类锁;synchronized关键字修饰普通的成员变量方法(非静态方法)时,同步锁为对象锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,所以使用类锁作为synchronized的同步锁时,会造成同一个JVM
内的所有线程,只能互斥进入临界区段。
//对 JVM 内的所有线程同步
public static synchronized void selfPlus(){
//临界区代码块
}
所以,使用synchronized关键字修饰static静态方法时,一个JVM
内所有争用线程公用一把锁,是非常粗粒度的同步机制。
- 但如果使用对象锁,并且
JVM
内的争用线程所争用的是不同对象锁,则争用线程可以同步进入临界区,锁的粒度就变细; - 当然,如果
JVM
内的争用线程争用的还是同一把对象锁,则也只能互斥进入临界区段,同样是非常粗粒度的同步机制。
通过synchronized关键字所抢占的同步锁,什么时候释放呢?
一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放。另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。
生产者消费者问题
生产者消费者问题( Producer-Consumer Problem),也称有限缓冲问题( Bounded-Buffer Problem),是一个多线程同步问题的经典案例。 生产者消费者问题描述了两类访问共享缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者线程的主要功能是生成一定量的数据放到缓冲区中,然后重复此过程。 消费者线程的主要功能是从缓冲区提取(或消耗)数据。 生产者消费者问题关键: (1)保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 (2)保证在生产者加入过程、消费者消耗过程中,不会产生错误的数据和行为。 生产者消费者问题不仅仅是一个多线程同步问题的经典案例,而且业内已经将解决该问题的方案,抽象成为了一种设计模式——“生产者-消费者”模式。“生产者-消费者”模式是一个经典的多线程设计模式. 它为多线程间的协作提供了良好的解决方案。
生产者和消费者模式中,至少有以下关键点: (1)生产者与生产者之间、消费者与消费者之间,对数据缓冲区( DataBuffer
)的操作是并发进行的。 (2)数据缓冲区(DataBuffer
)是有容量上限的。 DataBuffer
满后,生产者不能再加入数据;DataBuffer
空时,消费者不能再取出数据。 (3)数据缓冲区(DataBuffer
)是线程安全的。在并发操作数据区的过程当中,不能出现数据不一致情况;或者在多个线程并发更改共享数据后,不会造成脏数据情况出现。 (4)生产者或者消费者线程在空闲时,需要尽可能阻塞而不是执行无效的空操作,尽量节约CPU 资源。
一个线程不安全的实现版本
1. 不是线程安全的数据缓冲区(DataBuffer
)类
//共享数据区,类定义class NotSafeDataBuffer<T>{
public static final int MAX_AMOUNT = 10;
private List<T> dadaList = new LinkedList<>();
//保存数量
private AtomicInteger amount = new AtomicInteger(0);
//向数据区增加一个元素
public void add(T element) throws Exception{
if(amount.get() > MAX_AMOUNT){
Print.tcfo("队列已经满了!");
return;
}
dataList.add(element);
Print.tcfo(element + "");
amount.incrementAndGet();
//如果数据不一致,抛出异常
if(amount.get() != dataList.size()){
throw new Exception(amount + "!=" + dataList.size());
}
}
//从数据区取出一个元素
public T fetch() throws Exception{
if(amount.get() <= 0){
Print.tcfo(element + "");
amount.decrementAndGet();
//如果数据不一致,抛出异常
if(amount.get() != dataList.size()){
throw new Exception(amount + "!=" + dataList.size());
}
return element;
}
}
}
DataBuffer
类型的实例属性 dataList
保存具体数据元素,实例属性 amount 保存元素的数量。DataBuffer
类型有两个实例方法,实例方法 add( )用于向数据区增加元素; 实例方法 fetch()用于从数据区消耗元素。
- 在 add( )实例方法中,加入元素之前首先会对 amount 是否达到上限进行判断,如果数据区满,则不能加入数据;
- 在 fetch()实例方法中,消耗元素前首先会对 amount 否大于零进行判断,如果数据区空,则不能取出数据。
2. 生产者、消费者的逻辑与动作解耦
生产者-消费者模式有多个不同版本的实现,这些版本的区别在于数据缓冲区(DataBuffer
)类以及相应的生产、消费动作(Action)不同,而生产者类、消费者类的执行逻辑是相同的。分离变与不变,是软件设计的一个基本原则。现在将生产者类、消费者类与具体的生产、消费 Action 解耦,从而使得生产者类、消费者类的代码在后续可以复用。 生产者、消费者逻辑与对应 Action 解耦后的类结构图,具体如图所示。
“分离变与不变”原则的背后,蕴藏着丰富的软件工程思想,例如:信息的分装与隐藏、系统的模块化、使用分层构架等等。其中“变”是指易变的代码或者模块,“不变”就是指系统中不易变化的部分。在解耦后的生产者-消费者模式结构中,不变的部分为生产者类 Producer、消费者类 Consumer,后续可以直接复用,不需要修改代码;变化的部分为数据缓冲区(DataBuffer
)类以及相应的生产和消费动作,后续不同的生产者-消费者实现版本,只要编写各自的 DataBuffer
和 Action 实现即可。
3. 通用的 Producer 类实现
通用 Producer 类组合了一个 Callable 类型的成员 action 实例,代表了生产数据所需要执行的实际动作,需要在构造 Producer 实例时传入。通用生产者类的代码,具体如下:
public class Producer implements Runnable{ //生产的时间间隔,产一次等待的时间,默认为200ms public static final int PRODUCE_GAP = 200;
//总次数
static final AtomicInteger TRUN = new AtomicInteger(0);
//生产者对象编号
static final AtomicInteger PRODUCER_NO = new AtomicInteger(1);
//生产者名称
String name = null;
//生产的动作
Callable action = null;
int gap = PRODUCE_GAP;
public Producer(Callable action, int gap){
this.action = action;
this.gap = gap;
if(this.gap <= 0){
this.gap = PRODUCE_GAP;
}
name = "生产者-" + PRODUCER_NO.incrementAndGet();
}
public Producer(Callable action){
this.action = action;
this.gap = PRODUCE_GAP;
name = "生产者-" + PRODUCER_NO.incrementAndGet();
}
public Producer(Callable action){
this.action = action;
this.gap = PRODUCE_GAP;
name = "生产者-" + PRODUCER_NO.incrementAndGet();
}
@Override
public void run(){
while(true){
try{
//执行生产的动作
Object out = action.call();
//输出生产的结果
if(null != out){
Print.tcfo("第" + TRUN.get() + "轮生产:" + out);
}
//每一轮生产之后,稍微等待一下
sleepMilliSeconds(gap);
//增加生产轮次
TRUN.incrementAndGet();
} catch (Exception e){
e.printStackTrace();
}
}
}
}
4. 通用的 Consumer 类实现 通用 Consumer 类也组合了一个 Callable 类型的成员 action 实例,代表了消费者所需要执行的实际消耗动作,需要在构造 Consumer 实例时传入。通用 Consumer 类的代码,具体如下:
public class Consumer implements Runnable{ //消费的时间间隔,默认等待100毫秒 public static final int CONSUME_GAP = 100;
//消费总次数
static final AtomicInteger TURN = new AtomicInteger(0);
//消费者对象编号
static final AtomicInteger CONSUMER_NO = new AtomicInteger(1);
//消费者名称
String name;
//消费的动作
Callable action = null;
//消费一次等待的时间,默认为1000ms
int gap = CONSUMER_GAP;
public Consumer(Callable action, int gap){
this.action = action;
this.gap = gap;
name = "消费者-" + CONSUMER_NO.incrementAndGet();
}
public Consumer(Callable action){
this.action = action;
this.gap = gap;
this.gap = CONSUME_GAP;
name = "消费者-" + CONSUMER_NO.incrementAndGet();
}
@Override
public void run(){
while(true){
//增加消费次数
TURN.incrementAndGet();
try{
//执行消费动作
Object out = action.call();
if(null != out){
Print.tcfo("第" + TURN.get() + "轮消费:" + out);
}
//每一轮消费之后,稍微等待一下
sleepMilliSeconds(gap);
} catch (Exception e){
e.printStackTrace();
}
}
}
}
5. 数据区缓冲区实例、生产 Action、消费 Action 的定义
在完成了数据缓冲区类的定义、生产者类定义、消费者类定义之后,接下来定义一下数据缓冲区实例、生产 Action、消费 Action,具体的代码如下:
public class NotSafePetStore{ //共享数据区,实例对象
private static NotSafeDataBuffer<IGoods> notSafeDataBuffer = new NotSafeDataBuffer();
//生产者执行的动作
static Callable<IGoods> produceAction = () ->
{
//首先生成一个随机的商品
IGoods goods = Goods.produceOne();
//将商品加上共享数据区
try
{
notSafeDataBuffer.add(goods);
} catch (Exception e)
{
e.printStackTrace();
}
return goods;
};
//消费者执行的动作
static Callable<IGoods> consumerAction = () ->
{
// 从PetStore获取商品
IGoods goods = null;
try
{
goods = notSafeDataBuffer.fetch();
} catch (Exception e)
{
e.printStackTrace();
}
return goods;
};
public static void main(String[] args) throws InterruptedException
{
System.setErr(System.out);
// 同时并发执行的线程数
final int THREAD_TOTAL = 20;
//线程池,用于多线程模拟测试
ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_TOTAL);
for (int i = 0; i < 5; i++)
{
//生产者线程每生产一个商品,间隔500ms
threadPool.submit(new Producer(produceAction, 500));
//消费者线程每消费一个商品,间隔1500ms
threadPool.submit(new Consumer(consumerAction, 1500));
}
}
}
这里的缓冲区中的具体数据类型,使用一个自定义的 IGoods
(商品)类,从而让整个生产者和消费者演示程序,模拟出一个宠物商店的功能。 上面的实现版本 NotSafePetStore.java
中,定义了三个重要的静态成员: (1)数据缓冲区静态实例。以元素类型为 IGoods
,定义了一个不安全的 NotSafeDataBuffer
数据缓冲区实例。 (2)生产者 Action 静态实例。这是一个 Callable<IGoods>类型的匿名对象,其具体的动作为: 首先调用 Goods.produceOne( )
产生一个随机的商品,然后通过调用 notSafeDataBuffer.add( )
,方法,将这个随机商品加入数据缓冲区实例中,完成生产者的动作。 (3)消费者 Action 静态实例。这也是一个 Callable<IGoods>类型的匿名对象,其具体的动作为:调用 notSafeDataBuffer.fetch()
方法,从数据区取出一个商品,完成消费者的动作。
6. 组装出一个生产者和消费者模式的简单实现版本利用以上 NotSafePetStore
类所定义的三个静态成员,可以快速组装出了一个简单的生产者和消费者模式 Java 实现版本,具体的代码如下:
public static void main(String[] args) throws InterruptedException{ System.setErr(System.out); //同时并发执行的线程数
final int THREAD_TOTAL = 20;
//线程池,应用于多线程模拟测试
ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_TOTAL);
for(int i = 0; i < 5; i++){
//生产者线程每生产一个商品,间隔500ms
threadPool.submit(new Producer(producerAction, 500));
//消费者线程每消费一个商品,间隔1500ms
threadPool.submit(new Consumer(comsumerAction, 1500));
}
}
在NotSafePetStore 的 main 方法中,利用 for 循环向线程池提交了 5 个生产者线程, 5 个消费者实例。每个生产者实例生产一个商品,间隔 500ms;消费者实例每消费一个商品,间隔 1500ms;也就是说,生产的速度大于消费的速度。 启动main方法,程序开始并发执行,稍微等待一段时间,问题就出来了,部分结果截取如下:
从以上异常可以看出,在向数据缓冲区进行元素的增加或者提取时,多个线程在并发执行对amount、 dataList
两个成员的操作时次序已经混乱,导致了数据不一致现象出现和线程安全问题的产生。