(1)Volatile
volatile的特性
volatile变量具有下列特性:
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 阻止编译时和运行时的指令重排。
- 原子性。这里所说的原子性是对任意单个volatile变量的读/写,但是类似于volatile++这种复合操作不具有原子性。
package day02; import java.util.concurrent.TimeUnit; /** * @author: zdc * @date: 2020-03-19 */ public class _1VolatileTest { /* volatile*/ boolean flag = true; public void m(){ System.out.println("m start"); while (flag){ } System.out.println("m end"); } public static void main(String[] args) { _1VolatileTest v = new _1VolatileTest(); new Thread(v::m,"t").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } v.flag = false; } }
什么是可见性?
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,我们可以简单的理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。下面的例子中两个类的执行效果是相同的。
public class VolatileFeatureExample { volatile long v1 = 0L; public void set(long l) { v1 = l; } public void getAndIncrement() { v1++; } public long get() { return v1; } } ---------------------------------------------- public class VolatileFeatureExample { long v1 = 0L; public synchronized void set(long l) { v1 = l; } public void getAndIncrement() { long temp = get(); temp += 1L; set(temp); } public synchronized long get() { return v1; } }
volatile是如何实现可见性的呢?
Java的内存模型:
指令在CPU中执行,CPU运行速度较快,因此为减少从内存中频繁读写数据的开销,在cpu与内存的操作之间,有个高速缓存的的区域
获取数据流程:
- 从缓存中获取Data
- 缓存中存在,则直接返回
- 缓存中不存在
- 从内存中获取Data数据
- 将Data数据写入缓存
- 返回Data数据
上面的流程中,第一步会导致一致性问题,分析如下
若内存中Data已更新,但缓存中数据未更新,此时返回缓存中Data,返回的是旧数据。
解决方案:
- 总线上加LOCK#锁
- 因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存
- 缓存一致性协议
- 在内存中数据发生变更后,同时使所有缓存中的数据失效,在访问缓存中数据时,优先判断是否已经失效,若失效则从内存中获取最新的数据回写到缓存中,并返回
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存保存该线程读写共享变量的副本。因此也存在上面的一致性问题,即如何保证线程对共享变量的修改后,其他的线程能访问到最新的共享变量。
指令重排序
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
举例说明
int i;
boolean ans;
i = 10;
ans = true;
上面的代码中,i
和ans
的赋值先后顺序由于指令重排,可能会出现ans=true
时,i
依然为0的情况。
Volatile关键字
用法
- 在变量前面加上
volatile
即可
作用
- 确保共享变量的修改,对其他线程都是立即可见的
- 禁止指令重排(即当访问or修改
volatile
修饰的共享变量时,确保前面的代码都执行完了)
原理和实现机制
- 修改
volatile
声明的共享变量,会强制要求修改后的值写入内存,并失效其他线程的本地内存中的副本 - 汇编之后,加入volatile关键字时,会多出一个lock前缀指令
- 它确保指令重排序时不会把其后面的指令排到lock指令之前,也不会把前面的指令排到lock指令之后
例子::Java并发-懒汉式单例设计模式加volatile的原因
class SingletonClass{ 2 private static SingletonClass instance = null; 3 4 private SingletonClass() {} 5 6 public static SingletonClass getInstance() { 7 if(instance==null) { 8 synchronized ( SingletonClass.class) { 9 if(instance==null) 10 instance = new SingletonClass();//语句1 11 } 12 } 13 return instance; 14 } 15 }
上面的代码在多线程下调用可能会报错,具体报错原因:
在语句1中并不是一个原子操作,在JVM中其实是3个操作:
1.给instance分配空间、
2.调用 Singleton 的构造函数来初始化、
3.将instance对象指向分配的内存空间(instance指向分配的内存空间后就不为null了);
在JVM中的及时编译存在指令重排序的优化,也就是说不能保证1,2,3执行的顺序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
通过添加volatile就可以解决这种报错,因为volatile可以保证1、2、3的执行顺序,没执行玩1、2就肯定不会执行3,也就是没有执行完1、2instance一直为空
锁优化:
锁细化:不应该把锁加在整个方法上。
锁粗化:在征用特别频繁的地方。
以对象做锁时,为使它不发生改变,应该加final。
(2)CAS --无锁优化 或称自旋。
package day02; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * @author: zdc * @date: 2020-03-19 */ public class _2ActomicInteger { //int count=0; AtomicInteger count = new AtomicInteger(0); void m(){ for (int i = 0; i < 10000; i++) { count.incrementAndGet(); // count++; } } public static void main(String[] args) { _2ActomicInteger test = new _2ActomicInteger(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(test::m,"thread_"+i)); } threads.forEach((t)->t.start()); //让主线程最后运行 得到结果 threads.forEach((t)->{ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(test.count); } }
CAS算法理解 https://www.jianshu.com/p/ab2c8fce878b
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了.
package day02; import java.sql.Time; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * @author: zdc * @date: 2020-03-19 */ public class _3ABATest { private static AtomicInteger count = new AtomicInteger(10); public static void main(String[] args) {
//10-》11-》10 new Thread(()->{ System.out.println(Thread.currentThread().getName()+"预期值是10?"+count.compareAndSet(10,11)); System.out.println(Thread.currentThread().getName()+"预期值是11?"+count.compareAndSet(11,10)); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ System.out.println(Thread.currentThread().getName()+"预期值是10?"+count.compareAndSet(10,12)); },"B").start(); } }
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1); 可以添加版本号 解决ABA