一、synchronized保证线程安全的原理
- 对象锁机制:
- 当一个线程访问用
synchronized
修饰的方法或代码块时,它首先要获取该对象的锁(monitor)。每个对象都有一个与之关联的锁,这个锁在Java中被称为监视器(monitor)。
- 如果该对象的锁已经被其他线程持有,那么当前线程就会被阻塞,进入该对象的等待队列。只有当持有锁的线程释放了锁,等待队列中的线程才有机会竞争并获取锁,从而继续执行被
synchronized
保护的代码。
- 例如,假设有两个线程
ThreadA
和ThreadB
,如果ThreadA
先访问一个synchronized
修饰的方法,它会获取对象锁,在ThreadA
执行完该方法释放锁之前,ThreadB
试图访问同一个synchronized
方法时就会被阻塞。
- 方法修饰:
- 当
synchronized
修饰实例方法时,锁是当前对象实例(this
)。例如:
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 业务逻辑
}
}
- 当
synchronized
修饰静态方法时,锁是该类的Class
对象。因为静态方法属于类,所以锁是针对整个类级别的。例如:
public class SynchronizedStaticExample {
public static synchronized void staticSynchronizedMethod() {
// 业务逻辑
}
}
- 代码块修饰:
- 当
synchronized
修饰代码块时,可以指定锁对象。例如:
public class SynchronizedBlockExample {
private final Object lock = new Object();
public void synchronizedBlockMethod() {
synchronized (lock) {
// 业务逻辑
}
}
}
- 这样可以更细粒度地控制锁的范围,只有进入
synchronized
代码块时才需要获取锁,而不像修饰方法那样整个方法执行期间都持有锁,从而提高了并发性能。
二、synchronized在Java内存模型中对可见性、原子性和有序性的影响机制
- 可见性:
- 在Java内存模型中,每个线程都有自己的工作内存,线程对变量的操作都在工作内存中进行,而主内存是所有线程共享的。
- 当一个线程获取锁时,它会将主内存中的共享变量刷新到自己的工作内存中。当线程释放锁时,会将工作内存中的共享变量刷新回主内存。
- 例如,假设有一个共享变量
count
,线程ThreadA
在synchronized
代码块中修改了count
的值,当ThreadA
释放锁时,count
的新值会被刷新回主内存。此时,如果线程ThreadB
获取锁并进入synchronized
代码块,它会从主内存中获取到count
的最新值,从而保证了可见性。
- 原子性:
synchronized
关键字可以保证被它修饰的代码块或方法在同一时刻只有一个线程能够执行,这就确保了对共享资源的操作是原子性的。
- 例如,对于一个简单的自增操作
count++
,它实际上包含了读取count
的值、增加1、写回count
的值三个步骤,在多线程环境下如果不进行同步,可能会出现数据竞争问题。但是如果将这个操作放在synchronized
代码块或方法中,就可以保证这三个步骤是原子性的,不会被其他线程干扰。
- 有序性:
synchronized
关键字通过内存屏障来保证有序性。在获取锁时,会有一个“读屏障”,它会确保该线程之前的所有读操作都已经完成,并且之后的读操作不会被重排序到获取锁之前。在释放锁时,会有一个“写屏障”,它会确保该线程之前的所有写操作都已经完成,并且之后的写操作不会被重排序到释放锁之后。
- 例如,假设有以下代码:
public class SynchronizedOrderExample {
private int a;
private int b;
public synchronized void method() {
a = 1;
b = 2;
}
}
- 在释放锁时,
a = 1
和b = 2
的写操作不会被重排序到释放锁之后,从而保证了有序性。同样,在其他线程获取锁进入该方法时,之前的读操作也不会被重排序到获取锁之前,保证了操作的顺序正确性。