Java锁
公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁
- check-then-act
- read-modify-write
乐观锁与悲观锁
乐观锁与悲观锁是在数据库中引入的名词
悲观锁:指对数据被外界修改持悲观态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。
乐观锁:它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测
公平锁和非公平锁
-
概念:
-
公平锁
: 是指多个线程按照申请锁的顺序来获取锁。 -
非公平锁
: 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象,但优点在于吞吐量比公平锁大
-
-
如何创建:
-
并发包中ReentrantLock的创建可以指定构造函数的Boolean类型来得到公平锁或非公平锁,默认是非公平锁
-
Lock lock = new ReentrantLock(true);
-
-
对于Synchronized而言,也是一种非公平锁
- 比如:A线程进入了同步代码块,B.C线程则会堵塞挂起,当A线程完成任务后,B,C都有可能获取锁
-
没有公平性需求的情况下,尽量使用非公平锁,因为公平锁会带来额外的开销
可重入锁和递归锁
- 可重入锁又名递归锁
- 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码。在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
- ReetrantLock/Synchronized 就是典型的可重入锁
- 原理:
- 在锁的内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。
- 当线程获取了锁,计数器的值会从0变为1,其他线程再来获取会发现锁的所有者不是自己,而被阻塞挂起
- 但是当获取了锁的线程再来获取锁时,会发现锁的拥有者是自己,再次+1.
- 可重入锁的最大作用时避免死锁
public class ReeterLockDemo {
public static void main(String[] args){
Phone phone = new Phone();
new Thread(() ->{
try{
phone.sengSMS();
}catch (Exception e){
e.getStackTrace();
}
},"t1").start();
new Thread(() ->{
try{
phone.get();
}catch (Exception e){
e.getStackTrace();
}
},"t2").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.getStackTrace();
}
/**
* new Thread(() ->{
* try{
* phone.get();
* }catch (Exception e){
* e.getStackTrace();
* }
* },"t3").start();
*/
System.out.println();
System.out.println();
System.out.println();
Thread t3 = new Thread(phone);
Thread t4 = new Thread(phone);
t3.start();
t4.start();
}
}
class Phone implements Runnable{
public synchronized void sengSMS()throws Exception{
System.out.println(Thread.currentThread().getId()+"\t invoked sendSMS()");
sengEmail();
}
public synchronized void sengEmail()throws Exception{
System.out.println(Thread.currentThread().getId()+"\t ####invoked sendEmail()");
}
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get() {
//只要匹配就可以多把锁
lock.lock();
lock.lock();
try{
System.out.println(Thread.currentThread().getId()+"\t invoked get()");
set();
}catch (Exception e){
e.getStackTrace();
}finally {
lock.unlock();
lock.unlock();
}
}
public void set() {
lock.lock();
try{
System.out.println(Thread.currentThread().getId()+"\t invoked set()");
}catch (Exception e){
e.getStackTrace();
}finally {
lock.unlock();
}
}
}
自旋锁
- Unssafe类+CAS思想(自旋)
-
是指尝试获锁的不会立即阻塞,而是采用循环的方式去尝试获取锁
-
由于Java中的线程和操作系统中的线程是一一对应的,所以当一个线程在获取锁失败后,会被切换到内核状态而被挂起。所以从用户态到内核状态的开销是比较大的。
-
自旋锁则是不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取锁
-
优缺点
- 好处是减少线程上下文切换的消耗
- 缺点是 循环会消耗CPU。
-
//手写自旋锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t come in"); //开始自旋 如果是null,则更新为当前线程,否者自旋 while(!atomicReference.compareAndSet(null, thread)){ System.out.println(Thread.currentThread().getName()+"\t wait"); } } //解锁 public void myUnlock(){ Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock"); }
独占锁(写锁)/共享锁(读锁)/互斥锁
改进型锁:读写锁
根据锁能被一个线程占有还是多个线程共同持有,锁可以分为共享锁和独占锁
- 独占锁是一种悲观锁,由于每次访问访问资源都先加上互斥锁,这限制了并发性
- 共享锁则是一种乐观锁
- ReetrantLock\Synchronized都是独占锁
总结
- 读-读能共存
- 读-写不能共存
- 写-写不能共存
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 相应的写锁未被任何线程持有 | 对读线程是共享的,对写线程排他的 | 允许多个读线程可以同时读取共享变量,并保证读线程读取共享变量期间没有其他任何线程能够更新这些共享变量 |
写锁 | 该写锁未被其他任何线程持有并且相应的读锁为被其他任何线程持有 | 对写线程和读线程都是排他的 | 使得写线程能够以独占的方式访问共享变量 |
- 写操作: 原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
//资源类
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock= new ReentrantReadWriteLock();
//写操作
public void put(String key, Object value){
// 创建一个写锁
rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"\t 正在写入:" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入完成:" + value);
}catch (Exception e){
e.getStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
//读操作
public void get(String key){
//上锁
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"\t 读取:" + key);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读取完成:" +result );
}catch (Exception e){
e.getStackTrace();
}finally {
rwLock.readLock().unlock();
}
}
public void clearMap(){
map.clear();;
}
}
- 写锁一次只能一个线程进入,执行写操作,而读锁是多个线程能够同时进入,进行读取的操作
有助于提高锁性能的建议
-
减少锁的持有时间
-
减少锁的粒度
-
用读写分离锁来替换独占锁
-
锁分离
-
锁粗化
虚拟机在遇到一连串地对同一个锁不断进行请求和释放的操作的时候,便会把所有的锁操作整合对锁的一次请求,从而减少对锁请求同步的次数
Java虚拟机对锁优化所做的努力
锁偏向
- 锁偏向是一种对加锁操作的优化手段
- 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作。
- 对几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能时同一个线程请求相同的锁。
- 使用Java虚拟机参数 -XX-UseBiasedLocking 开启偏向锁
轻量级锁
如果偏向锁失败,那么虚拟机不会立即挂起线程,他还会使用一种称为轻量级锁的优化手段。
- 它只是将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁
自旋锁
锁消除
java 虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间