在Java多线程编程中,volatile和synchronized是两个常用的关键字,用于保证共享变量的可见性和线程安全。虽然它们都可以用于实现线程安全,但是它们的运行机制和使用方式有很大不同。
1. volatile
Java中的volatile关键字用于修饰变量,表示该变量可能会被多个线程同时访问并修改,因此需要保证该变量的可见性。使用volatile修饰的变量,每次读取时都会从主内存中读取,每次修改时都会立刻写回主内存,而不会使用本地缓存。这样能够保证不同线程之间对该变量的修改是可见的。
优点:
- 简单有效:volatile关键字使用简单,可以轻松地保证变量的可见性,而无需使用复杂的同步机制。
- 轻量级:相比于synchronized关键字等同步机制,volatile更加轻量级,适用于简单的变量状态同步。
缺点:
- 不能保证原子性:volatile关键字只能保证变量的可见性,但不能保证变量操作的原子性,因此在涉及到多个变量操作的情况下,仍然需要额外的同步机制来保证原子性。
- 无法解决竞态条件:虽然volatile可以保证变量的可见性,但无法解决多线程之间的竞态条件问题,因此在一些需要原子性操作的情况下,仍然需要使用锁等同步机制来保证线程安全。
volatile关键字适用于那些不需要进行原子性操作,但需要保证可见性的变量场景,比如标识线程状态的变量、双重检查锁等。
下面是一个简单的示例,使用volatile保证i变量的可见性:
public class VolatileExample {
private volatile int i = 0;
public void increase() {
i++;
}
public int getI() {
return i;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
// 线程1:不断增加i的值
Thread thread1 = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increase();
}
});
// 线程2:输出i的值
Thread thread2 = new Thread(() -> {
while (example.getI() < 1000) {
// 可能会一直循环等待,直到i的值被线程1修改为1000
}
System.out.println("i的值为:" + example.getI());
});
// 启动线程
thread1.start();
thread2.start();
}
}
在这个示例中,我们创建了一个VolatileExample类,其中包含一个volatile修饰的变量i。在increase方法中,我们对i进行了增加操作;在getI方法中,我们返回i的当前值。在main方法中,我们创建了两个线程,线程1负责不断增加i的值,线程2负责输出i的值。由于i被volatile修饰,因此线程2可以正确地读取到线程1修改后的i的值,从而保证了变量i的可见性。
2. synchronized
Java中的synchronized关键字用于保证共享资源的互斥访问,即同一时间只有一个线程能够访问被synchronized修饰的代码块或方法。当某个线程获取到锁后,其他线程必须等待该线程释放锁之后才能继续执行。synchronized关键字可以用于保证线程安全,避免数据竞争和死锁等问题。
优点:
- 简单有效:synchronized关键字使用简单,通过在关键代码块或方法前加上synchronized关键字,即可实现线程安全。
- 自动释放锁:当线程执行完synchronized代码块或方法时,会自动释放锁,避免了手动管理锁的繁琐操作。
缺点:
- 性能开销较大:由于synchronized关键字会涉及到锁的获取和释放操作,因此在高并发情况下,会存在一定的性能开销。
- 可能导致死锁:如果在程序设计中使用不当,可能会导致死锁问题,即两个或多个线程相互等待对方持有的锁而无法继续执行。
synchronized关键字是保证线程安全的一种有效手段,特别适用于那些需要临界区保护的代码段,如对共享资源的读写操作等。然而,在使用synchronized关键字时,需要注意合理地设计锁粒度,以及避免可能导致死锁的情况发生。
下面是一个简单的示例,使用synchronized保证对count变量的操作是线程安全的:
public class SynchronizedExample {
private int count = 0;
// 使用synchronized修饰方法,保证线程安全
public synchronized void increment() {
count++;
}
// 使用synchronized修饰代码块,保证线程安全
public void incrementWithSyncBlock() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 线程1:不断增加count的值,使用synchronized修饰的方法
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
// 线程2:不断增加count的值,使用synchronized修饰的代码块
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.incrementWithSyncBlock();
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出count的值
System.out.println("count的值为:" + example.getCount());
}
}
在这个示例中,我们创建了一个SynchronizedExample类,其中包含一个count变量用于计数。我们演示了两种使用synchronized关键字的方式来保证线程安全:一种是修饰方法,另一种是修饰代码块。在main方法中,我们创建了两个线程分别执行增加count值的操作,一个线程使用synchronized修饰的方法,另一个线程使用synchronized修饰的代码块。通过synchronized关键字,我们可以确保两个线程对count的操作是线程安全的,避免了数据竞争的问题。
3. volatile和synchronized的区别
1.可见性:
- volatile关键字主要是用于保证共享变量的可见性。当多个线程同时访问某个共享变量时,如果不加volatile关键字,则可能会出现多个线程同时修改该变量但是其中的某些线程没有看到其他线程对该变量的修改。加上volatile后,该变量的值将被及时更新到主内存中,使得所有线程都能看到最新的值。
- synchronized关键字不仅保证了可见性,还保证了原子性和有序性。除了将变量从私有内存同步到主内存外,synchronized还会将线程工作内存中的数据与主内存中的数据进行同步,同时保证了代码块的原子性和有序性。
2.原子性:
- volatile关键字仅保证了被修饰变量的读写操作是原子性的。但是,对于复合操作(例如i++),volatile无法保证原子性,因为这是一个复合操作,包括读取、修改、写入三个步骤,并不能保证这三个步骤的连续性。
- synchronized关键字可以保证被修饰代码块的原子性,即同一时间只有一个线程能够执行该代码块,其他线程需要等待。
3.适用范围:
- volatile关键字适用于只有一个线程写,多个线程读的场景,例如标识位、状态标志等。
- synchronized关键字适用于临界区代码,需要保证原子性和有序性的场景,例如对共享资源的读写操作。
4.性能开销:
- volatile关键字的性能开销相对较低,因为它仅仅是对变量的可见性进行了保证。
- synchronized关键字的性能开销相对较高,因为它需要在进入和退出临界区时获取和释放锁,并且涉及到线程的上下文切换。