多线程的学习(四)阻塞队列与volatile

什么是生产者消费者模式

​ 某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。

​ 单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。

1、你把信写好——相当于生产者制造数据

2、你把信放入邮筒——相当于生产者把数据放入缓冲区

3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区

4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据

优点

  • 解耦
  • 支持并发
  • 支持忙闲不均

个人理解:

线程1:消费者线程

线程2:生产者模型

生产者---修改对象值告诉--消费者

使用队列写一个生成者消费者模式

使用一个队列写一个生产者消费者的模式。

public class demo {
    //创建一个队列
    public static BlockingQueue<String> queue = new LinkedBlockingQueue(3);
    /*生产者-消费者模式的核心在于容器的容量,如果容器已经满了则通知生产者不要生产;
     如果容器已经空了则通知消费者不要继续消费。因此容器的加减应该做同步,
     容器的容量作为通知的条件。这其实就是阻塞队列的实现。
    如果换成一般队列
    判断队列为空isEmpty  等着队列加值 阻塞消费者 ---》队列加值 唤醒消费者
    判断队列为满size==容量 等着队列取值 阻塞生产者 ---》队列取值 唤醒生产者
    同步问题如何解决:使用锁
    对前两者加值取值的操作增加一个锁操作
    //放元素
    put(E e){
    //获得锁 保证同步
        takeLock.lockInterruptibly();
        try {
            while(队列满了吗){
              //存储队列的线程阻塞
               .await();
            }
            存储数据;
            if (队列没有满?)
                //唤醒阻塞的线程
        } finally {
       //解锁
            takeLock.unlock();
        }
    }
    */

    public static void main(String[] args) {
        Thread p1 = new Thread(new ProductThread("1"));
        Thread p2 = new Thread(new ProductThread("2"));
        Thread p3 = new Thread(new ProductThread("3"));
        Thread p4 = new Thread(new ProductThread("4"));

        Thread c1 = new Thread(new ConsumerThread());
        Thread c2 = new Thread(new ConsumerThread());
        Thread c3 = new Thread(new ConsumerThread());
        Thread c4 = new Thread(new ConsumerThread());
        c1.start();
        c2.start();
        c3.start();
        c4.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();

    }
    
    //生产者线程
    static class ProductThread implements Runnable{
        private String name;
        public ProductThread(String name){
            this.name = name;
        }
        public void run() {
            try {
                System.out.println("放:"+name);
                //往队列中存放名字
                queue.put(name);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //消费者线程
    static class ConsumerThread implements Runnable{
        public void run() {
            try {
                //检索并移除此双端队列表示的队列的头部
                System.out.println("加:"+queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

JAVA内存模型

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

这是个代码:int i = i+1;

这是个执行流程:

1591243593511.png

**但是多线程操作的时候会出现,两个线程都从内存取了初始的I,同时在高速缓存中进行运算,结果写回内存的数字只是i+1;而不是i+2 **

如何解决?

1.加锁,保证线程同步

2.缓存一致性协议

什么是缓存一致:

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

1591244152736.png

并发编程中的一些概念:原子性,可见性,有序性 。

  • 原子性
    • 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性
    • 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性
    • 即程序执行的顺序按照代码的先后顺序执行。
  • 想并发程序正确地执行,必须要保证原子性、可见性以及有序性。

Java内存模型中涉及到的概念有:

  • 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生
  • 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
  • 定义了程序中变量的访问规则
1591244346978.png

int i = 10;

流程:

1.在工作线程对变量i所在的工作内存中进行赋值

2.将值写入缓存中

java中的原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
int x = 10;         //语句1
int y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

语句1:将10写入工作内存  原子性操作
语句2:读取x的值,将x的值写入工作内存  非原子性操作
语句3和语句4包括3个操作:读取x的值,进行加1操作,写入新的值。  非原子性操作

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

java中的可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

java中的有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile关键字

  • volatile
    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    • 禁止进行指令重排序。

案例1

线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

代码可能出现的问题:

线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

解决方案:假如volatile修饰变量

  • 使用volatile关键字会强制将修改的值立即写入主存
  • 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
  • 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取

案例2(原子性不保证案例)

public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(() -> {
                for(int j=0;j<100000;j++){
                    test.increase();
                    System.out.println(test.inc);
                }
            }).start();
        }
    }
}

输出结果最后一个数:999974

虽然inc使用了volatile,但是结果显然出现了问题

问题在哪?

程序没有保证原子性,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。三个子操作会分别执行,可能产生割裂的效果。

虽然用了volatile关键字,对一个变量的写操作先行发生于后面对这个变量的读操作,线程1没有来的及对变量进行修改,所以此时线程2不知道变量已经被修改了,从内存取出来的还是原有的变量

如何解决?

使用同步代码块

使用lock

采用AtomicInteger(待研究)

案例3(有序性案例)

int x = 2;        //语句1
int y = 0;        //语句2
volatile boolean flag = true;  //语句3
int x1 = 4;         //语句4
int y1 = -1;       //语句5

由于volatile可以保证变量不进行重排序,所以一定程度上保证了有序性
上述代码中,语句3采用了volatile修饰,所以在执行代码的过程时,语句3一定是在第3行,他能保证语句1 2一定执行完毕,但是他并不能保证语句1与语句2执行的先后顺序

volatitle原理与实现机制

volatile的原理和实现机制

​ 《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

使用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

常用于状态的标记。以及双重检查(待学习)

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。