多线程之synchronized

1. synchronized 的核心作用

synchronized 的主要作用是:

  • 互斥性:确保同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法。
  • 可见性:当一个线程释放锁时,它对共享变量的修改会立即同步到主内存,其他线程在获取锁后可以看到最新的值。

2. synchronized 的实现原理

synchronized 是基于 Java 对象头中的 Monitor(监视器锁) 实现的。每个 Java 对象都有一个与之关联的 Monitor,synchronized 通过获取和释放 Monitor 来实现线程同步。

Monitor 的工作机制:

  1. 进入区(Entry Set):当一个线程尝试获取锁时,如果锁未被占用,线程会成功获取锁并进入临界区;如果锁已被占用,线程会进入进入区等待。
  2. 临界区(Critical Section):持有锁的线程可以执行被 synchronized 修饰的代码。
  3. 等待区(Wait Set):如果线程调用了 wait() 方法,它会释放锁并进入等待区,直到被 notify() 或 notifyAll() 唤醒。
  4. 退出区:线程执行完临界区代码后会释放锁,其他线程可以竞争锁。
3. synchronized 的使用方式

synchronized 可以用于以下三种场景:

(1) 修饰实例方法
public synchronized void method() {
    // 同步代码
}
  • 锁对象是当前实例(this)。
  • 同一实例的多个线程会互斥执行该方法。
(2) 修饰静态方法
public static synchronized void staticMethod() {
    // 同步代码
}
  • 锁对象是当前类的 Class 对象(如 MyClass.class)。
  • 所有线程会互斥执行该静态方法。
(3) 修饰代码块
public void method() {
    synchronized (lockObject) {
        // 同步代码
    }
}
  • 锁对象是 lockObject,可以是任意对象。
  • 只有持有 lockObject 锁的线程可以执行代码块。

4. synchronized 的可见性

synchronized 不仅保证互斥性,还保证可见性:

  • 线程在释放锁时,会将工作内存中的共享变量刷新到主内存。
  • 线程在获取锁时,会从主内存中读取最新的共享变量值。

5. synchronized 的示例
示例 1:修饰实例方法
public class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}
  • 多个线程调用 increment() 方法时,会互斥执行,确保 count 的线程安全。
示例 2:修饰代码块
public class Counter {
    private int count = 0;
    private final Object lock = new Object();

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

    public int getCount() {
        return count;
    }
}
  • 使用 lock 对象作为锁,确保 count++ 的线程安全。

6. synchronized 的锁升级机制

在 Java 6 之后,synchronized 引入了锁升级机制,以提高性能:

  1. 无锁状态:对象未被任何线程锁定。
  2. 偏向锁:当只有一个线程访问同步代码时,JVM 会将锁标记为偏向锁,避免 CAS 操作。
  3. 轻量级锁:当多个线程竞争锁时,JVM 会将偏向锁升级为轻量级锁,使用 CAS 操作竞争锁。
  4. 重量级锁:当竞争激烈时,JVM 会将轻量级锁升级为重量级锁,线程会进入阻塞状态。
7.互斥锁性能优化

synchronized实现机制中,说到了它的底层是通过Monitor监视器的相关操作达成的,而Monitor监视器的相关指令,比如monitorenter/monitorexit是依赖于操作系统层面的Mutex Lock互斥锁来实现的。由于使用Mutex Lock需要将线程从用户态切到内核态,这种切换的代价比较大。与此同时,大部分使用synchronized锁的代码,在运行时很多时候其实是没有多线程并发的。这个时候单个线程还是要走Mutex Lock的话,就是性能的极大浪费。

因此从jdk1.6开始,Java对synchronized锁做了大量的优化,比如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术的使用,从而尽可能减少synchronized锁操作的开销。这些技术都是JVM自动帮我们做的优化措施,不需要显式使用,了解即可。

  • 锁粗化(Lock Coarsening) : 减少不必要的紧连在一起的unlock/lock操作,将多个连续的,使用同一把锁的同步代码块扩展成一个范围更大的同步代码块。
  • 锁消除(Lock Elimination) : 通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护。
逃逸分析是Java虚拟机的优化技术,它是一种用来分析对象的动态作用域的技术。比如说,当一个对象在方法里面被生成后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这就是方法逃逸;它还有可能被外部线程访问到,比如赋值给可以在其他线程中访问的实例变量,这就是线程逃逸;从 不逃逸,到 方法逃逸,再到 线程逃逸,反映的是对象由低到高的不同的逃逸程度。另外,JIT是即时编译,将那些在JVM启动之后运行过很多次的热点代码编译成本地方法直接执行而不再由JVM解释器执行。
  • 轻量级锁(Lightweight Locking) : 如果一个同步代码块,在运行期间虽然有多个线程会访问到,但很少会被同时访问,这个时候JVM会对synchronied做一个只用轻量级锁的优化。这种轻量级锁是相对与synchronied本身借助内核的互斥锁Mutex Lock而言的,互斥锁Mutex Lock被称为重量级锁。轻量级锁在获取锁的时候,并不直接使用内核互斥锁,而是采用一个CAS的原子指令来尝试在该锁的对象头上做一个标志,表示该锁被某线程获取到。如果获取失败,会自旋一定次数重复尝试,超过自旋次数仍然失败才会升级为重量级锁,进入阻塞状态。CASCompare And Swap的缩写,它是一种乐观锁,用来实现原子性指令,后续章节有介绍。
自适应自旋(Adaptive Spinning) : 当线程在获取轻量级锁的过程中执行CAS操作失败时,不会马上升级为重量级的 Mutex Lock,而是会走一个循环重复尝试,这就是自旋。如果尝试一定的次数后仍然没有成功的话,再去获取重量级的 Mutex Lock进入到阻塞状态。自适应的意思是自旋的次数不是固定的,是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋更多次数;但如果某个锁的自旋很少成功获得过锁,那么以后就有可能直接省略掉自旋。
  • 偏向锁(Biased Locking) : 轻量级锁虽然"轻",但仍然需要执行CAS自旋操作,这个操作还是要消耗CPU的。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除对内核互斥锁的使用,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏向锁的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

8. synchronized 的优缺点
优点:
  • 简单易用,语法直观。
  • 保证互斥性和可见性。
  • 自动释放锁,避免死锁。
缺点:
  • 性能开销较大,因为线程在获取锁失败时会进入阻塞状态。
  • 不支持超时机制,可能导致死锁。
  • 不支持中断,线程在等待锁时无法被中断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值