7种编写线程安全类的方式

几乎所有的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的GUI框架是一个典型的例子。
你可以通过继承Thread类并且配合实例变量来实现线程本地实例。下面的例子中成员变量pool和workQueue对于每个线程来说都是本地的。

package java.util.concurrent;
public class ForkJoinWorkerThread exends Thread{
    final ForkJoinPoll pool;
    final ForkJoinPool.WorkQueue workQueue;
}

另外一种实现方式是使用 java.lang.ThreadLocal类来实现线程变量的隔离。下面是一个使用ThreadLocal类的例子

    public class CallbackState{
    public static final ThreadLocal<CallbackStatePerThread> callbackStatePerThread = new ThreadLoal<CallbackStatePerThread>(){
    @Override
    protected CallbackStatePerThread initialValue(){
        return getOrCreateCallbackStatePerThread();
    }};
}

通过使用ThreadLocal类的initiValue()方法来包装你的实例变量。

你可以使用下面的语句来使用实例变量

    CallbackStatePerThread callbackStatePerThread = CallbackState.callbackStatePerThread.get();

通过调用get()方法,你就能获取仅和当前线程关联的变量了。

当应用服务在多线程情况下处理请求的应用场景下。ThreadLocal会导致较高的内春消耗,所以并不推荐在这中情况下使用ThreadLocal。

消息传递
如果你不想使用上面的方式来实现线程无关,那么你需要一种方式来实现线程间通信。可以考虑使用 JAVA的并发处理包java.util.concurrent中的并发队列,或者使用Akka框架。下面的例子展示了如何通过Akka框架。
发送消息

    target.tell(message,getSelf());

接收消息

    @Override
    public Receive crateReceive(){
    return receiveBuilder().match(String.class,s -> System.out.println(hs.toLowerCase())).build();
}

不可变状态
为了防止写入线程在接收线程读取数据的时候更改写入的信息,信息本身必须是不可变的。Akka框架本身要求线程间传递的数据必须不可变更。

你必须通过final关键字声明一个不可变更的实例变量。final声明的变量的引用不可变,而不是变量引用指向的对象不变,考虑这种情况,应当考虑使用java的原生类型或者使用final修饰类本身。

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

使用java.util.concurrent包的数据结构
线程间的消息传递使用了并发并发队列。ConcurrentQueue使用并发包提供的一个数据结构。该包中的Map,queue,dequeue,set,list等数据结构都是经过充足测试的实现线程安全的方案。

同步代码块
使用同步代码块可以保证同一时间内只有一个线程执行该代码块。但是当使用嵌套同步代码块的时候也要考虑死锁的问题。

使用volatile关键字
通常情况下,未使用volatile关键字的变量会被缓存在方法堆栈中。通过volatile关键字,JVM会始终在公共堆栈中维持该成员变量的最新值。当变量的更新并不依赖于变量的当前值或者确保只有一个线程更新该成员变量时,可以考虑使用volatile关键字。
volatile关键字保证的是变量的引用不可变,而不是该变量引用指向的对象不可变。当使用数组的时候需要考虑使用并发包的原子类java.util.concurrent.atomic.AtomicReferenceArray

其他的技术

  • 原子更新-提供原子操作。
  • ReentrantLock(重入锁)-提供比同步锁更加灵活的功能。
  • ReentrantReadWriteLock(读写锁),读锁可以多线程共享,写锁与读写锁互斥,效率高于ReentrantLock。
  • StampedLock-非重入的读写锁。

总结
实现线程安全最好的方式避免共享状态。对于要线程间共享的状态,你可以使用消息传递配合不可变类,或者并发数据结构,并发同步块和volatile变量来处理。

我也很期待了解你们在实现线程安全时使用的技术

以上内容翻译自http://vmlens.com/articles/techniques_for_thread_safety/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值