线程安全性是指:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,则称这个类时线程安全的。
线程安全性的三个方面:
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
代码基础
1.CountDownLatch
countDownLatch字面意思是计数器向下减的闭锁。计数器的初始值为线程的数量。每当一个线程完成了自已的任务,调用countDown()方法后,计数器的值就会减1。当计数器的值为0的时候,表示所有的线程完成了任务。在闭锁上等待的线程就可以恢复执行。 ,如下图所示
计数器的初始值为3,Ta线程执行await()方法之后,就进入了等待状态,只有当T1,T2,T3都执行countDown()方法后,计数器的值为0,Ta线程才能继续执行。
2.Semaphore
Semaphore是一种计数信号量,用于控制同一时间的请求并发量。
如图所示,对于一个十字路口,同一时间只允许两辆车通过同一个点,当两辆车中的一个离开时,就可以允许等待的车中的一辆通过。
3.代码示例
public class Concurrency {
//请求总数
private static int clientTotal = 1000;
//同时允许执行的线程总数
private static int threadTotal = 20;
private static int count = 1;
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0;i<clientTotal;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count="+count);
}
上述代码执行多次,其输出结果都不相同,说明不是线程安全的。线程的执行体只有一句话,count++。接下来根据jmm(Java memory model java内存模型)模型,来分析一下为什么count++操作不是线程安全的。
jmm模型规范了一个线程如何和何时看到一个共享变量被其他线程修改的值,以及在必须时如何同步的访问共享变量。
对于count++操作线程不安全根据上图就很好解释了。假设线程A从主内存获取到count的拷贝保存在A的本地内存中,值为1,线程B也从主内存获取count的拷贝保存到本地内存中,值为1。A和B同时执行+1操作,count变为了2,写入到主内存中。此时count的值为2,但实际应该是3。
4.AtomicXXX
上述count++操作线程不安全,为了变为线程安全,我们使用java.util.concurrent.Atomic包下的AtomicInteger来替换int。 代码示例:
public class AtomicIntegerConcurrency {
//请求总数
private static int clientTotal = 1000;
//同时允许执行的线程总数
private static int threadTotal = 20;
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0;i<clientTotal;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
count.addAndGet(1);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count="+count.get());
}
}
其输出结果始终是1000。 接下来看一下AutomicXXX为什么是线程安全的。
//AtomicInteger
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
//Unsafe
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
内部实现主要为Unsafe.compareAndSwapXXX,简称cas。cas算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。 CAS算法的过程是这样:CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。仅当V值等于E值时,才会将V的值设为N,并返回当前V的值。使用while循环,不断判断当前的值是否与预期的值相等,不相等,则重新获取预期的值,再次进行判断,知道相等时,才将新的值写入。
AtomicBoolean的使用。下面代码可以用于某些只要求执行一次的情景
public class AtomicBooleanConcurrency {
//请求总数
private static int clientTotal = 1000;
//同时允许执行的线程总数
private static int threadTotal = 20;
private static AtomicBoolean isExecuted = new AtomicBoolean(false);
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0;i<clientTotal;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
if (isExecuted.compareAndSet(false,true)){
//虽然有1000个线程执行,但是这句话只输出一次,因为AtomicBoolean的compareAndSet是原子操作,从false变为true只会执行一次
System.out.println("execute success");
}
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("isExecuted="+isExecuted.get());
}