有关Volatile和JMM的相关小结
1、Volatile
JVM提供的轻量级同步机制
特性:保证可见性,不保证原子性,禁止指令重排序
原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
整体完整,要么成功,要么失败
volatile不保证原子性验证
验证可见性:
import java.util.concurrent.TimeUnit;
class MyData{
//volatile int number = 0;
int number = 0;
public void changeNumber(){
this.number = 20;
}
}
/**
* 验证volatile的可见性
* number变量之前没有被volatile修饰
*/
public class Demo2 {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
//暂停一会
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
},"AA").start();
//main线程
while(myData.number == 0){
//main线程就一直在这里等待循环,知道number不在为0
//由于没有课件性,所以程序将一直死循环
}
System.out.println(Thread.currentThread().getName()+"\t mission is over:"+ myData.number);
}
}
没有加volatile修饰时:
使用volatile修饰后:
验证原子性:
import java.util.concurrent.TimeUnit;
class MyData{
volatile int number = 0;
// int number = 0;
public void changeNumber(){
this.number = 20;
}
//此时number前面是加了volatile关键字修饰,不保证原子性
public void addPlusPlus(){
number++;
}
}
/**
* 验证volatile的可见性
* number变量之前没有被volatile修饰
* 原子性:
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
* 整体完整,要么成功,要么失败
* volatile不保证原子性验证
*
*/
public class Demo2 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i=0;i<20;i++){
new Thread(()->{
for (int j=0;j<1000;j++){
myData.addPlusPlus();
}
},"AA").start();
}
//Thread类的activeCount()方法用于返回当前线程的线程组中活动线程的数量
while (Thread.activeCount()>2){
Thread.yield();//当线程数大于2时礼让
}
//等待20个线程全部计算完后,再用main线程取得最终的结果值,具有原子性应该是20000
System.out.println(Thread.currentThread().getName()+"\t value:"+myData.number);
}
}
没有被volatile修饰:
被修饰后:
修饰之后依然存在数据丢失,即不保证原子性
由于线程运行太快,导致多线程下来不及更新数据,造成数据的丢失,因此判定volatile不能保证原子性!
解决方法:
* 加同步机制:synchronized(太重,不推荐)
* JUC(常用的并发工具类)包下的原子引用:AtomicInteger
AtomicInteger的构造方法无参时,默认为0,该类有相应的自增,自减等方法。
方法:
getAndIncrement():–>i++
incrementAndGet():—>++i
decrementAndGet():–>--i
getAndDecrement():–>i–
代码验证:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int number = 0;
// int number = 0;
public void changeNumber(){
this.number = 20;
}
//此时number前面是加了volatile关键字修饰,不保证原子性
public void addPlusPlus(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();//i++操作
}
}
/**
* 验证volatile的可见性
* number变量之前没有被volatile修饰
* 原子性:
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割
* 整体完整,要么成功,要么失败
* volatile不保证原子性验证
*
*/
public class Demo2 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i=0;i<20;i++){
new Thread(()->{
for (int j=0;j<1000;j++){
myData.addPlusPlus();
myData.addAtomic();
}
},"AA").start();
}
//Thread类的activeCount()方法用于返回当前线程的线程组中活动线程的数量
while (Thread.activeCount()>2){
Thread.yield();//当线程数大于2时礼让
}
//等待20个线程全部计算完后,再用main线程取得最终的结果值,具有原子性应该是20000
System.out.println(Thread.currentThread().getName()+"\t int value:"+myData.number);
System.out.println(Thread.currentThread().getName()+"\t atomicInteger value:"+myData.atomicInteger);
}
}
使用原子引用和没有使用原子引用的对比:一个存在数据的丢失,一个没有
为什么使用原子引用就可以解决呢?
CAS–自旋锁等
指令重排序:
案例:
在什么地方可以使用volatile?
单例模式DCL代码
传统懒汉式单例:
/**
* 单例模式
*/
public class SingletonDemo {
// private static SingletonDemo instace = null;//懒汉式
private static volatile SingletonDemo instace = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
public static SingletonDemo getInstace(){//可以使用synchronized解决
if(instace == null){
instace = new SingletonDemo();
}
return instace;
}
public static void main(String[] args) {
//多线程并发,会出现错误
for (int i = 0 ; i < 1000; i++) {
new Thread(() -> {
SingletonDemo.getInstace();
},String.valueOf(i)).start();
}
}
}
1.可以使用synchronized解决,但是不推荐(太重);
2.使用DCL模式(Double check Lock 双端检索机制),即锁前后分别判断是否为空;代码如上(还不是最完美的,还存在指令重排,在一定及其微妙的情况下,出现问题)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEVvPCGr-1572517610817)(C:\Users\Entity-G\AppData\Roaming\Typora\typora-user-images\1572505720875.png)]
指令重排序只会保证语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性;
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
3.Volatile+DCL:具体代码如上(主要-禁止指令重排)
private static volatile SingletonDemo instace = null;
手写缓存
为什么要用CAS而不使用synchronized?
synchronized:每次只允许一条线程来执行,保证了一致性,但是降低了并发性
CAS:没有使用synchronized,而是使用do-while,可以反复进行比较,知道成功,既保证了一致性,也保证了并发性
CAS是什么?如何实现?
CAS(Compare-And-Set):比较、交换;是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法,调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
代码实现:
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS:比较、交换
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
//期望值相同,交换(修改)
System.out.println(atomicInteger.compareAndSet(5,1000)+"\t current data: "+atomicInteger);//true current data: 1000
//期望值不同,则返回false,并重新获取值
System.out.println(atomicInteger.compareAndSet(5,1024)+"\t current data: "+atomicInteger);//false current data: 1000
}
}
CAS的底层原理?Unsafe怎样理解?
1.自旋锁
2.Unsafe:CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
总结:
CAS(CompareAndSwap):比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作;
否则继续比较直到主内存和工作内存中的值一致为止
CAS应用:
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的缺点?
1.循环时间长开销大;
2.只能保证一个共享变量的原子操作;
3.引出来ABA问题??
ABA问题?原子引用更新(AtomicReference)?如何规避ABA问题??
CAS会导致“ABA问题”
CAS算法实现的一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么这个时间差会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时线程one进行CAS操作发现内存中任然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
ABA问题解决:理解原子引用+新增一种机制,那就是修改版本号(类似时间戳)
import org.springframework.aop.ThrowsAdvice;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA问题的解决
* AtomicStampedReference -- 带时间戳的原子引用
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);//版本号为1
public static void main(String[] args) {
System.out.println("=========以下是ABA问题的产生========");
//ABA线程
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"AA").start();
new Thread(()->{
//保证ABA问题的引发
try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100,2019));
},"BB").start();
System.out.println("=========以下是ABA问题的解决========");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获取版本号
System.out.println(Thread.currentThread().getName()+"\t 第1次版本号:"+stamp);
try {TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第2次版本号:"+atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第3次版本号:"+atomicStampedReference.getStamp()+1);
},"CC").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第1次版本号:"+stamp);
//保证ABA问题的引发
try {TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t 修改是否成功:"+result+"\t 当前版本号:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t 当前线程最新值:"+atomicStampedReference.getReference());
},"DD").start();
}
}
2、JMM是什么?–线程安全获得保障
JVM:java虚拟机
JMM(Java Memory Model):java内存模型,本身是一种抽象的概念,并不真实存在,它描述了一组规则和规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
*JMM关于同步的规定:
1.线程解锁前,必须把共享变量的值刷新回主内存;
2.线程加锁前,必须读取主内存的最新值到自己的工作内存;
3.加锁解锁是同一把锁。
JMM的三大特性:可见性、原子性、有序性
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(也叫栈空间),工作内存是每个线程的私有数据区域,而Java内训模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的读写操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成再将变量协会主内存,不能直接草操作主内存中的变量,各个线程中的工作内存存储着主内存中变量副本拷贝,因此同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
读写速度:硬盘<内存<CPU
MySQL:基于硬盘的存储
Redis:基于内存的存储
线程安全获得保证:
1.工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,他们都可以是一个线程修改后的变量立即对其他线程可见;
2.对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排序优化。