目录
一.voliate:轻量级的同步机制,保证可见性;不保证原子性;禁止指令重排
一.voliate:轻量级的同步机制,保证可见性;不保证原子性;禁止指令重排
JMM(Java内存模型)三大特性:可见性;原子性;有序性
运行速率:硬盘<内存<cpu,由于CPU计算快,计算好的数据可以先放在缓存cache里,虚拟机在内存里。
可见性测试:
AAAA线程对main线程不可见,代码会一直运行,解决方法:将number变量定义前加一个voliate
原子性测试:
其中,Thread.yield()方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。为什么是2?因为默认后台有两个线程:一个是main线程,一个是GC线程; 就是main线程等20个线程算完了就通知您,可以看这20个线程用多少时间。最后计算结果达不到20000.因为voliate不保证原子性;因为禁止指令重排, 当执行++的过程中, 即使知道变量被修改也只能硬着头皮执行完++的全部步骤, 不能中断下来重头读取,写覆盖了
如何解决原子性:(1)方法上加syn关键字,但syn是重量锁不太好;(2)使用原子类AtomicInteger.getandIncrement ,底层是Unsafe的CAS实现的
有序性:
禁止指令重排,编译器一般会优化,而volatile保证顺序 ,避免多线程出现乱序情况,j通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化,内存屏障的另一个作用就是强制刷出各种CPU的缓存数据,保证CPU上的线程读取到的数据都是最新版本。
哪些地方用到过volatile?
1.单例模式 单例模式的7中创建方式
上面是单线程的懒汉式,三步:(1)定义一个静态私有变量;(2)创建私有构造器(3)创建静态方法;构造方法只被打印一次,并且下面的都为true,==比较的地址说明是独一份。
在多线程下 ,违背了单例模式,加syn不好,解决方式是使用DCL(Double Check Lock,双端检测锁) 的单例模式(多并发模式下),就是在加锁前后都进行判断,如下图。
但是DCL不一定线程安全,因为有指令重排序的存在,而加入volatile可以禁止指令重排。
就是这个地址已经被引用了,但是内容还没初始化是空。
2.copyOnwriteArrayList(解决ArrayList线程不安全的方法之一)
二.CAS:compareAndSet比较并交换
CAS是什么? 它是一条CPU并发原语,执行连续就保证了原子性
是atomicInteger类的一个方法,AtomicInteger中用的就是Unsafe的CAS操作,返回是Boolean类型,atomicInteger.compareAndSet(expect,update),如果与expect值相同就写入更新值,否则就返回主内存最新的值。这个过程保证原子性
CAS底层原理
var1是AtomicInteger对象本身,var2是该对象的引用地址,var4是需要变动的数量,var5是通过var1 var2找出主内存中真实的值,比较成功就+1,失败就一直while直到比较成功,类比自旋锁
CAS的缺点
(1).因为有do while,一直失败会给CPU带来很大的开销,一直自旋循环;它没有加锁,是通过互相比较,并发性强;
(2).只能保证一个共享变量的原子操作;而syn可以一段代码
(3).引发ABA问题
三.原子类AtomicInteger的ABA问题
CAS导致ABA问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变化。ABA就是其中一个线程将A改成了B,又将B改成A,而另一个线程再操作时任务内存的值没有变过。
如何解决ABA问题?
新增一个机制:修改版本号(类似时间戳stamped),AtomicStampedReference
四.集合类的不安全
4.1 ArrayList线程不安全解决方法
java.util.concurrentModificationException并发的常见异常,JUC修改异常
导致原因:一个人正在写入,另外一个人过来抢夺,导致数据不一致异常。即并发修改异常
解决方案:
(1)使用vector();
(2)使用collections类,调用collections.synchronizedList(new ArrayList<>());
(3) juc包下的CopyOnWriteArrayList,写时复制,读写分离的思想,在写之前先拷贝一个数组,在后面写len+1
何为写时复制
源码:
CopyOnWrite容器即写时复制的容器。在添加元素时,会先复制一份当前容器,往新的容器里添加元素,添加完后再将原容器的引用指向新的容器。这样做可以对容器进行并发的读而不需要加锁,因为当前容器不会添加任何元素。这是一种读写分离的思想即读和写在不同的容器里。
4.2 HahSet,底层是hashmap
hashset的add方法加一个值,map加键值对两个值,因为set只关心key,不关心value
1.多线程报错类型也是java.util.concurrentModificationException
2.解决方案:(1)collections类的synchronizedSet方法; (2)CopyonWriteArraySet,底层还是
CopyonWriteArrayList
4.3 HashMap
1.多线程报错类型也是java.util.concurrentModificationException
2.解决方案:
(1)使用Collections.synchronizedMap(Map)创建线程安全的map集合;
(2)Hashtable
(3)ConcurrentHashMap
ConcurrentHashMap 里面用了分段锁selgment
具体底层原理看这里:concurrentHashMap的分段锁 详细讲解
原理上来说,ConcurrentHashMap jdk1.7采用了分段锁技术,其中 Segment 继承于 ReentrantLock。
JDK1.8
的实现已经抛弃了Segment
分段锁机制,利用CAS+Synchronized
来保证并发更新的安全,和HashMap
类似底层采用数组+链表+红黑树的存储结构。
不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
参考:尚硅谷周阳老师面试 我把ConcurrentHashMap & HashTable的知识点都整理了一下_敖丙-CSDN博客_concurrenthashmap 敖丙