并发编程atomic&collections详解
大家里面的案例可以从gitHub下载下来自己看一下
地址:https://github.com/JolyouLu/Juc-study.git 代码在Juc-Atominc下
Atominc简介
在之前的多线程学习中,我们人使用到了Volatile他能保证一致性和有序性,但是不能保证原子性,我们可以使用加锁来确保操作的原子性,加锁还是对程序运行速度会有一定的影响,锁加的不对会极大的影响程序速度,那我们如何在不加锁的情况下确保原子性操作呢,我们的java.util.concurrent.atomic包下的类,都是可以无锁进行原子性操作,底层原理应用到了CAS操作
原子操作类归纳
类型 | 类名 |
---|---|
基本类型 | AtomicBoolean |
基本类型 | AtomicInteger |
基本类型 | AtomicLong |
数组 | AtomicIntergerArray |
数组 | AtomicLongArray |
数组 | AtomicReferenceArray |
引用类型 | AtomicReference |
引用类型 | AtomicReferenceArrayFleldUpdater |
原子更新字段类 | AtomicIntegerFieldUpdater |
原子更新字段类 | AtomicLongFieldUpdater |
原子更新字段类 | AtomicStampedFieldUpdater |
AtomicInteger
AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作是我们工作中最常用的一个原子类,一般用于并发统计。常见的api有
java.util.concurrent.atomic.AtomicInteger#incrementAndGet 原子自增1
java.util.concurrent.atomic.AtomicInteger#decrementAndGet原子自减1
java.util.concurrent.atomic.AtomicInteger#addAndGet原子在当前值加上给定的值。
java.util.concurrent.atomic.AtomicInteger#get返回当前最新的值
CAS算法
什么是CAS算法,CAS(Compare-And-Swap)即比较与交换,CAS算法是硬件对于并发操作提供的数据共享支持,CAS的变量都带有Volatile保证了可见性,CAS怎么保证原子性呢,CAS中包含了三个操作数:内存值 V,预估值 A,更新值 B,只有V == A 时,CPU会尝试把B赋值到V上,否则不做任何操作,大致意思就是首先去获取当前内存中的值V,在我修改之前我再去获取一次内存放到A,那如果这是V == A 那意思就是内存中的值还没修改这时我只需要把B(新值)写入主内存即可完成修改了,因为B回写主内存只有一步所以可以保证原子性。
案例
public class OrdersOrdersInteger {
//原子性操作类
static AtomicInteger count = new AtomicInteger(0);
//普通int类
// static Integer count = new Integer(0);
public String getOrdersNo(){
SimpleDateFormat data = new SimpleDateFormat("YYYYMMDDHHMMSS");
//保证原子性 不会有重复订单id
return data.format(new Date())+count.incrementAndGet();
//无法保证原子性递增 出现生产重复订单id
// return data.format(new Date())+count++;
}
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(1);
ExecutorService exeuctor = Executors.newFixedThreadPool(10);
final OrdersOrdersInteger orderServer=new OrdersOrdersInteger();
for (int i =0;i<10;i++){
exeuctor.submit(new Runnable() {
@Override
public void run() {
System.out.println(orderServer.getOrdersNo());;
}
});
}
latch.countDown();
exeuctor.shutdown();
}
}
ABA问题
ABA问题就是,假设我现在主内存有一个int=10,然后线程1和2需要对这个值进行运算,但是有点不同的是线程1需要加10后减10,然后线程2需要加10,然后线程1和2同时拷贝当前内存的值10作为副本放入A(预估值)中,线程1比程序2快一点的对int进行了加10,减10然后回写主内存当前主内存为10,然后线程2进行了加10放入B,获取当前V(内存值),比较一下自己之前拿到的A(预估值),发现都是10如果把B赋值给V,这中间就存在一个ABA的问题,线程1对内存进行了2次操作后得到10回写到主内存中了,然后线程2修改比较V和A的时候,看似相同都是10,其实不一样现在的V(被线程1修改2次的),A(初始值),其实线程2要发起当前操作重新获取A值进行运算比对V,但是CPU并不知道,CPU只知道10=10可以修改
以下是模拟ABA问题
public class AtomicIntegerABAExample {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1对 修改100为101后又修改101为100
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
//线程2修改 100为101成功,正常情况我们是不允许成功的因为V值和A值的值虽然相同但是 他们本质是不同的
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); // true
System.out.println(atomicInt.get());
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
}
}
ABA问题的解决方法
经过上面问题分析,解决ABA问题我们可以使用一个版本号标记当前版本,每次更新后版本会+1,如果线程,即主内存int=10,线程12进入运算获取当前内存副本放A中,如果线程1运算了2次 加10 减10,当前主内存一个是10 (版本2),然后线程2进行进行加10操作,比较V=10(版本2),和自己的A=10(版本0),虽然数字一样但是版本不同所以这个加10操作是不会被更新,线程要放弃本次操作,重新取值运算
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
//线程1对 修改100为101后又修改101为100 每次修改会版本会加一
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
//线程2对 修改100为101,但是比较的是版本0的值
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(c3); // 修改失败false
}
});
refT1.start();
refT2.start();
}
}
Collections简介
HashMap(线程不安全)
为什么HashMap线程不安全执行如下案例
public class HashMapExample {
public static final Map<String,String> map = new HashMap<>();
public static void main(String[] args) {
//线程1 插入0-1000
new Thread(){
public void run(){
for (int i=0;i<1000;i++){
map.put(String.valueOf(i),String.valueOf(i));
}
}
}.start();
//线程2 插入1000-2000
new Thread(){
public void run(){
for (int j=1000;j<2000;j++){
map.put(String.valueOf(j),String.valueOf(j));
}
}
}.start();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出
for(int i=0;i<2000;i++){
System.out.println("第:"+i+"元素,值:"+map.get(String.valueOf(i)));
}
}
}
执行代码后我们查看输出的时候会发现有的key对应的value是null的,这个原因是因为map在put时候发现容量不够需要扩容的时候需要重新编排哈希,然后这时你去获取他哈希的位置就是空的了
HashTable&ConcurrentHashMap(线程安全)
HashTable&ConcurrentHashMap的相同点是线程安全的,不同点就是锁的方式不一样
HashTable
我们使用HashTable运行如下代码发现并没有任何问题
public class HashTableExample {
public static final Map<String, String> map = new Hashtable<>();
public static void main(String[] args) {
new Thread(){
public void run(){
for (int i = 0;i<1000;i++){
map.put(String.valueOf(i),String.valueOf(i));
}
}
}.start();
new Thread(){
public void run() {
for(int j=1000;j<2000;j++){
map.put(String.valueOf(j), String.valueOf(j));
}
}
}.start();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出
for(int i=0;i<2000;i++){
System.out.println("第:"+i+"元素,值:"+map.get(String.valueOf(i)));
}
}
}
HashTable中的put方法是加了重量级锁synchronized(不推荐用性能比较低)
ConcurrentHashMap
public class ConcurrentHashMapExample {
public static final Map<String, String> map = new ConcurrentHashMap<>();
public static void main(String[] args) {
new Thread(){
public void run(){
for (int i = 0;i<1000;i++){
map.put(String.valueOf(i),String.valueOf(i));
}
}
}.start();
new Thread(){
public void run() {
for(int j=1000;j<2000;j++){
map.put(String.valueOf(j), String.valueOf(j));
}
}
}.start();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出
for(int i=0;i<2000;i++){
System.out.println("第:"+i+"元素,值:"+map.get(String.valueOf(i)));
}
}
}
ConcurrentHashMap中的put方法使用了分段锁性能比较因为锁的颗粒度更小了所以性能更优
ArrayList(线程不安全)
ArrayList之所以线程不安全和HashMap原理差不多,ArrayList在add的时候需要扩容,需要重新索引
CopyOnWriteArrayList(线程安全)
读写分离、读多写少 黑白名单
步骤:
- 如果写操作未完成,那么直接读取原数组的数据
- 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据
- 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取