synchronized与volatile关键字

1. synchronized关键字

  • synchronized的工作机制:
    • 锁机制:当一个线程进入被synchronized修饰的方法或代码块时,会获取到对象锁。如果另外一个线程进入到同步区域,该线程将会被阻塞。
    • 互斥性:synchronized保证在同一时刻只有一个线程可以执行同步代码块,保持数据一致性。
    • 内存可见性:当一个线程修改了共享变量之后,其他线程能看到这个更新的值。

2. volatile关键字

  • Java程序里的每个线程都会把变量拷贝到自己的工作内存里,以便更快访问。可问题来了:多个线程读写同一个变量时,如果各自藏着不同的副本,就好比几个人用自己的小本子记账,迟早对不上。
  • volatile做的第一件事,就是可见性。
    • 标记为volatile的变量,一旦某个线程修改了它,其他线程就能立刻看到最新的值。背后靠的是禁止把它缓存在线程工作内存,并强制把写操作刷新到主内存。
    • 第二件事是有序性,也叫“禁止指令重排序”。编译器和CPU会对指令做一些合法的优化,比如把步骤换个顺序以提高效率。volatile变量的读写会在它前后插入内存屏障(memory barrier),让这部分不能随便乱换顺序,从而让多线程观察到的执行顺序更靠谱。
  • volatile 主要用在这样的简单场景:
    • 开关控制:比如一个线程控制一个循环是否继续跑。线程A把keepRunning改成false,线程B就能马上看到并停止循环。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      volatile boolean keepRunning = true;

      // 线程A
      void stop() {
      keepRunning = false; // 告诉大家别跑了
      }

      // 线程B
      void run() {
      while (keepRunning) { // 每次都会检查最新的状态
      // ...干活...
      }
      // 线程A把keepRunning改成false后,这里能很快看到并停下来
      }
  • volatile不能保证复杂操作的原子性,比如count++这种读-改-写的操作,如果两个线程同时对一个volatilecountcount++,它们可能同时读取到旧值,然后各自加1,最后结果只增加了1,而不是2。

3. 区别

  • volatile只能用来修饰变量,每次使用这个变量只能去主内存中读取;synchronized可以用来修饰代码块和方法,本质上就是给这段代码块或者方法加锁,要想执行,必须先获得锁。
  • 由于指令的执行顺序是不可预知的,可能会发生指令重排问题,用volatile可以避免指令重排;synchronized是一种同步机制,可以用来解决线程安全问题。
  • volatile能够保证数据的可见性,但不能保证数据的原子性;synchronized两者都能保证。