在并发编程中,同步控制是最重要也是最需要保障的。想象这样的场景:你去银行取了100万,余额理应还有900万,但是ATM机却告诉你,余额还剩1000万,那这多出来的100万哪里来的呢?这里我将介绍JAVA中控制线程同步最基本的方法。
同步控制基本方法
Synchronized和他的伙伴
ReentrantLock和他的女朋友们
Synchronized、wait()、notify()
Synchronized可以理解为锁,用它可以对一个共享的对象加锁,使得,每次都只有一个线程能访问到这个共享,可以理解为十个人同时要用一个浴室。wait()则是指小明取得了浴室的钥匙,洗头时,发现没带洗发水,然后,交还浴室的锁,回宿舍拿海飞丝。notify()指的是小张快洗完了,并且大喊“老子马上要洗完澡了,你们可以准备抢浴室的使用权了”。sleep()是指小二去洗澡,洗到一半,突然思考人生十分钟,什么也不干,就像俗话说的“占着茅坑不拉屎”。
并发中最经典的例子——生产者消费者,可以帮助你好好理解上面的浴室,小明,小张,小二。
class Shop{
//商店就100个架子,衣服数量超过100就不能上架
private final int MAX = 100;
private int goodsNum = 0;
public void buy(int num){
//获得商店的锁
synchronized (this) {
while(goodsNum+num > MAX){
System.out.println("进货:"+num+"件,太多:失败。库存:"+goodsNum);
try {
//等待卖货
wait();
} catch (InterruptedException e) {
System.out.println("等待卖出被打断");
}
}
goodsNum = goodsNum+num;
System.out.println("进货:"+num+"件:成功。库存:"+goodsNum);
//通知卖货的,我进货了,你可以卖衣服啦
this.notifyAll();
}
}
public void sale(int num){
//获得商店的锁
synchronized (this) {
//卖货时发现库存不够了
while(goodsNum-num < 0){
System.out.println("卖出:"+num+"件,库存不够:失败。库存:"+goodsNum);
try {
//等待进货
wait();
} catch (InterruptedException e) {
System.out.println("等待进货被打断");
}
}
goodsNum = goodsNum-num;
System.out.println("卖货:"+num+"件:成功。库存:"+goodsNum);
//通知进货的,衣服卖掉了,可以进货了
this.notifyAll();
}
}
}
class Buyer extends Thread{
private Shop shop;
private int num;
public Buyer(Shop shop,int num){
this.shop = shop;
this.num = num;
}
public void run(){
shop.buy(num);
}
}
class Saler extends Thread{
private Shop shop;
private int num;
public Saler(Shop shop,int num){
this.shop = shop;
this.num = num;
}
public void run(){
shop.sale(num);
}
}
public class MyConsAndProd {
public static void main(String[] args) {
Shop shop = new Shop();
//使用Executors启动了10个消费者和生产者
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 10; i < 100; i+=10) {
executorService.execute(new Buyer(shop, i));
executorService.execute(new Saler(shop, i+5));
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdownNow();
}
}
OUTPUT:
进货:10件:成功。库存:10
卖出:15件,库存不够:失败。库存:10
进货:20件:成功。库存:30
卖货:15件:成功。库存:15
进货:30件:成功。库存:45
卖货:25件:成功。库存:20
进货:50件:成功。库存:70
卖货:45件:成功。库存:25
卖出:65件,库存不够:失败。库存:25
使用了Synchronized、wait()、notifyall()模拟了一家商店卖货进货的过程(画的丑,看官见谅。。。。)。
流程如下:
注意:使用wait()和notifyall()需要先获得对象的锁。
ReentrantLock、await、signal
相较于使用Synchronized,ReentrantLock多了一些功能,并且它使得代码更加具有可读性。使用方法如下:
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for(i =0;i<100;i++){
try {
//对锁请求,可以响应中断的
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
i++;
} finally{
//释放锁
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
使用ReentrantLock虽然增加了代码的行数,但是可读性确实大大增加了。更重要的是,它多了更多的功能。
功能一:lock.lockInterruptibly();在申请锁的过程中,可以响应中断,要知道Synchronized在请求锁的过程中是不可中断的。
功能二:lock.tryLock(timeout, unit);在申请锁的过程中,我们可以限定一个时间timeout和时间单位unit,告诉线程:“如果你小子一段时间内拿不到锁,那就自己中断吧,别等下去了!”而Synchronized在没有外部中断的条件下,则会傻傻的等待着,如此专一。
有同学读过JDK源码可能会知道ReentrantLock中没有wait和notify方法,那么如何实现Synchronized中的wait()和notify()的功能?ReentrantLock有一个好搭档——Condition,有了它,wait()和notify()功能就有了。
Condition condition = lock.newCondition();
condition.await();//类似于wait()
condition.awaitUninterruptibly();//类似于wait()但是不会响应中断
condition.signal();//类似于notify()
condition.signalAll();//类似于notifyAll()
大家可以使用ReentrantLock对上面商店进货、卖衣服进行的过程仿真。