目录
synchronized简介
synchronized实现原理
对象锁(monitor)机制
synchronized的happens-before关系
锁获取和锁释放的内存语义
synchronized优化
CAS操作
什么是CAS?
CAS的操作过程
扩展知识点
CAS的问题
Java对象头
偏向锁
轻量级锁
重量级锁
各种锁的比较
例子
Java中的关键字synchronized用于实现线程之间的同步。它可以用于方法或代码块,用于保证在同一时间只有一个线程可以访问被synchronized修饰的方法或代码块。
对于方法而言,可以使用synchronized修饰整个方法,也可以使用synchronized修饰方法内的某个代码块。
当多个线程同时访问一个被synchronized修饰的方法或代码块时,只有一个线程能够获取到锁,并执行该方法或代码块,其他线程则需要等待。
synchronized关键字的使用可以有效地解决多线程并发访问共享资源时可能出现的数据不一致和线程安全性问题。通过保证同一时间只有一个线程访问共享资源,可以避免竞态条件和数据竞争等并发问题。
需要注意的是,synchronized关键字只能用于同一个JVM内的线程之间的同步,对于分布式系统中的线程同步,需要使用其他的机制来实现。
synchronized实现原理主要包括以下几个关键点:
需要注意的是,synchronized关键字提供的是独占锁(排它锁),即同一时间只有一个线程能够获取到锁并执行临界区的代码。这种互斥访问机制确保了线程安全性,避免了多个线程同时访问共享资源造成的数据竞争和不一致性。
总结来说,synchronized通过对象监视器和内部锁实现了线程的互斥访问和同步操作。它提供了一种简单而有效的方式来实现线程安全性,并且具备可重入特性,能够避免死锁问题的发生。
在Java中,每个对象都有一个与之关联的Monitor对象,也称为对象锁,用于实现对象级别的同步。
Monitor对象内部维护了一个计数器和一个等待队列。计数器用于记录当前对象被锁定的次数,等待队列用于存放等待获取锁的线程。当一个线程进入synchronized代码块或方法时,它会尝试获取该对象关联的Monitor的锁。
如果锁是可用的,那么该线程会获得锁,并且进入临界区执行代码;如果锁被其他线程持有,那么该线程就会进入Monitor的等待队列中。等待队列内部维护了多个等待线程,这些线程都处于阻塞状态,等待被唤醒。
当持有锁的线程执行完临界区的代码后,会释放Monitor的锁,并唤醒等待队列中的一个线程。被唤醒的线程将进入就绪状态,并重新尝试获取Monitor的锁。如果获取成功,那么该线程就可以进入临界区执行代码;如果获取失败,那么线程将再次进入等待队列,等待下一次的唤醒。
需要注意的是,synchronized通过Monitor和内部锁来实现线程之间的互斥访问和同步操作,确保了多个线程对共享资源的安全访问。如果多个线程同时访问同一个对象的synchronized代码块或方法,那么只有一个线程可以进入临界区执行代码,其他线程将被阻塞在Monitor的等待队列中,等待获取锁。
总之,对象锁(Monitor)机制是Java中实现线程同步和互斥访问的重要机制之一,它通过锁定对象来保证线程之间的互斥性和同步性,避免了多个线程对共享资源的竞争和不一致性。
下面是一个使用Java对象锁(监视器/Monitor)机制的示例代码,
public class Main {
private int count = 0;
private Object lock = new Object(); // 定义一个对象作为锁
public void increment() {
synchronized (lock) { // 使用lock对象进行同步
count++; // 临界区代码,对共享资源进行操作
}
}
public void decrement() {
synchronized (lock) {
count--; // 临界区代码,对共享资源进行操作
}
}
}
上述示例中,我们使用了 synchronized 关键字和 lock 对象实现了对象锁(Monitor)机制。具体解释如下:
synchronized关键字可以确保线程之间的 happens-before关系。happens-before是 Java 并发编程中的一个重要概念,用于描述并发操作的顺序性。
下面是关于 synchronized的 happens-before 关系的一些规则:
1. 当线程 A 释放一个 synchronized 块的锁时,随后线程 B 获取同一个锁。这个释放的动作 happens-before 后续获取锁的操作。这保证了线程 B 看到在线程 A 中所做的任何修改。
2. 当线程 A 启动线程 B(通过 Thread.start() 方法)时,线程 B 中的任何操作 happens-after 线程 A 对于同一个 synchronized 块的所有先前操作。这使得在线程 B 中看到线程 A 所做的初始化工作。
3. 当线程 A 完成一个构造器,并将该对象引用交给线程 B 时(如通过一个共享变量或者通过方法),线程 B 看到线程 A 在构造器中所有的写操作。这个规则适用于使用 synchronized 或 volatile 等方式确保可见性的情况。
这些规则确保了在多线程环境下,通过 synchronized 同步的代码块或方法可以建立 happens-before 关系,从而提供有序性和可见性,避免数据竞争和不一致的结果。
请注意,synchronized 并不是唯一确保 happens-before 关系的机制,还有其他的同步机制和内存模型规则,如 volatile 变量、Lock 接口等。合理的使用这些机制可以确保多线程环境下的正确性和一致性。
对于synchronized加锁操作:
对于synchronized释放锁操作:
由此可见,synchronized关键字的内存语义确保了共享变量值的可见性。当线程A释放锁时,它所做的修改将对线程B可见。而在线程B获取锁之前,它会从主内存中获取最新的共享变量值,以确保对最新值的操作。
这种基于Java内存抽象模型的synchronized的内存语义,提供了一种可靠的线程间通信机制,确保了共享变量的一致性和可见性,避免了数据竞争和不一致的结果。
在聊到锁的优化也就是锁的几种状态前,有两个知识点需要先关注:(1)CAS操作 (2)Java对象头,这是理解下面知识的前提条件。
CAS(Compare and Swap)是一种乐观锁策略,用于实现无锁并发操作。它假设多个线程访问共享资源时不会产生冲突,并通过比较并交换的方式来检测冲突并解决。
使用锁的悲观锁策略认为每次执行临界区代码都可能产生冲突,因此获取锁的线程会阻塞其他线程的访问。而CAS操作则采用乐观的思想,假设不会出现冲突,不会阻塞其他线程的操作。当发生冲突时,CAS会尝试比较当前内存中的值与期望值,如果相等,说明没有冲突,将新值写入内存;如果不相等,说明有其他线程已经修改了内存中的值,需要重试当前操作直到成功为止。
CAS操作通常包含三个参数:需要操作的内存位置(或称为变量)、期望值和新值。它在原子性的基础上,通过比较当前值与期望值是否相等来判断是否出现冲突,并根据结果进行相应的处理。CAS操作可以避免传统锁机制中的竞争和阻塞,提高并发度和性能,但也需要考虑冲突处理和重试机制。
总结来说,CAS是一种乐观锁策略,假设多个线程访问共享资源时不会出现冲突,并使用比较交换的方式来判断并解决冲突。它可以实现无锁并发操作,避免了阻塞停顿的状态,提高了程序的性能和并发度。
CAS(Compare and Swap)操作的具体过程如下:
1. 读取:首先,从内存中读取需要操作的变量的当前值,将其保存为当前值A。
2. 比较:然后,将当前值A与预期值O进行比较。
3. 判断:如果当前值A与预期值O相等,则说明没有其他线程修改过该变量的值。
4. 交换:在比较相等的情况下,CAS会尝试使用新值N来更新内存中的变量。这个更新操作是原子的,即不会被其他线程中断或修改。
5. 判断更新结果:如果更新成功,表示CAS操作成功,返回更新后的值。如果更新失败,表示有其他线程在当前线程读取值之后修改了变量的值,CAS操作需要重新进行。
6. 重试:在更新失败的情况下,CAS操作可能会重新读取当前的值,并再次尝试比较和交换操作,直到成功为止。
需要注意的是,CAS操作是乐观锁的一种实现方式,它假设操作不会发生冲突并进行操作,只有在实际发生冲突时才会进行重试。CAS操作是非阻塞的,即操作不会阻塞线程,因此可以提高并发性能。
元老级的synchronized(未优化前)最主要的问题是:
元老级的synchronized(未优化前)主要问题是阻塞同步带来的性能问题,而CAS则是一种非阻塞同步的方式。
在传统的synchronized中,当多个线程竞争同一个锁时,其中一个线程获得了锁,其他线程将被阻塞,等待锁的释放。这种阻塞和唤醒操作会引入线程上下文切换以及锁竞争带来的开销,导致性能下降。相比之下,CAS(Compare and Swap)是一种非阻塞同步的原子操作。它不会像 synchronized 那样直接阻塞线程,而是通过比较内存中的预期值和当前值是否相等来判断是否发生了竞争。如果没有竞争,CAS会原子地将新值写入内存。如果发生了竞争,CAS不会使线程挂起,而是进行一定次数的尝试,直到成功为止。这种非阻塞的特性使得CAS在高并发环境下能够更好地提高性能。
因此,synchronized是阻塞同步(互斥同步),而CAS是非阻塞同步。CAS避免了线程挂起和唤醒的性能开销,因此在一些高并发场景下可以更加高效地实现并发控制。
CAS(Compare and Swap)是一种非阻塞同步的原子操作,它避免了线程阻塞和唤醒的开销,因此在高并发场景中具有一定的优势。但是,CAS也存在以下一些问题:
Java对象头中的Mark Word确实会存储对象的hashcode、年龄值和锁标志位等信息,这些信息会被虚拟机用于支持Java的运行时环境和垃圾回收器。
在Java SE 1.6中,锁的状态分为无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。它们的升级顺序是:无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态。当多个线程竞争同一个锁时,锁的状态就会逐渐升级到更高的级别,以保证锁的正确性和效率。但是,一旦锁的状态被升级,就不能再降级了。
具体而言,Java对象头中的锁标志位会随着锁状态的变化而发生改变。例如,在偏向锁状态下,锁标志位会指向持有该锁的线程ID,以标识该对象是否偏向于某个特定的线程。在轻量级锁状态下,锁标志位会指向对象头中的锁记录(Lock Record),以记录获得该锁的线程信息。在重量级锁状态下,锁标志位会指向Monitor对象(Monitor Object),以表示该对象已经被多个线程竞争过程中,有一个线程已经获得了锁,其他线程需要阻塞等待。
有了CAS操作和Java对象头的基础知识,我们可以谈论几种与synchronized相关的锁的优化策略:
偏向锁是Java中一种针对单线程访问同步块优化的锁机制。它的目标是在无竞争的情况下消除同步操作的开销,提高程序的性能。
当一个线程访问同步块时,如果该同步块的对象没有被其他线程访问过,那么该线程会尝试获取对象的偏向锁,并将对象头中的Mark Word的锁标志位设置为偏向锁状态。同时,Mark Word中还会记录持有该锁的线程ID。
之后,如果再有其他线程来访问同步块,它会先检查对象的Mark Word中的锁标志位。如果发现对象的偏向锁已经被其他线程占用,那么这个线程会通过CAS(Compare and Swap)操作尝试获取锁,失败的话就升级为轻量级锁或重量级锁。但如果发现对象的偏向锁还是当前线程占有,那么就直接进入同步块,无需进行额外的加锁操作。
偏向锁的优点是减少了多线程竞争的开销,避免了无竞争情况下的同步操作。这对于大部分情况下由同一个线程来访问同步块的场景非常有效。然而,如果存在多线程竞争的情况,偏向锁就会失效,需要升级为轻量级锁或重量级锁。
需要注意的是,偏向锁并非适用于所有情况,它的性能优势在于无竞争的场景下,而在多线程竞争激烈的情况下,使用偏向锁可能会增加额外的开销。因此,在实际开发中,需要根据具体的场景和并发情况来选择是否使用偏向锁。
轻量级锁是一种线程同步的机制,用于提高多线程并发操作的性能。当一个线程尝试获得锁时,会将对象头中的Mark Word复制到线程栈帧中的一个用于存储锁记录的空间,并使用CAS(Compare and Swap)操作将对象头中的Mark Word替换为指向锁记录的指针。如果CAS成功,表示当前线程成功获取了锁,可以进入临界区执行操作。如果CAS失败,表示其他线程已经获得了锁或者存在竞争,当前线程会尝试使用自旋来等待锁的释放。
在轻量级锁状态下,线程之间会进行自旋,不会立即阻塞线程,以减少线程切换的开销。自旋的目的是期望持有锁的线程能够快速释放锁,从而让当前线程能够顺利获取锁,继续执行。自旋过程中,线程会不断尝试CAS操作来获取锁,如果成功则获得锁并进入临界区,如果失败则继续自旋。
轻量级解锁是通过原子的CAS操作将保存在锁记录中的Displaced Mark Word替换回对象头中的Mark Word。如果CAS操作成功,表示当前线程成功释放了锁,其他线程就有机会获取锁。如果CAS操作失败,表示有其他线程尝试获取锁,当前线程会将锁升级为重量级锁。一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
总结来说,轻量级锁通过自旋等待锁的释放,减少线程的阻塞和唤醒带来的开销,提高多线程并发操作的性能。但如果自旋时间过长或者存在较多的竞争,锁可能会升级为重量级锁,采用阻塞和唤醒操作来实现线程同步。
需要注意的是,轻量级锁的效果取决于资源竞争的程度。当并发程度较低时,轻量级锁的效果优于重量级锁;但当并发程度较高时,轻量级锁会导致大量的自旋操作,浪费CPU时间,此时重量级锁效果更好。
重量级锁是一种线程同步的机制,用于保证临界区的互斥访问。当一个线程尝试获得锁时,会进入阻塞状态,等待锁被释放。在重量级锁的实现中,JVM会为每个锁对象创建一个监视器(monitor)对象,用于协调多个线程之间的同步。
重量级锁之所以称之为“重量级”是因为它需要使用操作系统提供的线程阻塞和唤醒功能来进行线程同步,这需要涉及用户态和内核态之间的切换,开销较大。而轻量级锁则是基于CAS操作实现的,不需要进入内核态,开销较小。
当多个线程竞争同一个锁时,除了第一个成功获取锁的线程外,其他线程都会进入阻塞状态,等待持有锁的线程释放锁。被阻塞的线程会进入一个等待队列中,JVM使用操作系统提供的条件变量来实现线程的阻塞和唤醒。当持有锁的线程释放锁之后,JVM会从等待队列中选择一个线程唤醒,然后将其移动到就绪队列中,允许其参与到线程调度中来。
总的来说,重量级锁通过使用操作系统提供的线程阻塞和唤醒功能来实现线程同步,保证多个线程能够互斥访问临界区。但由于涉及到用户态和内核态之间的切换开销较大,在多线程并发操作中可能会影响程序的性能。因此在实际开发中,应该根据具体情况选择使用轻量级锁还是重量级锁。
偏向锁、轻量级锁和重量级锁是Java中用于实现线程同步的不同机制,它们在性能和适用场景上有所区别。
偏向锁:
轻量级锁:
重量级锁:
总体来说,偏向锁适用于单线程场景,轻量级锁适用于少量线程交替访问的场景,而重量级锁适用于多个线程竞争同一个锁的场景。使用合适的锁机制可以提高程序的性能和并发性。在实际开发中,应根据具体需求和并发情况选择合适的锁机制。
以下是一个简单的Java代码示例,演示了偏向锁、轻量级锁和重量级锁的使用:
public class Main {
private static int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 偏向锁示例
synchronized (lock) { // 第一个线程获取锁
System.out.println("偏向锁示例:");
count++;
System.out.println("计数: " + count);
Thread.sleep(2000); // 模拟长时间持有锁
// 另一个线程尝试获取锁,由于偏向锁的存在,直接获得锁而无需竞争
new Thread(() -> {
synchronized (lock) {
System.out.println("偏向锁被自动升级为轻量级锁");
count--;
System.out.println("计数: " + count);
}
}).start();
Thread.sleep(1000);
}
// 轻量级锁示例
synchronized (lock) { // 第一个线程获取锁
System.out.println("\n轻量级锁示例:");
count++;
System.out.println("计数: " + count);
// 另一个线程尝试获取锁,进入自旋等待
new Thread(() -> {
synchronized (lock) { // 自旋等待锁的释放
System.out.println("轻量级锁获得成功");
count--;
System.out.println("计数: " + count);
}
}).start();
Thread.sleep(1000); // 模拟自旋等待时间
}
// 重量级锁示例
synchronized (lock) { // 第一个线程获取锁
System.out.println("\n重量级锁示例:");
count++;
System.out.println("计数: " + count);
// 多个线程竞争锁,其中一个线程进入阻塞等待
new Thread(() -> {
synchronized (lock) { // 竞争锁失败,进入阻塞等待
System.out.println("重量级锁获得成功");
count--;
System.out.println("计数: " + count);
}
}).start();
Thread.sleep(1000); // 模拟阻塞等待时间
}
}
}
在这个示例中,我们使用一个共享的lock对象作为锁。在每个部分中,首先通过synchronized关键字来获取锁。
通过以上示例,我们可以看到不同类型锁在不同情况下的行为和效果。请注意,示例中的线程调度和执行顺序可能会有所不同,因此具体的输出结果可能会有所偏差。