这个代码的意思很简单,就是先定义5000个请求,200个线程同时执行。请求的操作是对count++。所以预期的结果应该是5000个请求,count++ 5000次,所以输出结果应该为5000.在main方法中写的是创建线程池-连接线程池-操作-释放线程。
但是实际运行结果每次都不一样,每次的结果都小于5000,这个说明这个类是线程不安全的。
运行结果
//第一次
count:4996
//第一次
count:4998
//第三次
count:4991
定义
那什么事线程安全呢?
书中的定义:
当多个线程访问一个类时,如果不考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步以及在调用方代码不必作其他协调,这个类的行为依然正确,那么就称这个类是线程安全的
视频给出的定义:
当多个线程访问一个类时,不管运行环境采用何种调度方式,或者这些进程如何交替执行,并且在主调度代码中不需要额外的同步或协同,这个类行为都表现出正确的行为,那么称为这个类为线程安全的。
其实仔细一看,两个定义差不多,所谓的线程安全,都是对类而言的,如果多个线程同时访问这个类,这个类响应的结果都和预期的一样,那么说明这个线程是安全。
线程的安全性体现在三个方面:原子性、可见性、有序性
-
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
-
可见性:一个线程对主内存的修改可以及时的被其他线程看到
-
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
原子性
JDK中atomic包
原子性是怎么实现的呢,在jdk中有一个Atomic包是保存可以确保线程的安全性
看下面的例子:
package com.mmall.concurrency.example.count;
import com.mmall.concurrency.annoations.ThreadSafe;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
@ThreadSafe
public class CountExample2 {
// 请求总数
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();
System.out.println(count.get());
}
private static void add() {
count.incrementAndGet();
// count.getAndIncrement();
}
}
其实可以发现和文中最开始给出的例子想要做的事是一样的,只不过这里的count是AtomicInteger 类型的,也就是atomic包下的封装类。
public static AtomicInteger count = new AtomicInteger(0);
然后在count++ 操作时变成
count.incrementAndGet();
这个很有意思,这个才是确保这个类是线程安全的关键,可以看一下源码
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
可以看到,调用了unsafe.getAndAddInt(this, valueOffset, 1) 方法,往下一层
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;
}
可以看到getAndAddInt方法的具体实现,var1为当前的对象,var2是当前count的值,var4是要加值,这里每次都是加1,所以var4的值为1.
var5的值是取主内存中当前count值,compareAndSwapInt()方法的关键是,如果从主内存中取出的当前count值var5与传入的当前count值var2不一致,那么就会重新取主内存count的值var5,只有当主内存的count值和当前count值相同时,才会将执行var5=varchat+var4
操作。最后返回var5的值,这样就确保了每次在执行加一操作的时候得到的当前的count值都是最新的。
可以看到真正的核心就是compareAndSwapInt()方法啦,这就是是automic确保线程安全的CAS方法的具体实现。
另外再拓展一点:automic包下的AtomicLong类。看类名可以知道和AtomicInteger类一样,只是不同数据对应不同数据类型。但是在java8中新增一种类LongAdder.这个类和AtomicLong实现的功能是一样的,那为什么要在java8中新增一个LongAdder来替代AtomicLong呢。
刚刚上面讲了CAS的具体实现是通过循环一直判断,只有当值一样时候才会执行后续操作,低并发的时候还好,但是高并发的时候就会导致循环的次数增多,总而导致性能降低,使用LongAdder类,在低并发的时候和AtomicLong保持一直,但是在高并发的时候通过分散将压力分散到各个节点,总而提高高并发时的性能(具体的源码我也没有看,所以也是听被人说的这个,有兴趣的同学可以自己研究下)。但是LongAdder有个致命的缺点,就是虽然提高了性能,但是有的时候结果会出现偏差(通过离散模型统计的,在统计的时候,如果有并发更新,就可能会出现误差),导致如果需要结果是准确无误且唯一的时候最好使用AtomicLong。
在提一下atomic包下atomicStampReference类,这个类是解决CAS的ABA问题(是指一个线程操作某个值的时候,其他线程先修改了一次,然后又修改成原来的值。这样当前线程就会认为这个没有任何操作),但是实际上这个值是进行了2次操作的,值是把值改回去了,那怎么解决ABA问题呢,atomicStampReference类在线程操作某个值的时候,不仅会判断值,还会判断当前的版本号是否一致。一致才能进行下一步操作。线程在进行写或修改操作时会进行版本号加一。这样就能规避掉ABA问题了。
在看一下AtomicBoolean类。这个多线程调用类类让方法执行一次,也就是说比如5000个线程同时访问,但是只会有一个线程执行这个方法,其他的线程都不执行。
看一下代码:
package com.mmall.concurrency.example.atomic;
import com.mmall.concurrency.annoations.ThreadSafe;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
@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) {
e.printStackTrace();
给大家的福利
零基础入门
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
同时每个成长路线对应的板块都有配套的视频提供:
因篇幅有限,仅展示部分资料
网络安全面试题
绿盟护网行动
还有大家最喜欢的黑客技术
网络安全源码合集+工具包
所有资料共282G,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!