什么是synchronized关键字?
synchronized
是 Java 编程语言中的一个关键字,它提供了一种内置的锁机制来支持线程间的同步,从而防止多个线程同时访问某个类的实例变量或静态变量。当某个线程进入由 synchronized
保护的代码块或方法时,它会获得相应的锁,其他线程则无法同时进入该代码块或方法,直到锁被释放。
java内存的可见性问题
在了解synchronized关键字的底层原理之前,需要了解java的内存模型,看看synchronized关键字如何起作用的。
这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,它包含了控制器、运算器、缓 存等。同时Java内存模型规定,线程对共享变量的操作必须在自己的本地内存中进行,不能直接在主内 存中操作共享变量。这种内存模型会出现什么问题呢?
1. 线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地 内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,这时主内存及本地内存中的X的值都 为1。
2. 线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内 存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,此时主内存及本地内存B中的X 值为2,本地内存A中的X值为1。
3. 线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为 1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。
该问题Java内存模型是通过synchronized关键字和volatile关键字就可以解决,那么synchronized关键 字是如何解决的呢,其实进入synchronized块就是把在synchronized块内使用到的变量从线程的本地内 存中擦除,这样在synchronized块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获 取,解决了内存不可见问题。
synchronized的三大特性是什么?
-
可见性(Visibility):
当一个线程进入一个被synchronized
保护的代码块或方法时,它首先会获得监视器锁(monitor lock)。在获得锁的过程中,Java内存模型确保线程能够看到一个一致性的共享变量视图。这意味着,当一个线程释放锁时,它会将共享变量的修改刷新到主内存中,因此其他线程在随后获得锁时,能够看到一个已经被更新过的变量值。这保证了synchronized
块或方法内的共享变量修改的可见性。 -
原子性(Atomicity):
synchronized
块或方法内的代码是原子的,即一旦一个线程进入了synchronized
保护的区域,它就可以不受干扰地执行完该区域的代码。其他线程在此期间无法进入这个区域,直到当前线程释放锁。因此,对于synchronized
保护的代码块或方法,其内部的操作是原子的,不会被其他线程中断。这确保了代码执行的完整性,避免了原子性问题。 -
有序性(Ordering):
synchronized
还提供了一定程度的“Happens-Before”关系,这意味着在synchronized
块或方法内部的所有操作都必须在释放锁之前完成,并且这些操作对于随后获得同一个锁的线程是可见的。这确保了即使在Java内存模型允许指令重排序的情况下,synchronized
块或方法内部的代码执行顺序对于其他线程来说仍然是有序的。
synchronized可实现什么类型的锁?
- 悲观锁:
synchronized
实现的是一种悲观锁策略。悲观锁在数据处理过程中总是假设最坏的情况,即认为数据会在处理过程中被其他线程修改,因此每次访问共享资源时都会进行加锁操作,以确保数据的一致性。 - 非公平锁:
synchronized
默认实现的是非公平锁。非公平锁的特点在于,当多个线程尝试获取同一个锁时,并不会按照它们请求锁的顺序来分配锁,而是允许等待的线程以随机的方式获取锁。这种机制可能会产生线程饥饿现象,但可以提高系统的整体吞吐量。 - 可重入锁:
synchronized
也是可重入锁的实现。可重入锁的特点是,一个已经拥有锁的线程可以再次获取这个锁,而不会产生死锁。在Java中,如果一个线程进入了一个由synchronized
保护的同步块或方法,并且这个线程已经持有这个锁,那么它可以无限制地重新进入这个同步块或方法,而无需再次获取锁。 - 独占锁或排他锁:
synchronized
实现的锁是独占锁,这意味着在任何时候,只有一个线程可以持有这个锁。当一个线程持有锁时,其他试图进入同步块或方法的线程都会被阻塞,直到锁被释放。
synchronized关键字的使用方式
synchronized主要有三种使用方式:修饰普通同步方法、修饰静态同步方法、修饰同步方法块。
1. 修饰普通同步方法
public class SynchronizedInstanceMethod {
public synchronized void ordinarySynchronizedMethod() {
// 同步代码块,同一时间只有一个线程可以执行此方法
System.out.println("Ordinary synchronized method executed by thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
SynchronizedInstanceMethod obj = new SynchronizedInstanceMethod();
new Thread(() -> obj.ordinarySynchronizedMethod(), "Thread-1").start();
new Thread(() -> obj.ordinarySynchronizedMethod(), "Thread-2").start();
}
}
2. 修饰静态同步方法
public class SynchronizedStaticMethod {
public static synchronized void staticSynchronizedMethod() {
// 同步代码块,同一时间只有一个线程可以执行此方法
System.out.println("Static synchronized method executed by thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(() -> SynchronizedStaticMethod.staticSynchronizedMethod(), "Thread-1").start();
new Thread(() -> SynchronizedStaticMethod.staticSynchronizedMethod(), "Thread-2").start();
}
}
3. 修饰同步方法块
public class SynchronizedBlock {
private final Object lock = new Object();
public void synchronizedBlockMethod() {
// 同步代码块,同一时间只有一个线程可以执行这个代码块
synchronized (lock) {
System.out.println("Synchronized block executed by thread: " + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
SynchronizedBlock obj = new SynchronizedBlock();
new Thread(() -> obj.synchronizedBlockMethod(), "Thread-1").start();
new Thread(() -> obj.synchronizedBlockMethod(), "Thread-2").start();
}
}
在以上三个示例中:
- 第一个示例中,
ordinarySynchronizedMethod
方法被synchronized
修饰,它对整个方法体进行同步,保证了同一时间只有一个线程能够访问这个方法。 - 第二个示例中,
staticSynchronizedMethod
是一个静态同步方法,它对整个静态方法体进行同步,保证了同一时间只有一个线程能够访问这个类的静态同步方法。 - 第三个示例中,
synchronizedBlockMethod
方法中有一个同步代码块,它使用synchronized(lock)
对特定的lock
对象进行同步,保证了同一时间只有一个线程能够执行这个同步代码块。这里使用了一个私有的Object
类型的lock
变量作为锁对象,而不是直接同步在方法或类上,这提供了更大的灵活性。