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
两个成员的操作时次序已经混乱,导致了数据不一致现象出现和线程安全问题的产生。