什么是生产者消费者模式
某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
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;
这是个执行流程:
**但是多线程操作的时候会出现,两个线程都从内存取了初始的I,同时在高速缓存中进行运算,结果写回内存的数字只是i+1;而不是i+2 **
如何解决?
1.加锁,保证线程同步
2.缓存一致性协议
什么是缓存一致:
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
并发编程中的一些概念:原子性,可见性,有序性 。
- 原子性
- 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性
- 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性
- 即程序执行的顺序按照代码的先后顺序执行。
- 想并发程序正确地执行,必须要保证原子性、可见性以及有序性。
Java内存模型中涉及到的概念有:
- 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生
- 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
- 定义了程序中变量的访问规则
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)该变量没有包含在具有其他变量的不变式中
常用于状态的标记。以及双重检查(待学习)