深入浅出Java多线程(九):synchronized与锁

引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第九篇内容:synchronized与锁。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在现代软件开发中,多线程技术是提升系统性能和并发能力的关键手段之一。Java作为主流的编程语言,其内置的多线程机制为开发者提供了丰富的并发控制工具,其中synchronized关键字及其背后的锁机制扮演了至关重要的角色。理解并掌握synchronized的使用原理与特性,有助于我们设计出高效且线程安全的应用程序。

Java中的每个对象都可以充当一把锁,这意味着任何实例方法或静态方法可以通过synchronized关键字来实现同步控制,从而确保同一时间只有一个线程能访问临界资源。例如,一个简单的实例方法同步:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

在这个例子中,increment方法被synchronized修饰,使得在同一时刻只能有一个线程对count变量进行递增操作,避免了数据竞争带来的不一致性问题。

同时,类锁的概念也是基于对象锁------类的Class对象同样可以作为锁,用于同步类的静态方法或某一特定对象实例上的代码块,如:

public class SharedResource {
    public static synchronized void modifyStaticData() {
        // 修改共享静态数据
    }
}

这里,modifyStaticData方法通过类锁保护了所有实例共享的静态资源,保证了在多线程环境下的数据安全性。

深入探究Java多线程中的synchronized关键字及锁机制,我们会发现Java虚拟机为了优化锁的性能,引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,并且支持锁的自动升级和降级策略。这些机制能够根据实际的并发场景动态调整锁的表现形式,以最小化锁的获取和释放开销,进而提高系统的并发性能和响应速度。接下来,我们将逐一剖析这些概念和技术细节,以便更全面地理解和运用Java中的锁机制。

Java锁基础


在Java多线程编程中,锁机制是实现并发控制的核心手段之一。这里的"锁"基于对象的概念,任何Java对象都可以充当一把锁来保护共享资源的访问,确保同一时间只有一个线程可以执行临界区代码。synchronized关键字作为Java内置的关键同步工具,被广泛用于实现线程间的互斥操作。

synchronized关键字详解

synchronized关键字主要有三种使用形式:

  1. 实例方法锁定 :当synchronized关键字修饰实例方法时,它隐式地获取了当前对象实例作为锁:

    public class SynchronizedExample {
        private int counter;
    
        public synchronized void increment() {
            counter++;
        }
    }
    

    在上述代码中,increment方法被synchronized修饰,意味着每次仅有一个线程能执行该方法内部逻辑,即修改counter变量。

  2. 静态方法锁定 :如果synchronized修饰的是静态方法,则锁对象为类的Class对象,所有实例共享这把锁:

    public class SynchronizedExample {
        private static int sharedCounter;
    
        public static synchronized void incrementStatic() {
            sharedCounter++;
        }
    }
    

    在这个例子中,对incrementStatic方法的访问将受到类锁的保护,确保在多线程环境下,对sharedCounter的更新是原子性的。

  3. 代码块锁定 :通过synchronized关键字包裹一个代码块,显式指定锁对象:

    public class SynchronizedExample {
        private final Object lock = new Object();
    
        public void blockLockingMethod() {
            synchronized (lock) {
                // 临界区代码
            }
        }
    

    在这里,我们创建了一个独立的对象lock用作锁,只有获得了这把锁的线程才能执行代码块内的内容。

synchronized关键字保证了其修饰的方法或代码块在同一时间只能由单个线程访问,从而避免了因多个线程同时修改数据导致的数据不一致问题,有效地实现了多线程环境下的同步控制。随着JVM对锁性能优化的不断深入,还引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,使得Java多线程同步更加灵活高效。

synchronized原理


在Java多线程编程中,synchronized关键字所实现的同步机制深入底层,与JVM内部对象头结构密切相关。每个Java对象都拥有一个对象头(Object Header),它是内存中存放对象元数据的地方,包含了对象的Mark Word区域,这个区域用于存储对象的hashCode、GC分代年龄以及锁状态等信息。

Java对象头与锁状态

对象头结构:非数组类型的Java对象,其对象头占用2个机器字宽,对于32位系统是32位,64位系统则是64位。Mark Word中的一部分空间被用来记录锁的状态,包括无锁、偏向锁、轻量级锁和重量级锁四种状态。

长度 内容 作用
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果是数组)

这里着重关注一些Mark Word 的内容:

锁状态 29bit或者61bit 第1bit是否偏向锁 第2bit锁标志位
无锁 0 01
偏向锁 线程ID 1 01
轻量级锁 指向栈中锁记录的指针 此时第1bit不用于标识偏向锁 00
重量级锁 指向互斥量(重量级锁)的指针 此时第1bit不用于标识偏向锁 10

锁状态转换

  • 无锁状态:没有任何线程持有该对象锁,所有线程都可以尝试修改资源。

  • 偏向锁:当一个线程首次获得锁时,会将当前线程ID写入对象头的Mark Word中,后续进入同步代码块时只需检查是否为当前线程持有即可快速获取锁。例如,若只有一个线程长期访问某一对象,则可以避免不必要的CAS操作和自旋消耗。

    class BiasedLockExample {
    private int count;

      public void increment() {
          synchronized (this) {
              count++;
          }
      }
    

    }

在上述例子中,如果increment方法仅由一个线程执行,那么JVM可能会将对象标记为偏向锁,从而提高效率。

  • 轻量级锁:当存在多个线程竞争同一锁,但实际发生锁竞争的概率较小的情况下,JVM使用轻量级锁来避免频繁的线程阻塞和唤醒开销。轻量级锁通过CAS操作试图将当前线程栈中的锁记录地址替换到对象头的Mark Word中,如失败则表明存在锁竞争,转而升级为自旋或重量级锁。
  • 重量级锁:当锁竞争激烈时,轻量级锁无法满足需求,就会升级为依赖于操作系统的互斥量(mutex)实现的重量级锁。此时线程将被挂起,直到锁释放后重新调度,降低了CPU的利用率但确保了线程间互斥性。

Java虚拟机通过对象头的Mark Word动态调整锁状态以适应不同场景下的并发控制需求,实现了从偏向锁、轻量级锁到重量级锁的平滑过渡,有效提升了多线程环境下程序的性能表现。通过灵活运用和理解这些锁状态及其背后的原理,开发者能够更好地优化多线程应用中的同步逻辑。

Java锁升级机制


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

偏向锁

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

public class BiasedLockExample {
    private int sharedResource;

    public void access() {
        synchronized (this) {
            // 仅有一个线程长期访问此方法时,偏向锁生效
            sharedResource++;
        }
    }
}

如果其他线程尝试获取已被偏向的锁,系统会检查偏向锁是否有效并进行撤销操作,通过CAS尝试替换Mark Word的内容。若失败,则表明存在锁竞争,此时偏向锁升级至轻量级锁。 其操作流程如下图:

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

轻量级锁

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

public class LightweightLockExample {
    private int sharedResource;

    public void access() {
        Object lock = new Object();
        synchronized (lock) {
            // 若多个线程短暂交替访问此方法,轻量级锁生效
            sharedResource++;
        }
    }
}

自旋次数并非固定不变,而是采用了适应性自旋策略,即根据历史成功率动态调整自旋次数。如果经过若干次自旋后仍未能获得锁,则轻量级锁升级为重量级锁。 轻量锁操作流程如下:

重量级锁

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

public class HeavyweightLockExample {
    private static final Object lock = new Object();

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

    private void performHeavyOperation() {
        // 执行耗时较长的操作...
    }
}

重量级锁虽然会导致线程阻塞及上下文切换,但它确保了在高度竞争环境下的公平性和线程安全。当调用wait()notify()方法时,即使原本是轻量级或偏向锁,也会先膨胀成重量级锁,以便正确管理线程的阻塞和唤醒状态。

总结来说,Java锁的升级机制是一种根据实际运行状况动态调整同步成本的技术手段,使得在多种并发场景下都能尽可能保持高效率和线程安全性。

锁对比与选择


在Java多线程同步中,有三种主要的锁类型:偏向锁、轻量级锁和重量级锁。每种锁都有其特定的适用场景及性能特性。

偏向锁

  • 优点:当只有一个线程长期独占对象锁时,偏向锁几乎无额外开销,获取和释放锁的速度接近非同步方法调用。
  • 缺点:当存在锁竞争或者程序执行过程中锁的所有者发生变化时,需要撤销偏向锁并升级为更高级别的锁,这个过程会产生额外的系统开销。
  • 适用场景:适用于大部分时间只由一个线程访问同步块的场合。

案例:

public class BiasedLockExample {
    private int sharedResource;

    public void exclusiveAccess() {
        synchronized (this) {
            // 若只有主线程频繁访问此方法,则偏向锁效率高
            sharedResource++;
        }
    }
}

轻量级锁

  • 优点:相比于重量级锁,轻量级锁通过自旋避免了线程上下文切换带来的开销,在没有其他线程竞争的情况下能快速获得锁,提高了程序响应速度。
  • 缺点:如果多个线程同时争夺锁,轻量级锁会导致较多的CAS操作以及可能的长时间自旋等待,反而浪费CPU资源。
  • 适用场景:适用于线程间对锁的竞争不激烈且锁持有时间较短的情况。

案例:

public class LightweightLockExample {
    private final Object lock = new Object();

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

    private void processData() {
        // 执行一些快速计算或短期持有的共享资源访问...
    }
}

重量级锁

  • 优点:确保了线程间的互斥性和公平性,不会因自旋消耗过多CPU资源,阻塞未获得锁的线程,保证了系统的稳定性。
  • 缺点:获取和释放锁涉及操作系统层面的信号量操作,导致较大的上下文切换开销,因此在高并发、锁竞争激烈的场景下性能较低。
  • 适用场景:适用于高度竞争性的环境,即大量并发线程同时请求同一锁资源的情况。

案例:

public class HeavyweightLockExample {
    private static final Object LOCK = new Object();

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

    private void accessSharedResource() {
        // 访问公共资源,如数据库连接、文件写入等耗时较长的操作...
    }
}

综上所述,根据应用中的具体并发模式和锁争用情况,合理选择合适的锁类型至关重要。在实际编程中,JVM会根据实际情况自动进行锁状态的调整和升级,但开发人员也应具备理解这些锁机制的能力,并适时调整JVM参数以优化程序性能。例如,若确定应用程序不存在偏向锁的优势场景,可考虑禁用偏向锁功能。

总结与建议

Java多线程中,synchronized关键字及锁机制的运用涉及到从偏向锁到轻量级锁再到重量级锁的动态升级过程。在设计并发程序时,理解并合理选择锁策略对于提高系统性能至关重要。

偏向锁旨在优化单一线程访问临界区的场景,通过记录当前持有锁的线程ID来避免无竞争时的额外开销。但当其他线程尝试获取锁时,需撤销偏向锁,并可能升级为轻量级锁。

public class BiasedLockDemo {
    private int count;

    public void increment() {
        synchronized (this) {
            // 偏向锁适用于只有一个线程长期执行此方法的情况
            count++;
        }
    }
}

轻量级锁利用CAS操作和自旋机制,减少线程阻塞带来的上下文切换成本,在低竞争环境下提升响应速度。然而,若存在持续锁竞争,过多的自旋可能导致CPU空耗,此时会转为重量级锁。

public class LightweightLockDemo {
    private final Object lock = new Object();

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

    private void doWork() {
        // 执行快速计算或读取共享资源的操作...
    }
}

重量级锁虽然开销较大,但确保了互斥性和公平性,尤其适合于高度竞争的同步场景。它通过操作系统互斥量实现,能够防止长时间占用CPU资源的自旋等待。

public class HeavyweightLockDemo {
    private static final Object LOCK = new Object();

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

    private void accessSharedResource() {
        // 访问需要严格同步控制的公共资源...
    }
}

在实际开发中,JVM默认启用偏向锁和轻量级锁功能,但根据具体应用场景,可以通过调整JVM参数如-XX:UseBiasedLocking-XX:+/-UseLightweightLocking等来控制锁行为。同时,关注代码结构,尽可能减少不必要的锁竞争,优化数据结构,是提高多线程程序效率的关键所在。通过深入理解锁升级机制和每种锁的特点,开发者可以更好地权衡并发处理中的性能和安全性问题。

本文使用 markdown.com.cn 排版