[Java并发]锁
锁
锁的分类#
- 公平锁/非公平锁
- 可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
公平锁/非公平锁#
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock
而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁
。非公平锁的优点在于吞吐量比公平锁大。
new ReentrantLock(true); // 构造一个公平的 ReentrantLock
对于Synchronized
而言,也是一种非公平锁
。由于其并不像ReentrantLock
是通过AQS
的来实现线程调度,所以并没有任何办法使其变成公平锁。
可重入锁#
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock
而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock
重新进入锁。
对于Synchronized
而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock
而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized
而言,当然是独享锁。
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
乐观锁/悲观锁#
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
-
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
-
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
-
乐观锁在并发比较低的情况下性能比较好,因为没有上锁释放锁的开销,但是在并发比较高的情况下,常常会因为cas去修改原数据的时候发现数据已经被另一线程修改了,所以线程手里的数据作废,大量冲突导致大量的计算资源的浪费。试想,在毫无并发的情况下,CAS机制不仅能正确完成任务,而且没有加锁解锁的开销,但是锁机制却必须要加锁解锁,哪怕是没有任何竞争,白白浪费了时间。
-
另外,CAS机制只能保证单个变量操作的原子性,当涉及到多个变量的时候,CAS机制是无能为力的,而锁机制却可以通过对整个代码块进行加锁处理;
分段锁#
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
偏向锁/轻量级锁/重量级锁#
这三种锁是指锁的状态,并且是针对Synchronized
。在Java 5通过引入锁升级的机制来实现高效Synchronized
。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁#
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现
Lock类 #
之前已经说道,JVM提供了synchronized关键字来实现对变量的同步访问以及用wait和notify来实现线程间通信。在jdk1.5以后,JAVA提供了Lock类来实现和synchronized一样的功能,并且还提供了Condition来显示线程间通信。
Lock类是Java类来提供的功能,丰富的api使得Lock类的同步功能比synchronized的同步更强大。本文章的所有代码均在Lock类例子的代码
本文主要介绍一下内容:
- Lock类
- Lock类其他功能
- Condition类
- Condition类其他功能
- 读写锁
Lock类实际上是一个接口,我们在实例化的时候实际上是实例化实现了该接口的类Lock lock = new ReentrantLock();
。用synchronized的时候,synchronized可以修饰方法,或者对一段代码块进行同步处理。
前面讲过,针对需要同步处理的代码设置对象监视器,比整个方法用synchronized修饰要好。Lock类的用法也是这样,通过Lock对象lock,用lock.lock
来加锁,用lock.unlock
来释放锁。在两者中间放置需要同步处理的代码。
具体的例子如下:
public class MyConditionService {
private Lock lock = new ReentrantLock();
public void testMethod(){
lock.lock();
for (int i = 0 ;i < 5;i++){
System.out.println("ThreadName = " + Thread.currentThread().getName() + (" " + (i + 1)));
}
lock.unlock();
}
}
测试的代码如下:
MyConditionService service = new MyConditionService();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
Thread.sleep(1000 * 5);
不加lock的结果
ThreadName = Thread-0 1
ThreadName = Thread-0 2
ThreadName = Thread-2 1
ThreadName = Thread-1 1
ThreadName = Thread-3 1
ThreadName = Thread-4 1
ThreadName = Thread-0 3
ThreadName = Thread-2 2
ThreadName = Thread-1 2
ThreadName = Thread-3 2
ThreadName = Thread-4 2
ThreadName = Thread-0 4
ThreadName = Thread-2 3
ThreadName = Thread-1 3
ThreadName = Thread-3 3
ThreadName = Thread-4 3
ThreadName = Thread-0 5
ThreadName = Thread-2 4
ThreadName = Thread-1 4
ThreadName = Thread-3 4
ThreadName = Thread-4 4
ThreadName = Thread-2 5
ThreadName = Thread-1 5
ThreadName = Thread-3 5
ThreadName = Thread-4 5
加了lock的结果
ThreadName = Thread-0 1
ThreadName = Thread-0 2
ThreadName = Thread-0 3
ThreadName = Thread-0 4
ThreadName = Thread-0 5
ThreadName = Thread-1 1
ThreadName = Thread-1 2
ThreadName = Thread-1 3
ThreadName = Thread-1 4
ThreadName = Thread-1 5
ThreadName = Thread-2 1
ThreadName = Thread-2 2
ThreadName = Thread-2 3
ThreadName = Thread-2 4
ThreadName = Thread-2 5
ThreadName = Thread-3 1
ThreadName = Thread-3 2
ThreadName = Thread-3 3
ThreadName = Thread-3 4
ThreadName = Thread-3 5
ThreadName = Thread-4 1
ThreadName = Thread-4 2
ThreadName = Thread-4 3
ThreadName = Thread-4 4
ThreadName = Thread-4 5
总之,就是每个线程的打印1-5都是同步进行,顺序没有乱。
通过下面的例子,可以看出Lock对象加锁的时候也是一个对象锁,持有对象监视器的线程才能执行同步代码,其他线程只能等待该线程释放对象监视器。
public class MyConditionMoreService {
private Lock lock = new ReentrantLock();
public void methodA(){
try{
lock.lock();
System.out.println("methodA begin ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
Thread.sleep(1000 * 5);
System.out.println("methodA end ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void methodB(){
try{
lock.lock();
System.out.println("methodB begin ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
Thread.sleep(1000 * 5);
System.out.println("methodB end ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
测试代码如下:
public void testMethod() throws Exception {
MyConditionMoreService service = new MyConditionMoreService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadA aa = new ThreadA(service);
aa.setName("AA");
aa.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
ThreadB bb = new ThreadB(service);
bb.setName("BB");
bb.start();
Thread.sleep(1000 * 30);
}
public class ThreadA extends Thread{
private MyConditionMoreService service;
public ThreadA(MyConditionMoreService service){
this.service = service;
}
@Override
public void run() {
service.methodA();
}
}
public class ThreadB extends Thread{
private MyConditionMoreService service;
public ThreadB(MyConditionMoreService service){
this.service = service;
}
@Override
public void run() {
super.run();
service.methodB();
}
}
结果如下:
methodA begin ThreadName=A time=1485590913520
methodA end ThreadName=A time=1485590918522
methodA begin ThreadName=AA time=1485590918522
methodA end ThreadName=AA time=1485590923525
methodB begin ThreadName=B time=1485590923525
methodB end ThreadName=B time=1485590928528
methodB begin ThreadName=BB time=1485590928529
methodB end ThreadName=BB time=1485590933533
可以看出Lock类加锁确实是对象锁。针对同一个lock对象执行的lock.lock
是获得对象监视器的线程才能执行同步代码 其他线程都要等待。
在这个例子中,加锁,和释放锁都是在try-finally。这样的好处是在任何异常发生的情况下,都能保障锁的释放。
Lock类其他的功能
如果Lock类只有lock和unlock方法也太简单了,Lock类提供了丰富的加锁的方法和对加锁的情况判断。主要有
- 实现锁的公平
- 获取当前线程调用lock的次数,也就是获取当前线程锁定的个数
- 获取等待锁的线程数
- 查询指定的线程是否等待获取此锁定
- 查询是否有线程等待获取此锁定
- 查询当前线程是否持有锁定
- 判断一个锁是不是被线程持有
- 加锁时如果中断则不加锁,进入异常处理
- 尝试加锁,如果该锁未被其他线程持有的情况下成功
实现公平锁
在实例化锁对象的时候,构造方法有2个,一个是无参构造方法,一个是传入一个boolean变量的构造方法。当传入值为true的时候,该锁为公平锁。默认不传参数是非公平锁。
公平锁:按照线程加锁的顺序来获取锁
非公平锁:随机竞争来得到锁
此外,JAVA还提供isFair()
来判断一个锁是不是公平锁。
获取当前线程锁定的个数
Java提供了getHoldCount()
方法来获取当前线程的锁定个数。所谓锁定个数就是当前线程调用lock方法的次数。一般一个方法只会调用一个lock方法,但是有可能在同步代码中还有调用了别的方法,那个方法内部有同步代码。这样,getHoldCount()
返回值就是大于1。
下面的方法用来判断等待锁的情况
获取等待锁的线程数
Java提供了getQueueLength()
方法来得到等待锁释放的线程的个数。
查询指定的线程是否等待获取此锁定
Java提供了hasQueuedThread(Thread thread)
查询该Thread是否等待该lock对象的释放。
查询是否有线程等待获取此锁定
同样,Java提供了一个简单判断是否有线程在等待锁释放即hasQueuedThreads()
。
下面的方法用来判断持有锁的情况
查询当前线程是否持有锁定
Java不仅提供了判断是否有线程在等待锁释放的方法,还提供了是否当前线程持有锁即isHeldByCurrentThread()
,判断当前线程是否有此锁定。
判断一个锁是不是被线程持有
同样,Java提供了简单判断一个锁是不是被一个线程持有,即isLocked()
下面的方法用来实现多种方式加锁
加锁时如果中断则不加锁,进入异常处理
Lock类提供了多种选择的加锁方法,lockInterruptibly()
也可以实现加锁,但是当线程被中断的时候,就会加锁失败,进行异常处理阶段。一般这种情况出现在该线程已经被打上interrupted的标记了。
尝试加锁,如果该锁未被其他线程持有的情况下成功0
Java提供了tryLock()
方法来进行尝试加锁,只有该锁未被其他线程持有的基础上,才会成功加锁。
上面介绍了Lock类来实现代码的同步处理,下面介绍Condition类来实现wait/notify机制。
锁膨胀#
Condition类#
Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。
下面,看一个例子,显示简单的等待/通知
public class ConditionWaitNotifyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await(){
try{
lock.lock();
System.out.println("await的时间为 " + System.currentTimeMillis());
condition.await();
System.out.println("await结束的时间" + System.currentTimeMillis());
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void signal(){
try{
lock.lock();
System.out.println("sign的时间为" + System.currentTimeMillis());
condition.signal();
}finally {
lock.unlock();
}
}
}
测试的代码如下:
ConditionWaitNotifyService service = new ConditionWaitNotifyService();
new Thread(service::await).start();
Thread.sleep(1000 * 3);
service.signal();
Thread.sleep(1000);
结果如下:
await的时间为 1485610107421
sign的时间为1485610110423
await结束的时间1485610110423
condition对象通过lock.newCondition()
来创建,用condition.await()
来实现让线程等待,是线程进入阻塞。用condition.signal()
来实现唤醒线程。唤醒的线程是用同一个conditon对象调用await()
方法而进入阻塞。并且和wait/notify一样,await()和signal()也是在同步代码区内执行。
此外看出await结束的语句是在获取通知之后才执行,确实实现了wait/notify的功能。下面这个例子是展示唤醒制定的线程。
ConditionAllService service = new ConditionAllService();
Thread a = new Thread(service::awaitA);
a.setName("A");
a.start();
Thread b = new Thread(service::awaitB);
b.setName("B");
b.start();
Thread.sleep(1000 * 3);
service.signAAll();
Thread.sleep(1000 * 4);
结果如下:
begin awaitA时间为 1485611065974ThreadName=A
begin awaitB时间为 1485611065975ThreadName=B
signAll的时间为1485611068979ThreadName=main
end awaitA时间为1485611068979ThreadName=A
该结果确实展示用同一个condition对象来实现等待通知。
对于等待/通知机制,简化而言,就是等待一个条件,当条件不满足时,就进入等待,等条件满足时,就通知等待的线程开始执行。为了实现这种功能,需要进行wait的代码部分与需要进行通知的代码部分必须放在同一个对象监视器里面。执行才能实现多个阻塞的线程同步执行代码,等待与通知的线程也是同步进行。对于wait/notify而言,对象监视器与等待条件结合在一起 即synchronized(对象)
利用该对象去调用wait以及notify。但是对于Condition类,是对象监视器与条件分开,Lock类来实现对象监视器,condition对象来负责条件,去调用await以及signal。
Condition类的其他功能#
和wait类提供了一个最长等待时间,awaitUntil(Date deadline)
在到达指定时间之后,线程会自动唤醒。但是无论是await或者awaitUntil,当线程中断时,进行阻塞的线程会产生中断异常。Java提供了一个awaitUninterruptibly
的方法,使即使线程中断时,进行阻塞的线程也不会产生中断异常。
读写锁#
Lock类除了提供了ReentrantLock
的锁以外,还提供了ReentrantReadWriteLock
的锁。读写锁分成两个锁,一个锁是读锁,一个锁是写锁。读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的。
看下面的读读共享的例子:
public class ReadReadService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
try{
try{
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() +
" " + System.currentTimeMillis());
Thread.sleep(1000 * 10);
}finally {
lock.readLock().unlock();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
测试的代码和结果如下:
ReadReadService service = new ReadReadService();
Thread a = new Thread(service::read);
a.setName("A");
Thread b = new Thread(service::read);
b.setName("B");
a.start();
b.start();
结果如下:
获得读锁A 1485614976979
获得读锁B 1485614976981
两个线程几乎同时执行同步代码。
下面的例子是写写互斥的例子
public class WriteWriteService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void write(){
try{
try{
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName() +
" " +System.currentTimeMillis());
Thread.sleep(1000 * 10);
}finally {
lock.writeLock().unlock();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
测试代码和结果如下:
WriteWriteService service = new WriteWriteService();
Thread a = new Thread(service::write);
a.setName("A");
Thread b = new Thread(service::write);
b.setName("B");
a.start();
b.start();
Thread.sleep(1000 * 30);
结果如下:
获得写锁A 1485615316519
获得写锁B 1485615326524
两个线程同步执行代码
读写互斥的例子:
public class WriteReadService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
try{
try{
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(1000 * 10);
}finally {
lock.readLock().unlock();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void write(){
try{
try{
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(1000 * 10);
}finally {
lock.writeLock().unlock();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
测试的代码如下:
WriteReadService service = new WriteReadService();
Thread a = new Thread(service::write);
a.setName("A");
a.start();
Thread.sleep(1000);
Thread b = new Thread(service::read);
b.setName("B");
b.start();
Thread.sleep(1000 * 30);
结果如下:
获得写锁A 1485615633790
获得读锁B 1485615643792
两个线程读写之间也是同步执行代码。
可重入锁 ReentrantLock#
无锁
偏向锁
轻量级锁
重量级锁
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步