1. 同步器的意义
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
- 共享:资源可以由多个线程同时访问
- 可变:资源可以在其生命周期内被修改
引出的问题:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!同步器的本质就是加锁
2. 如何解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临
界资源,也称作同步互斥访问。
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
3. synchronized
synchronized是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。相当于加了一把锁,但是不需要加锁和解锁的操作,所以称它为隐式锁。synchronized取得的锁都是对象锁或类锁,而不是把一段代码或方法当做锁(锁的是堆中的对象)。
在Java中,synchronized可以用于以下三种方式:
- 同步代码块(Synchronized Block):使用synchronized关键字来标记一个代码块,以保证同一时间只有一个线程可以执行该代码块。在进入synchronized代码块之前,线程需要获得所操作对象的锁。当线程执行完synchronized代码块或遇到异常时,会释放该对象的锁。
synchronized (obj) {
// 被保护的代码块
}
- 同步方法(Synchronized Method):使用synchronized关键字修饰方法,以确保同一时间只有一个线程可以执行该方法。当一个线程进入synchronized方法时,它会自动获取该方法所属对象的锁,其他线程将被阻塞直到锁被释放。synchronized修饰实例方法(非static方法) 时,获取的是对象锁(即类的实例对象),作用范围是整个方法,作用的对象是调用该方法的单个对象。
public synchronized void synchronizedMethod() {
// 被保护的方法体
}
- 静态同步方法(Static Synchronized Method):使用synchronized关键字修饰静态方法,以确保同一时间只有一个线程可以执行该静态方法。与普通的同步方法一样,进入静态同步方法时,会获取该方法所属的Class对象的锁。静态方法是属于类的而不属于对象的,所以synchronized修饰类方法(static方法) 时,取的是类锁(即Class本身,注意:不是实例),作用的对象是这个类的所有对象。
public static synchronized void staticSynchronizedMethod() {
// 被保护的静态方法体
}
3.1 synchronized底层原理
synchronized关键字是Java中用于实现线程同步和互斥访问共享资源的机制。其底层原理主要涉及对象头(Object Header)、Monitor和内存屏障(Memory Barrier)。当一个线程进入synchronized代码块或方法时,它需要获取锁,以下是synchronized的底层原理:
- 对象头(Object Header):每个Java对象都有一个对象头,用于存储对象的元数据信息,包括锁的信息。对象头中的锁信息包括锁标志位和指向持有该锁的线程的指针。
- Monitor(监视器):Monitor是synchronized实现同步的关键结构,每个对象都与一个Monitor相关联。Monitor内部维护了一个等待队列和一个持有锁的线程队列。当一个线程获取到锁时,它就成为了Monitor的拥有者。
- 互斥访问:当一个线程需要进入synchronized代码块或方法时,它会尝试获取对象的Monitor。如果Monitor的锁标志位为可用状态(unlocked),则线程将成功获取锁,并将锁标志位设置为锁定状态(locked),同时将持有锁的线程设置为自己。如果锁标志位已被其他线程锁定,则该线程将进入Monitor的等待队列,等待锁的释放。
- 释放锁:当持有锁的线程执行完synchronized代码块或方法,或者遇到异常时,它将释放锁,即将锁标志位设置为可用状态,并唤醒等待队列中的某个线程,使其成为新的锁持有者。
- 内存屏障(Memory Barrier):synchronized还涉及内存屏障的使用。内存屏障用于保证在不同线程之间的内存可见性和指令重排序的正确性。在synchronized的释放操作和获取操作之间,会插入内存屏障,以确保在释放锁之前的所有修改都能对获取锁的线程可见,并防止指令重排序。
通过对象头的锁信息、Monitor的等待队列和持有锁的线程队列,以及内存屏障的使用,synchronized实现了线程的互斥访问和同步操作。它保证了在同一时间只有一个线程能够进入synchronized代码块或方法,并且能够正确地处理多线程并发访问共享资源的问题。
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与
Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。每个同步对象都有一个自己的Monitor(监视器锁)。
4. synchronized的优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。
Java对synchronized进行了多项底层优化,以提高其性能和并发效率。下面列出了一些常见的底层优化技术:
- 自适应锁:JVM会根据锁的使用情况动态调整锁的实现方式。例如,当一个锁一直被多个线程竞争时,JVM可能会将其升级为重量级锁,以减少线程间的竞争。而当一个锁只被一个线程使用时,JVM可能会将其降级为轻量级锁或偏向锁,以减少锁操作的开销。
- 锁粗化(Lock Coarsening):当JVM检测到连续的加锁和解锁操作在逻辑上是对同一个锁对象的操作时,它会将这些操作整合成一个更大的锁操作,以减少加锁和解锁的次数。
- 锁消除(Lock Elimination):JVM会对一些代码块进行静态分析,如果发现某个锁在整个代码块中没有实际的影响,即没有对共享数据进行修改或访问,那么JVM会将该锁消除,以减少锁操作的开销。
- 偏向锁(Biased Locking):JVM引入了偏向锁的概念,当一个锁对象被第一个线程获取时,JVM会将该锁标记为偏向锁,并将线程ID记录在锁对象头中。接下来,当同一个线程再次获取该锁时,JVM会直接判断为可偏向状态,而不需要进行加锁和解锁的操作,从而减少了锁操作的开销。
- 适应性自旋(Adaptive Spinning):JVM根据过去同步块的竞争情况来决定线程在自旋期间的策略。如果过去自旋成功率较高,JVM会增加自旋时间;如果自旋成功率较低,JVM会减少自旋时间或直接让线程进入阻塞状态,以避免浪费CPU资源。
这些优化措施使得synchronized在许多场景下能够提供良好的性能和并发效率。然而,对于某些高度竞争的场景,或者需要更细粒度的锁控制的情况,可以考虑使用显式的锁机制,如java.util.concurrent包中的ReentrantLock,以获得更高的灵活性和性能。