说明
当程序中出现多个进程对同一资源进行操作时,因为对数据的操作非常密集,可能会对资源过度操作,这时就需要用到线程的同步技术。
以一个抢红包程序为例,红包数量为3个,开启5个线程来模拟抢红包行为,红包数量抢完了,就不能再抢了。代码如下:
/*** 抢红包*/
class GetBonus implements Runnable {/*** 创建一个红包数量*/private static int bonusQuantity = 3;@Overridepublic void run() {// 如果红包数量大于0if (bonusQuantity > 0) {System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");// 红包数量-1bonusQuantity--;} else {System.out.println(Thread.currentThread().getName() + "抱歉,红包已经被抢完了");}System.out.println(bonusQuantity);}
}
public class EssayTest {private static void main(String[] args) {// 创建红包线程GetBonus getBonus = new GetBonus();// 创建5个Thread线程并命名、启动new Thread(getBonus, "黄蓉").start();new Thread(getBonus, "郭靖").start();new Thread(getBonus, "杨过").start();new Thread(getBonus, "小龙女").start();new Thread(getBonus, "张无忌").start();}
}
线程同步方式
为了解决上面对线程对统一资源操作不同步的问题,有以下几种方式
一、syschronized代码块
syschronized是java的关键字,被syschronized代码块包含的程序,同时只能有一个线程在执行。可以将程序中对资源操作的核心代码包起来,实现线程同步。修改后的代码如下:
/*** 抢红包*/
class GetBonus implements Runnable {/*** 创建一个红包数量*/private static int bonusQuantity = 3;@Overridepublic void run() {// 用synchronized代码块将核心代码包含起来,()内的必须是一个对象且唯一,通常用this,即GetBonus类的地址,在内存中是唯一的。synchronized (this) {// 如果红包数量大于0if (bonusQuantity > 0) {System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");// 红包数量-1bonusQuantity--;} else {System.out.println(Thread.currentThread().getName() + "抱歉,红包已经被抢完了");}System.out.println("红包剩余:" + bonusQuantity);System.out.println("---------------------------------");}}
}
二、锁方法
即将方法用syschronized修饰,表明该方法是一个同步方法,同时只能有一个线程在执行,修改后的代码如下:
/*** 抢红包*/
class GetBonus implements Runnable {/*** 创建一个红包数量*/private static int bonusQuantity = 3;@Overridepublic void run() {get();}/*** 将抢红包的代码抽取成一个方法,并用synchronized修饰*/private synchronized void get() {// 如果红包数量大于0if (bonusQuantity > 0) {System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");// 红包数量-1bonusQuantity--;} else {System.out.println(Thread.currentThread().getName() + "抱歉,红包已经被抢完了");}System.out.println("红包剩余:" + bonusQuantity);System.out.println("---------------------------------");}
}
值得思考的是,这里可不可以将run()方法用synchronized修饰,设置成一个同步方法,一步到位呢?
三、ReentrantLock
使用java的ReentrantLock类,自定义设置上锁和解锁的代码区间,修改后的代码如下:
import java.util.concurrent.locks.ReentrantLock;/*** 抢红包*/
class GetBonus implements Runnable {/*** 创建一个红包数量*/private static int bonusQuantity = 3;/*** 创建一个锁对象*/ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {// 加锁lock.lock();// 如果红包数量大于0if (bonusQuantity > 0) {System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");// 红包数量-1bonusQuantity--;} else {System.out.println(Thread.currentThread().getName() + "抱歉,红包已经被抢完了");}// 解锁lock.unlock();System.out.println("红包剩余:" + bonusQuantity);System.out.println("---------------------------------");}
}
值得一提的是,阿里巴巴代码规范提示,lock.lock()必须紧跟try代码块,且unlock要放到finally第一行。应该是考虑到代码被上锁后,解锁的lock.unlock()代码有可能一直没被执行的情况。
总结
如果涉及到多线程对同一资源的操作,为了避免出现资源溢出的情况,可以用synchronized代码块、synchronized方法和ReentrantLock类对象上锁、解锁等方法,在实际使用时,还应该考虑到,被锁住的代码越多,程序执行效率越低,所以应该尽量只锁住“核心代码”。