synchronized与ReentrantLock比较
- 它们都是线程同步方式,都是阻塞式实现方式(即一个线程获取锁后,其他要获取该锁的线程处于阻塞状态)
- 都是可重入性锁,即可以对某一对象重复加锁。常见于递归,例如方法A调用加锁的方法B,而方法B又递归调用自身。如果不可以重入,递归调用时发现方法B已经上锁,需要等待。这是就出现了自身等待自身释放锁的情况。
- 两者性能差不多。在synchronized优化之前,性能较差。在优化后引入了偏向锁,轻量级锁(自旋锁),性能较ReentrantLock差不多。
- synchronized是Java关键字使用比较简单,加锁、释放锁都是自动进行的;而ReentrantLock是API层面的锁,是为了替代优化前synchronized而开发出的java.util.current包下的工具类。需要手动lock(),unlock(),一般配合try、catch、finally(释放锁)使用
- synchronized是非公平锁,线程唤醒是随机的。ReentrantLock可实现公平锁,即按照等待时间唤醒线程
- ReentrantLock可响应中断,使用lockInterruptibly()方法上锁时可以响应中断。当线程互相获取临界资源时而发生了死锁,ReentrantLock可以自动中断其中一个线程释放资源,从而使其他线程正常结束。
package thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReenTrantLockDemo {
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(new ThreadDemo(lock1, lock2), "t1");
Thread t2 = new Thread(new ThreadDemo(lock2, lock1), "t1");
t1.start();
t2.start();
t1.interrupt();
}
static class ThreadDemo implements Runnable{
Lock lock1 ;
Lock lock2;
ThreadDemo(Lock lock1, Lock lock2){
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
lock1.lockInterruptibly();//可以响应中断的获取锁方式
lock2.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束");
}
}
}
}
ReentrantLock分析
ReentrantLock手动加锁、释放锁,锁的粒度比较小,适合加锁复杂的情况下使用。
ReentrantLock基于AQS(AbstractQueuedSynchronizer)和SupportLock。AQS利用硬件原语指令CAS(Compare-And-Swap)实现轻量级多线程控制,不会引起CPU上下文切换和调度。同时提供内存可见性和原子性。
源码分析
使用链表作为等待队列,状态state用volatile修饰,保证个线各线程可见。
在初始化锁时通过传入参数(true)或者不传构造公平锁或非公平锁。
非公平方式获取锁,在lock方法中检查该对象是否被锁住,如果没有直接获取锁,否则调用acquire方法申请锁。
synchronized
在字节码文件中:
同步代码块:synchronized对代码块的同步使用monitorenter和monitorexit控制,当执行代码遇到monitorenter时,尝试获取当前monitor的锁,如果获得锁则继续往下执行,否则阻塞进入等待队列。继续执行到monitorexit时,释放锁。
方法:对于方法的同步,是将该方法的access_flag的synchronized的标志位置1,表示该方法是一个同步方法。
在Hotspot虚拟机中锁信息存放在对象头中,在对象头中还存放的有hashcode、GC分代信息、线程持有的锁、锁状态标志、偏向ID等。
锁分级
在Hotspot虚拟机中,Java锁分为四种:偏向锁、轻量级锁、自旋锁、重量级锁。锁只能升级,不可以降级,即轻量级锁可以升级为自旋锁而自旋锁不能降级为轻量级锁。
偏向锁
偏向锁是一种优化后的锁,它可以减少切换锁的开销。在Java中锁的获取大多数情况下都被同一线程获取到,而为了减少同一线程锁切换的开销,从而引入偏向锁。当一个线程获得锁之后,就进入偏向锁模式,当该线程继续请求该锁的时候,可以直接将锁给该线程,省去了切换的开销。当有其他线程请求锁的时候,则退出偏向锁,进入轻量级锁状态。
轻量级锁
在偏向锁失败后,会进入轻量级锁状态。轻量级锁是为了减少传统重量级锁使用用操作系统互斥而设计的。Java经验发现,在同一时间段大多只有一个线程在执行,很少有其他线程在同一时刻竞争锁,线程大多是交替执行。所以为了减少操作系统互斥的开销,引入轻量级锁。当在同一时刻有多个线程竞争锁时,轻量级锁则会升级为自旋锁。
自旋锁
当线程因阻塞而需要执行挂起操作时,操作系统会从用户态转成核心态,需要耗费较长的时间。
当轻量级锁失败后,为了优化锁的执行效率,会进入自旋锁。自旋锁是让线程执行无意义的循环等待需要的锁,从而避免了线程的挂起操作而引起的系统从用户态转为核心态。但是这个等待时间是有限,当一段时间后该线程仍未获得锁,则将线程挂起,进入重量级锁,减少处理器资源的消耗。
重量级锁
重量级锁就是通常意义上的锁,利用监视器(monitor)实现。当前线程会被阻塞挂起,系统需要从用户态转为核心态来执行该操作。需要耗费较多的时间。这就是synchronized在优化之前效率低下的原因。