Lock与synchronized的区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
两者在锁的相关概念上区别:
1.可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
1 2 3 4 5 6 7 8 9 | class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } } |
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
2.可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
看一下这2个类的源代码就清楚了:
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
1 | ReentrantLock lock = new ReentrantLock(true); |
如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
另外在ReentrantLock类中定义了很多方法,比如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。
4.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
性能比较
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
当需要以下高级特性时,才应该使用Lock:可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized。
同步的实现当然是采用锁了,java中使用锁的两个基本工具是 synchronized 和 Lock。
一直很喜欢synchronized,因为使用它很方便。比如,需要对一个方法进行同步,那么只需在方法的签名添加一个synchronized关键字。
synchronized 用在方法和代码块上有什么区别呢?
synchronized 用在方法签名上(以test为例),当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行方法test,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。否则synchronized关键字将失去意义。
(
但是如果该方法为类方法,即其修饰符为static,那么synchronized 意味着某个调用此方法的线程当前会拥有该类的锁,只要该线程持续在当前方法内运行,其他线程依然无法获得方法的使用权!)
synchronized 用在代码块的使用方式:synchronized(obj){//todo code here}
当线程运行到该代码块内,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会形成互斥!特别的,当obj == this时,表示当前调用该方法的实例对象。即
public void test() {
...
synchronized(this) {
// todo your code
}
...
}
此时,其效果等同于
public synchronized void test() {
// todo your code
}
使用synchronized代码块,可以只对需要同步的代码进行同步,这样可以大大的提高效率。
小结:
使用synchronized 代码块相比方法有两点优势:
1、可以只对需要同步的使用
2、与wait()/notify()/nitifyAll()一起使用时,比较方便
----------------------------------------------------------------------------------------------------------------------------------------------------------
wait() 与notify()/notifyAll()
这三个方法都是Object的方法,并不是线程的方法!
wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!
notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是
对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,
wait()和notify()必须在synchronized代码块中调用。
notifyAll()则是唤醒所有等待的线程。
为了说明这一点,举例如下:
两个线程依次打印"A""B",总共打印10次。
public
class
Consumer
implements
Runnable {
@Override
public
synchronized
void
run() {
//
TODO
Auto-generated method stub
int
count = 10;
while
(count > 0) {
synchronized
(Test.
obj
) {
System.
out
.print(
"B"
);
count --;
Test.
obj
.notify();
// 主动释放对象锁
try
{
Test.
obj
.wait();
}
catch
(InterruptedException e) {
//
TODO
Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public
class
Produce
implements
Runnable {
@Override
public
void
run() {
//
TODO
Auto-generated method stub
int
count = 10;
while
(count > 0) {
synchronized
(Test.
obj
) {
//System.out.print("count = " + count);
System.
out
.print(
"A"
);
count --;
Test.
obj
.notify();
try
{
Test.
obj
.wait();
}
catch
(InterruptedException e) {
//
TODO
Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
测试类如下:
public
class
Test {
public
static
final
Object
obj
=
new
Object();
public
static
void
main(String[] args) {
new
Thread(
new
Produce()).start();
new
Thread(
new
Consumer()).start();
}
}
这里使用static obj作为锁的对象,当线程Produce启动时(假如Produce首先获得锁,则Consumer会等待),打印“A”后,会先主动释放锁,然后阻塞自己。Consumer获得对象锁,打印“B”,然后释放锁,阻塞自己,那么Produce又会获得锁,然后...一直循环下去,直到count = 0.这样,使用Synchronized和wait()以及notify()就可以达到线程同步的目的。
----------------------------------------------------------------------------------------------------------------------------------------------------------
除了wait()和notify()协作完成线程同步之外,使用Lock也可以完成同样的目的。
ReentrantLock 与synchronized有相同的并发性和内存语义,还包含了中断锁等候和定时锁等候,意味着线程A如果先获得了对象obj的锁,那么线程B可以在等待指定时间内依然无法获取锁,那么就会自动放弃该锁。
但是由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();
同样的例子,使用lock 如何实现呢?
public
class
Consumer
implements
Runnable {
private
Lock
lock
;
public
Consumer(Lock lock) {
this
.
lock
= lock;
}
@Override
public
void
run() {
//
TODO
Auto-generated method stub
int
count = 10;
while
( count > 0 ) {
try
{
lock
.lock();
count --;
System.
out
.print(
"B"
);
}
finally
{
lock
.unlock(); //主动释放锁
try
{
Thread. sleep(91L);
}
catch
(InterruptedException e) {
//
TODO
Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public
class
Producer
implements
Runnable{
private
Lock
lock
;
public
Producer(Lock lock) {
this
.
lock
= lock;
}
@Override
public
void
run() {
//
TODO
Auto-generated method stub
int
count = 10;
while
(count > 0) {
try
{
lock
.lock();
count --;
System.
out
.print(
"A"
);
}
finally
{
lock
.unlock();
try
{
Thread. sleep(90L);
}
catch
(InterruptedException e) {
//
TODO
Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
调用代码:
public
class
Test {
public
static
void
main(String[] args) {
Lock lock =
new
ReentrantLock();
Consumer consumer =
new
Consumer(lock);
Producer producer =
new
Producer(lock);
new
Thread(consumer).start();
new
Thread( producer).start();
}
}
使用建议:
在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。