【Java】详解volatile和synchronized关键字

volatilesynchronized都是Java中用于控制并发的关键字,但是它们的使用场景和原理是不同的。

  1. volatile关键字:
    • 特点:volatile关键字主要有两个特性:保证变量的可见性和防止指令重排。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为线程间的共享变量值存在于主存,而每个线程都有自己的工作内存,线程间通信需要通过主存来完成。这可能导致一个线程在主存中修改了一个变量的值,另一个线程还继续使用它在工作内存中的变量值,造成数据的不一致。另外,volatile关键字可以防止指令重排。编译器优化的时候,不能够将其后面的代码排到其前面来执行。当要对一个volatile变量进行写操作时,JMM会先将操作执行,然后再执行volatile写操作,即不会进行指令重排。
    • 优点:volatile相对于synchronized是一种较为轻量级的同步策略,性能开销相对较小,线程访问volatile变量不会被阻塞。
    • 缺点:volatile无法保证复合操作的原子性。例如,count++操作实际上是分为三步执行的:读取count的值,对count加1,将新值写回。
    • 适用场景:当对变量的写入依赖于当前值,例如使用该变量做判断,或者写入的值与当前值无关时,可以使用volatile。例如,一个布尔标记位。
    • 不适用场景:当需要对多个操作进行原子性保证,或者需要对某些方法或者某些代码块进行同步控制的时候,不能够使用volatile
public class VolatileExample {
    private volatile int counter = 0;

    public void incrementCounter() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementCounter();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementCounter();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Counter: " + example.getCounter()); // 输出结果可能小于2000
    }
}

在上述示例中,尽管使用了volatile关键字,但是incrementCounter()方法并不是线程安全的,因为counter++操作不是原子性的,所以最后输出的结果可能小于2000。

  1. synchronized关键字:
    • 特点:synchronized关键字可以保证代码块或者方法的原子性,即在同一时刻,只有一个线程可以执行synchronized修饰的代码块或者方法。另外,它还可以保证变量的可见性和防止指令重排。当一个线程进入synchronized代码块或者方法时,其他线程不能进入,只有当该线程退出后,其他线程才能进入,这就保证了原子性。当一个线程退出synchronized代码块或者方法时,它对共享变量的修改会立即写入主存,当其他线程进入后,它会从主存读取共享变量的新值,这就保证了可见性。另外,编译器和处理器在执行synchronized代码块或者方法时,不会进行指令重排。
    • 优点:synchronized关键字可以保证代码块或者方法的原子性,可以解决多个线程同时访问共享资源导致的线程安全问题。
    • 缺点:synchronized关键字的性能开销较大,当一个线程进入synchronized代码块或者方法时,其他线程会被阻塞,可能导致线程的阻塞和唤醒带来大量的性能开销。
    • 适用场景:当需要对多个操作进行原子性保证,或者需要对某些方法或者某些代码块进行同步控制的时候,可以使用synchronized
    • 不适用场景:当只需要保证变量的可见性,而不需要保证原子性,或者对性能有较高要求的时候,不适合使用synchronized
public class SynchronizedExample {
    private int counter = 0;

    public synchronized void incrementCounter() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedExample example = new SynchronizedExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementCounter();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementCounter();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Counter: " + example.getCounter()); // 输出结果为2000
    }
}

在上述示例中,使用了synchronized关键字,incrementCounter()方法是线程安全的,因为synchronized关键字保证了counter++操作的原子性,所以最后输出的结果为2000。

你可能感兴趣的:(架构师之路,java,开发语言,volatile,synchronized)