锁的分类
- 锁的本质就是线程等待,将并发执行或者并行执行的代码转换为串行执行。可以分为线程阻塞和线程自旋,区别在于
-
- 阻塞:要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源。如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间, 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
-
- 自旋:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源。
-
公平锁/非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象
- 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
* 构造器
* public ReentrantLock() { 默认使用非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) { 可以根据构造时的参数决定使用公平true或者非公平false
sync = fair ? new FairSync() : new NonfairSync();
}
对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁
### 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。
对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
重入需要保证对应释放次数,需要注意sychronized不需要自行编程实现,但是Lock必须使用try/finally结构
读写锁,主要是提高并发性
- 读锁不互斥,一个线程读操作时允许其他线程获取读锁
- 写锁属于互斥锁
- 一个线程持有读锁,其他线程不能获取写锁;一个线程持有写锁,其他线程不能获取读锁;
- 当前线程拥有读锁,再获取写锁时,出现了死锁;当前线程拥有写锁,可以再次申请读锁;
- 当前线程拥有读锁或者写锁,再次申请相同的锁,立即获取,允许重入
Java中读写锁的实现ReentrantReadWriteLock,注意读写锁并没有实现Lock接口,而是实现了ReadWriteLock
Lock lock=new ReentrantReadWriteLock(); 语法错误
class A {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void aa() {
lock.readLock().lock();
try {
System.out.println("read......"+Thread.currentThread().getName());
bb();
System.out.println("read.....end"+Thread.currentThread().getName());
} finally {
lock.readLock().unlock();
}
}
public void bb() {
lock.readLock().lock();
try {
System.out.println("write......");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("write.....end");
} finally {
lock.readLock().unlock();
}
}
}
### 独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁, 其写锁是独享锁。
- 读锁的共享锁可保证并发读是非常高效的,多线程中读写\写读\写写的过程是互斥的
- 在一个线程中读锁和读锁、写锁和写锁不互斥。在一个线程中持有读不能申请写,但是持有写可以申请读
- 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
- 对于synchronized而言,当然是独享锁。
### 乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
- 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的.CAS悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
### 偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对synchronized。在Java5通过引入锁升级机制来实现高效Synchronized。这三种锁的状态是通过对象在对象头中的字段来表明的,实际上就是使用Object Monitor控制对多个线程的工作过程进行协调。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁采用的是操作系统Mutex Lock的互斥,会让其他申请的线程进入阻塞,性能降低。
- 只有一个线程进入临界区,偏向锁
- 多个线程交替进入临界区,轻量级锁
- 多线程同时进入临界区,重量级锁
- 偏向锁-->轻量级锁---重量级锁
### 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
自旋锁的本质:执行几个空方法,稍微等一等,也许是一段时间的循环,也许是几行空的汇编指令。
自适应自旋锁:
该锁在jdk1.6的时候被引入,线程的自旋次数不再是固定值了而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,为了避免浪费处理器资源,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接升级为重量级锁
### 锁消除
即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持
那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的
synchronized和lock比较[面试]
synchronized优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛。缺点:悲观的排他锁,不能进行高级功能
lock优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁。缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪