Synchronized关键字/锁
Synchronized关键字/锁

使用场景
可分为三种:
//修饰实例方法,对当前实例对象加锁
public synchronized void instanceLock(){
}
//修饰静态方法,对当前类的Class对象加锁 本质上也是对象锁
public static synchronized void classLock(){
}
//修饰代码块,指定一个加锁的对象,给对象加锁
public void blockLock(){
Object o = new Object();
synchronized (o){
}
} 如何实现加锁,为什么说是重量级的?
Java对象头
在JVM中,对象在内存分为三块区域:
- 对象头
| 内容 | 说明 |
|---|---|
| Mark Word | 存储对象的hashcode或锁信息等 |
| Klass Point | 指向对象的类元数据的指针 |
| Array length | 数组的长度 |
- 实例数据 存放类的数据信息,父类信息
- 对其填充 虚拟机要求对象起始地址必须是8字节的整数倍,所以它为了字节对齐。
当当前线程是重量级锁时,Mark Word为指向堆中的monitor对象的指针。
而重量级锁归根到底就是monitor对象的争夺。
- 当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
- 如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
- 同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
所有的互斥,其实在这里,就是看你能否获得monitor的所有权,一旦你成为owner就是获得者。
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
monitor监视器是C++写的,在虚拟机的ObjectMonitor.hpp文件中。而ObjectMonitor的实现又涉及到操作系统的互斥量(mutex),而互斥量又涉及到了用户态和内核态的转换。当线程的synchronized锁还没有释放时,另一个线程需要操作系统切换内核态去阻塞它,这种切换是很耗资源的,效率也是很低的,所以说synchronized(1,6前)是重量级锁。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
} 大家说熟悉的锁升级过程,其实就是在源码里面,调用了不同的实现去获取获取锁,失败就调用更高级的实现,最后升级完成。
Java1.6对锁的优化
偏向锁
Hotspot作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。于是引入了偏向锁。
偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不需要了,提高了程序的性能。
实现原理
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向线程ID。
当下次该线程进入这个同步块时,会先去检查Mark Word里是不是存放着自己的ID。
如果是的话,直接执行代码;
如果不是,说明有线程正在竞争。使用CAS方式替换对象头中的线程ID,就是先去测试当前偏向锁字段是否为0。
若为0,说明竞争线程退出同步代码块,不存活了。该线程会将偏向锁字段设置为1,再将Mark Work重新设置为自己的线程ID,仍然为偏向锁。
若为1,说明竞争线程还在,偏向锁已经被它获取了。则该线程会撤销偏向锁,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁。
撤销偏向锁
偏向锁使用了一种**等到竞争出现才释放锁的机制**。
偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程实际开销很大:
- 在一个安全点上停止拥有锁的线程;
- 遍历线程栈,如果存在锁记录,需要修复锁记录和Mark Word,使其变成无锁状态;
- 唤醒被停止的线程,将当前锁升级为轻量级锁。
轻量级锁
实现原理
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们叫做Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word。
然后线程尝试用CAS操作将锁的Mark Word替换为指向锁记录的指针。
如果成功,当前线程获得锁。
如果失败,说明Mark Word已经被替换成了其他线程的锁记录了,然后该线程就尝试使用自旋来获取锁。
但是自旋是非常消耗CPU资源的,所以JDK采取了适应性自旋。就是如果当前线程自旋成功了,那下次自旋的次数会更多,如果自旋失败了,次数则减少。
当自旋失败后,这个线程就会被堵塞,同时锁也会升级为重量级锁。
轻量级锁的释放
轻量级锁解锁时,会使用CAS将之前复制在栈帧中的Displaced Maek Word替换回Mark Word中。(因为替换回去后,Mark Word不再指向该线程,下次线程进入该同步代码块,就可以进行偏向操作了)
若替换成功,说明整个过程没有其他线程访问。
若替换失败,说明当前线程在执行同步代码块期间,有其他线程在访问,锁已经膨胀为重量级锁了。
锁的升级
每一个线程在准备获取共享资源时,第一步会去检查Mark Word里面存放的是不是自己的线程ID。如果是的话,获得偏向锁。
如果不是,先进行CAS操作去替换Mark Word里面的线程ID,若成功,还是偏向锁;
如果失败,升级为轻量级锁。每个线程都尝试通过CAS操作将Mark Word指向自己的锁记录。
若成功,获得轻量级锁。
若失败,不断自旋,自旋到一定程度时,就将锁膨胀为重量级锁。并且该线程被堵塞,等待之前线程释放锁唤醒自己。
synchronized和Lock的区别


lock() 和 lockInterruptibly() 的区别
lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

ReentrantLock的可重入、可中断、非公平锁和公平锁
可重入
就是一个线程在获取了锁之后,再次去获取同一个锁,这时候仅仅是把state状态值进行累加。如果该线程释放了一次锁,就将state-1。当减到0时,其他线程才有机会获取锁。(这个线程释放锁后,会通知AQS等待队列里的线程节点。)
可中断(lockInterruptibly)
可以中断等待获取锁的线程而直接返回,不用等到获取锁才响应。
可中断可以解决死锁问题
public class IntLock implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,产生死锁
*/
public IntLock(int lock) {
this.lock = lock;
}
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly(); // 如果当前线程未被 中断,则获取锁。
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+",执行完毕!");
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+",执行完毕!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 查询当前线程是否保持此锁。
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + ",退出。");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock intLock1 = new IntLock(1);
IntLock intLock2 = new IntLock(2);
Thread thread1 = new Thread(intLock1, "线程1");
Thread thread2 = new Thread(intLock2, "线程2");
thread1.start();
thread2.start();
Thread.sleep(1000);
thread2.interrupt(); // 中断线程2
}
} 上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便形成 thread1 和 thread2 之间的相互等待。代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已获得的 lock2。这个操作导致 thread1 顺利获得 lock2,从而继续执行下去。
非公平锁和公平锁
ReentrantLock可以自己设置是否为公平锁。非公平锁就是上来就抢,抢不到再排队。(可以查看源码)公平锁是乖乖排队。
两者的不同体现在acquire()方法。
synchronized 关键字和 volatile 关键字的区别
它们两个是互补的存在!
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。volatile关键字能保证数据的内存可见性,但是只能对单个volatile变量的读写有原子性。但synchronized对整个临界区代码的执行具有原子性。volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。



查看28道真题和解析