前言
相信学习java的人都知道Thread这个线程类,它可以用来开启一个子线程运行,不会阻塞主线程。但是随之都会有让大家苦恼的问题,如果存在多个Thread的实例同时运行,那么就会造成线程错乱的问题,尤其是访问公共资源的时候,特别容易造成混乱,也一直没有找到好的方法解决。但是这学期学了操作系统课程,里面有信号量在进程同步方面的应用,十分的方便,那么在java里面有没有封装对信号量的使用呢?答案是YES,Semaphore这个类就是我今天要讲的东西,它可以很方便实现信号量在进程同步方面的作用,下面让我们拭目以待。
实现
下面我就拿一个苹果橘子问题来说明问题,并用Java来实现它的具体操作。
问题描述
桌子上有一个盘子,每次只能放入一只水果;爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专吃盘子中的苹果,女儿专吃盘子中的橘子。
问题分析
问题有以下的要点:
爸爸和妈妈不能同时往盘子里放水果
儿子只能吃盘子里的苹果
女儿只能吃盘子里的橘子
也就是说,在爸爸或妈妈放水果之前需要判断盘子里是否已经有水果,在儿子或女儿吃水果之前先判断盘子里是否有水果。而且,表示父亲的线程一定运行在表示儿子的线程之前,母亲和女儿同理。
按照之前的思路,我先设置一个变量表示盘子里是否有水果,然后在父亲线程和母亲线程判断这个变量,比如:
//父亲线程:
if(盘子为空){
//开始放苹果,设置盘子不为空
}
//母亲线程:
if(盘子为空){
//开始放橘子,设置盘子不为空
}
乍一看好像好像可以,但是其实是错误的。如果盘子开始为空,父亲线程和母亲线程同时运行到if的判断语句,那么都可以通过,然后就放了水果,导致同时放了水果出现错误。
但是引入了信号量之后,问题就迎刃而解了。
在此之前,我需要补充一下信号量的P和V操作。我们给信号量一个初值,比如为1,当执行P操作的时候,我们先把信号量的值减去1,然后判断现在的值是否不小于0,如果是则继续执行,如果不是则该线程被加入到队列之中等待运行。当执行V操作的时候,我们把信号量的值加1,然后判断现在的值是否不大于0,如果是则从队列之中取出一个线程运行,否则就返回。
我们设置一个信号量,并把初值设置为1,当父亲线程执行P操作的时候,此时信号量的值变为了0,父亲线程继续执行,当母亲线程执行P操作的时候,此时的值变为了-1,母亲线程就被阻塞添加到队列之中等待运行,就不会出现同时放水果的问题。
下面来看一下具体的代码:
import java.util.concurrent.Semaphore;
public class MyMain {
private Thread fatherThread,motherThread,sonThread,daughterThread;
private Semaphore plateFull = new Semaphore(1); //盘子开始可以放水果
private Semaphore appleEmpty = new Semaphore(0); //开始不能吃苹果
private Semaphore orangeEmpty = new Semaphore(0); //开始不能吃橘子
public static void main(String[] args){
MyMain myMain = new MyMain();
myMain.start();
}
public void start(){
fatherThread = new Thread(new Runnable() {
@Override
public void run(){
while(true){
try {
plateFull.acquire();
System.out.println("father puts an apple");
appleEmpty.release();
Thread.sleep(3000);
} catch (Exception e) {}
}
}
});
motherThread = new Thread(new Runnable(){
@Override
public void run(){
while(true){
try {
plateFull.acquire();
System.out.println("mother puts an orange");
orangeEmpty.release();
Thread.sleep(1000);
} catch (Exception e) {}
}
}
});
sonThread = new Thread(new Runnable() {
@Override
public void run(){
while(true){
try {
appleEmpty.acquire();
System.out.println("son eats an apple");
plateFull.release();
} catch (Exception e) {}
}
}
});
daughterThread = new Thread(new Runnable(){
@Override
public void run() {
while(true){
try {
orangeEmpty.acquire();
System.out.println("daughter eats an orange");
plateFull.release();
} catch (Exception e) {}
}
}
});
fatherThread.start();
motherThread.start();
sonThread.start();
daughterThread.start();
}
}
我共设置了三个信号量,一个用来表示盘子里是否有水果,一个表示盘子里是否苹果,最后一个表示盘子里是否有橘子。
看一下运行结果:
mother puts an orange
daughter eats an orange
father puts an apple
son eats an apple
mother puts an orange
daughter eats an orange
mother puts an orange
daughter eats an orange
mother puts an orange
daughter eats an orange
father puts an apple
son eats an apple
mother puts an orange
daughter eats an orange
mother puts an orange
daughter eats an orange
father puts an apple
son eats an apple
mother puts an orange
daughter eats an orange
从结果中我们可以很明显的看到,父亲线程是在儿子线程之前运行,母亲线程在女儿线程之前运行。
小结
我没有设置太多的标志变量,只是用了三个信号量就可以实现四个线程的有序运行。信号量的运用范围还是挺广的,比如我们可以使用它来确保一个对象在使用之前必须已经被初始化,如果初始化和操作在两个不同的线程中运行的话,这样确实很方便而且还不会造成错误。