当一个进程中存在多个线程时,且此时有可能对同一资源进行访问,那么此时可能会发生安全问题,因此在访问之前需要将这些资源放在同步代码块中或同步方法中,通过synchronized关键字声明,此时只能存在一个线程并且该线程在获取到对应的锁后才能对共享资源操作,完成之后其他线程才能获得该锁进而操作共享资源。
这些是多线程在处理同一数据时的一些基本操作,那么此时若有多个线程要对同一资源操作,但实现不同的功能。比如在火车站或地铁站都会设有安检,乘客的行李物品从一边进,从另一边出,而生产者和消费者模式就与其类似。
现在来分析一下,如果此时有一物品Package要进安检进行检测然后要出来,所以要有两种操作,创建一个Resource类,创建两个方法一个控制进入,一个控制输出。
class Resource{
private String name;
private boolean flag = false;
public synchronized void set(String name){
//当flag为真,输入线程等待,不进行设值操作,
//当flag为假,进行设值,并将flag反转,唤醒输出线程
if(flag)
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
this.name = name;
System.out.println(Thread.currentThread().getName()+"-->in"+this.name);
flag =true;
this.notify();
}
public synchronized void out(){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"-->out"+this.name);
flag = false;
this.notify();
}
}
那么这里的flag有什么作用呢,首先要了解线程的等待唤醒机制,由于只有输入一个Package才能输出一个,所以两个线程应交替执行,但在有相同锁的情况下,线程会抢cpu执行权,而这时会有不确定性,所以可能会出现与实际情况不符,因此要在一个线程A执行时,应保证另一线程B处于等待状态,A执行完毕后唤醒线程B然后处于等待,同理B也模仿这一行为,这就避免了逻辑错误的发生。然后看flag,在这里如果令当前线程等待的条件与传入的值不匹配时才进行方法中的操作,否则进入等待状态,当正常操作完后唤醒另一线程,并将flag反转,使得交替的工作正常进行。
下面是完整的程序:
class Resource{
private String name;
private boolean flag = false;
public synchronized void set(String name){
//当flag为真,输入线程等待,不进行设值操作,
//当flag为假,进行设值,并将flag反转,唤醒输出线程
if(flag)
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
this.name = name;
System.out.println(Thread.currentThread().getName()+"-->in"+this.name);
flag =true;
this.notify();
}
public synchronized void out(){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"-->out"+this.name);
flag = false;
this.notify();
}
}
class Producer implements Runnable{
Resource res;
public Producer(Resource res)
{
this.res = res;
}
@Override
public void run() {
// TODO 自动生成的方法存根
while (true) {
res.set("\t"+"Package");
}
}
}
class Consumer implements Runnable{
Resource res = new Resource();
public Consumer(Resource res) {
this.res = res;
}
@Override
public void run() {
// TODO 自动生成的方法存根
while (true) {
res.out();
}
}
}
public class ProdAndCons {
public static void main(String[] args) {
Resource res = new Resource();
Producer p = new Producer(res);
Consumer c = new Consumer(res);
//创建两个线程进行测试
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
可以看到输入与输出交替,这是两个线程进行操作的效果,如果有更多的线程呢?
在主方法中再创建两个线程
...
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
...
可以看到当有两个以上线程执行时,会出现一进两出或多出的现象,这是什么原因,可以分析一下,比如此时有四个线程a,b,c,d,a和b控制输入,c和d控制输出。在程序进行一段时间后如果flag为false,则由a和b中抢到执行权的一个执行输入操作,假设a执行了输入,然后反转flag再唤醒线程,此时因为满足flag条件则a等待,而b,c,d都具有执行资格,又假设b抢到执行权,由于flag为真,b也等待。然后执行c,c执行了输出操作,反转flag,然后唤醒处于等待且时间最长的a,而c本身失去了执行资格,d也是如此,接下来a有执行一次输入,然后唤醒的正是线程b,而b已经是等待状态所以不必判断直接执行输入操作,这就导致了两次输入一次输出的情况,当然继续分析也会出现其他结果。
那么如何解决这一问题?由于在某时刻某个线程可能被唤醒时省略了判断的一步,从而发生安全问题,所以将if判断改为while,此时的线程就会被判断不止一次。
这下解决了输入输出不一致的逻辑问题,但是程序执行一会儿会自动停止,因为当c和d在等待时,线程a执行后在等待前唤醒了正在等待的b,但是在while循环里会再次判断,a会将flag反转为不可执行状态,而b恰好满足,因此b又失去了执行资格,此时没有线程执行,程序就会停止。所以解决的方法就是在每次线程执行完后唤醒所有正在等待的线程,即调用notifyAll方法。
这下问题都解决了,达到了开头所说的行李过安检的效果,也是所谓的生产者消费者。