Java内存模型
synchronized内存语义
解决共享变量的可见性和实现性操作
- 进入synchronized的内存语义: 就是把synchronized块内使用到的变量从线和工作内存中清除(这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取)
- 退出synchronized的内存语义是把synchronized块内对共享变量的修改刷新到主内存
volatile关键字
虽然提供可见性保障,但并不能保证操作的原子性
当一个变量声明为volatile时,线程写入时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
一般什么时候使用volatile关键字?
- 写入变量的值不依赖变量的当前值。(如果依赖当前值,将是获取 -- 计算 -- 写入)三步操作,这三步操作不是原子性的,而volatile不保证原子性
- 读写变量时没有加锁。因为加锁本身已经保证了内存的可见性,这时不需要把变量声明为volatile的。
CAS
指令重排序
伪共享
Cache内部是按行存储的,其中第一行称为一个Cache行。Cache行是Cache与主内存进行数据交换的单位,Cache行的大小一般为2的幂次数字节。
JDK 8 提供sun.misc.Contented
注解,来解决伪共享问题。@Contended
注解中用于Java核心类,比如rt包下的类,如果用户类路径下的类需要使用这个注解,则需要添加JVM参数: -XX:-RestrictContented
填充宽度默认为128,要自定义宽度可以设置-XX:ContendedPaddingWidth
参数
锁
乐观锁和悲观锁
悲观观锁: 在数据处理前先对数据进行加锁,并在整个处理过程中,使数据处理锁定状态。(适用于并发量大的情况) 乐观锁: 在访问记录前不会加锁,而是进行数据提交更新时,才会正式对数据冲突进行检测。(失败可以选择重试指定次数或者什么都不做)
公平锁和非公平锁
根据线程获取锁的抢占机制 ,锁可以分为公平锁(线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,先请求的线程最早先获取到锁)和非公平锁(不一定先来先得)。
公平锁较非公平锁性能开销大,非必要情况下,尽量使用非常公平锁。
new ReetrantLock(true)
公平锁new ReetrantLock(false)
非公平锁new ReetrantLock()
非公平锁
独占锁和非共享锁
根据线程只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁(任何时候都只能有一个线程得到锁 如ReetrantLock)和共享锁(可以被多个线程持有,如:ReadWriteLock 里的读锁)。
可重入锁
当一个线程要再次获取它自己已经获取的锁时是否会被阻塞? 否: 可重入 , 是: 不可重入
synchronzied
内部是可重入锁。 可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。
自旋锁
Java线程与操作系统中的线程是一一对应的,所以当一个线程获取锁失败后,会被切换到时内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。从用户状态切换到内核状态是开锁是比较大的,在一定程度上会影响并发性能。
自旋锁则是,当线程在获取锁时,如果发现锁已被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试(默认次数是10,可以使用-XX:PreBlockSpinsh
参数设置该值。如果尝试指定次数后仍没能获取到时锁,则当前线程才会被阻塞挂起。
由此看来自
旋锁是使用 PU 时间换取线程阻塞与调度的开销,但是很有可能这些 PU 时间 白白浪费。