锁Lock
Lock是一个接口
ReentrantLock是唯一实现了Lock接口的类
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行
tryLock()方法有返回值
如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,不会一直等待。
lockInterruptibly()方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
对线程调用interrupt()方法能够中断线程的等待过程。
死锁
避免死锁
加锁顺序
- 按照顺序加锁是一种有效的死锁预防机制。即公平锁。
加锁时限
- 尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。
死锁检测
- 针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
饥饿
原因:
- 高优先级线程吞噬所有的低优先级线程的CPU时间。
- 线程被永久堵塞在一个等待进入同步块的状态。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)。
解决办法:
如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒
为了提高等待线程的公平性,我们使用锁方式来替代同步块。
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序
会造成优先级反转或者饥饿现象。
synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
非公平锁的优点在于吞吐量比公平锁大。
读写锁
读写锁是一个接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock实现了ReadWriteLock接口
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.readLock().lock();
rwl.readLock().unlock();
rwl.writeLock().lock();
rwl.writeLock().unlock();
默认非公平锁
public ReentrantReadWriteLock() {
this(false);
}
可设为公平锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
可重入锁
- 可重入锁又名递归锁
- 指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
- synchronized和ReentrantLock都是可重入锁
假如synchronized不具备可重入性:
假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,此时线程A需要重新申请锁。
将会无限等待
class Test {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。
可重入锁最大的作用是避免死锁。
Lock和synchronized
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- synchronized在发生异常时,会自动释放线程占有的锁很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁。
- Lock可以提高多个线程进行读操作的效率
synchronized代码块同步是使用monitorenter和monitorexit指令实现
悲观锁和乐观锁
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
java中的悲观锁就是Synchronized
乐观锁:假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。
在Java 5通过引入锁升级的机制来实现高效Synchronized。
这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
对象头又可以分为”Mark Word”和类型指针klass
Mark Word”是关键,默认情况下,其存储对象的HashCode、分代年龄和锁标记位。
偏向锁
是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
当线程访问同步方法method时,会在对象的对象头和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁
(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
自旋锁
- 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
- 这样的好处是减少线程上下文切换的消耗,缺点是循环会**消耗**CPU。
轻量级锁
- 是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁
- 其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG)。
轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。
如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
重量级锁
是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。
当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。
锁消除
Java虚拟机在JIT编译时
通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁
锁粗化
如果一系列操作都连续对同一个对象加锁解锁,虚拟机会把加锁同步的范围扩展到整个操作序列的外部
整个synchronized锁流程如下:
检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果自旋成功则依然处于轻量级状态。
如果自旋失败,则升级为重量级锁。
内存可见性
- 线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。