什么是线程安全以及如何实现?

上次修改时间:2020年4月17日html

做者 亚历杭德罗·乌加特java

1. 概述

Java支持开箱即用的多线程。这意味着,经过同时多个分隔的工做线程来运行不一样的字节码,JVM 可以提升应用程序性能。git

尽管多线程很强大,但它也是有代价的。在多线程环境中,咱们须要以线程安全的方式编写实现。这意味着不一样的线程能够访问共享的资源,而不会因错误的行为或产生不可预测的结果。这种编程方法被称为“线程安全”。github

在本教程中,咱们将探讨实现它的不一样方法。编程

2. 无状态实现

在大多数状况下,多线程应用中的错误是错误地在多个线程之间共享状态的结果。api

所以,咱们要研究的第一种方法是 使用无状态实现来实现线程安全。数组

为了更好地理解这种方法,让咱们考虑一个带有静态方法的简单工具类,该方法能够计算数字的阶乘:缓存

public class MathUtils {
    
    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}

factorial方法是一种无状态肯定性函数。 肯定性是指:给定特定的输入,它将始终产生相同的输出。安全

该方法既不依赖外部状态,也不维护自身的状态。所以,它被认为是线程安全的,而且能够同时被多个线程安全地调用。多线程

全部线程均可以安全地调用 factorial 方法,而且将得到预期结果,而不会互相干扰,也不会更改该方法为其余线程生成的输出。

所以,无状态实现是实现线程安全的最简单方法

3. 不可变的实现

若是咱们须要在不一样线程之间共享状态,则能够经过使它们成为不可变对象来建立线程安全类

不变性是一个功能强大,与语言无关的概念,在Java中至关容易实现。

当类实例的内部状态在构造以后没法修改时,它是不可变的

在Java中建立不可变类的最简单方法是声明全部字段为 privatefinal ,且不提供 setter:

public class MessageService {
    
    private final String message;
 
    public MessageService(String message) {
        this.message = message;
    }
    
    // 标准 getter
    
}

一个 MessageService 对象其实是不可变的,由于它的状态在构造以后不能更改。所以,它是线程安全的。

此外,若是 MessageService 其实是可变的,可是多个线程仅对其具备只读访问权限,那么它也是线程安全的。

所以,不变性是实现线程安全的另外一种方法

4. 线程私有 (ThreadLocal) 字段

在面向对象编程(OOP)中,对象实际上须要经过字段维护状态并经过一种或多种方法来实现行为。

若是咱们确实须要维护状态,则能够经过使它们的字段成为线程局部的来建立不在线程之间共享状态的线程安全类。

经过简单地在 Thread 类中定义私有字段,咱们能够轻松建立其字段为线程局部的类。

例如,咱们能够定义一个存储整数数组的 Thread 类:

public class ThreadA extends Thread {
    
    private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    @Override
    public void run() {
        numbers.forEach(System.out::println);
    }
}

而另外一个类可能拥有一个字符串数组:

public class ThreadB extends Thread {
    
    private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
    
    @Override
    public void run() {
        letters.forEach(System.out::println);
    }
}

在这两种实现中,这些类都有其本身的状态,可是不与其余线程共享。所以,这些类是线程安全的。

一样,咱们能够经过将 ThreadLocal 实例分配给一个字段来建立线程私有字段。

例如,让咱们考虑如下 StateHolder 类:

public class StateHolder {
    
    private final String state;
 
    // 标准的构造函数和 getter
}

咱们能够很容易地使其成为线程局部(ThreadLocal)变量,以下所示:

public class ThreadState {
    
    public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {
        
        @Override
        protected StateHolder initialValue() {
            return new StateHolder("active");  
        }
    };
 
    public static StateHolder getState() {
        return statePerThread.get();
    }
}

线程局部字段与普通类字段很是类似,不一样之处在于,每一个经过setter / getter访问它们的线程都将得到该字段的独立初始化副本,以便每一个线程都有本身的状态。

5. 同步集合类

经过使用collections框架 中包含的一组同步包装器,咱们能够轻松地建立线程安全的collections

例如,咱们可使用如下同步包装之一来建立线程安全的集合:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();

让咱们记住,同步集合在每种方法中都使用内在锁定(咱们将在后面介绍内在锁定)。

这意味着该方法一次只能由一个线程访问,而其余线程将被阻塞,直到该方法被第一个线程解锁。

所以,因为同步访问的基本逻辑,同步会对性能形成不利影响。

6. 支持并发的集合

除了同步集合,咱们可使用并发集合来建立线程安全的集合。

Java提供了 java.util.concurrent 包,其中包含多个并发集合,例如 ConcurrentHashMap

Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");

与同步对象不一样,并发集合经过将其数据划分为段来实现线程安全。例如,在 ConcurrentHashMap 中,多个线程能够获取不一样 Map 段上的锁,所以多个线程能够同时访问 Map

因为并发线程访问的先天优点,并发集合类具有远超同步集合类更好的性能

值得一提的是,同步集合和并发集合仅使集合自己具备线程安全性,而不使content变得线程安全

7.原子化对象

使用Java提供的一组原子类(包括 AtomicIntegerAtomicLongAtomicBooleanAtomicReference )也能够实现线程安全。

原子类使咱们可以执行安全的原子操做,而无需使用同步。原子操做在单个机器级别的操做中执行。

要了解解决的问题,让咱们看下面的 Counter 类:

public class Counter {
    
    private int counter = 0;
    
    public void incrementCounter() {
        counter += 1;
    }
    
    public int getCounter() {
        return counter;
    }
}

让咱们假设在竞争条件下,两个线程同时访问 increasingCounter() 方法。

从理论上讲, counter 字段的最终值为2。可是咱们不肯定结果如何,由于线程在同一时间执行同一代码块,而且增量不是原子的。

让咱们使用 AtomicInteger 对象建立 Counter 类的线程安全实现:

public class AtomicCounter {
    
    private final AtomicInteger counter = new AtomicInteger();
    
    public void incrementCounter() {
        counter.incrementAndGet();
    }
    
    public int getCounter() {
        return counter.get();
    }
}

这是线程安全的,由于在++增量执行多个操做的同时, 增量和获取 是原子的

8. 同步方法

尽管较早的方法对于集合和基元很是有用,但有时咱们须要的控制权要强于此。

所以,可用于实现线程安全的另外一种常见方法是实现同步方法。

简而言之,一次只能有一个线程能够访问同步方法,同时阻止其余线程对该方法的访问。其余线程将保持阻塞状态,直到第一个线程完成或该方法引起异常。

咱们能够经过使它成为同步方法,以另外一种方式建立线程安全版本的 creationCounter()

public synchronized void incrementCounter() {
    counter += 1;
}

咱们经过与前缀的方法签名建立一个同步方法 synchronized 关键字。

因为一次一个线程能够访问一个同步方法,所以一个线程将执行 crementCounter() 方法,而其余线程将执行相同的操做。任何重叠的执行都不会发生。

同步方法依赖于“内部锁”或“监视器锁”的使用。固有锁是与特定类实例关联的隐式内部实体。

在多线程上下文中,术语 monitor 是指对关联对象执行锁的角色,由于它强制对一组指定的方法或语句进行排他访问。

当线程调用同步方法时,它将获取内部锁。线程完成执行方法后,它将释放锁,从而容许其余线程获取锁并得到对方法的访问。

咱们能够在实例方法,静态方法和语句(已同步的语句)中实现同步。

9. 同步语句

有时,若是咱们只须要使方法的一部分红为线程安全的,那么同步整个方法可能就显得过度了。

为了说明这个用例,让咱们重构 increascountCounter 方法:

public void incrementCounter() {
    // 此处可有额外不需同步的操做
    // ...
    synchronized(this) {
        counter += 1; 
    }
}

该示例很简单,可是它显示了如何建立同步语句。假设该方法如今执行了一些不须要同步的附加操做,咱们仅经过将相关的状态修改部分包装在一个同步块中来对其进行同步

与同步方法不一样,同步语句必须指定提供内部锁的对象,一般是this引用。

同步很是昂贵,所以使用此选项,咱们尽量只同步方法的相关部分

9.1 其余对象做为锁

咱们能够经过将另外一个对象用做监视器锁定,来稍微改善 Counter 类 的线程安全实现。

这不只能够在多线程环境中提供对共享资源的协调访问,还可使用外部实体来强制对资源进行独占访问

public class ObjectLockCounter {
 
    private int counter = 0;
    private final Object lock = new Object();
    
    public void incrementCounter() {
        synchronized(lock) {
            counter += 1;
        }
    }
    
    // 标准 getter
}

咱们使用一个普通的 Object 实例来强制相互排斥。此实现稍好一些,由于它能够提升锁定级别的安全性。

将 this 用于内部锁定时,攻击者可能会经过获取内部锁定并触发拒绝服务(DoS)条件来致使死锁。

相反,在使用其余对象时, 没法从外部访问该私有实体。这使得攻击者更难得到锁定并致使死锁。

9.2 注意事项

即便咱们能够将任何Java对象用做内部锁定,也应避免将 _Strings_用于锁定目的:

public class Class1 {
    private static final String LOCK  = "Lock";
 
    // 使用 LOCK 做为内部锁
}
 
public class Class2 {
    private static final String LOCK  = "Lock";
 
    // 使用 LOCK 做为内部锁
}

乍一看,这两个相似乎将两个不一样的对象用做其锁。可是,intern,这两个“ Lock”值实际上可能引用字符串池上的同一对象。也就是说, Class1Class2 共享相同的锁!

反过来,这可能会致使在并发上下文中发生某些意外行为。

除了字符串以外,咱们还应避免将任何可缓存或可重用的对象用做内部锁。例如, Integer.valueOf() 方法缓存少许数字。所以,即便在不一样的类中,调用 Integer.valueOf(1) 也会返回相同的对象。

10. volatile 修饰的域

同步的方法和块很是适合解决线程之间的可变可见性问题。即便这样,常规类字段的值也可能会被CPU缓存。所以,即便是同步的,对特定字段的后续更新也可能对其余线程不可见。

为了不这种状况,咱们可使用 volatile 修饰的类字段:

public class Counter {
 
    private volatile int counter;
 
    // 标准构造函数、getter
    
}

使用 volatile 关键字,咱们指示 JVM 和编译器将 counter 变量存储在主内存中。这样,咱们确保每次 JVM 读取 counter 变量的值时,实际上都会从主内存而不是从 CPU 缓存读取它。一样,每次 JVM 将值写入 counter 变量时,该值将被写入主内存。

此外,使用 volatile 变量可确保也将从主内存中读取给定线程可见的全部变量

让咱们考虑如下示例:

public class User {
 
    private String name;
    private volatile int age;
 
    // 标准构造函数、getter
    
}

在这种状况下,JVM 每次将 age _volatile_ 变量写入主内存时,也会将非易失性 name 变量也写入主内存。这确保了两个变量的最新值都存储在主存储器中,所以对变量的后续更新将自动对其余线程可见。

一样,若是线程读取 易失性 变量的值,则该线程可见的全部变量也将从主内存中读取。

易失性 变量提供的这种扩展保证称为 彻底易失性可见性保证

11. 重入锁

Java 提供了一组改进的 Lock 实现,其行为比上面讨论的固有锁稍微复杂一些。

对于固有锁,锁获取模型至关严格:一个线程获取锁,而后执行方法或代码块,最后释放锁,以便其余线程能够获取它并访问该方法。

没有底层机制能够检查排队的线程并优先访问等待时间最长的线程。

ReentrantLock 实例使咱们可以作到这一点,从而防止排队的线程遭受某些类型的资源匮乏):

public class ReentrantLockCounter {
 
    private int counter;
    private final ReentrantLock reLock = new ReentrantLock(true);
    
    public void incrementCounter() {
        reLock.lock();
        try {
            counter += 1;
        } finally {
            reLock.unlock();
        }
    }
    
    // 标准构造函数、getter...
    
}

ReentrantLock 的构造函数有一个可选的 公平 _boolean_ 参数。若是设置为 true ,而且多个线程正试图获取锁,则 JVM 将优先考虑等待时间最长的线程,并授予对该锁的访问权限

12. 读/写锁

咱们能够用来实现线程安全的另外一种强大机制是使用 ReadWriteLock 实现。

一个 ReadWriteLock中 锁定实际使用一对相关的锁,一个用于只读操做和其余写操做。

结果,只要没有线程写入资源,就有可能有许多线程在读取资源。此外,将线程写入资源将阻止其余线程读取资源

咱们可使用 ReadWriteLock 锁,以下所示:

public class ReentrantReadWriteLockCounter {
    
    private int counter;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public void incrementCounter() {
        writeLock.lock();
        try {
            counter += 1;
        } finally {
            writeLock.unlock();
        }
    }
    
    public int getCounter() {
        readLock.lock();
        try {
            return counter;
        } finally {
            readLock.unlock();
        }
    }
 
    // 标准构造函数...
   
}

13. 结论

在本文中,咱们了解了Java中的线程安全性,并深刻研究了实现它的各类方法

像往常同样,本文中显示的全部代码示例均可以在GitHub上得到

相关文章
相关标签/搜索