线程安全性:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称为这个类是线程安全的。
1.线程安全的三个特性
原子性:互斥访问
可见性:线程对主内存的修改可以及时被其他行程观察到
有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
2.Atomic包和CAS理论
原子性:Atomic包
Atomic类的incrementAndGet()方法,
Unsafe类的源码:
image.png
解析:这里的三个参数,Object var1是指当前对象,是需要修改的类对象,即调用increamentAndGet方法的对象;
long var2 是指需要修改的字段的内存地址;int var4是要加上的值;var5是修改前字段的值,var5+var4是修改后字段的值。
var5在没有其他线程处理的情况下,值应该就是var2,所以在while中,当var2和var5相同时,才会执行更新var5+var4,否则的话,就去执行do里面,获取底层最新的数据作为var5。
=====》这就是CAS(Compare And Swap)。
CAS的缺点:
<1> 循环时间长,开销很大:
在执行getAndAddInt方法时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
<2> 只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
<3> ABA问题:
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
3.AtomicLong和LongAdder
https://blog.csdn.net/codingtu/article/details/89047291
LongAdder的increase()方法:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//第一个if进行了两个判断,(1)如果cells不为空,则直接进入第二个if语句中。
//(2)同样会先使用cas指令来尝试add,如果成功则直接返回。如果失败则说明存在竞争,需要重新add
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
而这一句a = as[getProbe() & m]其实就是通过getProbe()拿到当前Thread的threadLocalRandomProbe的probe Hash值。这个值其实是一个随机值,这个随机值由当前线程ThreadLocalRandom.current()产生。不用Rondom的原因是因为这里已经是高并发了,多线程情况下Rondom会极大可能得到同一个随机值。因此这里使用threadLocalRandomProbe在高并发时会更加随机,减少冲突。
这里使用到了Cell类对象,Cell对象是LongAdder高并发实现的关键。在casBase冲突严重的时候,就会去创建Cell对象并添加到cells中。
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
//提供CAS方法修改当前Cell对象上的value
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
总结:在并发处理上,AtomicLong和LongAdder均具有各自优势,需要怎么使用还是得看使用场景。看完这篇文章,其实并不意味着LongAdder就一定比AtomicLong好使,个人认为在QPS统计等统计操作上,LongAdder会更加适合,而AtomicLong在自增控制方面是LongAdder无法代替的。在多数地并发和少数高并发情况下,AtomicLong和LongAdder性能上差异并不是很大,只有在并发极高的时候,才能真正体现LongAdder的优势。
4. AtomicReference和AtomicReferenceFieldUpdater
5.原子性 Synchronized
<1> 使用方法:
- 1.修饰代码块:大括号括起来的代码,作用于调用的对象
- 2.修饰方法:整个方法,作用于调用的对象
- 3.修饰静态方法:整个静态方法,作用于所有对象
- 4.修饰类,括号括起来的代码,作用于所有对象
<2> 代码示例
1.
@Slf4j
public class SynchronizedExample1 {
//模拟情况1
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {}- {}", j, i);
}
}
}
//模拟情况2
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.test1(1);
});
executorService.execute(() -> {
//example2.test1(2);
example1.test1(1);
});
}
}
说明:
这是两个线程,执行同1个对象的synchronized修饰的代码块,符合<1>中的第1种情况,即
线程A执行了某对象的synchronized修饰的代码块,在未结束前,线程B执行到某方法的synchronized修饰的代码块时,无法调用,需等待。
结果:
test1 1- 1
test1 1- 2
test1 1- 3
test1 1- 4
test1 1- 5
test1 1- 6
test1 1- 7
test1 1- 8
test1 1- 9
test1 1- 0
test1 1- 1
test1 1- 2
test1 1- 3
test1 1- 4
test1 1- 5
test1 1- 6
test1 1- 7
test1 1- 8
test1 1- 9
此时,调用test2方法,也是相同的效果。
2.
@Slf4j
public class SynchronizedExample1 {
//模拟情况1
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {}- {}", j, i);
}
}
}
//模拟情况2
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.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
说明:
这是两个独立的线程,分别调用两个不同对象的synchronized修饰的方法,符合<1>中的第2种情况。
结果是互不影响,交替执行,即
test1 1- 0
test1 2- 0
test1 1- 1
test1 2- 1
test1 1- 2
test1 2- 2
test1 1- 3
test1 2- 3
test1 1- 4
test1 2- 4
test1 1- 5
test1 2- 5
test1 1- 6
test1 2- 6
test1 1- 7
test1 2- 7
test1 1- 8
test1 2- 8
test1 2- 9
test1 1- 9
此时,调用test2方法,也是相同的效果。并不需要等某个线程结束后,才能执行下一个线程,因为两个线程调用的是不同的对象。
另外,通过1、2可证明,如果一个方法内,synchronized修饰的是完整的代码块,那么效果与用synchronized修饰整个方法是一致的。
3.
@Slf4j
public class SynchronizedExample2 {
//模拟情况3,修饰1个类
public void test1(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test1 {}- {}", j, i);
}
}
}
//模拟情况4,修饰1个静态方法
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.test2(1);
});
executorService.execute(() -> {
//example2.test1(2);
example2.test2(2);
});
}
}
说明:
模拟的是synchronized修饰的静态方法,这个时候作用于所有对象,所以即使同一时间有两个独立的线程调用两个不同的对象的该方法,那么也得等某个线程执行完,才能执行下一个线程(虽然是不同的对象,也得等执行完)。
结果:
test2 1 - 0
test2 1 - 1
test2 1 - 2
test2 1 - 3
test2 1 - 4
test2 1 - 5
test2 1 - 6
test2 1 - 7
test2 1 - 8
test2 1 - 9
test2 2 - 0
test2 2 - 1
test2 2 - 2
test2 2 - 3
test2 2 - 4
test2 2 - 5
test2 2 - 6
test2 2 - 7
test2 2 - 8
test2 2 - 9
换成执行test1,也是相同的效果。用synchronized修饰某一个类,也是作用于所有对象。
<3> 计数问题:
使用synchronized解决计数问题,这个时候计数不用AtomicInteger,用int也能保证计数的正确性,只要对add()方法,加上synchronized和static关键字,这个时候作用于所有对象。
/**
* 使用synchronized解决计数问题
* Created by 凌战 on 2019/11/3
*/
@Slf4j
@ThreadSafe
public class CountExample3 {
//请求总数
public static int clientTotal=5000;
//同时并发执行的线程数
public static int threadTotal=200;
public static int count=0;
public static void main(String[] args) throws InterruptedException {
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 synchronized void add(){
count++;
}
}
6.总结:原子性对比
synchronized:不可中断锁,适合竞争不激烈,可读性好;
Lock:可中断锁,多样化同步,竞争激烈时能维持常态;
Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值。
网友评论