java各类锁的理解

在Java中,锁是用于多线程同步的关键机制,可以通过不同的锁来实现对共享资源的互斥访问。以下是一些常见的Java锁的深入理解:

1. Synchronized锁:

Synchronized是Java中的关键字,用于实现线程之间的互斥访问,确保在同一时刻只有一个线程可以执行被Synchronized修饰的代码块或方法。Java对象在内存中的布局包括对象头和实例数据两部分。对象头中的Mark Word用于存储对象的元数据信息,其中的一部分被用来存储锁相关的信息。以下是Synchronized锁的深入理解:

1.0.monitor

在Java字节码层面,synchronized关键字的实现会涉及到monitorentermonitorexit两个字节码指令。以下是一个简单的Java代码示例和对应的字节码:

Java代码:

public class SynchronizedExample {
    private static final Object lock = new Object();

    public void synchronizedMethod() {
        synchronized (lock) {
            // 临界区域
            // ...
        }
    }
}

对应的字节码:

public class SynchronizedExample {
  private static final java.lang.Object lock;

  public SynchronizedExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void synchronizedMethod();
    Code:
       0: getstatic     #2                  // Field lock:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: aload_1
       7: monitorexit
       8: goto          16
      11: astore_2
      12: aload_1
      13: monitorexit
      14: aload_2
      15: athrow
      16: return
    Exception table:
       from    to  target type
           6     8    11   any
          11    14    11   any
}

解释:

  • synchronizedMethod方法中,通过monitorenter指令获取锁,monitorexit指令释放锁。
  • getstatic #2:将lock字段(即指向lock对象的引用)推送到栈顶。
  • dup:复制栈顶数值并将复制值压入栈顶。这是为了在monitorexit指令之后还能使用lock
  • monitorenter:获取锁。如果当前线程获取了锁,则计数器加一;如果锁被其他线程持有,则当前线程阻塞等待。
  • monitorexit:释放锁。当锁的计数器减为零时,锁被完全释放。

这就是在字节码层面对synchronized关键字的基本实现。字节码中的monitorentermonitorexit指令是实现Synchronized锁机制的核心。

1.1. 内置锁(Intrinsic Lock):

  • Synchronized原理: Synchronized使用的是对象监视器(Object Monitor)作为内置锁,每个Java对象都与一个Object Monitor相关联。一个线程在进入Synchronized块之前会尝试获取对象的锁,如果锁被其他线程持有,则当前线程会被阻塞。

  • 锁的粒度: Synchronized支持对对象的方法或代码块进行锁定,可以在方法级别或代码块级别使用。

1.2. 锁的可重入性(Reentrant):

  • 概念: Synchronized是可重入锁,同一线程可以多次获得同一个锁,而不会被阻塞。这允许线程在持有锁的情况下调用自己类中的其他Synchronized方法。

  • 实现: 内置锁通过一个计数器来实现可重入性,每次成功获取锁,计数器加1,释放锁时计数器减1,只有当计数器为0时才真正释放锁。

1.3. 锁的互斥性:

  • 原子性: Synchronized确保了其修饰的代码块或方法的原子性执行,一个线程持有锁时,其他线程无法同时进入被锁定的代码块或方法。

1.4. 锁的等待与唤醒:

  • 等待: 当一个线程尝试获取一个被其他线程持有的锁时,它将被阻塞,进入等待状态。

  • 唤醒: 当一个线程释放了锁,等待在该锁上的线程将被唤醒,有机会争夺锁。

1.5. 对象监视器和锁的关系:

  • 每个对象一个锁: 对象监视器是与对象相关联的,每个对象都有一个用于同步的锁。

  • Class锁: 对于静态Synchronized方法,锁是与类相关联的,称为Class锁。

1.6. 性能考虑:

  • 重量级锁: Synchronized是重量级锁,存在竞争时可能涉及用户态到内核态的切换,因此在高并发情况下性能可能受到影响。

  • 轻量级锁和偏向锁: 为了提高性能,Java引入了轻量级锁和偏向锁,用于减小锁的开销。在适当的场景下,虚拟机会自动优化锁的实现。

1.7. Synchronized优化(锁升级):

        Mark Word中的一些位被用于表示锁的状态。主要有以下几个标志位:

  • 无锁状态: 表示当前对象没有被线程持有。如果对象的锁状态为无锁状态,则线程尝试使用CAS(Compare And Swap)将锁状态设置为偏向锁,并将持有锁的线程ID记录在Mark Word中。
  • 偏向锁(Biased Locking): 当一个线程获得了锁,并且没有竞争时,JVM会在对象头的标记位上设置偏向锁。这样,在之后该线程再次获取锁时,无需进行CAS等操作,提高性能。

  • 轻量级锁(Lightweight Locking): 在没有竞争的情况下,JVM会将对象头中的一部分标记为“偏向锁”(biased lock),表示该对象是被一个线程所拥有的。持有锁的线程ID与当前线程ID不同,则尝试使用CAS将锁状态设置为轻量级锁,并在对象的Mark Word中创建锁记录,避免进入重量级锁的状态。

  • 重量级锁(Heavyweight Locking): 当有多个线程竞争锁时,线程尝试使用CAS将锁状态设置为重量级锁,并在操作系统层面使用互斥量来实现锁。

1.8. 锁的并发性:

  • 细粒度锁: 使用Synchronized时,尽量使用细粒度锁,避免过大的锁范围,提高并发性。

  • 避免长时间持有锁: 尽量减小在Synchronized块中的执行时间,避免长时间持有锁。

1.9. 可见性与内存语义:

  • Synchronized保证可见性: 进入Synchronized块会清空工作内存,从主内存中读取共享变量,退出Synchronized块时会将修改刷新回主内存,保证了可见性。

  • 内存语义: Synchronized不仅保证互斥性,还具有一定的内存语义,保证了操作的有序性。

2. ReentrantLock锁:

ReentrantLock是Java中的可重入锁,它提供了与synchronized相似的同步机制,但相对于synchronizedReentrantLock提供了更多的灵活性和控制。以下是ReentrantLock锁的深入理解:

2.0.怎么实现的:

  • ReentrantLock是Java中可重入锁的一种实现,它使用了底层的AbstractQueuedSynchronizer (AQS)来实现锁的机制。AQS是一个提供了基本的同步原语的抽象框架,它可以用来实现各种形式的同步器。
  • AQS维护了一个状态和一个等待队列。状态表示被同步的资源的状态,可以是任意的int值。等待队列用来存放等待获取锁的线程

2.1. 可重入性:

  • ReentrantLock是可重入锁,同一线程可以多次获得同一个锁而不会被阻塞。

2.2. 锁的状态:

  • ReentrantLock的锁状态包含了持有锁的线程和重入次数。

2.3. 锁的基本用法:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void lockedMethod() {
        lock.lock(); // 获取锁
        try {
            // 临界区域
            // ...
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}
  • ReentrantLocklock()方法首先会调用Sync内部类的lock方法。在Sync内部类中,lock方法通过AQSacquire方法来尝试获取锁。

  • ReentrantLockunlock()方法则会调用Sync内部类的unlock方法,通过AQSrelease方法来释放锁。

2.4. 公平锁与非公平锁:

  • ReentrantLock有两个内部类:NonfairSyncFairSync,分别用于实现非公平锁和公平锁。默认情况下,ReentrantLock创建的是非公平锁。
  • 公平锁是按照线程请求锁的顺序来分配的,而非公平锁是不考虑线程的等待时间,有可能导致某些线程一直获取不到锁。

2.5. Condition条件:

  • ReentrantLock提供了Condition接口的实现,通过newCondition()方法创建。可以使用Condition实现更灵活的线程通信,例如等待某个条件满足后再继续执行。

2.6. 锁的中断响应:

  • ReentrantLock允许在等待锁的过程中响应中断,这与synchronized不同,可以通过lockInterruptibly()方法实现。

2.7. 锁的超时获取:

  • ReentrantLock支持在尝试获取锁时设置超时时间,通过tryLock(long time, TimeUnit unit)实现。

2.8. 锁的升级与降级:

  • ReentrantLock允许锁的升级和降级。在持有锁的情况下,可以调用lock.newCondition()创建一个新的Condition,然后释放主锁,等待某个条件满足时再获取锁。

2.9. 锁的可见性和内存语义:

  • synchronized一样,ReentrantLock同样保证了锁的可见性和一定的内存语义。

2.10. 锁的性能优化:

  • ReentrantLock相对于synchronized在高并发的情况下性能更好。在JDK6之后,JVM对synchronized进行了优化,但在某些场景下,ReentrantLock的性能仍有优势。

3. ReadWriteLock读写锁:

ReadWriteLock是Java中用于支持读写分离的锁,它允许多个线程同时读取共享资源,但在写操作时必须互斥。ReadWriteLock的核心思想是提高并发性,对于读取操作,多个线程可以同时进行,而写入操作则需要独占锁。

以下是ReadWriteLock的深入理解:

3.1. 读写锁接口:

  • ReadWriteLock接口有两个核心方法:readLock()用于获取读锁,writeLock()用于获取写锁。

3.2. ReentrantReadWriteLock实现:

  • Java提供了ReentrantReadWriteLock类来实现ReadWriteLock接口,它是ReentrantLock的扩展。

3.3. 读锁和写锁的关系:

  • 多个线程可以同时持有读锁,但在写锁被持有时,所有的读锁和其他写锁都会被阻塞。

3.4. 读锁共享性:

  • 读锁是共享的,多个线程可以同时获取读锁,以实现对共享资源的并发读取。

3.5. 写锁的独占性:

  • 写锁是独占的,一旦有线程持有写锁,其他线程无法同时获取写锁或读锁。

3.6. Reentrant特性:

  • ReentrantReadWriteLock是可重入的,同一个线程可以多次获取同一种锁。

3.7. 公平性和非公平性:

  • ReentrantLock类似,ReentrantReadWriteLock可以在构造函数中选择是公平锁还是非公平锁。

3.8. Condition的支持:

  • ReentrantReadWriteLock提供了Condition接口的实现,通过readLock.newCondition()writeLock.newCondition()来创建读锁和写锁的条件对象。

3.9. 降级和升级:

  • 读写锁支持锁的降级(从写锁降级为读锁)和升级(从读锁升级为写锁)操作,这使得在某些场景下可以更灵活地管理锁。

3.10. 性能考虑:

  • 读写锁适用于读操作频繁、写操作相对较少的场景,能够提高系统的并发性。

4. StampedLock锁:

StampedLock是Java 8引入的一种读写锁的实现,相比于传统的ReentrantReadWriteLockStampedLock提供了更多的功能和更高的性能。主要特点包括乐观读、悲观读和写锁,并支持锁的升级和降级。以下是对StampedLock的深入理解:

4.1. 三种锁模式:

  • 悲观读锁(readLock): 类似于ReentrantReadWriteLock的读锁,当线程获取悲观读锁时,其他线程无法获取写锁。

  • 写锁(writeLock): 与悲观读锁互斥,即悲观读锁和写锁不能同时存在。

  • 乐观读锁(tryOptimisticRead): 是一种乐观的、不阻塞的读锁。在使用乐观读锁时,其他线程可能同时进行写操作,但使用乐观读锁的线程随时可能发现数据不一致而重新获取悲观读锁。

4.2. 戳记(stamp):

  • 每次锁的状态变更都会导致一个戳记的生成。戳记是一个long类型的数值,可以用来判断锁的状态是否发生过变化。

4.3. 乐观读锁的使用:

  • 通过tryOptimisticRead()获取乐观读锁,并返回一个戳记。此时,其他线程可能进行写操作,但戳记可以用来验证在当前线程获取戳记后,锁的状态是否发生了变化。

  • 使用乐观读锁后,可以使用validate(stamp)方法来验证锁的状态是否发生了变化。如果验证通过,可以继续使用获取悲观读锁的方式来读取数据。

4.4. 锁的升级和降级:

  • StampedLock允许在不释放锁的情况下,从乐观读锁升级为悲观读锁,或者从悲观读锁降级为乐观读锁。

4.5. 非阻塞算法:

  • StampedLock采用非阻塞算法,大多数情况下不会阻塞线程。

4.6. 适用场景:

  • StampedLock适用于读操作远远多于写操作的场景,因为悲观读锁和写锁之间是互斥的。

4.7. 性能优化:

  • 在某些情况下,StampedLock的性能比ReentrantReadWriteLock更好,尤其是在读多写少的情况下。

5. LockSupport锁:

LockSupport是Java并发包中的工具类,提供了一种基于线程的阻塞和唤醒的机制。与Object类的wait()notify()方法相比,LockSupport更灵活,可以在线程之间传递许可,而不需要依赖对象的监视器。以下是对LockSupport的深入理解:

5.1. 阻塞和唤醒:

  • LockSupport提供了park()unpark(Thread thread)方法,用于阻塞当前线程和唤醒指定线程。

5.2. 许可的获取和释放:

  • 每个线程都有一个许可(permit)与之关联,许可的初始状态是可用的。park()方法会消耗许可,而unpark(Thread thread)方法会释放许可。

5.3. park和unpark的配对使用:

  • park()unpark(Thread thread)方法是成对使用的,即park()调用的线程被阻塞后,只有unpark(Thread thread)方法才能唤醒它。

5.4. 线程中断:

  • 当线程被park()阻塞时,如果线程被中断,park()方法会返回,并且不会消耗许可。可以通过Thread.interrupted()来判断线程是否被中断。

5.5. 阻塞和唤醒顺序:

  • park()unpark(Thread thread)的执行顺序可以是任意的,不一定是先阻塞后唤醒或先唤醒后阻塞。

5.6. 公平性:

  • LockSupport不保证公平性,即不保证等待时间最长的线程优先获得许可。

5.7. park和interrupt:

  • 当线程被中断后,park()方法不会抛出InterruptedException,但会返回,此时可以通过Thread.interrupted()来检查中断状态。

5.8. 实际应用:

  • LockSupport通常用于实现其他同步工具类,如ReentrantLockAQS中就用到了LockSupport来阻塞和唤醒线程。

5.9. 性能优势:

  • 相比于Objectwait()notify()方法,LockSupport的实现更加轻量,性能更好。

5.10. 底层实现:

  • LockSupport的实现依赖于底层的Unsafe类,提供了一种基于CAS操作的无锁的线程阻塞和唤醒的机制。

这些锁机制在不同场景下有各自的优劣,选择合适的锁要根据具体的应用需求和性能要求。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值