Java 锁类型 Synchronized和 ReentrantLock wait和sleep以及notify

本文介绍了Java中的Synchronized和ReentrantLock锁的使用及其底层实现,包括锁的获取和释放过程。讨论了公平锁与非公平锁、可重入锁的概念,以及两者在Java中的应用。同时,对比了Synchronized与ReentrantLock的区别,如灵活性、公平性以及在等待通知机制上的差异。最后,文章探讨了wait、notify和notifyAll以及sleep方法在多线程中的作用和区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程获取锁的过程:

 成功获取锁的线程,它会从等待队列中出列,并得到共享资源;

没有获取到锁的线程,继续在等待队列中,阻塞在lock方法;

线程释放锁的过程:

通过 在finally语句中的unlock方法,将锁释放,然后等待队列中的线程去竞争这个锁;

Java 中锁的类型,参考:https://www.cnblogs.com/button123/p/14759956.html

  • 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

  • 可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于 Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是 Reentrant Lock重新进入锁。

不可重入锁如何死锁的:


对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁(但避免下面这种情况)。

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

  • 独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

  • 互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock和 Synchronized
读写锁在Java中的具体实现就是ReadWriteLock,其读锁是共享锁,其写锁是独享锁;

  • 乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升

悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

CAS

当多个线程使用CAS获取锁,只能有一个成功,其他线程返回失败,继续尝试获取锁;

CAS操作中包含三个参数:V(需读写的内存位置)+A(准备用来比较的参数)+B(准备写入的新值)若A的参数与V的对应的值相匹配,就写入值B;若不匹配,就写入这个不匹配的值而非B;

AtomicBoolean:常用于布尔值在多线程下放在if中进行判断,因为多线程下的boolean是非线程安全的,可用AtomicBoolean的compareAndSet方法进行设定;

这个方法作用是:比较AtomicBoolean和expect的值,如果一致则把AtomicBoolean的值设成update,if中再接着往下面的逻辑走;如果不是的话就不更新值;

内部原理就是用到了CAS,性能优于Synchronized的实现方法;

  • 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

  • 偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

  • 自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现

public class SpinLock {

  private AtomicReference<Thread> sign =new AtomicReference<>();

  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current)){
    }
  }

  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

Synchronized底层实现:

1.在编译的时候,会生成对应的monitorenter/monitorexit指令同步块的进入和退出

2.JDK6之前,完全是依靠操作系统内部的互斥锁实现的。因此JVM 对此进行了大刀阔斧地改进,也就是采用常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁大大改进了其性能。

对象头:

因为java中任意对象都可以用作锁,储存对象和线程及锁之间的映射关系,所以将其存放在对象头中;

偏向锁JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁

轻量级锁:

  • 如果有另外的线程试图锁定某个用了偏向锁的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

锁的变换:

锁升级:

偏斜锁=》轻量级锁=》重量级锁(互斥锁)

锁降级:

 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级

ReentrantLock与Synchronized的区别:

  • ReentrantLock与Synchronized的区别:
    • ReentrantLock
      • 更加的灵活,但必须手动释放锁
      • 可选择是否为公平锁
      • 只适合代码块的锁
      • 依赖于JDK实现的(java类)
      • 可被中断,并抛出中断异常,释放锁
      • 可选择获取锁的超时时间,尝试获取锁
    • synchronized
      • 无需释放锁,自动处理
      • 可修饰方法,类,代码块
      • 非公平锁,如果阻塞则必须等待cpu调度
      • 依赖于JVM实现的(关键字)
  • ReentrantLock与Synchronized的共通点:
    • 都是互斥锁

Wait、Notify、NotifyAll和Sleep之间的关系:

首先了解线程调度器和时间分片:
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间,线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行(因为抢占式线程调度:当高优先级线程准备运行时,而低优先级线程正在运行,虚拟机会或早或晚暂停低优先级线程,让高优先级的线程运行)。线程调度并不受到Java虚拟机控制,而是通过抢占式线程调度策略进行线程控制。

时间分片举例: 假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行。通过时间片轮询机制可以保证同等优先级任务能够轮流的使用CPU资源。

1.Notify和NotifyAll区别:
notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到,这跟非公平锁的弊病是一个意思;

2.sleep和wait方法的区别:

  • wait会释放所持有锁;而sleep不会释放锁资源,直接让线程进入阻塞状态,因为其他线程拿不到锁资源,如果其他线程包含了Synchronized,那么该线程将不会得到执行
  • wait只能在同步方法和同步块中使用,而sleep任何地方都可以
  • wait无需捕捉异常,而sleep需要
  • sleep是Thread的方法,而wait是Object类的方法

线程沉睡和唤醒用wait和notify方法。(都在synchronize中使用,而synchronize一般都是在线程中的某个方法用的,因为为了同步线程,所以是一个Thread对象创建出多个threadA,B...引用 来调用这个线程方法)
注意线程的上述方法和Object自带的wait()和notifyAll()方法的区别,wait()和notifyAll()一定要在synchronize同步语句中添加,因为调用这两个方法一定要获得对象锁,synchronize块就是为了获取锁,有synchronize块的线程处于运行状态,没有它的线程则处于阻塞状态;
调用wait时,会将对象锁放弃;之后其他线程均分机会来获取这个锁

常见问题: 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值