什么是同步呢?同步就是指串行访问,在单线程的情况下,不用考虑同步的问题,因为不存在并行访问,而在多线程情况下,假设我们有个取款的方法, 它可能是这样的:
public static void 取款( int 款数 ){
int 余额 = get余额();
if( 余额>款数 ){ //代码1处
吐钱(); //代码2处
更新数据库余额();
}else{
System.out.println("余额不足!");
}
}
假设余额为100,款数为90, 当我们的线程A,B ,C同时进入代码1处时,得到的条件都将是true,于是在代码2处取款机吐了270元出去...这当然是有问题的,我们希望在A线程处理完直到数据库余额更新成功后,才允许B线程进来,此时B线程必然是不满足条件,这才能避免多吐钱的BUG;
要达到这种效果,就需要采用同步机制了,目的就是让方法内的一系列操作具备原子性,让线程由并行改为串行一个接一个的进入取款方法;
接下我们来举一个通用的例子,用来示范多种同步机制的使用:
//一个数字生成器
abstract class AbstractNumberGenerator{
//是否已停止生成
protected boolean isStop = false;
//获得下一个数字
protected abstract int next();
}
创建一个数字检查器,它是一个线程类,它不断的调用数字生成器的next()方法生成数字, 然后检查生成的数字,一旦发现数字不是偶数,则设置isStop为true,线程停止运行;
//检查生成的数字
class NumberChecker implements Runnable{
private AbstractNumberGenerator ang ;
private int id ;
public NumberChecker(AbstractNumberGenerator ang,int id) {
this.ang = ang;
this.id = id;
}
@Override
public void run() {
while(!ang.isStop ){
int number = ang.next();
if(number%2!=0){
ang.isStop = true;//停止继续生成数字
}else{
System.out.println(this.id+":"+number+" ");
}
}
}
}
再创建一个测试方法:
//演示锁机制
public static void testLock(AbstractNumberGenerator ang){
ExecutorService exec = Executors.newCachedThreadPool();
//开启5个线程去测试数字
for (int i = 0; i < 5; i++) {
exec.execute(new NumberChecker(ang,i));
}
exec.shutdown();
}
public static void main(String[] args) throws InterruptedException {
testLock(new EvenNumberGenerator());
// testLock(new EvenNumberGeneratorBySyn());
// testLock(new EvenNumberGeneratorByLock());
}
class EvenNumberGenerator extends AbstractNumberGenerator{
private int number = 0;
@Override
protected int next() {
number++;
Thread.yield();
number++;
return number;
}
}
输出******************************************************************
0:2
2:8
1:4
***********************************************************************
很遗憾,测试是不通过的,因为当一个线程在返回number前,很有可能另一个线程已经执行了一次number++,所以有可能不是偶数!
我们再创建一个采用synchronized加锁的偶数生成器
class EvenNumberGeneratorBySyn extends AbstractNumberGenerator{
private int number = 0;
@Override
protected synchronized int next() {
number++;
Thread.yield();
number++;
return number;
}
// 上面的写法等同于下面的写法(称之为同步代码块或者临界区),上面在方法上写关键字,就是给当前对象上锁,
// 下面synchronized(this)这种写法也是给当前对象上锁
// protected int next() {
// synchronized (this) {
// number++;
// Thread.yield();
// number++;
// return number;
// }
// }
}
你可以启动测试类注释掉的代码试试,测试是没问题的,因为next()方法采用了synchronized关键字,该方法将不在存在并发访问的问题,无论何时都只可能有一个线程能进入方法内部;
我们再创建一个采用Lock加锁的偶数生成器
class EvenNumberGeneratorByLock extends AbstractNumberGenerator{
private int number = 0;
ReentrantLock lock = new ReentrantLock();//互斥锁,除此之外还有ReadLock以及WriteLock
@Override
protected int next() {
//获取锁
lock.lock();
try {
number++;
Thread.yield();
number++;
return number;
} finally{
//释放锁
lock.unlock();
}
}
}
通过声明一把互斥锁,调用它的lock方法将拿到锁,当锁已被持有时,其它线程再调用Lock方法将会阻塞,直到持有锁的线程调用unlock方法释放锁才得以继续运行;
注意:我们总是会将unlock()的调用放在finally中执行,这是为了确保锁一定会被释放从而避免死锁的产生;
我们比较一下两种同步方法,synchronized简单明了,但可控制粒度没有lock这么细,lock虽然控制粒度很细,但却引入了编码的复杂度,不过lock还有些其它有用的方法,这些在下篇文章中再做介绍,看完下篇文章,相信你对何时使用哪种同步机制已经能够有点想法了;