一个进程可以有多个线程,多个线程并发会存在数据安全问题,对于解决多线程的安全问题,有两种方法:
1、同步代码块
synchronized(obj){具体执行的代码块}
其中obj为共享资源、共享对象(obj需要是Object的子类),此时的obj成为同步监视器。
2、同步方法
将核心的代码逻辑定义为一个方法,使用synchronized关键字进行修饰,此时不需要指定共享对象。
同步方法中不需要同步监视器,因为同步方法的监视器是this,也就是该对象本身。
最简单的消费者与生产者问题
我们来试一下最简单的消费者和生产者的问题解决,先描述一下问题:
生产者可以生产旺仔品牌的小馒头和娃哈哈品牌的矿泉水,生产了便放到商品区(一次只能放一个商品),消费者从商品区取走商品。
此时我们的共享对象是商品区Goods类。
我们先来看一下商品Goods类,商品有属性品牌brand和名称name,而且商品区只有一个位置,我们需要用flag来标识一下。
然后我们看一下生产者,生产者生产商品,商品的品牌和名称不能弄混,要等到商品区空了才可以放进去(即flag=false;)生产完了flag=true。
因此,flag=true时,生产者线程进入阻塞状态;flag=false时,生产者生产商品(set操作),商品生产完了之后,需要把flag设为true,并唤醒消费者去进行消费。
关于代码,我们此时用的是同步方法实现商品同步,生产者生产了商品,消费者才可以取,否则消费者不可以取商品,set和get都是原子操作。
接着我们看一下消费者,如果flag等于false的话,意味着生产者没有生产商品,此时消费者无法消费,需要让消费者线程进入到阻塞状态;在商品区有商品时(即flag=true;),消费者可以消费商品,消费完flag就为false,最后唤醒生产者去进行生产。
生产者也是需要在消费者消费了才可以去set商品,因此set和get都是原子操作。
最后,我们将代码整理好,需要写Goods、Consumer、Producer三个类。
先看看Goods类:
public class Goods {
private String brand;
private String name;
//默认是不存在商品的,如果值等于true的话,代表有商品
private boolean flag = false;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//消费者获取商品
public synchronized void get(){
/*
* 如果flag等于false的话,意味着生产者没有生产商品,此时消费者无法消费,需要让消费者线程进入到阻塞状态,等待生产者生产,当
* 有商品之后,再开始消费
* */
if (!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者取走了"+this.getBrand()+"----"+this.getName());
flag = false;
//唤醒生产者去进行生产
notify();
}
//生产者生产商品
public synchronized void set(String brand,String name){
//当生产者抢占到cpu资源之后会判断当前对象是否有值,如果有的话,以为着消费者还没有消费,需要提醒消费者消费,同时
//当前线程进入阻塞状态,等待消费者取走商品之后,再次生产,如果没有的话,不需要等待,不需要进入阻塞状态,直接生产即可
if(flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setBrand(brand);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "--" + this.getName());
//如果代码执行到此处,意味着已经生产完成,需要将flag设置为true
flag = true;
//唤醒消费者去进行消费
notify();
}
}
然后到生产者Producer:
我们测试10个数据。
public class Producer implements Runnable {
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
goods.set("娃哈哈","矿泉水");
} else {
goods.set("旺仔","小馒头");
}
}
}
}
接着就是Consumer:
public class Consumer implements Runnable {
private Goods goods;
public Consumer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for(int i = 0;i<10;i++){
goods.get();
}
}
}
最后,我们写个Test类运行程序:
public class Test {
public static void main(String[] args) {
Goods goods = new Goods();
Producer producer = new Producer(goods);
Consumer consumer = new Consumer(goods);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
输出结果:
以上是比较简单的做法,下面来说一种复杂点的做法。
使用阻塞队列来解决问题
先写Goods类:
这个比较容易,就写个构造函数和两个属性的get和set方法就好。
Consumer和Producer的方法就不在这里写了。
public class Goods {
private String brand;
private String name;
public Goods(String brand, String name) {
this.brand = brand;
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
再来看看ConsumerQueue类,需要用到BlockingQueue,先介绍一下BlockingQueue:
在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。
引用出处:不怕难之BlockingQueue及其实现—天外流星for
接下来,看看ConsumerQueue类的代码:
----先使用泛型来定义阻塞队列,再在run中用take()从队列中取出商品。
import java.util.concurrent.BlockingQueue;
public class ConsumerQueue implements Runnable {
//此处使用泛型来定义阻塞队列
private BlockingQueue<Goods> blockingQueue;
//构造方法
public ConsumerQueue(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
for(int i = 0;i<10;i++){
try {
//直接从队列中取出商品
Goods goods = blockingQueue.take();
System.out.println("消费者消费的商品是:"+goods.getBrand()+"--"+goods.getName());
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
再看看ProducerQueue类:
import java.util.concurrent.BlockingQueue;
public class ProducerQueue implements Runnable {
//这里和ConsumerQueue一样,定义阻塞对列和构造函数
private BlockingQueue<Goods> blockingQueue;
public ProducerQueue(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
for(int i = 0;i<10;i++){
Goods goods = null;
//先定义好商品Goods类
if(i%2==0){
goods = new Goods("娃哈哈","矿泉水");
}else{
goods = new Goods("旺仔","小馒头");
}
System.out.println("生产者开始生产商品:"+goods.getBrand()+"--"+goods.getName());
try {
//这里用put()方法将商品放入阻塞队列中,等待消费者取出
blockingQueue.put(goods);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
最后看Test类。
先介绍一下ArrayBlockingQueue:
ArrayBlockingQueue底层是使用一个数组实现队列的,并且在构造ArrayBlockingQueue时需要指定容量,也就意味着底层数组一旦创建了,容量就不能改变了,因此ArrayBlockingQueue是一个容量限制的阻塞队列。因此,在队列全满时执行入队将会阻塞,在队列为空时出队同样将会阻塞。
put(E e)方法在队列不满的情况下,将会将元素添加到队列尾部,如果队列已满,将会阻塞,直到队列中有剩余空间可以插入。
引用出处:不怕难之BlockingQueue及其实现—天外流星for
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Test {
public static void main(String[] args) {
BlockingQueue<Goods> queue = new ArrayBlockingQueue<Goods>(5);
ProducerQueue producerQueue = new ProducerQueue(queue);
ConsumerQueue consumerQueue = new ConsumerQueue(queue);
new Thread(producerQueue).start();
new Thread(consumerQueue).start();
}
}
输出结果和上一种做法一样的: