翻译自7 Techniques for Thread-Safe Classes
几乎每个Java应用程序都使用线程。像Tomcat这样的Web服务器在单独的工作线程中处理每个请求,甚至使用java.util.concurrent.ForkJoinPool来提高性能。
因此,以线程安全的方式编写类是非常有必要的,可以通过以下技术实现该目标。
无状态
当多个线程访问同一个类的静态变量时,必须协调对此变量的访问,以免出现同步问题。
其中最简单的方法是避免对类的静态变量访问。类中的静态方法仅使用局部变量和方法输入参数。从java.lang.Math类中截取一部分代码为例:
public static int subtractExact(int x, int y) {
int r = x - y;
if (((x ^ y) & (x ^ r)) < 0) {
throw new ArithmeticException("integer overflow");
}
return r;
}
无共享状态
如果无法避免状态的存在,那请不要共享状态。状态应该只由一个线程拥有。该技巧的一个示例是SWT或Swing图形用户界面框架的事件处理线程。
您可以通过继承Thread类并添加实例变量来实现线程局部实例变量。在以下示例中,每个启动的工作线程中都会有自己的变量pool和workQueue。
package java.util.concurrent;
public class ForkJoinWorkerThread extends Thread {
final ForkJoinPool pool;
final ForkJoinPool.WorkQueue workQueue;
}
实现线程局部变量的另一种方法是使用类java.lang.ThreadLocal来创建线程局部的字段。以下是使用java.lang.ThreadLocal的实例变量的示例:
public class CallbackState {
public static final ThreadLocal<CallbackStatePerThread> callbackStatePerThread =
new ThreadLocal<CallbackStatePerThread>()
{
@Override
protected CallbackStatePerThread initialValue()
{
return getOrCreateCallbackStatePerThread();
}
};
}
将实例变量的类型包装在java.lang.ThreadLocal中。可以通过方法initialValue()为java.lang.ThreadLocal提供初始值。
以下展示了如何使用该实例变量:
CallbackStatePerThread callbackStatePerThread = CallbackState.callbackStatePerThread.get();
通过调用方法get(),将得到只与当前线程关联的对象。
因为在应用程序服务器中,许多线程池用于处理请求,所以java.lang.ThreadLocal会导致此环境中的内存消耗过高。因此,不建议将java.lang.ThreadLocal用于由应用程序服务器的请求处理线程执行的类。
消息传递
如果不使用上述技巧共享状态,则需要使用线程通信的方法。即为在线程之间传递消息。可以使用java.util.concurrent包中的并发队列实现消息传递。或者使用像Akka这样的框架,这是一个actor风格并发的框架。以下示例显示如何使用Akka发送消息:
target.tell(message, getSelf());
接收消息:
@Override
public Receive createReceive() {
return receiveBuilder()
.match(String.class, s -> System.out.println(s.toLowerCase()))
.build();
}
不可变状态
为了避免发送线程在另一个线程读取消息时更改消息的问题,消息应该是不可变的。因此,Akka框架具有所有消息必须是不可变的约定
实现不可变类时,应将其字段声明为final。这不仅可以确保编译器能检查出字段是不可变的,而且即使出现错误也可以正确初始化。以下是final实例变量的示例:
public class ExampleFinalField
{
private final int finalField;
public ExampleFinalField(int value)
{
this.finalField = value;
}
}
使用java.util.concurrent中的数据结构
消息传递使用并发队列进行线程之间的通信。并发队列是java.util.concurrent包中提供的数据结构之一。java.util.concurrent包提供并发的map,queue,dequeue,set和list。这些数据结构经过高度优化和线程安全测试。
synchronized字段
如果上述技术都没有使用,还可以使用同步锁。通过在同步块处添加锁,确保一次只有一个线程可以执行此部分。
synchronized(lock)
{
i++;
}
请注意,当使用多个嵌套同步块时,会有死锁的风险。当两个线程试图获取对方线程持有的锁和对象时,就会发生死锁。
volatile字段
正常情况下,非易失性字段可以缓存在寄存器或高速缓存中。通过将变量声明为volatile,可以通知JVM和编译器始终返回最新的写入值。这不仅适用于变量本身,而且适用于写入volatile字段的线程所写的所有值。volatile实例变量的示例:
public class ExampleVolatileField
{
private volatile int volatileField;
}
更多技巧
- 原子更新:一种技术,可以在其中调用CPU提供的比较和设置等原子指令
- java.util.concurrent.locks.ReentrantLock:一种锁实现,提供比synchronized块更多的灵活性
- java.util.concurrent.locks.ReentrantReadWriteLock:一种锁实现,其中读读操作不加锁,读写和写写操作加锁
- java.util.concurrent.locks.StampedLock:一个非永久性的读写锁,以乐观锁的方式读取值。
结论
实现线程安全的最佳方法是避免共享状态。如果需要共享状态,可以使用消息传递、不可变类、并发数据结构、synchronized字段和volatile字段。如果想测试应用程序是否线程安全,请免费试用vmlens。