常见的几种锁
Posted xiaobai1202
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见的几种锁相关的知识,希望对你有一定的参考价值。
1.悲观锁 for update
悲观锁认为每次查询数据数据都会造成数据的更新或者丢失问题,所以每次查询都会加上排它锁。
如图所示,当两条线程同时访问该sql语句时,可能会造成脏读数据user_money为原来的两倍(假设线程一执行完第一句等待,线程二将两句全部执行完,这时线程一如果继续执行则会脏读数据)
使用悲观锁则通过在其后加for update后,仅允许一个连接查询数据也就是只要一个连接获得锁后,其他连接则只能等待该锁的释放。
缺点:每次都只有一个连接进行操作,效率非常低,适合查询量低的情况。
2.乐观锁 version
乐观锁认为每次查询都不会造成数据更新丢失,使用版本字段控制。(version机制)
如图所示,每一次执行更新操作时,都要将version字段加一,这样其他连接就不能通过之前的version查找到该条数据,从而保证不重复读写。
优点:可并发运行,效率高 缺点:需要增加一个维护字段version,而且可能会出现查不到数据的情况。
3.重入锁 ReentrantLock
重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
非重入锁进行以上操作的话就会产生死锁。
简单的说就是当外层获取到某一把锁,若该锁是可重入锁,则默认内层所有代码都获取了该锁。
可重入锁有一个状态计数器,每当获取一次该锁,计数器的数值就会加一。每次释放锁,计数器的数值就会减一,只有当计数器的值减到0的时候,才会真正释放该锁,其他线程才可重新获取该锁。
如果一把锁是不可重入锁时,当外层获取该锁后,内层的代码再次获取该锁时,由于外层没有释放,内层就获取不到而阻塞,导致程序等待,而外层也因此无法释放锁,就产生了死锁。
1 public class Test02 extends Thread { 2 ReentrantLock lock = new ReentrantLock(); 3 public void get() { 4 lock.lock(); 5 System.out.println(Thread.currentThread().getId()); 6 set(); 7 lock.unlock(); 8 } 9 public void set() { 10 lock.lock(); 11 System.out.println(Thread.currentThread().getId()); 12 lock.unlock(); 13 } 14 @Override 15 public void run() { 16 get(); 17 } 18 public static void main(String[] args) { 19 Test ss = new Test(); 20 new Thread(ss).start(); 21 new Thread(ss).start(); 22 new Thread(ss).start(); 23 } 24 25 }
4.读写锁 ReentrantReadWriteLock
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。
但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。
常用于缓存设计。
1 public class Cache { 2 private static volatile Map<String,Object> map=new HashMap<>(); 3 4 private static ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock(); 5 6 private static Lock r=reentrantReadWriteLock.readLock(); 7 private static Lock w=reentrantReadWriteLock.writeLock(); 8 /** 9 * 写 10 * @param key 11 * @param object 12 */ 13 public static void put(String key,Object object){ 14 try{ 15 // w.lock(); 16 System.out.println("正在写入key:"+key+",value:"+object+"开始。。"); 17 Thread.sleep(100); 18 Object obj=map.put(key,object); 19 System.out.println("写入key:"+key+",value:"+object+"结束。。"); 20 } catch (Exception e) { 21 e.printStackTrace(); 22 } finally { 23 // w.unlock(); 24 } 25 } 26 27 /** 28 * 读 29 * @param key 30 * @return 31 */ 32 public static Object get(String key){ 33 try{ 34 // r.lock(); 35 System.out.println("正在读取key:"+key+"开始。。"); 36 Thread.sleep(100); 37 Object obj=map.get(key); 38 System.out.println("读取key:"+key+",value:"+obj+"结束。。"); 39 return obj; 40 } catch (Exception e) { 41 e.printStackTrace(); 42 } finally { 43 // r.unlock(); 44 } 45 return null; 46 } 47 48 public static void main(String[] args) { 49 new Thread(new Runnable() { 50 @Override 51 public void run() { 52 for (int i = 0; i < 10; i++) { 53 Cache.put(i+"",i+""); 54 } 55 } 56 }).start(); 57 new Thread(new Runnable() { 58 @Override 59 public void run() { 60 for (int i = 0; i < 10; i++) { 61 System.out.println(Cache.get(i+"")); 62 } 63 } 64 }).start(); 65 } 66 } 67 68 输出如下: 69 正在写入key:0,value:0开始。。 70 正在读取key:0开始。。 71 读取key:0,value:null结束。。 72 写入key:0,value:0结束。。 73 正在写入key:1,value:1开始。。 74 null 75 正在读取key:1开始。。 76 读取key:1,value:null结束。。 77 null 78 写入key:1,value:1结束。。 79 正在写入key:2,value:2开始。。 80 正在读取key:2开始。。 81 写入key:2,value:2结束。。 82 读取key:2,value:null结束。。 83 null 84 正在写入key:3,value:3开始。。 85 正在读取key:3开始。。 86 写入key:3,value:3结束。。 87 读取key:3,value:null结束。。 88 null 89 正在读取key:4开始。。 90 正在写入key:4,value:4开始。。 91 写入key:4,value:4结束。。 92 读取key:4,value:4结束。。 93 4 94 正在写入key:5,value:5开始。。 95 正在读取key:5开始。。 96 读取key:5,value:null结束。。 97 null 98 正在读取key:6开始。。 99 写入key:5,value:5结束。。 100 正在写入key:6,value:6开始。。 101 写入key:6,value:6结束。。 102 正在写入key:7,value:7开始。。 103 读取key:6,value:6结束。。 104 6 105 正在读取key:7开始。。 106 读取key:7,value:null结束。。 107 null 108 正在读取key:8开始。。 109 写入key:7,value:7结束。。 110 正在写入key:8,value:8开始。。 111 写入key:8,value:8结束。。 112 正在写入key:9,value:9开始。。 113 读取key:8,value:8结束。。 114 8 115 正在读取key:9开始。。 116 写入key:9,value:9结束。。 117 读取key:9,value:9结束。。 118 9
可以看到,在进行写操作的时候进行了读取操作,这样就造成数据不安全,将注释掉的锁代码打开后就可以实现读写分离保证数据安全。
5. CAS 无锁机制
CAS : Compare And Swap 原子类底层使用CAS无锁机制实现保证线程安全,CAS无锁机制效率比有锁机制高。
(1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。
更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
(2)无锁的好处:
第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
第二,它天生就是死锁免疫的。
就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
(3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
(4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
(5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
(6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
6.自旋锁 AtomicReference
1 class SpinLock{ 2 private AtomicReference<Thread> sign=new AtomicReference<>(); 3 public void lock(){ 4 Thread thread=Thread.currentThread(); 5 while(!sign.compareAndSet(null,thread)){ 6 7 } 8 } 9 10 public void unlock(){ 11 Thread thread=Thread.currentThread(); 12 sign.compareAndSet(thread,null); 13 } 14 } 15 public class Test implements Runnable{ 16 static int sum; 17 private SpinLock lock; 18 public Test(SpinLock lock){ 19 this.lock=lock; 20 } 21 public static void main(String[] args) throws InterruptedException { 22 SpinLock spinLock=new SpinLock(); 23 for (int i = 0; i < 100; i++) { 24 Test test=new Test(spinLock); 25 Thread thread=new Thread(test); 26 thread.start(); 27 } 28 Thread.sleep(1000); 29 System.out.println(sum); 30 } 31 32 @Override 33 public void run() { 34 this.lock.lock(); 35 this.lock.lock(); 36 sum++; 37 this.lock.unlock(); 38 this.lock.unlock(); 39 } 40 }
当一个线程调用这个不可重入的自旋锁去加锁的时候没问题,当再次调用lock()的时候,因为自旋锁的持有引用已经不为空了,该线程对象会误认为是别人的线程持有了自旋锁
使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。
当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
7.分布式锁
如果想在不同的jvm中保证数据同步,使用分布式锁技术。
有数据库实现、缓存实现、Zookeeper分布式锁
具体各种实现方式请自行百度
以上是关于常见的几种锁的主要内容,如果未能解决你的问题,请参考以下文章