volatile 是 JVM 提供的一种轻量级的同步机制,它能够保证线程之间的可见性、不能保证原子性、禁止指令重排(保证有序性)。
下面补充一个概念:JMM(java money model)Java 内存模型
1.1 JMM
JMM 本身是一种抽象的概念,并不是真实存在,**它描述的是一组规则或规范,**通过这个规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式
JMM 本身要求可见性、原子性、有序性
JMM 是 Java 内存模型。每一个线程都有自己的工作内存,但是所有的共享变量都是存在主内存中(电脑上的物理内存),当线程要操作数据时,会先将主内存中的数据复制一份到工作内存,在操作处理完以后,在更新到主内存中
本身线程自己操作是对其他线程不可见的,并且线程之间无权互相访问,线程之间的通信(数据传递)必须通过主内存来实现
可见性就是当前线程在将从主内存取出的值更新将要刷回主内存时,会通知其他线程自己已将主内存中的值更新
1.1.1 JMM关于同步的规定- 线程解锁前,必须把共享变量的值刷新回主内存线程加锁前,必须读取主内存中的最新值到自己的工作内存加锁解锁是同一把锁并且加了多少把锁就要进行解锁多少次,成对出现
public class VolatileDemo {
public static void main(String[] args) {
Volatile01 volatile01 = new Volatile01();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程运行");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatile01.add();
System.out.println(Thread.currentThread().getName()+"修改值");
},"线程一").start();
while (volatile01.i == 10){
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}
class Volatile01{
int i = 10;
public void add(){
i = 60;
}
}
1.2 原子性
在执行一系列的语句中,要么语句都成功,要么都失败。
1.3 volatile不具有原子性和解决办法public class VolatileDemo02 {
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo2.addV();
}
},String.valueOf(i)).start();
}
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo2.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(demo2.i);
System.out.println(demo2.atomicInteger);
}
}
// 解决办法
class Demo2{
volatile int i = 0;
public void addV(){
i++;
}
// new AtomicInteger() 括号中没有定义则默认值为0
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement(); // 等同于 i++
}
}
1.4 有序性
有序性的对立是指令重排,指令重排是一种优化,提高运行效率
对于单线程来说经过指令重排可以使最后的结果和有序性执行的结果相同
但对于多线程就不能保证。所以虽然能够提高效率,但如果是多线程执行,可能因为重排的影响最终得到的结果不是我们想要的,那是不行的,因此就有必要在一些特定的情况下不允许指令重排的出现
虽然多线程可能出现指令重排,但是在进行重排时也要考虑数据的依赖性,比如:
int i = 0;
i = i + 5;
1.5 volatile实现禁止指令重排原理这种情况下因为第二条语句对i的创建是有依赖性的,那就不能在第一条语句前面执行。
先了解一个概念,内存屏障(Memory Barrier)又称为内存栅栏,是一个 cpu 指令
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 memory barrier 则会告诉编译器和 CPU,不管什么指令都不能和这条 memory barrier 指令重排序,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
内存屏障的另外一个作用是强制刷出各种 CPU 的缓存数据,因此任何CPU上线程都能读取到这些数据的最新版本。==》 可见性
1.5.1 作用- 保证特定的操作的执行顺序(有序性)保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
在多线程的情况下,在原本的单例模式可能会创建出多个实例。
1.6.1 实例演示public class SingletonDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Demo.getDemo();
},String.valueOf(i)).start();
}
}
}
class Demo{
private static Demo demo = null;
private Demo() {
System.out.println(Thread.currentThread().getName()+" 进入构造器方法");
}
public static Demo getDemo(){
if (demo == null)
demo = new Demo();
return demo;
}
}
1.6.2 解决方式运行结果会出现创建出多个实例对象。
- DCL(double check lock)双端检锁机制DCL + Volatile
在锁的前后进行检查
public static Demo getDemo(){
if (demo == null)
synchronized (Demo.class){
if (demo == null){
demo = new Demo();
}
}
return demo;
}
2.问题
虽然加入了双端检锁(在加锁的前后进行两次检查),但是当是多线程运行的时候,可能会出现指令重排的现象
顺序执行是:
- 分配空间对象完成初始化初始化对象指向分配空间
指令重排:
- 分配空间对象指向分配空间对象完成初始化
3.DCL + Volatile就会出现在于某一线程执行到第一次检测,读取到demo不为null时,但是当前demo的引用对象可能没有完成初始化,则就会造成,虽然当前分配给demo实例的内存空间不为空,但是其他线程此时读取的数据却为空的情况。
为了避免这种情况,我们采用第二种解决方式,在唯一实例上面加上volatile,禁止出现指令重排的现象。
private volatile static Demo demo;2.CAS
compareandswap(比较并交换),AtomicInteger 等类似实现原子性的底层原理
比较当前工作内存中的值和主内存中的值,如果预期的值和当前内存中的值相同,则执行下一步操作,否则继续比较直到主内存和工作内存中预期的值一致为止
它是一条 CPU 并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则进行更新,这个过程是原子的
完全依靠硬件的功能,通过 sun.misc.Unsafe 实现 CAS 原子操作
2.1 AtomicInteger.compareandset()方法分析原语,系统内核操作,执行过程中不允许被打断。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2021)+"t 目前的值为:"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1003)+"t 目前的值为:"+atomicInteger.get());
}
}
2.1.1 compareandset源码
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
2.2 AtomicInteger.getAndIncrement()源码分析拥有一个期望值和更新值,当期望值和主内存中的值相同是进行更新,否则失败。返回类型为boolean类型。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
1.参数解析我们发现它是通过unsafe.getAndAddInt这个方法实现
this:当前对象
valueOffset:内存地址偏移量,也就是当前对象的内存地址
1:在当前对象上增加1
2.2.1 unsafe.getAndAddInt()分析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;
传入对象、对象地址和增加量,第一步先获取到对象当前的值赋值给 var5
下一步进行增加,判断如果当前地址上的值和得到的 var5 相同则进行增加,否则继续循环获得当前对象上的值,直到在比对是的值和当前地址上的值相同为止
依靠自旋来实现类似锁同步的机制
private volatile int value;值本身是用volatile修饰的,当前线程修改不成功时,也会收到被修改后更新的通知。
进行下一步,将地址上的进行更新
2.2.2 compareAndSwapInt()方法底层如果继续往下寻找会发现public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);是一个本地方法,本地方法通过调用 c 语言的指针来直接操作内存地址上的变量,并且这个过程是不可中断的,具有原子性。
compareAndSwapInt() 是一个本地方法,源码在 unsafe.cpp 代码中
2.3 CAS和synchronized优点如果通过synchronized关键实现,虽然实现了原子性,但是只允许单线程进行操作则会降低并发性。
2.3.1 CAS缺点循环时间长开销大
如果一直发现值都不同则会一直比较,自旋(CAS实现 + UnSafe)
只能保证一个共享变量的原子操作
synchronized可以锁住代码块
引出的ABA问题
同时两个线程操作主内存中的值,A线程处理时间为10秒
B 线程处理的时间为2秒。某一时刻主内存中的值为1,两个线程同时拷贝回自己的工作内存,B 先将主内存中的值从 1 改为了 2
但在 A 线程处理的时间内,B 线程又将主内存的值从 2 改为了 1
改完后,A 线程来更新值,发现和原先的 1 相同,于是就进行了值修改,将主内存中的值从1改为3
只是值相同,但是实际上值1本身已经发生改变
此时的 1 已经不是原本的 1,数字可能看起来影响不大,但是我们把他扩大到实际业务中。
表面上看是没有问题的,但实际上这就造成了ABA问题
1.自定义原子引用解决ABA问题,我们先看看自定义原子引用,帮助我们解决ABA问题
除去jdk提供的原子引用,用户自己本身也是可以自定义。
2.实例public class AtomicReferenceDemo {
public static void main(String[] args) {
User zs = new User("zs", 22);
User za = new User("za", 25);
AtomicReference userAtomicReference = new AtomicReference<>(zs);
System.out.println(userAtomicReference.compareAndSet(zs, za)+"t 现在的用户为:"+userAtomicReference.get().toString());
}
}
class User{
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
3.ABA问题代码实现
public class AtomicReferenceDemo02 {
public static void main(String[] args) {
AtomicReference integerAtomicReference = new AtomicReference<>(5);
new Thread(() -> {
System.out.println(integerAtomicReference.compareAndSet(5, 6));
System.out.println(integerAtomicReference.compareAndSet(6, 5));
System.out.println("线程一,已完成修改并又将值改了回去");
},"线程一").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(integerAtomicReference.compareAndSet(5, 2021));
System.out.println(integerAtomicReference.get().toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程二").start();
}
}
4.解决ABA问题
在数据上面加上版本号。
4.1 实现演示实现:通过加上时间戳。
public class AtomicReferenceDemo03 {
public static void main(String[] args) {
AtomicStampedReference as = new AtomicStampedReference(100,1);
new Thread(() -> {
try {
int stamp = as.getStamp();
TimeUnit.SECONDS.sleep(1);
System.out.println(as.compareAndSet(100, 101, stamp, as.getStamp() + 1));
System.out.println(as.compareAndSet(101, 100, as.getStamp(), as.getStamp() + 1));
System.out.println(Thread.currentThread().getName()+"t 修改以后的值为:"+as.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程一").start();
new Thread(() -> {
try {
int stamp = as.getStamp();
TimeUnit.SECONDS.sleep(4);
System.out.println(as.compareAndSet(100, 2019, stamp, stamp + 1));
System.out.println(Thread.currentThread().getName()+"t 当前的值为:"+as.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程二").start();
}
}
4.2 AtomicStampedReference()解析
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
4.3 AtomicStampedReference.compareAndSet()方法解析参数为:初始值和设定版本号
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
expectedReference:期待的值
newReference:更新的值
expectedStamp:期待的版本
newStamp:更新的版本
3.集合线程安全 3.1 List 3.1.1 问题当都满足的时,才能更新,通过添加版本,解决了ABA问题
在多线程下的并发修改异常。
3.1.2 原因arrayslist中没有线程同步机制,无法保证线程执行的原子性
3.1.3 解决new vactor
底层通过加入synchronized关键字,性能太低,不推荐。(已弃用)
Collections.synchronizedlist
通过加入synchronized关键字
new copyonwritelist(写时复制技术)
通过加入锁(lock),将list进行复制,然后在复制的上面进行更新,更新成功后,通知其他线程用最新的list执行其他操作(推荐)
同上(list),类似的同步 synchronizedhashset 和 copyonwritehashset。
3.2.1 关于hashset底层底成是由hashmap实现
只有key没有value
所有value的值都是一个值private static final Object PRESENT = new Object();
new concurrentHashMap(推荐,性能和 HashMap 差距不大,并且可以保证并发安全。
4.自旋锁尝试获取锁的线程不会立即阻塞(也就是继续执行其他的操作,过一会继续来尝试获取)。而是采用循环的方式区尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
4.1 实例用循环来代替阻塞。
public class SpinLockDemo {
AtomicReference af = new AtomicReference();
public void myLock(){
// 获取当前在执行这个方法的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"进入方法");
while (!af.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+"在执行ing");
}
}
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"释放锁");
af.compareAndSet(thread,null);
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
try {
spinLockDemo.myLock();
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLockDemo.myUnLock();
}
},"aa").start();
// 让主线程休眠1s,确保aa线程先执行
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
},"bb").start();
}
}
5.枚举
可以作为一个简单的数据库使用
public enum EnumDemo {
ONE(1,"齐国"),TWO(2,"齐国1"),THREE(3,"齐国3"),FOUR(4,"齐国4"),FIVE(5,"齐国5"),SIX(6,"齐国6");
private Integer index;
private String name;
EnumDemo(Integer index, String name) {
this.index = index;
this.name = name;
}
public static EnumDemo getElement(int index){
EnumDemo[] arr = EnumDemo.values();
for (EnumDemo enumDemo : arr) {
if (index == enumDemo.getIndex()){
return enumDemo;
}
}
return null;
}
public Integer getIndex() {
return index;
}
public String getName() {
return name;
}
}
6.GC ROOT对象比如第一张表叫做ONE,里面的属性就是字段。
枚举根节点做可达性发现(根搜索路径),gc root是一个set集合,以下是可以作为 GC ROOT 的对象来源:
虚拟机栈中局部变量表所引用的对象方法区的类静态变量引用的对象方法区常量引用的对象本地方法栈中引用的对象 7.假如生产环境出现的CPU占用过高,谈谈分析思路和定位?
先找出cpu占用最高
ps -ef或jps -l找到具体的程序
定位到具体线程或代码
ps -mp [进程号] -o THREAD,tid,time
-o:用户自定义的显示方式
线程ID转换为16进制格式
通过系统自带的计算机转换
通过jstack定位
jstack [进程号] | grep [16进制线程id] -A60
-A60:打印前60行



