JAVA并发编程中关于锁的小结

        最近在学习java的并发编程时,遇到了很多锁的概念,有很多其实都是同一个锁的多种叫法而已,或者是某种锁的一个功能。为了更好的梳理这块知识,这里做一个小结,将锁的概念进行区分。

        先说目前我们所遇到的锁的名词,大致有如下这些:公平锁/非公平锁、可重入锁(递归锁)、独享锁/共享锁、互斥锁/读写锁、乐观锁/悲观锁、自旋锁、分段锁、分布式锁。

公平锁/非公平锁

        这个概念主要是指,当多个进程同时抢占一个资源时,由于该资源已经被占用,那么这些线程会进入等待队列。此时,如果是公平锁,那么所有的进程会按照访问时间依次进入队列,当资源被释放后,从队列中出队一个线程进行处理(由于队列是先进先出原则,所以保证了公平性);如果是非公平锁,那么会有两种情况进行处理:1、每个线程都有各自的优先级,每当资源被释放后,会根据线程优先级进行出队,优先级高的线程先获得资源抢占权。2、每个线程在访问资源发现被占用,进入等待队列前,会再进行一次抢占动作,如果此时能抢到锁,那么就继续执行,抢不到,就进入等待队列(AQS非公平锁原理)。

        非公平锁的优点是,高并发下的吞吐量比公平锁要高(毕竟少了入队出队操作),但是极端情况下会导致某些线程长时间抢不到资源而导致超时。

可重入锁(递归锁)

        这个概念是指,如果一个线程在获得锁的情况下,内部调用逻辑有同样的锁,那么它依然可以获得该锁,而不需要等待。如下,funcA方法本身有锁,调用的funcB方法也有锁,但是他们是在同一个线程逻辑内的锁,可以认为是一个锁,这样在调用funcB方法时,可以直接获取该锁而不用等待,避免死锁的发生。

synchronized void funcA() throws Exception{
    Thread.sleep(1000);
    funcB();
}

synchronized void funcB() throws Exception{
    Thread.sleep(1000);
}

        可以这么理解,你有一把家门钥匙,当你开门进入后,将门反锁(即上锁,避免其他人进入),屋里的其他房间都可以对你敞开,你要做的操作仅仅是:进入主卧--开门--离开主卧--关门,进入厨房--开门--离开厨房--关门。这个操作在Java中,就是用一个计数器来控制,当你重复获得一个锁时,该锁的计数器会加一,释放时减一,直到计数器为0,才标志着该资源被完全释放。

独享锁/共享锁

互斥锁/读写锁

        这两个概念一起说吧。独享的意思,就是互斥,这个很好理解,就是一个资源每次只能被一个线程占用,占用期间其他线程不能抢占,只能等待。共享锁是指一个资源同时可以被多个线程占用。java中的SynchronizedReentrantLock都是独享锁。不过Lock的另一个实现ReadWriteLock(读写锁)就有共享锁的实现。因为读操作并没有对数据进行更改,加不加锁其实不影响,而写操作就需要加锁避免脏数据的产生。为了应对这种场景,就产生了读写锁,提高并发效率。

乐观锁/悲观锁

自旋锁

        乐观和悲观,指的就是对待加锁的态度。悲观锁认为,不加锁的并发操作一定会出问题,当一个线程在操作数据时,认为这个数据一定被其他线程修改了,必须加锁保证数据一致性(哪怕当前就它一个线程);乐观锁认为,当一个线程在操作数据时,认为这个数据一定没有被其他线程修改,不需要加锁(也就是所谓的无锁机制)。

        悲观锁很好理解,只要加上锁就好。

        乐观锁因为认为不需要加锁,但是为了保证数据一致性,才用了CAS机制,即compare and swap(set),比较后再交换(赋值)。举两个例子来说明:

        1、在sql执行更新语句时,悲观锁会在这个方法只上加Synchronized关键字保证每次只有一个线程能更新。而乐观锁不需要加锁,他只需要在表中增加一个字段:version,每次修改之前先查询该值(比如10),然后执行update语句为

update table_name set name='zhangsan',version=version+1 where id=1 and version=10

        只有当version=10的时候,这条语句才会被执行,如果此时有另一个线程执行了这条语句,version会变成11,那么该线程会执行失败,有两种选择进行处理:A--退出,该情况可能的场景之一是用户修改姓名,连续点了两次提交,既然别的线程已经改了,那该线程直接结束就好;B--进入死循环,重新获取新的version,再去更新,直到更新成功为止,因为这里不停的循环等待,所以这种情况就被称为自旋锁。该情况可能的场景就是记录访问量、库存等等(当然,redis记录更好)

        2、Atomicinteger原子类的核心机制,采用了java的unsafe类,直接对内存进行操作,这里有三个概念,初始值、期望值和比较值。比如要自增,从1增加到2,那么初始值就是1,期望值就是2,比较值是从主存中拿回来的这个变量当前的值(涉及了JAVA内存模型,这里不细说)。如果比较值是1,说明当前可以自增,然后增加后,将2刷回主存;如果比较值大于1,说明已经有其他线程将该变量进行了自增,那么它就会在循环中开始自旋:将比较值赋给初始值,再进行一次CAS操作。

分段锁

        这是一种加锁思想,并不是具体的一种锁,是解决高并发下全局锁可能造成的性能损失而出现的,在ConcurrentHashMap中有很好的体现。在进行put操作时,并不会对整个hashmap进行加锁,会先根据hashcode算出将要插入的点,然后针对该点进行加锁,这样就保证了多个线程可以同时进行put操作了。

分布式锁

        分布式锁主要是针对分布式环境下,多台主机同时操作同一个资源,因为不在同一个jvm环境下,无法通过java自身进行控制,所以就需要借助第三方工具来实现。主要有两种方案:

        1、借助redis的setnx方法,该方法在插入一个值后成功后会返回1,失败返回0。因为redis本身是线程安全的,所以在操作时通过自旋的方式判断返回值

        2、借助zookeeper的临时节点。zk规定,同一个节点下,不能存在同名的节点,临时节点则会在断开连接的时候自动删除。所以当一个节点被创建后,对应的线程会获得锁,其他线程获取锁失败,此时不需要自旋,会执行线程等待状态。zk在节点删除时,会主动发出时间通知,其他线程在收到通知后,再进行一次申请锁操作,获得锁就会解除等待状态,否则继续等待。

小结

        1、java中有两种锁,Synchronized关键字和Lock锁,其中Lock锁是一个接口,其下有多种实现,最著名的就是ReentrantLockReadWriteLock,上面都有提到。

        2、ReentrantLock的字面意思,其实就是可重入锁的意思,它借助了AQS机制,进行了加锁。

        3、Synchronized是通过jvm来实现的非公平锁,所以无法改为公平锁。而ReentrantLock是通过代码的方式实现了加锁,所以可以进行公平和非公平的选择,默认情况下是非公平锁

        4、自旋锁(乐观锁)因为实质上并没有加锁,所以它的并发效率会很高,但是因为死循环的存在,会造成CPU的资源占用。所以在使用锁机制时,要针对不同的情况采用不同的锁。

        5、在使用自旋锁时,要考虑实际情况,不能一味的死循环等待。其实自旋锁也可以理解为一个重试机制,当超出一定次数后,可以考虑退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章