为什么需要锁
Java中的锁是一种同步机制,可以确保多个线程之间共享资源的互斥访问,从而避免出现数据竞争和线程安全问题。使用锁的主要目的是保证代码的正确性和可靠性。
Java中的锁能够解决以下实际问题:
- 数据竞争:在多线程环境中,如果多个线程同时访问共享数据,就会产生数据竞争问题。使用锁可以确保同一时间只有一个线程可以访问共享资源,避免数据竞争和数据不一致的问题。
- 线程安全:Java中的锁可以确保线程安全,避免多个线程之间的干扰和竞争,从而保证代码的正确性和可靠性。
- 性能优化:Java中的锁可以用于优化程序的性能,比如使用读写锁来实现对数据的读写分离,从而提高程序的并发性能。
- 死锁问题:Java中的锁可以用于避免死锁问题,比如使用一致性的加锁顺序,避免出现循环依赖的情况。
总之,Java中的锁机制是保证多线程并发安全的重要手段,可以用于解决数据竞争、线程安全、性能优化和死锁问题等实际问题。
Synchronized
synchronized是Java中的一个关键字,它提供了一种内置锁机制,用于确保多个线程在访问共享资源时的同步性。
使用方式
- 修饰方法:直接在方法声明上加上synchronized关键字,表示整个方法是同步的。此时,锁是当前实例对象(对于非静态方法)或Class对象(对于静态方法)。
- 修饰代码块:使用synchronized(object)来指定一个对象作为锁,只有持有该对象锁的线程才能进入synchronized块。这种方式可以更加灵活地控制需要同步的代码范围。
原理与特性
- 互斥性:当一个线程进入由synchronized修饰的代码块或方法时,它会获取对象的锁,其他试图进入该代码块或方法的线程将被阻塞,直到锁被释放。这确保了同一时间只有一个线程可以执行synchronized保护的代码段。
- 可重入性:对于同一个线程来说,synchronized块是可重入的,即一个线程可以多次获取同一个对象的锁。
- 可见性:synchronized保证了内存可见性,即当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。这是通过JVM的内存屏障指令实现的,确保了在获取锁之前和释放锁之后,相关的内存操作会被刷新到主内存或从主内存重新读取。
Synchronized 锁的升级过程
在Java中,synchronized关键字的锁升级过程是一个动态的过程,旨在提高并发性能并减少线程之间的争用。这个过程从最初的无锁状态开始,根据线程对锁的争用情况,逐步升级到更高级别的锁状态。
我们来看一下他的升级过程:
无锁状态
对象刚被创建时,没有线程对其加锁,此时处于无锁状态。
偏向锁
- 当第一个线程访问某个对象并尝试获取锁时,JVM会利用CAS(Compare-And-Swap)操作在对象的对象头(Mark Word)中记录下当前线程的ID和偏向锁标记位(通常设置为1)。
- 如果下一次还是这个线程访问该对象,则只需要检查对象头中的线程ID是否与自己的ID相同,如果相同则直接获得锁,无需再进行CAS操作。这种情况下,锁就保持在偏向锁状态,整个过程几乎没有任何性能开销。
- 如果在持有偏向锁期间,其他线程尝试访问该对象并获取锁,偏向锁会被撤销,并尝试升级为轻量级锁。
轻量级锁
- 当偏向锁被撤销后,锁会升级到轻量级锁状态。
- 在轻量级锁状态下,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的Mark Word复制到该锁记录中,同时对象头中会有一个指针指向这个锁记录。
- 当前线程会进入自旋(Spinning)状态,即不断尝试重新获取锁,而不是立即阻塞。自旋的目的是为了避免线程切换带来的性能开销,因为线程切换涉及到操作系统层面的操作,开销相对较大。
- 如果自旋过程中成功获取到锁,则继续执行后续代码;如果自旋超过一定次数(通常是10次)仍未获取到锁,或者有其他线程参与锁竞争,则轻量级锁会膨胀为重量级锁。
重量级锁
- 当轻量级锁无法满足并发需求时,锁会升级为重量级锁。
- 在重量级锁状态下,如果当前线程未获取到锁,则会进入阻塞状态,等待其他线程释放锁。当锁被释放后,阻塞的线程会被唤醒并重新尝试获取锁。
- 重量级锁的实现依赖于操作系统的互斥量(Mutex)或其他同步机制,因此涉及到用户态和内核态的切换,开销相对较大。
总结
- synchronized的锁升级过程是从无锁状态开始,根据线程对锁的争用情况逐步升级到偏向锁、轻量级锁和重量级锁的过程。
- 偏向锁和轻量级锁是JVM为了提高并发性能而引入的优化措施,它们可以减少线程切换带来的性能开销。
- 重量级锁是当轻量级锁无法满足并发需求时的最终选择,它依赖于操作系统的同步机制来实现。
你对Synchronized的升级过程了解了么?