《JAVA并发编程实战》——线程安全

}

private static void add() {

count++;

}

}

这个代码的意思很简单,就是先定义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);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

每年转战互联网行业的人很多,说白了也是冲着高薪去的,不管你是即将步入这个行业还是想转行,学习是必不可少的。作为一个Java开发,学习成了日常生活的一部分,不学习你就会被这个行业淘汰,这也是这个行业残酷的现实。

如果你对Java感兴趣,想要转行改变自己,那就要趁着机遇行动起来。或许,这份限量版的Java零基础宝典能够对你有所帮助。

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
jpg" alt=“img” style=“zoom: 33%;” />

最后

每年转战互联网行业的人很多,说白了也是冲着高薪去的,不管你是即将步入这个行业还是想转行,学习是必不可少的。作为一个Java开发,学习成了日常生活的一部分,不学习你就会被这个行业淘汰,这也是这个行业残酷的现实。

如果你对Java感兴趣,想要转行改变自己,那就要趁着机遇行动起来。或许,这份限量版的Java零基础宝典能够对你有所帮助。

[外链图片转存中…(img-dpUCUWj1-1712229028577)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值