文章目录
- 1、线程安全问题
- 2、Java内存模型
- 3、volatile
- 4、原子性操作(CAS)
- 5、synchronized
1、线程安全问题
共享内存(堆内存):可以在线程之间共享的内存称为共享内存或者是堆内存。
共享变量:所有实例字段、静态字段和数组元素都是存储在堆内存中,这些字段和数组都是共享变量。
冲突:如果至少有一个操作使写操作,则对同一个变量的两次访问是冲突的。(多读不冲突)
如果多个线程对同一共享变量的操作发生了冲突,我们就认为线程间操作存在线程安全问题。
2、Java内存模型
Java内存模型:Java Memory Model,简称JMM。
- Java内存模型描述的是多线程Java程序的执行时的规则,目的是为了解决多线程场景下的线程安全问题,共享变量时Java内存模型的规范对象。
- Java内存模型规范了策略,由JVM进行实现,volatile,synchronized只是其中的两种可见性策略
ex. volatile关键字在Java语言规范中定义,在JVM规范中进行实现。下图是在JVM规范的Field Description中截图:volatile会被编译为ACC_VOLATILE指令,JVM实现了该指定的功能,从而实现类volatile关键字的功能
Java语言规范 VS Java虚拟机规范
- Java虚拟机规范定义了Java虚拟机的功能,包括JVM运行时数据区,JVM需要屏蔽OS的底层差异等。
- JVM运行时数据区是包括:程序计数器,堆,方法区,虚拟方法栈,本地方法栈等。
- JMM是针并发的线程安全问题提出的一系列规范,是属于Java语言规范的一部分。
可以在 https://docs.oracle.com/javase/specs/ 查看Java语言规范以及Java虚拟机规范
Java语言规范也包括了Happens-Before原则,由JVM进行实现:
- 某个线程的每个动作都happens before该线程中该动作后面的动作
- 某个管程上的unlock动作happens before同一个管程上后续的lock动作
- 对于某个volatile字段的写操作happens before每个后续对该volatile字段的读操作
- 某个线程对象调用start()方法happens before被启动线程的任意动作
- 如果在线程t1中成功执行了t2.join(),则t2中所有操作对t1可见
- 如果a happens before b,b happens before c,则a happens before c
3、volatile
- volatile可见性
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。
内存屏障,又称内存栅栏,是一个 CPU 指令。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。 - volatile有序性
参考Happens-Before原则第3条
对于某个volatile字段的写操作happens before每个后续对该volatile字段的读操作
4、原子性操作(CAS)
- 原子操作:子操作可以是一个步骤,也可以是多个操作步骤,但其顺序不可以被打乱,也不可以被切割而只执行其中一部分(不可中断),要么全部执行成功,要么全部执行失败。
- 一行Java代码不一定是原子操作, ex. i++;
并发进行i++,volatile也无法保证处理后的数据是正确的 - Java中使用CAS保证原子操作,Compare And Swap,硬件级别的同步语句,CPU指令实现的原子性操作。乐观锁实现机制。
先比较旧值,如果相同,则进行交换
J.U.C package中,AoticInteger等就是使用CAS进行实现的,AoticInteger的自增是原子性操作,也因为是原子性操作,所以在并发情况下,是线程安全的。 - CAS操作相当于是串行的操作,在并发场景下,性能会较弱
如果只用一个变量存储 计数器,则只能一次一个线程更新该变量,其他线程要等待前面的线程执行完才可以执行修改 -> 慢
solution: 通过多个变量存储 计数器,提高并发性,通过每个线程修改修改random的变量,最后要获取计数器的时候,将多个变量sum一下就得到了总的计数器值,mysql的计数器表也可以通过这种方式进行实现,mysql会有行级锁,所以通过多个record进行计数。 - CAS存在的问题
- 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗;
2.针对单个变量的操作,不能用于多个变量来实现原子操作;
3.ABA问题:版本已经不一样了
5、synchronized
synchronized: 同步关键字,可重入,非公平,悲观排他锁
解析:
- synchronized关键字是由JVM实现的,另外synchronized关键字的加锁状态是存放在锁对象的对象头的Mark Word中。
Mark Word也是内存区域,长度和JVM有关,如果JVM是32bit,Mark Word就是32bit;如果JVM是64bit,Mark Word就是64bit;
Mark Word的值:分别用于记录synchronized不同级别的锁 - 轻量级锁:
线程抢锁过程: - a. 复制Mark Word到当前虚拟机栈(如果当前Mark Word不是未锁定的状态,则当前线程直接进入EntryList(等待锁的线程队列),并且锁等级提升为重量级锁)
b. 生成轻量级锁的Mark Word
c. 通过CAS进行抢锁,CAS成功则抢锁成功,虚拟机栈有一个对象owner会指向对象头的Mark Word
d. CAS失败,通过自旋不断抢锁(轻量级锁)
自旋 -> 导致CPU过高,所以自旋次数过多,锁升级: 重量级锁 - synchronized锁升级过程:锁升级后,就不会再降级,ex.当锁从轻量级锁变为重量级锁后,以后锁对象被锁的时候,都会使用重量级锁
锁实现机制:
CAS抢锁(修改对象头的Mark Word),修改成功就是抢到锁;
修改失败就自旋,但自旋有区别,轻量级锁不断占用资源,重量级锁挂起线程不占用资源。 - synchronized关键字数据结构整理:
a. 对象头:Mark Word,存放当前锁的状态
b. 对象监视器,Object Monitor,包括:
Owner:抢占到锁的线程
EntryList:等待队列,没有抢到锁的线程被放入entryList,且被挂起(BLOCKING)
WaitSet:等待池,抢到锁,然后调用wait挂起的线程就进入WaitSet
ps. notify会唤醒WaitSet中的线程,线程会抢锁,如果抢到,则进入Owner,否则进入EntryList
新线程抢已被占用的锁,如果Mark Word不是未锁定的标志位,(无法被复制到lock record),直接进入重量级锁