synchronized

synchronized

1. synchronized 的使用方法

  • 同步方法:在方法声明时加上 synchronized 关键字,这样当某个线程调用这个方法时,会先获取到该方法的锁(通常是该方法所属对象的锁),其他线程必须等待锁被释放后才能调用这个方法。
  • 同步代码块:使用 synchronized(对象) 来定义一个同步代码块,这里的对象就是锁对象。当线程进入这个代码块时,会先尝试获取这个对象的锁,获取到锁后才能执行代码块中的代码。

2. synchronized 的实现机制

  • 方法级同步:对于同步方法,JVM 在方法的常量池中添加一个 ACC_SYNCHRONIZED 标志。当线程调用这个方法时,会检查这个标志,如果设置了该标志,则需要先获取到方法的锁(通常是该方法所属对象的监视器锁),然后开始执行方法,方法执行完毕后再释放锁。
  • 代码块级同步:对于同步代码块,JVM 使用 monitorentermonitorexit 两条字节码指令来实现同步。monitorenter 指令用于获取锁,monitorexit 指令用于释放锁。每个对象都有一个监视器锁(monitor),当线程执行到 monitorenter 指令时,会尝试获取对象的监视器锁,如果获取成功,则计数器加一;当线程执行到 monitorexit 指令时,计数器减一。当计数器为 0 时,表示锁已经被释放,其他线程可以获取锁。

3. Monitor(监视器)

  • Monitor 是 Java 中用于实现同步的一种机制,它可以看作是一个特殊的对象,这个对象包含了一个特殊的房间(Entry Set)和一个等待房间(Wait Set)。
  • 当线程尝试获取对象的锁时,它会在 Entry Set 中等待,直到锁被释放。
  • 如果线程在持有锁的过程中因为某些原因被挂起(比如调用了 wait() 方法),那么它会被移到 Wait Set 中,等待其他线程唤醒它(比如调用 notify()notifyAll() 方法)。
  • Monitor 保证了同一时间只有一个线程可以访问被保护的数据和代码。

4. synchronized 的特性

  • 互斥性:同一时间点,只有一个线程可以获得锁,获得锁的线程才能处理被 synchronized 修饰的代码片段。
  • 阻塞性:只有获得锁的线程才能执行被 synchronized 修饰的代码片段,未获得锁的线程只能阻塞,等待锁释放。
  • 可重入性:如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的。这是因为 JVM 会维护一个锁计数器,当同一个线程多次获取锁时,计数器会递增;当释放锁时,计数器会递减,直到计数器为 0 时,锁才会被真正释放。

综上所述,synchronized 通过在方法或代码块级别添加同步机制,利用对象的监视器锁来保证线程安全。它是 Java 中实现线程同步的一种简单而有效的手段。

synchronized 是 Java 中用于保证线程安全的关键字,它通过一系列的机制来保证原子性、可见性和有序性。下面是对这三个方面的详细解释:

synchronized特性

1. 原子性

原子性是指一个操作是不可中断的,即该操作要么全部执行,要么全部不执行。在并发编程中,原子性用于保证某个操作在执行过程中不会被其他线程打断。

在 Java 中,synchronized 通过 monitorentermonitorexit 这两个字节码指令来保证原子性。当一个线程进入 synchronized 修饰的方法或代码块时,它会先尝试获取锁(通过 monitorenter 指令)。如果获取成功,则继续执行后续的代码;如果获取失败(因为锁已被其他线程持有),则该线程会被阻塞,直到锁被释放(通过 monitorexit 指令)为止。

由于 synchronized 保证了同一时间只有一个线程能够持有锁并执行相应的代码,因此它也就保证了这段代码在执行过程中的原子性。即使由于时间片耗尽或其他原因导致线程被中断,只要锁还没有被释放,该线程在下一次获得时间片时仍然会继续执行剩余的代码,直到完成。

2. 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。

Java 内存模型(JMM)规定了所有的变量都存储在主内存中,而每个线程都有自己的工作内存(也称为线程本地存储)。线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存。这可能导致线程1修改了某个变量的值,但线程2由于还没有从主内存中刷新该变量的副本,因此看不到修改后的值。

synchronized 关键字通过确保在进入同步块或同步方法时获取锁,并在退出时释放锁,来实现对变量的可见性保证。具体来说,当一个线程持有锁并执行同步代码块时,它会将自己工作内存中的变量副本更新到主内存中(在写操作时)。当其他线程尝试进入该同步代码块时,它们会先获取锁,并在获取锁后从主内存中读取最新的变量值到自己的工作内存中(在读操作时)。这样,就保证了线程之间对共享变量的可见性。

3. 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。然而,由于硬件和编译器的优化,指令可能会被重排序以提高性能。这种重排序在单线程环境下通常不会改变程序的执行结果,但在多线程环境下可能会导致问题。

Java 提供了 as-if-serial 语义来确保单线程程序的有序性。该语义要求编译器和处理器在优化时不能改变单线程程序的执行结果。然而,在多线程环境下,as-if-serial 语义并不能完全保证有序性。

为了解决这个问题,synchronized 关键字通过确保同一时间只有一个线程能够执行同步代码块来提供有序性保证。由于同步代码块在同一时间只能被一个线程执行,因此可以认为该代码块内的指令是按照它们在代码中出现的顺序执行的。这避免了由于指令重排序而导致的多线程问题。

总结来说,synchronized 通过确保同一时间只有一个线程能够执行同步代码块来提供原子性、可见性和有序性保证。这些特性使得 synchronized 成为 Java 中实现线程安全的一种重要手段。

锁升级

这篇文章的核心内容是介绍了Java中synchronized关键字的锁升级过程,主要包括无锁、偏向锁、轻量级锁和重量级锁四种状态。以下是对这些核心内容的简要概述:

1.无锁状态:

  • 当一个线程第一次访问一个对象的同步块时,JVM会在对象头中设置该线程的Thread ID,并将对象头的状态位设置为“偏向锁”。

2.偏向锁:

  • 当一个synchronized块被线程首次进入时,锁对象会进入偏向模式。
  • 偏向锁模式下,锁会偏向于第一个获取它的线程,JVM会在对象头中记录该线程的ID作为偏向锁的持有者。
  • 如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程ID相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态。
  • 触发条件:首次进入synchronized块时自动开启,假设JVM启动参数没有禁用偏向锁。
  • 注意:在JDK 15中,偏向锁已被废除。

3.轻量级锁:

  • 当有另一个线程尝试获取已被偏向的锁时,偏向锁会被撤销,锁会升级为轻量级锁。
  • 在轻量级锁状态中,JVM为对象头中的Mark Word预留了一部分空间,用于存储指向线程栈中锁记录的指针。
  • 当一个线程尝试获取轻量级锁时,JVM会:
    1. 将对象头中的Mark Word复制到线程栈中的锁记录(Lock Record)。
    2. 尝试通过CAS操作更新对象头的Mark Word。
  • 如果替换成功,则该线程获取锁成功;如果失败,则表示已经有其他线程获取了锁,则该锁状态就会升级到重量级锁状态。
  • 触发条件:当有另一个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。

4.重量级锁:

  • 当轻量级锁的CAS操作失败,即出现了实际的竞争,锁会进一步升级为重量级锁。
  • 当锁状态升级到重量级锁状态时,JVM会将该对象的锁变成一个重量级锁,并在对象头中记录指向等待队列的指针。
  • 如果一个线程想要获取该对象的锁(当前对象已被其他线程锁定时),则需要先进入等待队列,等待该锁被释放。当锁被释放时,JVM会从等待队列中选择一个线程唤醒,并将该线程的状态设置为“就绪”状态,然后等待该线程重新获取该对象的锁。
  • 触发条件:当轻量级锁的CAS操作失败,轻量级锁升级为重量级锁。
 wechat
天生我才必有用