2019独角兽企业重金招聘Python工程师标准>>>
1、什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
2、原子性
提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步操作,延迟初始化是竞态条件的常见情形。
要避免静态条件,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。
下面我们来看下这个例子:
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class AtomicDemo {
/**
* 请求总数
*/
public static int clientTotal = 100;
/**
* 线程池数量
*/
public static int threadTotal = 20;
/**
* 信号量获得许可的数量
*/
public static int permits = 10;
// public static int count = 0;
public static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(threadTotal);
// 允许访问资源的线程数目
final Semaphore semaphore = new Semaphore(permits);
// clientTotal个线程全部执行完毕后方可继续执行
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
// 返回可用的资源个数
if (semaphore.availablePermits() > 0) {
log.info("[{}进入,准备操作计数]", Thread.currentThread().getName());
} else {
log.info("[{}排队等候]", Thread.currentThread().getName());
}
try {
// 从信号量中获得操作许可
semaphore.acquire();
log.info("[{}开始计数]", Thread.currentThread().getName());
add();
log.info("[{}计数结束]", Thread.currentThread().getName());
// 释放一个操作许可,并返回信号量
semaphore.release();
} catch (InterruptedException e) {
log.error("exception", e);
} finally {
// 减少等待的线程个数
countDownLatch.countDown();
}
});
}
// 等待计数的结束(个数为0)
countDownLatch.await();
// 关掉线程池
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() throws InterruptedException {
// 模拟计数操作时间
final Random random = new Random();
TimeUnit.SECONDS.sleep(random.nextInt(10));
// count ++;
count.incrementAndGet();
}
}
AtomicInteger是java.util.concurrent.atomic包中的原子变量类,它能够实现原子的自增操作,这样就是线程安全的了。
3、加锁机制
除了使用原子变量的方式外,Java提供了一种内置的锁机制来支持原子性,用于实现线程安全性。即同步代码块(Synchronized Block):一个作为锁的对象引用,一个作为由这个锁保护的代码块。
synchronized (lock) {
// 访问或修改由锁保护的共享状态
}
在方法上增加synchronized关键字后,它能够保证同一时间只会有一个线程进入方法体,这样每个线程就可以全部执行完方法后再退出,方法体内操作就相当于是原子操作了,避免了竞态条件错误。
4、可见性
当一个线程修改了共享变量值,其他线程能够立即得知这个修改。
共享变量
如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
Java内存模型(Java Memory Model,JMM)
Java虚拟机规范中试图定义一种JMM来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一直的内存访问效果。JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。JMM如下图所示:
- 所有的变量都存储在主内存中。
- 每条线程都有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝。
- 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
-
不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
可见性实现原理
JMM是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
Java语言层面支持的可见性实现方式
- volatile(最轻量级的同步机制)
- synchronized
- final
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或J.U.C中的原子类)来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。
5、有序性
一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则(先行发生原则)。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
参考
书籍《Java并发编程实战》
书籍《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》