1️⃣定义
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步和协同,这个类都能够表现出正确的行为,那么就称这个类为线程安全的类;
2️⃣线程安全性的体现
原子性 : 提供了互斥访问,同一时刻只能有一个线程来对它进行操作;
有序性 : 一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该结果一般杂乱无序;
可见性 : 一个线程对主内存的修改可以及时的被其他线程观察到;
3️⃣原子性 : Atomic包(在Atomic包中都是使用CAS来保证原子性的)
① 我们之前做的累加计算的demo我们现在使用Atomic对它进行一下简单的改造;
@Slf4j
@ThreadSafe
public class AtomicExample1 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
// count.getAndIncrement();
}
}
大家可以看到,我们通过简单的改造将原来count的类型由int修改为AtomicInteger这个类就由线程不安全编程了线程安全,那么这是为什么呢?
② AtomicInteger源码解析
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
1. 在源码中我们可以看到incrementAndGet使用了一个unsage的类,
然后调用的是unsafe.getAndAddInt方法,接下来我们看一下这个
方法的源码;
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
其实这个方法在每次执行的时候都会去判断当前的值与底层的值是否一致,
如果一致才会执行加1的操作,如果不一致则重新循环取值然后接着判断进行
加1运算;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
compareAndSwapInt这个方法的核心就是CAS的核心;
return var5;
}
2. 在这个方法中主体是通过一个do while语句来进行实现的,核心的逻辑
是compareAndSwapInt这个方法,接下来我们看一下这个方法的源码;
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
3. 可以看到这是一个被native修饰的方法;
刚才我们处理的是int类型的,但是其他类型也是这样处理的,比如下面的AtomicLong 以及 LongAdder;
@Slf4j
@ThreadSafe
public class AtomicExample2 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static AtomicLong count = new AtomicLong(0);
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
// count.getAndIncrement();
}
}
@Slf4j
@ThreadSafe
public class AtomicExample3 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static LongAdder count = new LongAdder();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count.increment();
}
}
其实他们两个大体上是差不多的, LongAdder是1.8新增加的,这里我们就说一下他们两个之间的优缺点:
我们刚才在分析AtomicInteger源码时我们看到,它底层其实就是在一个死循环内进行循环的比较和运算不断的尝试修改目标值,如果竞争不激烈一般都是能成功的,但是如果竞争激烈的情况下就会容易会修改失败并且浪费性能;
这里有一个小的知识点,对于64位的(Long和Double)操作JVM允许拆分成两个32位的操作, LongAdder的思想是将value拆分成数组,然后进行运算,通过这样的操作可以很大程度上提升性能;
4️⃣ Atomic包中的其他类
① AtomicReference
@Slf4j
@ThreadSafe
public class AtomicExample4 {
private static AtomicReference<Integer> count = new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0, 2); // 2
count.compareAndSet(0, 1); // no
count.compareAndSet(1, 3); // no
count.compareAndSet(2, 4); // 4
count.compareAndSet(3, 5); // no
log.info("count:{}", count.get());
}
}
② AtomicIntegerFieldUpdater
@Slf4j
@ThreadSafe
public class AtomicExample5 {
private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
@Getter
public volatile int count = 100;
public static void main(String[] args) {
AtomicExample5 example5 = new AtomicExample5();
if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 1, {}", example5.getCount());
}
if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 2, {}", example5.getCount());
} else {
log.info("update failed, {}", example5.getCount());
}
}
}
③ AtomicBoolean
@Slf4j
@ThreadSafe
public class AtomicExample6 {
private static AtomicBoolean isHappened = new AtomicBoolean(false);
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
}
private static void test() {
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
}
小结 : Atomic包中的大部分类原理都是差不多,核心就是借助CAS的思想来保证原子性;
4️⃣原子性: 锁
synchronized : 依赖JVM实现;
Lock : 依赖特殊的CPU指令,代码实现(后续会进行详细讲解)
本篇笔记先重点讲解一下synchronized;
概览: synchronized是一个关键字,它是同步锁,它修饰的对象有四种;
① 修饰代码块:大括号括起来的代码,作用于调用对象;
② 修饰方法: 整个方法,作用于调用对象;
③ 修饰静态方法: 整个静态方法,作用于所有对象;
④ 修饰类: 括号括起来的部分,作用于所有对象;
①代码演示
@Slf4j
public class SynchronizedExample1 {
// 修饰一个代码块
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修饰一个方法(synchronized修饰的方法不能被继承)
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test2(1);
});
executorService.execute(() -> {
example2.test2(2);
});
}
}
@Slf4j
public class SynchronizedExample2 {
// 修饰一个类
public static void test1(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修饰一个静态方法
public static synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
SynchronizedExample2 example1 = new SynchronizedExample2();
SynchronizedExample2 example2 = new SynchronizedExample2();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
小结: 如果使用synchronized进行修饰的类或者方法,那么当前类或者方法只能有一个线程去执行,会导致其他的线程阻塞;synchronized是不可中断的锁,适合竞争不激烈的情况使用,可读性较好;
5️⃣原子性:对比
synchronized:不可中断的锁,适合竞争不激烈的情况,可读性较好;
lock:可中断锁,多样化同步,竞争激烈时能维持常态;
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值;
6️⃣可见性
可见性 : 是一个线程对主内存的修改可以及时的被其他线程观察到;
说起可见性我们就需要说一下什么时候不可见,下面我们就简单说一下导致不可见的常见原因:
① 线程交叉执行;
② 重排序结合线程交叉执行;
③ 共享变量更新后的值没有在工作内存与主内存间及时更新;
① 可见性:synchronized
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存;
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁);
② 可见性: volatile
通过加入内存屏障和禁止重排序优化来实现;
- 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存;
- 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量;
③ 使用volatile关键字优化计数器
@Slf4j
@NotThreadSafe
public class CountExample4 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static volatile int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
// 1、count
// 2、+1
// 3、count
}
}
当我们使用volatile优化代码后,这个类仍然不是线程安全的,这是为什么呢?我们来分析一下,当执行count++操作的时候,第一步先从主存中获取最新的值,第二步执行+1操作,第三步将新值从新写入主存;那么此时问题就来了,如果同时有两个线程执行,那么他们获取的都是最新的值,但是当他们执行+1以后的写入时,写入的值是相同的,就会丢失一次操作;
这也从侧面证明了volatile不具备原子性;
7️⃣有序性
有序性 : Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性;
我们可以通过volatile synchronized lock来保证线程的有序性;
happens-before原则:
① 程序次序规则 : 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
② 锁定规则 : 一个unlock操作先行发生于后面对同一个锁的lock操作;
③ volatile变量规则 : 对一个变量的写操作先行发生于后面对这个变量的读操作;
④ 传递规则 : 如果操作A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C;
⑤ 线程启动规则 : Thread对象的start()方法先行发生于此线程的每一个动作;
⑥ 线程中断规则 : 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
⑦ 线程终结规则 : 线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束 Thread.isAlive()的返回值手段检测到线程已经终止执行;
⑧ 对象终结规则 : 一个对象的初始化完成先行发生于它的finalize()方法的开始;