ToB企服应用市场:ToB评测及商务社交产业平台

标题: 深入浅出Java多线程(九):synchronized与锁 [打印本页]

作者: 怀念夏天    时间: 2024-4-25 10:05
标题: 深入浅出Java多线程(九):synchronized与锁
引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第九篇内容:synchronized与锁。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!
在现代软件开发中,多线程技术是提升系统性能和并发能力的关键手段之一。Java作为主流的编程语言,其内置的多线程机制为开发者提供了丰富的并发控制工具,其中synchronized关键字及其背后的锁机制扮演了至关重要的角色。理解并掌握synchronized的使用原理与特性,有助于我们设计出高效且线程安全的应用程序。
Java中的每个对象都可以充当一把锁,这意味着任何实例方法或静态方法可以通过synchronized关键字来实现同步控制,从而确保同一时间只有一个线程能访问临界资源。例如,一个简单的实例方法同步:
  1. public class Counter {
  2.     private int count = 0;

  3.     public synchronized void increment() {
  4.         count++;
  5.     }
  6. }
复制代码
在这个例子中,increment方法被synchronized修饰,使得在同一时刻只能有一个线程对count变量进行递增操作,避免了数据竞争带来的不一致性问题。
同时,类锁的概念也是基于对象锁——类的Class对象同样可以作为锁,用于同步类的静态方法或某一特定对象实例上的代码块,如:
  1. public class SharedResource {
  2.     public static synchronized void modifyStaticData() {
  3.         // 修改共享静态数据
  4.     }
  5. }
复制代码
这里,modifyStaticData方法通过类锁保护了所有实例共享的静态资源,保证了在多线程环境下的数据安全性。
深入探究Java多线程中的synchronized关键字及锁机制,我们会发现Java虚拟机为了优化锁的性能,引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,并且支持锁的自动升级和降级策略。这些机制能够根据实际的并发场景动态调整锁的表现形式,以最小化锁的获取和释放开销,进而提高系统的并发性能和响应速度。接下来,我们将逐一剖析这些概念和技术细节,以便更全面地理解和运用Java中的锁机制。
Java锁基础


在Java多线程编程中,锁机制是实现并发控制的核心手段之一。这里的“锁”基于对象的概念,任何Java对象都可以充当一把锁来保护共享资源的访问,确保同一时间只有一个线程可以执行临界区代码。synchronized关键字作为Java内置的关键同步工具,被广泛用于实现线程间的互斥操作。
synchronized关键字详解
synchronized关键字主要有三种使用形式:
synchronized关键字保证了其修饰的方法或代码块在同一时间只能由单个线程访问,从而避免了因多个线程同时修改数据导致的数据不一致问题,有效地实现了多线程环境下的同步控制。随着JVM对锁性能优化的不断深入,还引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,使得Java多线程同步更加灵活高效。
synchronized原理


在Java多线程编程中,synchronized关键字所实现的同步机制深入底层,与JVM内部对象头结构密切相关。每个Java对象都拥有一个对象头(Object Header),它是内存中存放对象元数据的地方,包含了对象的Mark Word区域,这个区域用于存储对象的hashCode、GC分代年龄以及锁状态等信息。
Java对象头与锁状态
对象头结构:非数组类型的Java对象,其对象头占用2个机器字宽,对于32位系统是32位,64位系统则是64位。Mark Word中的一部分空间被用来记录锁的状态,包括无锁、偏向锁、轻量级锁和重量级锁四种状态。
长度内容作用32/64bitMark Word存储对象的hashCode或锁信息等32/64bitClass Metadata Address存储到对象类型数据的指针32/64bitArray length数组的长度(如果是数组)这里着重关注一些Mark Word 的内容:
锁状态29bit或者61bit第1bit是否偏向锁第2bit锁标志位无锁001偏向锁线程ID101轻量级锁指向栈中锁记录的指针此时第1bit不用于标识偏向锁00重量级锁指向互斥量(重量级锁)的指针此时第1bit不用于标识偏向锁10锁状态转换
  1. class BiasedLockExample {
  2.     private int count;

  3.     public void increment() {
  4.         synchronized (this) {
  5.             count++;
  6.         }
  7.     }
  8. }
复制代码
在上述例子中,如果increment方法仅由一个线程执行,那么JVM可能会将对象标记为偏向锁,从而提高效率。
Java虚拟机通过对象头的Mark Word动态调整锁状态以适应不同场景下的并发控制需求,实现了从偏向锁、轻量级锁到重量级锁的平滑过渡,有效提升了多线程环境下程序的性能表现。通过灵活运用和理解这些锁状态及其背后的原理,开发者能够更好地优化多线程应用中的同步逻辑。
Java锁升级机制


在Java多线程同步中,synchronized关键字实现的锁具有动态升级的能力,从偏向锁到轻量级锁再到重量级锁,根据竞争情况自动调整以优化性能。
偏向锁

偏向锁是为了解决大多数情况下只有一个线程频繁获得锁的情况。当一个线程首次获取对象锁时,JVM会将其设置为偏向锁,并将该线程ID记录在对象头的Mark Word中。后续该线程再次进入同步代码块时,只需简单地验证Mark Word中的线程ID是否与当前线程一致即可快速获取锁。例如:
  1. public class BiasedLockExample {
  2.     private int sharedResource;

  3.     public void access() {
  4.         synchronized (this) {
  5.             // 仅有一个线程长期访问此方法时,偏向锁生效
  6.             sharedResource++;
  7.         }
  8.     }
  9. }
复制代码
如果其他线程尝试获取已被偏向的锁,系统会检查偏向锁是否有效并进行撤销操作,通过CAS尝试替换Mark Word的内容。若失败,则表明存在锁竞争,此时偏向锁升级至轻量级锁。其操作流程如下图:

下图总结了偏向锁的获得和撤销流程:


轻量级锁

轻量级锁主要应用于多个线程间交替访问同一对象但不存在大量持续竞争的场景。当线程试图获取锁时,它首先会在自己的栈帧中创建一个用于存储锁记录的空间(Displaced Mark Word),然后通过CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针。成功则表示获得锁;否则,线程开始自旋(循环尝试获取锁)。
  1. public class LightweightLockExample {
  2.     private int sharedResource;

  3.     public void access() {
  4.         Object lock = new Object();
  5.         synchronized (lock) {
  6.             // 若多个线程短暂交替访问此方法,轻量级锁生效
  7.             sharedResource++;
  8.         }
  9.     }
  10. }
复制代码
自旋次数并非固定不变,而是采用了适应性自旋策略,即根据历史成功率动态调整自旋次数。如果经过若干次自旋后仍未能获得锁,则轻量级锁升级为重量级锁。轻量锁操作流程如下:


重量级锁

重量级锁依赖于操作系统的互斥量(mutex)来实现线程间的互斥控制。当锁竞争激烈,轻量级锁无法满足需求时,锁状态会转换为重量级锁。这时,请求锁的线程会被挂起并放入等待队列中,直至持有锁的线程释放锁资源。
  1. public class HeavyweightLockExample {
  2.     private static final Object lock = new Object();

  3.     public void concurrentAccess() {
  4.         synchronized (lock) {
  5.             // 若大量并发线程同时访问此方法,可能导致锁升级为重量级锁
  6.             // 线程将被操作系统调度器挂起和唤醒
  7.             performHeavyOperation();
  8.         }
  9.     }

  10.     private void performHeavyOperation() {
  11.         // 执行耗时较长的操作...
  12.     }
  13. }
复制代码
重量级锁虽然会导致线程阻塞及上下文切换,但它确保了在高度竞争环境下的公平性和线程安全。当调用wait()或notify()方法时,即使原本是轻量级或偏向锁,也会先膨胀成重量级锁,以便正确管理线程的阻塞和唤醒状态。
总结来说,Java锁的升级机制是一种根据实际运行状况动态调整同步成本的技术手段,使得在多种并发场景下都能尽可能保持高效率和线程安全性。
锁对比与选择


在Java多线程同步中,有三种主要的锁类型:偏向锁、轻量级锁和重量级锁。每种锁都有其特定的适用场景及性能特性。
偏向锁
案例:
  1. public class BiasedLockExample {
  2.     private int sharedResource;

  3.     public void exclusiveAccess() {
  4.         synchronized (this) {
  5.             // 若只有主线程频繁访问此方法,则偏向锁效率高
  6.             sharedResource++;
  7.         }
  8.     }
  9. }
复制代码
轻量级锁
案例:
  1. public class LightweightLockExample {
  2.     private final Object lock = new Object();

  3.     public void concurrentAccess() {
  4.         synchronized (lock) {
  5.             // 若并发线程交替短暂持有锁,轻量级锁效果好
  6.             processData();
  7.         }
  8.     }

  9.     private void processData() {
  10.         // 执行一些快速计算或短期持有的共享资源访问...
  11.     }
  12. }
复制代码
重量级锁
案例:
  1. public class HeavyweightLockExample {
  2.     private static final Object LOCK = new Object();

  3.     public void criticalSection() {
  4.         synchronized (LOCK) {
  5.             // 在大量并发线程竞争同一锁时,重量级锁能确保公平性和稳定性
  6.             accessSharedResource();
  7.         }
  8.     }

  9.     private void accessSharedResource() {
  10.         // 访问公共资源,如数据库连接、文件写入等耗时较长的操作...
  11.     }
  12. }
复制代码
综上所述,根据应用中的具体并发模式和锁争用情况,合理选择合适的锁类型至关重要。在实际编程中,JVM会根据实际情况自动进行锁状态的调整和升级,但开发人员也应具备理解这些锁机制的能力,并适时调整JVM参数以优化程序性能。例如,若确定应用程序不存在偏向锁的优势场景,可考虑禁用偏向锁功能。
总结与建议

Java多线程中,synchronized关键字及锁机制的运用涉及到从偏向锁到轻量级锁再到重量级锁的动态升级过程。在设计并发程序时,理解并合理选择锁策略对于提高系统性能至关重要。
偏向锁旨在优化单一线程访问临界区的场景,通过记录当前持有锁的线程ID来避免无竞争时的额外开销。但当其他线程尝试获取锁时,需撤销偏向锁,并可能升级为轻量级锁。
  1. public class BiasedLockDemo {
  2.     private int count;

  3.     public void increment() {
  4.         synchronized (this) {
  5.             // 偏向锁适用于只有一个线程长期执行此方法的情况
  6.             count++;
  7.         }
  8.     }
  9. }
复制代码
轻量级锁利用CAS操作和自旋机制,减少线程阻塞带来的上下文切换成本,在低竞争环境下提升响应速度。然而,若存在持续锁竞争,过多的自旋可能导致CPU空耗,此时会转为重量级锁。
  1. public class LightweightLockDemo {
  2.     private final Object lock = new Object();

  3.     public void process() {
  4.         synchronized (lock) {
  5.             // 在短暂且交替访问同步块的情况下,轻量级锁能提供较好的性能
  6.             doWork();
  7.         }
  8.     }

  9.     private void doWork() {
  10.         // 执行快速计算或读取共享资源的操作...
  11.     }
  12. }
复制代码
重量级锁虽然开销较大,但确保了互斥性和公平性,尤其适合于高度竞争的同步场景。它通过操作系统互斥量实现,能够防止长时间占用CPU资源的自旋等待。
  1. public class HeavyweightLockDemo {
  2.     private static final Object LOCK = new Object();

  3.     public void criticalSection() {
  4.         synchronized (LOCK) {
  5.             // 当多个线程频繁争夺同一锁资源时,重量级锁能提供稳定的保护
  6.             accessSharedResource();
  7.         }
  8.     }

  9.     private void accessSharedResource() {
  10.         // 访问需要严格同步控制的公共资源...
  11.     }
  12. }
复制代码
在实际开发中,JVM默认启用偏向锁和轻量级锁功能,但根据具体应用场景,可以通过调整JVM参数如-XX:UseBiasedLocking、-XX:+/-UseLightweightLocking等来控制锁行为。同时,关注代码结构,尽可能减少不必要的锁竞争,优化数据结构,是提高多线程程序效率的关键所在。通过深入理解锁升级机制和每种锁的特点,开发者可以更好地权衡并发处理中的性能和安全性问题。
本文使用 markdown.com.cn 排版

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4