线程安全的7种技术

描述

本文翻译7 Techniques for Thread-Safe Classes。文中描述线程安全可以使用无状态、无共享状态、消息传递、不可变状态、Synchronized、Lock、Volatile7种技术实现。

正文

在 Java 中创建线程安全类的方法有很多。 在这里,我们将深入讨论状态处理、消息传递、volatile等。

几乎每个 Java 应用程序都使用线程。 像 Tomcat 这样的 web 服务器在单独的工作线程中处理每个请求,fat client(胖客户端) 在专用工作线程中处理长时间运行的请求,甚至批处理过程也使用 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 GUI框架的事件处理线程。

你可以通过扩展线程类和添加线程实例变量来实现线程局部实例变量。 在下面的示例中,字段 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 = 
    new ThreadLocal()
   {
      @Override
        protected CallbackStatePerThread  initialValue()
      { 
       return getOrCreateCallbackStatePerThread();
      }
   };
}

你可以在 java.lang.Threadlocal 中包装实例变量的类型。通过initialValue()为 java.lang.Threadlocal提供初始值。

下面展示了如何使用实例变量:

CallbackStatePerThread callbackStatePerThread = CallbackState.callbackStatePerThread.get();

通过调用 get() ,您将接收到当前线程关联的对象。

因为,在应用服务器中线程池用于处理请求,即 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。 这不仅可以确保编译器能够检查字段实际上是不可变的,而且还可以使它们在发布不正确的情况下正确地初始化
。 下面是最后一个实例变量的例子:

public class ExampleFinalField
{
    private final int finalField;
    public ExampleFinalField(int value)
    {
        this.finalField = value;
    }
}

使用 java.util.concurrent 下的数据结构

消息传递使用并发队列在线程之间进行通信。 并发队列是 java.util.Concurrent 包中提供的数据结构之一。 此包为Map、Queue、Dequeue、Set和List提供了并发处理类。 这些数据结构经过了高度优化,并经过了线程安全性测试。

Synchronized

如果不能使用上述技术之一,请使用同步锁。 通过在 synchronized 块中放置一个锁,可以确保一次只有一个线程可以执行此部分。

synchronized(lock)
{
    i++;
}

请注意,当您使用多个嵌套同步块时会面临死锁的风险。 当两个线程试图获取对方持有的锁时,就会发生死锁。

Volatile

普通的、nonvolatile的字段可以缓存在寄存器或缓存器中。 通过将变量声明为 volatile,可以告诉 JVM 和编译器始终返回最新写入的值。 这不仅适用于变量本身,而且适用于已写入 volatile 字段的线程所写的所有值。 下面是一个 volatile 实例变量的例子:

public class ExampleVolatileField
{
    private volatile int  volatileField;
}

如果写操作不依赖于当前值,则可以使用 volatile 字段。 或者,如果您可以确保一次只有一个线程可以更新字段。

更多的技巧

我排除了以下高级技术:

  • 原子更新:一种调用原子指令的技术,比如由 CPU 提供的 compare 和 set
  • Reentrantlock:比 synchronized 块提供更多灵活性的锁实现
  • Reentrantreadwritelock:一个读不阻塞的锁实现
  • StampedLock:一个非标准的读写锁,可以乐观地读取值

总结

实现线程安全的最佳方法是避免共享状态。 对于共享状态,您可以用不可变类和消息解析组合,也可以同步块和 volatile 字段一起使用并发数据结构组合。 如果您想测试您的应用程序是否线程安全的,可以免费尝试 vmlens。

我很高兴听到您介绍实现线程安全类所使用的技术。

原文

https://dzone.com/articles/7-techniques-for-thread-safe-classes

你可能感兴趣的:(线程安全的7种技术)