> 文档中心 > Java内置锁的核心原理(一)

Java内置锁的核心原理(一)


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 代码新加了 sum1Locksum2Lock 两个新的成员属性。这两个属性没有参与业务处理, 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内所有争用线程公用一把锁,是非常粗粒度的同步机制。

  1. 但如果使用对象锁,并且JVM内的争用线程所争用的是不同对象锁,则争用线程可以同步进入临界区,锁的粒度就变细

  2. 当然,如果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{    public static final int MAX_AMOUNT = 10;    private List 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()用于从数据区消耗元素。

  1. 在 add( )实例方法中,加入元素之前首先会对 amount 是否达到上限进行判断,如果数据区满,则不能加入数据;

  2. 在 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 notSafeDataBuffer = new NotSafeDataBuffer();​    //生产者执行的动作    static Callable produceAction = () ->    {        //首先生成一个随机的商品        IGoods goods = Goods.produceOne();        //将商品加上共享数据区        try        {            notSafeDataBuffer.add(goods);        } catch (Exception e)        {            e.printStackTrace();        }        return goods;    };    //消费者执行的动作    static Callable 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类型的匿名对象,其具体的动作为: 首先调用 Goods.produceOne( )产生一个随机的商品,然后通过调用 notSafeDataBuffer.add( ),方法,将这个随机商品加入数据缓冲区实例中,完成生产者的动作。 (3)消费者 Action 静态实例。这也是一个 Callable类型的匿名对象,其具体的动作为:调用 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 两个成员的操作时次序已经混乱,导致了数据不一致现象出现和线程安全问题的产生。