既生 synchronized 何生 JUC 的 显式 locks ?

本文探讨了JUC的locks包如何通过提供可抢占的锁机制和多个条件变量来弥补synchronized关键字的不足。介绍了locks包中支持中断、超时及非阻塞获取锁的方法,并给出了有界缓冲区的示例。

新事物的出现要不是替代老事物,要么就是对老事物的补充

JUC 的 locks 就是对 synchronized 的补充

一、synchronized 有不足

  1. 从开始竞争锁 到拿到锁这段时间,调用线程一直是阻塞状态,啥都干不了
  2. 已经占有的资源也释放不了,别的线程也无法获得;这种不可抢占的情况,更容易带来死锁(死锁产生的原理就是:我占着你要用的资源不给你,你却不能抢)
  3. 只有一个条件变量(条件等待队列),用于线程间的协调、通信

二、改进意见

采用更多的措施以避免死锁 :

  1. 不能抢占 变为 可抢占,占用部分资源的线程进一步申请其他资源时

    • 如果能快速申请到,就申请
    • 如果不能快速申请到,可以主动释放它占有的资源
  2. 使用者自己可创建多个条件变量,用于线程间的协调、通信。

三、可抢占的方法论

3.1 能够响应中断

synchronized 的问题是:持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。

3.2 支持超时

如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

3.3 非阻塞地获取锁

如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

四、可抢占的实现 - JUC 的 locks

// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit)  throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
复制代码

五、多个 Condition(条件变量或条件等待队列)

等效于管程中的条件变量,条件变量用于线程间的同步。通过java.util.concurrent.locks.Lock#newCondition()来创建,每个 Condition 都具有一个 waitSet;这样锁对象就具备了多个waitSet; Condition 提供了以下方法:用于线程的协作(线程等待/激活)。

//线程加入此条件变量的等待队列;类似Object.wait();使用时也要放到while循环体内。
java.util.concurrent.locks.Condition#await()
java.util.concurrent.locks.Condition#awaitUninterruptibly()
java.util.concurrent.locks.Condition#awaitNanos()
java.util.concurrent.locks.Condition#await(long, java.util.concurrent.TimeUnit)
java.util.concurrent.locks.Condition#awaitUntil()
//激活此条件变量中的一个线程;类似Object.notify()
java.util.concurrent.locks.Condition#signal()
//激活此条件变量中的所有线程;类似Object.notifyAll()
java.util.concurrent.locks.Condition#signalAll()
复制代码

java doc 示例:一个有界缓冲,两个条件等待队列,分别被生产者线程和消费者线程来使用。

  • 生产者线程在 notFull 条件队列中等待;意思为生产者线程要阻塞等待,直到 有界缓冲不是满的,才能 put
  • 消费者线程在 notEmpty 条件队列中等待;意思为消费者线程要阻塞等待,直到有界缓冲不是空的,才能 take
class BoundedBuffer {
 final Lock lock = new ReentrantLock();
 final Condition notFull  = lock.newCondition();
 final Condition notEmpty = lock.newCondition();

 final Object[] items = new Object[100];
 int putptr, takeptr, count;

 public void put(Object x) throws InterruptedException {
   lock.lock();
   try {
     while (count == items.length)
       notFull.await();
     items[putptr] = x;
     if (++putptr == items.length) putptr = 0;
     ++count;
     notEmpty.signal();
   } finally {
     lock.unlock();
   }
 }

 public Object take() throws InterruptedException {
   lock.lock();
   try {
     while (count == 0)
       notEmpty.await();
     Object x = items[takeptr];
     if (++takeptr == items.length) takeptr = 0;
     --count;
     notFull.signal();
     return x;
   } finally {
     lock.unlock();
   }
 }
}
复制代码

六、JUC中locks的使用规范

6.1 保证锁的释放

 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }
复制代码
 Lock lock = ...;
 if (lock.tryLock()) {
   try {
     // manipulate protected state
   } finally {
     lock.unlock();
   }
 } else {
   // perform alternative actions
 }
复制代码

6.2 循环体中使用 await()

while (XXX)
    condition.await();
复制代码

6.3 同步代码块中使用 await()

必须先持有锁

 l.lock();
 try {
    ...
    while (XXX)
      condition.await();
    ...
 } finally {
   l.unlock();
 }
复制代码

七、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

<think>嗯,用户问的是Javasynchronized关键字如保证原子性、有序性和可见性,需要了解实现原理。首先,我得回忆一下synchronized的基本机制。根据引用里的内容,synchronized通过monitorenter和monitorexit这两个字节码指令来实现锁的获取和释放。那原子性方面,引用4提到synchronized块之间的操作具备原子性,因为JVM的lock和unlock操作确保了代码块整体作为一个原子执行。 接下来是有序性。用户可能知道指令重排序会影响多线程的执行结果,而synchronized防止这个呢?我记得锁的互斥性会让线程串行执行,这样在同步块内的代码即使被重排序,也不会影响到其他线程看到的顺序,所以synchronized通过互斥来保证有序性,引用1也提到这点,把并发执行变成序列化。 然后是可见性。JMM中,当线程释放锁时,会把变量刷回主内存,获取锁时则会从主内存重新读取,确保共享变量的可见性。引用4提到基础数据类型的原子性由JVM保证,而synchronized通过lock/unlock提供更大范围的原子性,这也涉及到可见性。 可能用户还关心底层实现,比如汇编层的lock指令,引用1里提到lock cmpxchg,这个指令会锁定总线或缓存行,确保操作的原子性,同时内存屏障防止指令重排。这部分需要结合JVM和硬件层面来解释。 另外,用户提供的引用中有同步方法和同步代码块的例子,说明不同使用方锁的对象不同,比如类对象或实例对象,这可能影响到锁的范围,但核心原理应该是一样的。需要明确说明不同用法但原理相同。 还要注意,用户的问题可能涉及到对这三个性质的具体实现机制的区别,比如原子性是通过互斥,可见性是通过内存同步,有序性是通过禁止重排序。需要分点详细说明,确保回答结构清晰。 最后,成的相关问题要围绕synchronized的实现细节、锁升级过程、与volatile的区别以及实际应用中的注意事项,这样能帮助用户深入理解。</think>### Javasynchronized关键字如保证原子性、有序性和可见性? #### 1. **原子性** **定义**:原子性指一个操作是不可中断的,要么全部执行成功,要么完全不执行。 **实现原理**: - synchronized通过互斥锁(`monitorenter`和`monitorexit`字节码指令)保证同步代码块内的操作作为一个整体执行[^4]。 - 线程进入同步代码块前必须获取锁,其他线程无法同时执行该代码块,从而避免了竞态条件。例如: ```java public synchronized void increment() { count++; // 即使包含多个底层操作(读-改-写),也能保证原子性 } ``` #### 2. **可见性** **定义**:一个线程修改共享变量后,其他线程能立即看到修改后的值。 **实现原理**: - 在释放锁时,JVM强制将工作内存中的变量刷新到主内存; - 在获取锁时,JVM会清空本地内存,从主内存重新加载变量[^4]。 - 通过内存屏障(Memory Barrier)禁止指令重排序,确保共享变量的修改对所有线程可见[^1]。 #### 3. **有序性** **定义**:程序执行的顺序符合代码的先后顺序。 **实现原理**: - synchronized通过互斥锁将并发执行变为串行执行,相当于隐禁止了同步代码块内的指令重排序[^1]。 - 例如以下代码中,`doSth1()`和`doSth2()`内部的代码不会被重排序到同步块外: ```java public void doSth1() { synchronized (this) { // 代码块内的操作不会被重排序到锁外 } } ``` --- ### 底层实现细节 1. **锁的升级过程**: synchronizedJVM中经历了从偏向锁→轻量级锁→重量级锁的升级过程,以适应不同竞争场景[^1][^3]。 2. **汇编层实现**: 最终通过`lock cmpxchg`(CAS指令)实现原子操作,该指令会锁定总线或缓存行,确保原子性和可见性[^1][^4]。 --- ### 示例代码分析 ```java public class SynchronizedTest { // 同步代码块(锁类对象) public void doSth1() { synchronized (SynchronizedTest.class) { // monitorenter System.out.println("HelloWorld"); } // monitorexit } // 同步方法(锁当前实例) public synchronized void doSth2() { // 方法标记ACC_SYNCHRONIZED System.out.println("HelloWorld"); } } ``` - 同步代码块:通过`monitorenter`和`monitorexit`控制锁的获取和释放[^2][^4]。 - 同步方法:通过方法访问标志`ACC_SYNCHRONIZED`隐实现锁机制[^3]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值