说明
当程序中出现多个进程对同一资源进行操作时,因为对数据的操作非常密集,可能会对资源过度操作,这时就需要用到线程的同步技术。
以一个抢红包程序为例,红包数量为3个,开启5个线程来模拟抢红包行为,红包数量抢完了,就不能再抢了。代码如下:
/**
* 抢红包
*/
class GetBonus implements Runnable {
/**
* 创建一个红包数量
*/
private static int bonusQuantity = 3;
@Override
public void run() {
// 如果红包数量大于0
if (bonusQuantity > 0) {
System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");
// 红包数量-1
bonusQuantity--;
} 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;
@Override
public void run() {
// 用synchronized代码块将核心代码包含起来,()内的必须是一个对象且唯一,通常用this,即GetBonus类的地址,在内存中是唯一的。
synchronized (this) {
// 如果红包数量大于0
if (bonusQuantity > 0) {
System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");
// 红包数量-1
bonusQuantity--;
} else {
System.out.println(Thread.currentThread().getName() + "抱歉,红包已经被抢完了");
}
System.out.println("红包剩余:" + bonusQuantity);
System.out.println("---------------------------------");
}
}
}
二、锁方法
即将方法用syschronized修饰,表明该方法是一个同步方法,同时只能有一个线程在执行,修改后的代码如下:
/**
* 抢红包
*/
class GetBonus implements Runnable {
/**
* 创建一个红包数量
*/
private static int bonusQuantity = 3;
@Override
public void run() {
get();
}
/**
* 将抢红包的代码抽取成一个方法,并用synchronized修饰
*/
private synchronized void get() {
// 如果红包数量大于0
if (bonusQuantity > 0) {
System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");
// 红包数量-1
bonusQuantity--;
} 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();
@Override
public void run() {
// 加锁
lock.lock();
// 如果红包数量大于0
if (bonusQuantity > 0) {
System.out.println("恭喜" + Thread.currentThread().getName() + "您成功抢到一个20元的红包");
// 红包数量-1
bonusQuantity--;
} 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类对象上锁、解锁等方法,在实际使用时,还应该考虑到,被锁住的代码越多,程序执行效率越低,所以应该尽量只锁住“核心代码”。