常见的几种锁

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分布式锁

具体各种实现方式请自行百度 

以上是关于常见的几种锁的主要内容,如果未能解决你的问题,请参考以下文章

mybatis的几种锁

iOS开发之用到的几种锁整理

面试中常被问到的(19)常见几种锁

Laravel:如何在控制器的几种方法中重用代码片段

Java线程并发中常见的锁

java中的12种锁