synchronized详细

synchronized是什么

        synchronized是Java中实现线程同步的关键字,用于保护共享资源的访问,确保在多线程环境中同一时间只有一个线程能够访问特定的代码段或方法。它提供了互斥性,可见性和有序性三个基本特征,确保了线程间操作的原子性和数据的一致性。

  • 互斥性:当一个线程进入了synchronized代码块或方法时,其他视图进入该同步区域的线程必须等待,直至拥有锁的线程执行完毕并释放锁。这确保了在同一时间只能有一个线程访问共享资源,避免了竞态条件和数据不一致的问题。

  • 可见性:synchronized关键字还确保了线程间的数据可见性。一旦一个线程在synchronized块中修改了共享变量的值,其他随后进入同步区域的线程可以看到这个更改。这是因为解锁过程会将工作内存中的最新值刷新回主内存,而加锁过程则会强制从主内存中重新加载变量的值。

  • 有序性: synchronized 提供的第三个特性是有序性,它可以确保在多线程环境下,对于同一个锁的解锁操作总是先行于随后对同一个锁的加锁操作。这就建立了线程之间的内存操作顺序关系,有效地解决了由于编译器和处理器优化可能带来的指令重排序问题。

synchronized可以实现的锁

  • 可重入锁(Reentrant Lock): synchronized 实现的锁是可重入的,允许同一个线程多次获取同一个锁,而不会造成死锁。这意味着线程在持有锁的情况下可以再次获取相同的锁,而不会被阻塞。

  • 排它锁/互斥锁/独占锁: synchronized 实现的锁是排它的,也就是说,在任意时刻只有一个线程能够获取到锁,其他线程必须等待该线程释放锁才能继续执行。这确保了同一时刻只有一个线程可以访问被锁定的代码块或方法,从而保证了数据的一致性和完整性。

  • 悲观锁: synchronized 实现的锁属于悲观锁,因为它默认情况下假设会发生竞争,并且会导致其他线程阻塞,直到持有锁的线程释放锁。悲观锁的特点是对并发访问持保守态度,认为会有其他线程来竞争共享资源,因此在访问共享资源之前会先获取锁。

  • 非公平锁: 在早期的 Java 版本中,默认实现的 synchronized 是非公平锁,也就是说,线程获取锁的顺序并不一定按照它们请求锁的顺序来进行,而是允许“插队”,即已经在等待队列中的线程可能被后来请求锁的线程抢占。

synchronized使用

        synchronized关键字可以修饰方法、代码块或静态方法,用于确保同一时间只有一个线程可以访问被synchronized修饰的代码片段。

  • 修饰实例方法: 当synchronized修饰实例方法时,锁住的是当前实例对象(this)。这意味着在同一时刻,只能有一个线程访问此方法,其他线程需要等待当前线程执行完毕才能执行该方法。修饰实例方法的方式可以确保对实例变量的访问是线程安全的。

public synchronized void methodName() {
    // synchronized 代码块
}
  • 修饰静态方法: 当synchronized修饰静态方法时,锁住的是类的Class对象。因此,无论多少个该类的实例存在,同一时刻也只有一个线程能够访问此静态同步方法。这种方式可以确保对静态变量的访问是线程安全的。

public static synchronized void staticMethodName() {
    // synchronized 代码块
}
  • 修饰代码块: 使用 synchronized 关键字修饰一个代码块,将需要同步的代码包裹在 synchronized 关键字所修饰的代码块中。通过指定对象作为锁,可以更精确地控制同步范围,只有持有该对象锁的线程才能执行被synchronized修饰的代码块。

synchronized (obj) {
    // 需要同步的代码块
}

        synchronized关键字确保了对共享资源的访问是线程安全的。但过多地使用synchronized可能会导致性能问题,因此在设计并发程序时需要权衡考虑。

synchronized底层原理

        synchronized 的底层原理涉及到 Java 对象头(Object Header)和 Monitor(监视器)两个关键概念。

Java 对象头:

        在 Java 虚拟机中,每个对象都有一个对象头,用于存储对象的元数据信息,包括对象的哈希码、GC 相关信息、锁状态等。对象头通常包含一个标记字段(Mark Word),用于标识对象的锁状态。

Monitor(监视器):

        Monitor 是一种同步机制,负责管理对象的锁。每个对象都与一个 Monitor 相关联。当一个线程尝试进入一个被synchronized修饰的代码块或方法时,它会尝试获取对象的 Monitor。如果 Monitor 处于无锁状态,则当前线程会尝试将其锁定;如果 Monitor 已经被其他线程锁定,则当前线程会进入阻塞状态,直到持有锁的线程释放锁。

        在 JDK 1.6 之前,synchronized 关键字的实现确实被认为是重量级锁。其原理基于操作系统提供的互斥量(Mutexes)来实现线程间的同步,这涉及了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和 CPU 资源的消耗。

具体而言,在 JDK 1.6 之前,synchronized 的实现过程大致如下:

  • 当线程尝试进入 synchronized 代码块时,它会尝试获取对象的 Monitor(监视器)。

  • 如果 Monitor 的锁状态是无锁状态(Unlocked),则当前线程将尝试获取锁。如果获取成功,则进入临界区执行代码。

  • 如果锁已经被其他线程持有,则当前线程将进入阻塞状态,等待锁被释放。

  • 当持有锁的线程退出临界区并释放锁时,等待的线程将被唤醒,重新尝试获取锁。

这种实现方式存在以下问题:

  • 需要从用户态到内核态的切换,以及线程上下文的切换,导致性能开销较大。

  • 对于竞争不激烈的情况,阻塞等待锁的线程可能会浪费大量的 CPU 时间。

  • 线程在竞争锁的过程中可能会发生多次上下文切换,影响性能。

  • 因此,在高并发、低锁竞争的情况下,早期版本的 synchronized 实现可能成为性能瓶颈。

        为了解决这些问题,在 JDK 1.6 中引入了偏向锁(Biased Locking)和轻量级锁(Lightweight Locking)等优化机制,提高了 synchronized 的性能和并发能力。

底层实现机制如下:

  • 偏向锁(Bias Locking):当一个线程第一次访问一个同步块时,它会尝试获取对象的偏向锁。如果对象没有被其他线程锁定,那么当前线程会尝试将对象的偏向锁设置为自己。之后,当该线程再次访问同步块时,不需要再次获取锁,直接进入同步块执行。这样可以提高同步操作的性能,减少不必要的竞争。

  • 轻量级锁(Lightweight Locking):如果对象已经被其他线程获取了偏向锁,但当前线程又想要获取锁,就会升级为轻量级锁。轻量级锁的获取过程包括尝试将对象头的 Mark Word 设置为指向当前线程的锁记录(Lock Record)和CAS(Compare and Swap)操作。如果锁竞争激烈,会升级为重量级锁。

  • 重量级锁(Heavyweight Locking):如果对象的锁被多个线程竞争,那么会升级为重量级锁。重量级锁使用操作系统的互斥量(Mutex)来实现同步,涉及到用户态和内核态的切换,性能相对较低,但能够确保线程的互斥访问。

作用于同步代码块

        当synchronized关键字作用于同步代码块时,Java 虚拟机使用 monitorenter 和 monitorexit 指令来实现同步。具体的工作流程如下:

  • 当线程进入同步代码块时,首先执行 monitorenter 指令,该指令尝试获取对象的 Monitor(即锁)。

  • 如果对象的 Monitor 处于无锁状态,则当前线程将成功获取锁,并继续执行同步代码块中的逻辑;同时,Monitor 的拥有者标记为当前线程。

  • 如果对象的 Monitor 已经被其他线程锁定,则当前线程将进入阻塞状态,直到获取到锁为止。

  • 当线程退出同步代码块时,执行 monitorexit 指令,释放对象的 Monitor,并清空拥有者标记。

作用于方法

        而当synchronized关键字作用于方法时,Java 虚拟机会通过在方法的访问标志中设置ACC_SYNCHRONIZED 标志来实现方法级别的同步。具体的工作流程如下:

  • 当线程调用同步方法时,首先尝试获取方法所属对象的 Monitor。

  • 如果方法所属对象的 Monitor 处于无锁状态,则当前线程将成功获取锁,并执行方法中的逻辑;同时,Monitor 的拥有者标记为当前线程。

  • 如果方法所属对象的 Monitor 已经被其他线程锁定,则当前线程将进入阻塞状态,直到获取到锁为止。

  • 当方法执行结束时,释放方法所属对象的 Monitor,并清空拥有者标记。

  • 需要注意的是,对于静态同步方法,Java 虚拟机会使用类的 Class 对象作为 Monitor,而对于实例同步方法,则会使用实例对象的 Monitor。这样可以确保静态方法和实例方法之间的互斥性。

JVM锁的膨胀升级详细图例

来看图中第一个例子:

        这里有两个线程,都去操作同一个对象Object,对象头里有MarkWord,刚开始线程1访问对象的时候,线程2未进入到同步代码块,而线程1进入了同步代码块,它先要做一点事情,即检查当前对象头中的ThreadID是否是线程1,如果不是,会使用CAS修改MarkWord,将对象头中的ThreadID指向线程1,然后执行同步代码块,如果是,则直接执行同步代码块。

        现在线程2启动,访问同步代码块,也会检查对象头中的ThreadID是否是线程2,尝试使用CAS修改MarkWord,但修改不了(ThreadID是Null才能改,若不是Null则不能改),则CAS失败,会开启偏向锁的撤销,在线程1到达安全点时会暂停它(STW,Stop The World,与GC有关),然后检查线程1是否退出了同步代码块,如果退出了,则解锁,将对象头中的ThreadID置位空,偏向锁状态改为0,恢复为无所状态,如果未退出,则会升级为一个轻量级锁。

        这里就反映出了偏向锁的性能问题,它的撤销过程要做的事情非常多,因此少量同步的场景,不要使用偏向锁,偏向锁只适合一个线程使用的场景,而轻量级锁适合竞争不激烈的场景,业务比较简单,很快可以执行完成,线程间顺序交替执行的场景,而大量同步的场景,不要使用重量级锁,有性能问题(那么使用什么呢?)

再来看图中第二个例子:

        还是有两个线程,线程1和线程2都会在栈上分配内存空间,拷贝MarkWord到Lock Record中,然后通过CAS去修改对象的MarkWord,此时有可能线程1修改成功,线程2修改失败,若成功,则对象头中的锁记录指针指向当前栈上Lock Record的指针,升级为轻量级锁,然后执行同步代码块,若失败,会发生自旋获取锁(次数可设置,参数-XX:PreBlockSpin,默认是10次,且是自适应自旋),自旋一定次数依然没有成功,则会发生锁膨胀,升级为重量级锁,线程阻塞。

        这其中的优化点即是,当线程2修改失败时,并没有让马上阻塞,而是进行自适应自旋,若一直失败,则锁膨胀,升级为重量级锁,线程阻塞。

        线程1在执行同步代码块后,会去使用CAS修改Mark Word,若成功,则释放锁,若失败,则释放锁,唤醒阻塞的线程,开始新一轮的锁竞争(重量级锁的撤销)。

        再谈一下重量级锁的撤销,即GC线程在垃圾回收时,会看当前的锁对象除了GC线程外有无其他线程,若没有,则重量级锁会直接降级为无锁,重量级锁的降级不是降级为轻量级锁、偏向锁,而是垃圾回收器将它降级为无锁。

synchronized锁实现与升级过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值