Thread-safety
线程安全是我们设计一个类是必须考虑的问题,在一些常用的工具类库上,是否线程安全也会作为一个很重要的标注告诉使用者。常见的实现线程安全的手段有哪些呢?
无状态
即,将接口方法设计为无状态的,例如spring中的单例,原则上我们都会设计为无状态的
例如,实现一个方法,用于计算数组元素的和
@ThreadSafe
public class Math1 {
public int sum(int... arr) {
return Arrays.stream(arr).sum();
}
}
这个就是无状态的,不依赖任何的外部变量
public class Math2 {
int tmpSum;
public int sum(int... arr) {
tmpSum = 0;
for (int i : arr) {
tmpSum += i;
}
return tmpSum;
}
}
这个是有状态的,在多线程环境下,sum 方法的调用会依赖变量 tmpSum
,由于多线程对 tmpSum 的访问不是互斥的,所以有可能会导致结果错误。如果代码逻辑就是有状态的,那必须保证多线程在读写共享变量时的同步互斥,上面的代码可以修改成
@ThreadSafe
public class Math3 {
int tmpSum;
public synchronized int sum(int... arr) {
tmpSum = 0;
for (int i : arr) {
tmpSum += i;
}
return tmpSum;
}
}
测试用例
@Test
void sum() throws InterruptedException {
AtomicInteger errCnt = new AtomicInteger(0);
CountDownLatch stopWatch = new CountDownLatch(threads);
CyclicBarrier barrier = new CyclicBarrier(threads);
for (int i = 0; i < threads; i++) {
Pair pair = rand();
executor.execute(() -> {
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
int sum = math.sum(pair.arr);
try {
assertEquals(pair.sum, sum);
} catch (AssertionFailedError e) {
errCnt.incrementAndGet();
} finally {
stopWatch.countDown();
}
});
}
stopWatch.await();
assertEquals(0, errCnt.get(), "data race occurred?");
}
变量不可变
这种很容易理解,即将多线程访问的资源、变量,设为仅可读,不可改变的,这样就不会有问题了,常见的手段有:
使用线程本地变量
使用ThreadLocal
例如 jdk 中常见的,数字格式化 NumberFormat 和日期格式化 SimpleDateFormat 类,都不是线程安全的。这种不是线程安全的类,既要保证使用上不出现多线程环境的问,又要避免创建的对象过多。一般我们都是使用 ThreadLocal 来解决。
例如,数字格式化
private static final ThreadLocal<NumberFormat> FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(NumberFormat::getNumberInstance);
public static String format(int n) {
return FORMAT_THREAD_LOCAL.get().format(n);
}
包装为线程安全
包装为同步容器
可以使用,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();
单个变量的包装
使用 java.util.concurrent.atomic 包下的工具类对共享变量进行包装,使用 cas 的操作方式。
使用并发容器
java.util.concurrent 包下的容器,例如常见的 ConcurrentHashMap, ConcurrentSkipListMap。
这些容器不需要包装,就是线程安全的。
使用synchronized
使用 synchronized 关键字修饰对象,代码块,方法,可以保证内部操作的原子性,共享变量的可见性
使用Lock
可以使用 java.util.concurrent.locks.Lock 的子类,对多线程的访问进行控制
重入锁Reentrant Locks
public class ReentrantLockCounter {
private int counter;
private final ReentrantLock reLock = new ReentrantLock(true);
public void incrementCounter() {
reLock.lock();
try {
counter += 1;
} finally {
reLock.unlock();
}
}
}
读写锁
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();
}
}
}
读写锁是一种优化,即读与读之间是可以同时进行的即共享,但是写与读、写与写之间是互斥的,排他的。
StampedLock
ReadWriteLock
它有个潜在的问题:写线程和其他操作都是互斥的,因此如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
为了能够进一步提升并发执行效率,Java 8 引入了新的读写锁:StampedLock
。
StampedLock
和ReadWriteLock
相比,改进之处在于:读的过程中也允许获取写锁并允许写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
private int a;
private int b;
final StampedLock lock = new StampedLock();
public int sum() {
long stamp = lock.tryOptimisticRead();
int x = a, y = b;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
x = a;
y = b;
} finally {
lock.unlockWrite(stamp);
}
}
return x + y;
}
读数据时,先尝试乐观读,乐观读的过程不是原子的,因此,乐观读之后会返回一个标记,通过校验这个标记,可以知道乐观读的代码是否是原子运行的,如果是,则没必要加悲观读的锁。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)