Java打造线程安全类的7个技巧

258 阅读4分钟

翻译自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