骑驴找蚂蚁

全干工程师

Java-Logo

Java线程中的内存一致性

Java作为一种通用且广泛使用的编程语言,提供了对多线程的支持,允许开发人员创建可同时执行多个任务的并发应用程序。然而,并发带来好处的同时也带来了挑战,其中一个需要考虑的关键方面就是Java线程的内存一致性。

在多线程环境中,多个线程共享同一个内存空间,从而导致与数据可见性和一致性相关的潜在问题。内存一致性是指多个线程之间内存操作的顺序和可见性。在Java中,Java内存模型(JMM)定义了线程如何与内存交互的规则和保证,确保了一定程度的一致性,从而实现可靠和可预测的行为。

Java 中的内存一致性是如何工作的?

理解内存一致性需要掌握原子性、可见性和操作排序等概念。让我们深入了解这些方面,以获得更清晰的认识。

  • 原子性
  • 可见性
  • 排序
  • 线程同步
  • 内存一致性保证
  • 管理内存一致性的实用技巧

原子性

在多线程环境中,原子性是指操作的不可分割性。原子操作指的是瞬时发生的操作,没有来自其他线程的任何交错操作。在Java中,某些操作(如读取或写入原始变量(long 和 double 除外))保证是原子操作。但是,复合操作(如递增非易失性 long)则不是原子操作。

下面是一个演示原子性的代码示例:

public class AtomicityExample {

  private int counter = 0;
  public void increment() {
	  counter++; // long 或 double 不是原子的
  }
  public int getCounter() {
	  return counter; // int 的原子(以及除 long 和 double 之外的其他基本类型)
  }
}

对于 long 和 double 的原子操作,Java 提供了java.util.concurrent.atomic包,其中包含AtomicLongAtomicDouble等类,如下所示:

import java.util.concurrent.atomic.AtomicLong;

public class AtomicExample {
  private AtomicLong atomicCounter = new AtomicLong(0);
  public void increment() {
	  atomicCounter.incrementAndGet(); // Atomic operation
  }

  public long getCounter() {
	  return atomicCounter.get(); // Atomic operation
  }
}

可见性

可见性是指一个线程对共享变量所做的更改是否对其他线程可见。在多线程环境中,线程可能会在本地缓存变量,从而导致一个线程所做的更改其他线程无法立即看到。为了解决这个问题,Java提供了volatile关键字。

public class VisibilityExample {

  private volatile boolean flag = false;
  public void setFlag() {
	  flag = true; // 立即对其他线程可见
  }
  public boolean isFlag() {
	  return flag; // 始终从内存中读取最新值
  }
}

使用volatile可以确保任何读取变量的线程都能看到最新写入的内容。

排序

排序与操作的执行顺序有关。在多线程环境中,不同线程执行语句的顺序不一定与代码中编写语句的顺序一致。Java内存模型定义了建立happens-before关系的规则,以确保一致的操作顺序。

public class OrderingExample {

  private int x = 0;
  private boolean ready = false;
  public void write() {
	  x = 42;
	  ready = true;
  }

  public int read() {
	  while (!ready) {
		  // 等待至准备就绪
	  }
	  return x; // 由于发生happens-before的关系,保证看到写的内容
  }

}

通过理解原子性、可见性和排序这些基本概念,开发人员可以编写线程安全的代码,并避免与内存一致性相关的常见陷阱。

线程同步

Java提供同步机制来控制对共享资源的访问并确保内存一致性。两种主要的同步机制是同步方法/块和 java.util.concurrent包。

同步方法和模块

同步关键字确保一次只能有一个线程执行同步方法或块,从而防止并发访问并保持内存一致性。下面是一个简短的代码示例,演示如何在Java中使用synchronized关键字:

public class SynchronizationExample {

  private int sharedData = 0;

  public synchronized void synchronizedMethod() {

	  // 安全地访问和修改共享数据

  }
  public void nonSynchronizedMethod() {

	  synchronized (this) {

	  // 安全地访问和修改共享数据

	  }
  }

}

虽然同步提供了实现同步的直接方法,但由于其固有的锁定机制,在某些情况下可能会导致性能问题。

java.util.concurrent包

java.util.concurrent包引入了更灵活、更细化的同步机制,如LocksSemaphoresCountDownLatch。这些类提供了更好的并发控制,比传统的同步更有效。

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {

  private int sharedData = 0;
  private Lock lock = new ReentrantLock();
  public void performOperation() {
	  lock.lock();
	  try {
		  // 安全地访问和修改共享数据
	  } finally {
		  lock.unlock();
	  }
  }
}

使用锁可以对同步进行更精细的控制,在传统同步过于粗放的情况下,可以提高性能。

内存一致性保证

多线程程序中的操作具有一致性和可预测的执行顺序:

  1. 程序顺序规则: 线程中的每个操作都发生在该线程中程序顺序靠后的每个操作之前。
  2. 监控器锁定规则: 监视器上的解锁发生在该监视器随后的每次锁定之前。
  3. Volatile变量规则:对volatile字段的写入发生在随后对该字段的每次读取之前。
  4. Thread Start规则: 在线程上调用Thread.start会发生在启动线程的任何操作之前。
  5. Thread Termination规则: 线程中的任何操作都会在其他线程检测到该线程已终止之前发生。

管理内存一致性的实用技巧

在介绍了基础知识后,让我们来探讨一些在 Java 线程中管理内存一致性的实用技巧。

合理使用volatile

volatile虽然能确保可见性,但并不能为复合操作提供原子性。对于不需要考虑原子性的简单标志或变量,应谨慎使用volatile

public class VolatileExample {

  private volatile boolean flag = false;

  public void setFlag() {
	  flag = true; // 其他线程立即可见,但不是原子性的
  }

  public boolean isFlag() {
	  return flag; // 始终从内存中读取最新值
  }
}

采用线程安全集合

Javajava.util.concurrent包中提供了常用集合类的线程安全实现,如ConcurrentHashMapCopyOnWriteArrayList。在许多情况下,使用这些类可以消除显式同步的需要。

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
  private Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
  public void addToMap(String key, int value) {
	  concurrentMap.put(key, value); // 线程安全操作
  }

  public int getValue(String key) {
      return concurrentMap.getOrDefault(key, 0); // 线程安全操作
  }
}

原子操作的原子类

要对int和long等变量进行原子操作,可考虑使用java.util.concurrent.atomic包中的类,如AtomicIntegerAtomicLong

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
  private AtomicInteger atomicCounter = new AtomicInteger(0);
  public void increment() {
	  atomicCounter.incrementAndGet(); // 原子操作
  }

  public int getCounter() {
	  return atomicCounter.get(); // 原子操作
  }
}

与其在同步方法中使用粗粒度同步,不如考虑使用细粒度锁来提高并发性和性能。

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class FineGrainedLockingExample {
  private int sharedData = 0;
  private Lock lock = new ReentrantLock();

  public void performOperation() {
	  lock.lock();
	  try {
		  // 安全地访问和修改共享数据
	  } finally {
		  lock.unlock();
	  }
  }
}

了解 "Happens-Before"的关系

了解这些关系有助于编写正确且可预测的多线程代码。

总结

Java线程中的内存一致性是多线程编程的一个关键方面。开发人员需要了解 Java 内存模型,理解其提供的保证,并明智地使用同步机制。通过使用volatile(用于可见性)、锁(用于细粒度控制)和原子类(用于特定操作)等技术,开发人员可以确保并发Java应用程序中的内存一致性。

本文章翻译于Understanding Memory Consistency in Java Threads

留言