Java有哪些锁?

230 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

写在前面

Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等。

在《深入理解Java虚拟机》中,有这样一段话:

synchronized关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。

海明威在他的《午后之死》说过的:“冰山运动之雄伟壮观,是因为他只有八分之一在水面上。”

对于程序员来说,synchronized只是个关键字而已,用起来很简单。之所以我们可以在处理多线程问题时可以不用考虑太多,就是因为这个关键字帮我们屏蔽了很多细节。

那么,本文就围绕synchronized展开,主要介绍其用法、原理,以及如何提供原子性、可见性和有序性保障的等。

悲观锁

悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

应用

synchronized、vector、hashtable

缺点

他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁

乐观锁

乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。

应用

CAS

缺点

写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)

自旋锁

当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。

应用

CAS

优点

避免了线程切换的开销,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力

缺点

占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费

可重入锁

可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLock 和synchronized 都是可重入锁,可重入锁的一个优点是在一定程度上可以避免死锁。

应用

synchronized、Reentrantlock、Lock

优点

避免死锁

缺点

他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁

公平锁

多个线程按照申请锁的顺序来获取锁

应用

Reentrantlock(true)

非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

应用

synchronized、reentrantlock(false)

共享锁

共享锁是指该锁可被多个线程所持有。

独锁

只能有一个线程获取锁,以独占的方式持有锁

应用

synchronized、vector、hashtable、ReentrantReadWriteLock中写锁