Java同步锁-synchronized与lock
一、synchronized关键字
1、synchronized简介
synchronized实现同步的基础:java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。
表现形式为:代码块同步和方法同步。
2、synchronized的使用场景
- 方法同步
public synchronized void method1(){}复制代码
锁住的是该对象的一个实例,当不同线程调用该实例对象中该同步方法,线程只有一个得到锁,其余被阻塞。但如果不同线程同时对该类的不同实例对象执行该同步方法,则不会阻塞,因为他们使用不同的锁。
- 代码块同步
synchronized(this)( //ToDo}
或
synchronized(普通变量){ }复制代码
同上
- 静态方法同步
public synchronized static void method3(){}复制代码
锁住的是该类,当不同线程调用该类的该static同步方法时,就只能有一个线程获得锁,其余线程被阻塞。
- 静态代码块
synchronized(Test.class){ //ToDo}
或
synchronized(静态变量){ //ToDo}复制代码
同上
3、synchronized锁升级
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁和重量级锁
锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。
- 偏向锁
大所述情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得所的代价更低而引入偏向锁。
当一个线程访问同步代码并获取锁时,会对对象头和栈帧中的锁记录里保存锁偏向的线程ID,以后该线程再进入和退出同步锁时,不需要进行CAS操作来加锁和解锁,而只是简单地测试一下对象头的Mark Word里是否存储执行当前线程的偏向锁。
偏向锁作用是:在没有别的线程竞争的时候,一直偏向当前线程,当前线程可以一直执行。
- 轻量级锁(自旋锁)
轻量级锁,由偏向锁升级而来。
偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
- 重量级锁
轻量级锁膨胀之后,就升级为重量级锁。
重量级锁时依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁(synchronized就是重量级锁)。
偏向锁
优点:加锁和解锁不需要额外的消耗
缺点:线程存在竞争,会带来额外的锁撤销的消耗
场景:单一线程访问同步块场景
轻量级锁
优点:竞争的线程不会阻塞,提高了程序的响应速度。
缺点:线程自旋时不释放CPU
场景:追求响应时间,同步块执行速度非常快。
重量级锁
优点:线程竞争不使用自旋,释放CPU
缺点:线程阻塞,响应时间缓慢。
场景:追求吞吐量,同步块执行速度较长。
二、Lock接口
1、Lock接口
Lock,锁对象。在Lock接口出现之前,Java程序时靠synchronized关键字实现锁功能。而在Java SE5.0之后并发包中新增了Lock接口来实现锁的功能。
它能提供synchronized关键字类似的同步功能,但需要显示获得锁和释放锁,至于二者区别后文补充。
Lock接口的主要方法:
- void lock()
执行该方法时,若锁处于空闲状态,当前线程将获得锁。相反,如果锁已经被其他线程持有,则禁止当前线程获得锁。
- boolean tryLock()
若锁可用,则获得锁,并立即返回true,否则返回false。
tryLock()和Lock()的区别在于:
tryLock()只是尝试获得锁,若锁不可用,不会导致当前线程被禁止,当前线程仍然继续往下执行代码。
Lock()则是一定要获得锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下自行。
- void unlock()
执行该方法时,当前线程将释放持有锁,锁只能由持有者释放,若线程没有持有锁,则执行该方法,可能导致异常方式。
- Condition newCondition()
条件对象,获得等待通知组件。该组件和当前的锁绑定,当前线程只有获得锁,才会调用该组件的await()方法,调用后,当前线程将释放锁。
2、Reentrantlock的使用
Reentrantlock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
.....................
try {
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//操作
}catch(Exception e){
//异常处理
} finally {
lock.unlock(); //释放锁
}复制代码
reentrantlock锁,在高并发的条件下使用的性能远远高于synchronized关键字。
并且reentratnlock公平和非公平锁的队列都是基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。
3、ReadWriteLock接口
ReadWriteLock接口中主要方法如下:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}复制代码
ReadWriteLock管理一组锁,一个是只读锁,一个是写锁。
Java并发库中ReentrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
读锁在同一时刻允许有多个线程在访问,但是写进程访问时,所有的读线程和其他写进程都被阻塞。
读写锁维护一对锁,一个读锁和一个写锁,通过读写锁分离,使得并发性相比一般的排它锁有很大的提升。
ReentrantReadWriteLock读写锁的几个特性:
- 公平选择性
- 重入性
- 锁降级
读写锁例子(程序来源于网上:blog.csdn.net/canot/artic…
):
public class Cache{
static Map<String,Object> map = new HashMap<String,Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock rLock = rwl.readLock();
static Lock wLock = rwl.writeLock();
//获取一个key对应的value
public static final Object get(String key){
r.lock();
try{
return map.get(key);
}finally{
r.unlock();
}
}
//设置key对应的value并返回旧的value
public static fianl Object put(String key,Object value){
w.lock();
try{
return map.put(key,value);
}final{
w.unlock();
}
}
//清空缓存
public static fianl void clear(){
w.lock();
try{
map.clear();
} finally{
w.unlock();
}
}
}复制代码
- 读写锁的锁降级
锁降级是指写锁降级成为读锁。如果当前线程持有写锁,然后将其释放再获取读锁的过程不能称为锁降级。锁降级指的在持有写锁的时候再获取读锁,获取到读锁后释放之前写锁的过程称为锁释放。
(程序来源于:blog.csdn.net/qq_38737992…
)
public void work() {
reentrantReadWriteLock.readLock().lock();
if (!update) {
reentrantReadWriteLock.readLock().unlock();
// 锁降级开始
reentrantReadWriteLock.writeLock().lock();
try {
if (!update) {
// 准备数据
++index;
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
update = true;
}
reentrantReadWriteLock.readLock().lock();
} finally {
reentrantReadWriteLock.writeLock().unlock();
// 锁降级结束,降级为读锁
}
}
try {
// 使用数据
for (int i=0; i<5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + index);
}
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
复制代码
三、synchronized和Lock区别
1、synchronized和Lock区别
synchronized是关键字,而Lock是接口
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
synchronized自动释放锁(a线程执行完同步代码会自动释放锁,b线程执行过程中发生异常会释放锁)
lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁。
synchronized关键字的两个线程1和线程2,若当前线程1获得锁,线程2等待,如果线程1阻塞,线程2会一直等待下去。
而lock锁不一定会等待下去,如果尝试获得不到锁,线程可以不用一直等待就结束了。
synchronized的锁可重入、不可中断、非公平。
lock锁可重入,可中断、可公平
lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
不可重入锁:自旋锁、wait()、notify()、notifyAll()
不可重入锁,即不可递归调用,递归调用会发生死锁
2、reentrantlock和synchronized区别
reentrantLock拥有synchronized相同的并发性和内存语义,此外还多列锁投票、定时锁等候和中断所等候
使用synchronized锁,A不释放,B将一直等待下去
使用reentrantlock锁,A不释放,B等待一段时间就会中断等待,而干别的事情。
synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且代码执行时出现异常,JVM会走到哪个释放锁定。但是Lock不行
在资源竞争不是很激烈的情况下,synchronized的性能优于reentrantlock锁,而竞争激烈的情况下,synchronized的性能下降几十倍,而reentrantlock的性能维持常态。
- 性能分析
synchronized会多次自旋,以获得锁,在这个过程中等待的线程不会被挂起,因而节省了挂起和唤醒的上下文切换的开销
而reentrantlock,不会自旋,而是直接挂起
因而在线程并发量不大的情况下,synchronized因为拥有自旋锁、偏向锁和轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比reenttrantlock高效。
默认情况下synchronized非公平锁;reentrantlock默认是非公平锁。
- 绑定多个条件
一个Reentrantlock对象可以同时绑定多个Condition对象,
而在synchronized中,锁对象的wait()
和notify()
或notifyAll()
方法可以实现一个隐含的条件。
如果要和多余一个添加关联的时候,synchronized就不得不额外地添加一个锁,而Reentrantlock则无须这么做只需要多次调用new Condition()
方法即可。