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
关键字解决的是多个线程之间访问资源的同步性。