Synchronized 关键字详解

目录

1、Synchronized 关键字简单介绍

(1)Synchronized 的使用示例

(2)什么是可重入锁?

2、Synchronized 的实现原理

Java中的监视器锁(Monitor Lock)机制

3、Synchronized 的锁优化过程详解

(1)Java 锁的膨胀过程详解

(2)偏向锁的实现原理解析

(3)操作系统的互斥量(mutex)的实现原理(重量级锁)


1、Synchronized 关键字简单介绍

        synchronized 是 Java 中的一种同步机制,用于实现线程之间的互斥和同步。当一个线程需要访问一个对象的同步代码块时,如果这个同步代码块已经被另一个线程所占用,那么当前线程就会被阻塞,直到另一个线程执行完毕并释放锁。只有获取到锁的线程才能执行同步代码块。

        synchronized 可以用于两种方式:

  1. 修饰方法:使用 synchronized 关键字修饰一个方法时,该方法被称为同步方法。当一个线程访问一个对象的同步方法时,其他线程将被阻塞,直到该线程执行完毕并释放锁。同步方法的锁对象是当前实例对象。
  2. 修饰代码块:使用 synchronized 关键字修饰一段代码时,称该代码块为同步代码块。同步代码块必须指定某个对象作为锁,当一个线程访问同步代码块时,必须先获得锁,其他线程将被阻塞,直到该线程执行完毕并释放锁。

        在使用 synchronized 时,需要注意以下几点:

  1. synchronized 是一种重量级的操作,会影响性能。在使用 synchronized 时,应尽可能减小同步块的范围,避免锁的竞争。
  2. synchronized 锁的范围应该尽量小,只保护必要的代码块,避免对整个方法或对象进行锁定。
  3. synchronized 锁定的对象不应该被修改,否则会导致死锁的发生。
  4. 在使用 synchronized 时,需要考虑线程间的协调和通信,以避免死锁和活锁的发生。

Synchronized 锁的类型:

        synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时都会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。

        synchronized 是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原因。

        同时,synchronized是一个典型的可重入锁,可重入锁最大的作用是避免死锁。

(1)Synchronized 的使用示例

        Synchronized 可以用来保证代码块或方法的同步执行,下面是一些 Synchronized 的使用示例:

        (1)保证方法的同步执行

public synchronized void synchronizedMethod() {
    // 这里是需要同步的代码
}

        (2)保证代码块的同步执行

public void someMethod() {
    synchronized(this) {
        // 这里是需要同步的代码
    }
}

        使用 synchronized 可以保证同步代码块在同一时刻只能被一个线程执行,从而保证了线程安全。

(2)什么是可重入锁?

        可重入锁是一种特殊的互斥锁,也被称为递归锁。在一个线程已经持有某个锁的情况下,同一个线程再次请求该锁时,请求会成功,不会造成死锁。可重入锁的意义在于,避免线程因为无法获得自己已经持有的锁而进入死锁状态

        在Java 中,synchronized 和 ReentrantLock 都是可重入锁。其中 synchronized 是 JVM 内置的锁,而 ReentrantLock 是 JDK 提供的一个可重入的互斥锁。在使用可重入锁时,同一个线程多次获取同一个锁时,不会造成死锁或者无限等待的情况,因为锁已经被当前线程占用,因此该线程可以直接进入临界区。// 一般Java中的锁都是可重入锁,不可重入锁会带来很多死锁问题

2、Synchronized 的实现原理

        Synchronized 的实现原理基于Java中的监视器锁(Monitor Lock),每个Java对象都与一个监视器相关联,监视器是一个同步队列,用于实现线程同步。

        当一个线程想要访问一个被Synchronized保护的代码块时,它必须首先获得这个对象的监视器。如果这个监视器已经被其他线程获得了,那么这个线程就会被阻塞,直到该监视器被释放。如果该监视器未被其他线程占用,则获得该监视器并进入同步块。当线程执行完同步块中的代码时,它会释放监视器,这样其他线程就可以继续访问共享资源了。

        Java中的每个对象都有一个锁(Lock)和一个等待集(wait set)。当线程试图进入同步代码块时,它必须先获得对象的锁。如果锁已经被其他线程持有,则该线程将被阻塞,直到锁被释放。同时,该线程会将自己加入对象的等待集中。当锁被释放时,等待集中的线程就会被唤醒,试图重新获取锁。这种等待集的机制是基于操作系统的信号量(semaphore)实现的,Java中的监视器机制使用了类似的机制。

        所以,Synchronized是通过Java对象的监视器和等待集实现了线程的同步和互斥,保证了线程安全的同时也降低了锁的开销。

// 每个Object对象中都内置了一个monitor对象,monitor对象存在于每个Java对象的对象头中(存储的是指针)。monitor相当于一个许可证,线程拿到许可证即可以进行操作,没有拿到则需要阻塞等待。

Java中的监视器锁(Monitor Lock)机制

        Java 中的监视器锁(Monitor Lock)是一种基于内置锁(Intrinsic Lock)实现的同步机制,它通过线程获取对象的内置锁来实现线程的同步访问。

        每个 Java 对象都与一个监视器相关联,监视器包含一个计数器和一个线程等待队列,计数器记录持有该对象锁的线程数量。当一个线程请求获得对象的锁时,如果对象的计数器为0,则该线程可以立即获取该对象的锁并将计数器加1,如果对象的计数器不为0,则该线程将进入该对象的线程等待队列并等待被唤醒。当一个线程释放对象的锁时,它将减少计数器并从等待队列中唤醒一个等待线程,让其获取该对象的锁。// 这个机制很重要,基于一个计数器,一个等待队列

        监视器锁(Monitor Lock)的优点是简单易用,只需要使用 synchronized 关键字即可实现对共享资源的访问控制。缺点是只能实现互斥访问,不能进行读写分离,也无法实现公平性控制,容易导致线程饥饿问题

3、Synchronized 的锁优化过程详解

        Synchronized 在实现过程中,有一种锁优化机制——锁的粗化(Lock Coarsening)、锁的消除(Lock Elimination)、锁的膨胀(Lock Inflation)//JDK 6 以后的版本中对 Synchronized 进行了锁优化

        锁的粗化指的是,当连续的加锁和解锁操作在同一个代码块中出现时,虚拟机会将其合并成一个范围更大的锁,从而避免频繁加锁和解锁所带来的性能消耗。

        锁的消除指的是,当虚拟机检测到一些对象不存在竞争,也就是说,该对象只被一个线程访问,那么它就会自动消除对这些对象的加锁操作,从而减少不必要的锁操作。// 偏向锁

        锁的膨胀指的是,当虚拟机检测到一个线程在短时间内多次加锁和解锁同一个对象时,就会将这个锁升级成重量级锁,以避免频繁的用户态和内核态切换所带来的性能开销。

        通过锁的粗化、锁的消除和锁的膨胀,Synchronized 锁的效率得到了很大的提升,可以更好地支持多线程应用程序的并发处理。

(1)Java 锁的膨胀过程详解

        Java中的锁通常有四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这些状态是在不同的竞争条件下产生的,并且代表着不同的锁竞争强度

        当一个线程访问一个没有被锁住的synchronized块时,它会在对象头中记录一个指向它的线程ID。这个线程在之后的访问中可以直接获取锁,这被称为偏向锁

        偏向锁可以大大减少竞争,因为它不需要任何同步来获取锁。这对于对象的成员方法的同步非常有用,因为它们通常由单个线程访问,例如在单线程情况下运行的Swing事件分派线程。

        当两个或更多个线程竞争访问同一个synchronized块时,它们将尝试通过轻量级锁来获取锁。当线程获取锁时,它将锁拷贝到它自己的锁记录中,并使用CAS操作将锁存储到对象头中。如果锁被占用,则线程尝试自旋获取锁。如果锁占用的时间很短,自旋获取锁的时间会很短,因此这种方式比使用重量级锁更高效。

        如果线程无法获得锁,则轻量级锁会膨胀为重量级锁重量级锁涉及到内核级别的线程同步,因此它的性能相对较低。当线程释放锁时,锁状态将恢复为无锁状态。

        锁的状态转换通常是自动的,不需要手动干预。然而,如果需要进行手动干预,可以使用JVM参数来控制锁的状态转换。例如,可以通过-XX:BiasedLockingStartupDelay参数来控制偏向锁的延迟启动时间,从而改变默认设置。

(2)偏向锁的实现原理解析

        偏向锁是 Java 中一种针对轻量级同步场景的锁优化技术。它的基本思想是,如果一个线程获得了一个对象的锁,那么在之后的一段时间内,这个线程再次请求这个锁时,可以直接获得锁,无需竞争。

        当一个对象被创建时,默认情况下它不是偏向锁,也就是说没有被任何线程所占有。当一个线程第一次进入同步块时,会尝试获取锁并把对象头中的 Mark Word 设置为偏向锁。同时,这个线程会把这个对象头中的 Thread ID 信息记录下来,表示这个线程已经持有了这个对象的锁。// 偏向锁的实现原理,无同步竞争,减少锁的开支

        之后,当这个线程再次进入同步块时,无需进行加锁操作,而只需检查对象头中的 Thread ID 信息是否为当前线程即可。如果是,表示这个线程已经持有了这个对象的锁,直接执行同步块即可。这样的好处是避免了竞争,减少了线程上下文切换的次数,从而提升了程序的执行效率

        需要注意的是,当其他线程尝试获得偏向锁的对象的锁时,偏向锁会自动撤销,变为轻量级锁或重量级锁,其他线程再次进入同步块时,需要进行加锁操作,也就是说这个对象的锁被多个线程竞争。这种情况下,偏向锁的优化就失效了。

对象头中的 Mark Word:

        在 Java 虚拟机中,每个对象都有一个对象头,用于存储对象的元信息,如对象的哈希码、锁状态、GC 信息等。其中,Mark Word 是对象头中的一部分,用于存储对象的锁状态和一些其他的标记信息。

        Mark Word 的内容根据对象的状态不同而有所不同,以下是一些常见情况下 Mark Word 的内容:

  1. 无锁状态:对象尚未被任何线程加锁。此时 Mark Word 中的状态位为 01,同时存储了对象的哈希码和分代年龄等信息。
  2. 偏向锁状态:对象曾经被一个线程加过偏向锁,并且该线程仍然持有锁。此时 Mark Word 中的状态位为 01,存储了锁的持有者线程 ID、偏向锁标识位、对象分代年龄和哈希码等信息。
  3. 轻量级锁状态:对象正在被多个线程竞争锁。此时 Mark Word 中的状态位为 00,存储了指向锁记录的指针。
  4. 重量级锁状态:对象被一个线程持有,并且有多个线程正在竞争锁。此时 Mark Word 中的状态位为 10,存储了指向重量级锁的指针。

        需要注意的是,不同的 JVM 实现可能有不同的 Mark Word 结构和标志位含义,上述仅是一些常见情况下的示例。

(3)操作系统的互斥量(mutex)的实现原理(重量级锁)

        操作系统中的互斥量(mutex)是一种用于保护临界区的同步机制,用于确保同时只有一个线程访问临界资源,从而避免并发问题。互斥量的实现原理可以分为用户态和内核态两种方式。

        在用户态下,互斥量通常是通过原子操作和忙等待实现的。原子操作是指不可中断的操作,比如 Test-and-Set 操作,可以保证多个线程并发访问时不会出现竞争问题。忙等待是指线程在访问共享资源时,一直循环检查是否可以进入临界区,这种方式效率比较高,但是会占用 CPU 资源,造成浪费。// 用户状态下,线程不用进行等待,效率比较高

        在内核态下,互斥量的实现是基于操作系统提供的原语,通常是通过系统调用实现的。当线程需要获取互斥量时,会调用系统调用将自己挂起,等待互斥量可用。当互斥量可用时,操作系统会将线程唤醒,使其可以进入临界区。这种方式避免了忙等待,但是由于涉及到内核态和用户态之间的切换,因此效率相对较低。// 内核模式下,线程被挂起

        在实际使用中,需要根据具体情况选择合适的实现方式。如果临界区较小,线程数较少,可以使用用户态的互斥量实现;如果临界区较大,线程数较多,应该使用内核态的互斥量实现。在Java中,重量级锁的实现依赖于操作系统提供的同步原语。通常情况下,Java的重量级锁会对应到操作系统的互斥量(mutex)。因此,重量级锁的实现和操作系统的互斥量实现原理相似

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值