在分布式开发中,锁是线程控制的重要途径。Java为此也提供了2种锁机制,synchronized和lock。做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方。
我们先从最简单的入手,逐步分析这2种的区别。
一、synchronized和lock的用法区别
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
二、synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
三、synchronized和lock用途区别
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
四、总结
synchronized是关键字,Lock是接口
// lock接口的方法
void lock();
void unlock();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void lockInterruptibly() throws InterruptedException;
Condition newCondition();
synchronized是隐式的加锁,lock是显式的加锁
// synchronized 作用于方法上时,是看不到有锁的释放的
// lock 作用于代码块, 经常是 try lock finally unlock
// 一起使用, 是有主动去加锁和解锁的
synchronized可以作用于方法上,lock只能作用于方法块
// synchronized作用于静态方法和普通方法的区别是?
1.作用于静态方法时,锁的对象是class
2.作用于普通方法是,锁的可以是任意的对象
3.作用于代码块时,反编译后,monitorenter monitorexit exception时的monitorexit
4.作用于方法时,反编译后,方法的flag里面加一个acc_synchronized访问标志
// lock 作用于代码块, 经常是 try lock finally unlock 一起使用
synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁
// lock更加灵活,提供了多种方式的加锁方法
tryLock(long time, TimeUnit unit) // 带超时时间的加锁方法
lockInterruptibly // 可中断加锁方法
synchronized底层采用的是objectMonitor,lock采用的AQS
// objectMonitor
A,B线程竞争获取锁时,加入B线程未获取到锁,会进入到objectMonitor的
entrylist(同步队列)中,获取到锁的线程调用
wait后会进入waitset(等待队列)
// lock的AQS
通过一个int类型的state变量,来判断锁是否被获取,
未获取到锁的线程会通过cas加入到双端队列的尾部
synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列
// lock中可以有多个等待队列 condition
synchronized只支持非公平锁,lock支持非公平锁和公平锁
// lock 支持非公平锁和公平锁, 默认创建是非公平锁
公平锁与非公平锁的区别就在于获取锁的方式不同,
公平锁获取,当前线程必须检查sync queue里面是否已经有排队线程。
而非公平锁则不用考虑这一点,当前线程可以直接去获取锁。
// 非公平锁可能出现线程饥饿问题
synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal)
// condition 用await()替换wait(),用signal()替换notify(),
用signalAll()替换notifyAll(),传统线程的通信方式,
Condition都可以实现,这里注意,Condition是被绑定到Lock上的,
要创建一个Lock的Condition必须用newCondition()方法。
// synchronized wait()只有一个阻塞队列,
notifyAll会唤起所有阻塞队列下的线程,而使用lock/condition,
可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signalAll();
return x;
} finally {
lock.unlock();
}
}
}
/* 上面代码中线程间共享的资源是一个Oject数组items,
因为需要对其进行并发操作,所以显然需要制定同步策略。
对容器的基本操作无非就是增删改查,但是显然我们不能只是简单的增,
简单的删,因为我们得考虑边界情况,数组有长度,
增只有在容器还有空间的情况下才能增;删除则只有在容器有元素的情况下才能减。
于是在put方法中我们得先判断一下是否还有位置才能对容器进行add操作;
在take方法中我们得先判断是否有元素才能进行remove操作,
而且这个判断逻辑是无法避免的。执行判断逻辑的时候我们已经获得了锁
(能不能在实际add或者remove的时候再加锁,显然不能,
因为判断逻辑的实现依赖共享资源items,所以必须在判断逻辑之前已经获得锁),
如果判断逻辑为false我们只能先释放已经获得的锁并且等待条件满足
再继续获取锁执行(这也是Condition中await方法的作用)。
*/
// 为什么需要使用Condition(什么时候需要使用await/signalAll/signal方法)?
/*
因为有时候获得锁的线程发现其某个条件不满足导致不能继续后面的业务逻辑,
此时该线程只能先释放锁,等待条件满足。那可不可以不释放锁的等待呢?
比如将await方法替换为sleep方法(这也是面试经常问的await和sleep的区别)?
显然不行,因为等待的条件显然和共享的资源是有关的,在这个例子里,
take方法会等待notEmpty条件,notEmpty指的是items不为空,
意味着此时items是空的,那么就只有对items执行add操作,
即其它线程调用put方法才有机会达到notEmpty的条件,
所以如果使用sleep(不释放锁)来等待而不是await(释放锁)来等待,
则会导致notEmpty这个条件永远满足不了。
总结起来,就是获得锁的线程发现某个条件不满足而不能继续执行,
而且该条件需要其它线程对共享资源进行操作才能触发,所以必须释放锁。
*/