首页 » 编写高质量代码:改善Java程序的151个建议 » 编写高质量代码:改善Java程序的151个建议全文在线阅读

《编写高质量代码:改善Java程序的151个建议》建议127:Lock与synchronized是不一样的

关灯直达底部

很多编码者都会说,Lock类和synchronized关键字用在代码块的并发性和内存上时语义是一样的,都是保持代码块同时只有一个线程具有执行权。这样的说法只对了一半,我们以一个任务提交给多个线程运行为例,来看看使用显式锁(Lock类)和内部锁(synchronized关键字)有什么不同。首先定义一个任务:


class Task{

public void doSomething(){

try{

//每个线程等待2秒钟,注意将此时的线程转为WAITING状态

Thread.sleep(2000);

}catch(Exception e){

//异常处理

}

StringBuffer sb=new StringBuffer();

//线程名称

sb.append(/"线程名称:/"+Thread.currentThread().getName());

//运行的时间戳

sb.append(/",执行时间:/"+Calendar.getInstance().get(13)+/"s/");

System.out.println(sb);

}

}


该类模拟了一个执行时间比较长的计算,注意这里使用的是模拟方式,在使用sleep方法时线程的状态会从运行状态转变为等待状态。该任务要具备多线程能力时必须实现Runnable接口,我们分别建立两种不同的锁实现机制,首先看显式锁实现:


//显式锁任务

class TaskWithLock extends Task implements Runnable{

//声明显式锁

private fnal Lock lock=new ReentrantLock();

@Override

public void run(){

try{

//开始锁定

lock.lock();

doSomething();

}finally{

//释放锁

lock.unlock();

}

}

}


这里有一点需要说明的是,显式锁的锁定和释放必须在一个try……finally块中,这是为了确保即使出现运行期异常也能正常释放锁,保证其他线程能够顺利执行。

内部锁的处理也非常简单,代码如下:


//内部锁任务

class TaskWithSync extends Task implements Runnable{

@Override

public void run(){

//内部锁

synchronized(/"A/"){

doSomething();

}

}

}


这两个任务看着非常相似,应该能够产生相似的结果吧?我们建立一个模拟场景,保证同时有三个线程在运行,代码如下:


public static void runTasks(Class<?extends Runnable>clz)throws Exception{

ExecutorService es=Executors.newCachedThreadPool();

System.out.println(/"***开始执行/"+clz.getSimpleName()+/"任务****/");

//启动三个线程

for(int i=0;i<3;i++){

es.submit(clz.newInstance());

}

//等待足够长的时间,然后关闭执行器

TimeUnit.SECONDS.sleep(10);

System.out.println(/"------/"+clz.getSimpleName()+/"任务执行完毕-----n/");

//关闭执行器

es.shutdown();

}

public static void main(Stringargs)throws Exception{

//运行显式锁任务

runTasks(TaskWithLock.class);

//运行内部锁任务

runTasks(TaskWithSync.class);

}


按照一般的理解,Lock和synchronized的处理方式是相同的,输出应该没有差别,但是很遗憾的是,输出差别其实很大。输出如下:


*****开始执行TaskWithLock任务******

线程名称:pool-1-thread-1,执行时间:33 s

线程名称:pool-1-thread-2,执行时间:33 s

线程名称:pool-1-thread-3,执行时间:33 s

------TaskWithLock任务执行完毕-----

*****开始执行TaskWithSync任务******

线程名称:pool-2-thread-1,执行时间:43 s

线程名称:pool-2-thread-3,执行时间:45 s

线程名称:pool-2-thread-2,执行时间:47 s

------TaskWithSync任务执行完毕-----


注意看运行的时间戳,显式锁是同时运行的,很显然在pool-1-thread-1线程执行到sleep时,其他两个线程也会运行到这里,一起等待,然后一起输出,这还具有线程互斥的概念吗?

而内部锁的输出则是我们的预期结果:pool-2-thread-1线程在运行时其他线程处于等待状态,pool-2-thread-1执行完毕后,JVM从等待线程池中随机获得一个线程pool-2-thread-3执行,最后再执行pool-2-thread-2,这正是我们希望的。

现在问题来了:Lock锁为什么不出现互斥情况呢?

这是因为对于同步资源来说(示例中是代码块),显式锁是对象级别的锁,而内部锁是类级别的锁,也就是说Lock锁是跟随对象的,synchronized锁是跟随类的,更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程的共享变量。都说代码是最好的解释语言,我们来看一个Lock锁资源的代码:


public static void main(Stringargs){

//多个线程共享锁

final Lock lock=new ReentrantLock();

//启动三个线程

for(int i=0;i<3;i++){

new Thread(new Runnable(){

@Override

public void run(){

try{

lock.lock();

//休眠2秒钟

Thread.sleep(2000);

System.out.println(Thread.currentThread().getName());

}catch(InterruptedException e){

e.printStackTrace();

}finally{

lock.unlock();

}

}

}).start();

}

}


读者可以执行一下,会发现线程名称Thread-0、Thread-1、Thread-2会逐渐输出,也就是一个线程在执行时,其他线程就处于等待状态。注意,这里三个线程运行的实例对象是同一个类(都是Client$1类的实例)。

那除了这一点不同之外,显式锁和内部锁还有什么不同呢?还有以下4点不同:

(1)Lock支持更细粒度的锁控制

假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁就很难实现。显式锁的示例代码如下:


class Foo{

//可重入的读写锁

private final ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();

//读锁

private final Lock r=rwl.readLock();

//写锁

private final Lock w=rwl.writeLock();

//多操作,可并发执行

public void read(){

try{

r.lock();

Thread.sleep(1000);

System.out.println(/"read……/");

}catch(InterruptedException e){

e.printStackTrace();

}finally{

r.unlock();

}

}

//写操作,同时只允许一个写操作

public void write(Object_obj){

try{

w.lock();

Thread.sleep(1000);

System.out.println(/"Writing……/");

}catch(InterruptedException e){

e.printStackTrace();

}finally{

w.unlock();

}

}

}


可以编写一个Runnable的实现类,把Foo类作为资源进行调用(注意多线程是共享这个资源的),然后就会发现这样的现象:读写锁允许同时有多个读操作但只允许有一个写操作,也就是当有一个写线程在执行时,所有的读线程和写线程都会阻塞,直到写线程释放锁资源为止,而读锁则可以有多个线程同时执行。

(2)Lock是无阻塞锁,synchronized是阻塞锁

当线程A持有锁时,线程B也期望获得锁,此时,如果程序中使用的是显式锁,则B线程为等待状态(在通常的描述中,也认为此线程被阻塞了),若使用的是内部锁则为阻塞状态。

(3)Lock可实现公平锁,synchronized只能是非公平锁

什么叫非公平锁呢?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁,JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,这叫做非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁(保证每个线程的等待时间均衡)。需要注意的是,即使是公平锁,JVM也无法准确做到“公平”,在程序中不能以此作为精确计算。

显式锁默认是非公平锁,但可以在构造函数中加入参数true来声明出公平锁,而synchronized实现的是非公平锁,它不能实现公平锁。

(4)Lock是代码级的,synchronized是JVM级的

Lock是通过编码实现的,synchronized是在运行期由JVM解释的,相对来说synchronized的优化可能性更高,毕竟是在最核心部分支持的,Lock的优化则需要用户自行考虑。

显式锁和内部锁的功能各不相同,在性能上也稍有差别,但随着JDK的不断推进,相对来说,显式锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大则选择Lock,快捷、安全则选择synchronized。

注意 两种不同的锁机制,根据不同的情况来选择。