java 的synchronized和volatile

java代码安全性的三大要素

在Java并发编程中,原子性、可见性和有序性是保证代码安全性的三大要素。为了解决这些问题,Java提供了多个关键字和机制。

  1. 原子性是指一个或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。对于原子性,Java的synchronized关键字和Lock相关的工具类可以起到保障作用,它们可以确保整个过程中的操作要么全部完成,要么就都不完成,从而保证了原子性。
    以下是使用synchronized关键字的代码:
public class AtomicExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

  1. 可见性是指当一个线程修改了一个变量的值,其他线程能够立即看到这个修改。volatile关键字在Java中主要用于保证变量的可见性,它可以确保当一个线程修改了volatile变量的值,新值对于其他线程立即可见。

  2. 有序性是指程序中的语句按照代码的先后顺序依次执行。在Java中,可以使用synchronized关键字来保证有序性,因为它会锁定代码块,确保同一时刻只有一个线程能够访问该代码块,从而避免了编译优化和处理器指令重排导致的有序性问题。
    以下是使用volatile关键字的代码:

public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true;
    }

    public boolean checkFlag() {
        return flag;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        // 创建两个线程,分别设置和检查flag的值
        Thread t1 = new Thread(example::setFlag);
        Thread t2 = new Thread(() -> System.out.println("Flag value: " + example.checkFlag()));
        t1.start();
        t1.join();
        t2.start();
        t2.join();
    }
}

volatile解析

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
使用volatile关键字但可能导致原子性问题:
public class VolatileExample {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("最终结果:" + example.getCount());
    }
}

该段代码中的最后累加值并不等于预想的2000。

volatile禁止指令重排

禁止指令重排,是指在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排。这种重排可以分为源代码优化的重排、指令并行的重排、内存系统的重排等步骤。虽然这样的优化可以提高程序的运行效率,但在多线程环境下却可能导致问题,因为线程之间的执行顺序可能受到影响,从而影响程序的正确性。

在这种情况下,我们可以使用volatile关键字来禁止指令重排。volatile关键字可以确保变量的可见性和有序性,防止编译器和处理器对代码进行重新排序。这是通过内存屏障实现的,内存屏障是一个CPU指令,用于保证特定操作的执行顺序。

尽管synchronized关键字也保证了一定的有序性,但其与volatile关键字的实现原理和角度是不同的。synchronized是通过块与块之间看起来像原子操作来保证有序性的,而volatile则是在底层通过内存屏障防止指令重排,使变量前后的指令之间保持有序可见。
下面这段话摘自《深入理解Java虚拟机》:

== 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令 ==

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  • 它会强制将对缓存的修改操作立即写入主存;

  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

总结

  1. volatile关键字在某些情况下性能要优于synchronized
  2. volatile关键字无法保证操作的原子性

你可能感兴趣的:(java,开发语言,jvm)