synchronized关键字
synchronized特性
synchronized是一种对象锁(锁的是对象而非引用),作用粒度是对象,java中每个对象都可以上锁(同一时间只有一个线程能上锁成功),而且通过对象内部存储的markword标记锁状态。
特性:
- 原子性:保证操作不可分割(被Synchronized修饰的对象、方法和代码块,在执行操作之前都必须获得类或对象的锁,直到执行完毕才能释放锁,这个期间是无法被打断的),要么所有的操作都执行,要么都不执行
- 可见性:jmm中主内存和每个线程的工作内存的数据
- 有序性:程序是顺序执行的
- 可重入性:可以重复使用的锁(可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁)
synchronized底层实现
在jvm中,java对象由三部分组成:
-
对象头:主要是MarkWord(存储对象的hashcode(对象的引用地址)、锁信息或GC分代年龄或锁状态标志),类型指针(该对象是哪个类的实例,数组长度(数组才有))
-
实例数据:真实记录一个对象包含的数据,比如说一个person对象,里面可能包含年龄、性别、身高等等
其中数据为字符串的,要引用到字符串常量池。
-
对齐填充:填充部分仅起到占位符的作用, 原因是HotSpot要求对象起始地址必须是8字节的整数,假如不是,就采用对齐填充的方式将其补齐8字节整数倍,那么为什么是8呢?原因是64位机器能被8整除的效率是最高的。
是由一对monitorenter和monitorexit指令来实现同步的,每一个锁对象都会管理一个monitor(监视器,它才是真正的锁对象),当计数器count为0时释放锁
synchronized锁优化
synchronized锁升级
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
锁有四种状态,并且根据实际情况进行锁的升级:无锁——>偏向锁——>轻量级锁——>重量级锁
- 偏向锁:减少线程获取锁的代价(它的本质就是让锁来记住请求的线程),如果一个线程获取了锁,那么锁就进入了偏向模式,此时Mark Word的结构也就变为偏向锁结构。此后获取锁不再需要额外的操作,只需要判断对象头里面的锁标志位和线程id即可,如果是当前线程,直接进入同步操作,减少开销;
- 轻量级锁:由偏向锁升级而来,当第二个线程申请(只是申请,并没有竞争)同一个锁对象时,偏向锁就会升级为轻量级锁。
- 重量级锁:由轻量级锁升级,当多个线程同时竞争锁时,就会升级成重量级锁,此时性能开销会变大(追求吞吐量-高并发)
synchronized锁消除和锁粗化
锁消除:jvm在即时编译时,对上下文进行扫描,去除不可能存在竞争的锁。比如锁的对象是私有变量,就不存在竞争关系
锁粗化:扩大锁的范围,避免反复加锁和释放锁
for(int i=0;i<1000;i++){
synchronized (Test.class){
System.out.print("hello");
}
}
//锁粗化
synchronizde(Test.class){
for(int i=0;i<1000;i++){
System.out.print("hello");
}
}
synchronized自旋锁和自适应自旋锁
自旋锁:当其它线程获取轻量级锁失败时,不会挂起,而是会进行自旋锁的优化手段,即不断的尝试去获取锁,在一定的时间内(或者在一定的自旋的次数内),如果获取到锁,就顺利执行;如果到时还没获取到锁,就会挂起;自旋锁适用于锁占有者占有锁的时间比较短的情况;
缺点:如果锁一直被其它线程长时间占有,会带来许多的性能开销
自适应自旋锁:对自旋锁的进行优化,它的自旋次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的占有者的状态来决定;
synchronized和lock区别
1、synchronized是java的关键字; Lock是一个类
2、synchronized无法判断获取锁的状态; Lock可以判断是否获取到了锁
3、synchronized 会自动释放锁; Lock 必须手动释放锁(否则会发生死锁)
4、synchronized 线程1(获得锁,阻塞) 线程2(傻傻的等); Lock锁就不一定会等,会尝试获得锁
5、synchronized 可重入锁,不可中断,非公平锁; Lock,可重入锁,可中断可不中断,可以设置公平/非公平
6、synchronized 适合少量的代码同步问题; Lock适合大量的代码同步问题
7、synchronized可以锁方法或代码块,lock只能锁代码块
volatile关键字
- 保证了多个线程对某个共享变量进行操作时的可见性
- 禁止指令重排,保证有序性
指令重排序:为了优化程序性能,对原有指令的执行顺序进行优化重新排序。(重排序发生的阶段:编译重排序,cpu重排序)
volatile如何实现禁止指令重排?
内存屏障(memory barrier):相当于一个同步点,在执行这个同步点指令(lock addl)之前的所有读写操作都执行完毕之后才可以执行这个同步点指令之后的操作。
内存屏障的作用:是一种cpu指令,阻止屏障两侧的指令重排序,内存屏障可以让cpu或编译器在内存访问上有序。
内存屏障的插入策略:
- 在每个volatile写操作前加插入一个StoreStore屏障,在volatile写之后插入一个StoreLoad屏障
- 在每个volatile读操作后插入一个LoadLoad屏障——>LoadStore屏障
volatile重排序的规则:
- 如果第一个操作是volatile读,那么无论第二个操作是什么,都不能重排序
- 如果第二个操作是volatile写,无论第一个操作是什么,都不能重排序
- 如果第一个操作是volatile写,第二个操作是volatile读,不能重排序
volatile使用场景:
- 修饰标记变量:volatile boolean flag=false;
- 单例模式中的双重检查
CAS详解
比较并交换,CAS是一条CPU的原子指令,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,操作过程是原子的;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 注意是静态的
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置
} catch (Exception ex) { throw new Error(ex); }
}
//重点呀!!!
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
底层原理
Unsafe是CAS的核心类,Unsafe类的所有方法都是native的,由于java无法访问底层系统,需要通过本地方法(native)访问,Unsafe类可以直接操作特定内存的数据
valueOffset:表示变量的值在内存中的偏移地址
CAS的应用
Java中java.util.concurrent.atomic并发包中的数据进行处理就是利用的CAS原理,以AtomicInteger为例
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
CAS缺点
- ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决:ABA问题的解决思路就是使用版本号,类似于数据库的版本控制,。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
atomic包里提供了一个类AtomicStampedReference来解决ABA问题,这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
- 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
CAS在执行自增自减的时候,如果不成功,会一直循环,直道成功为止
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
原子类
AtomicInteger详解
AtomicInteger类中的操作的思想基本上是基于CAS+volatile。保证在多线程高并发情况下的i++的操作,
//使用unsafe的CAS来更新
private static final Unsafe unsafe = Unsafe.getUnsafe();
//偏移量
private static final long valueOffset;
static {
try {
//初始化value的起始地址的偏移量
valueOffset = unsafe.objectFieldOffset
(java.util.concurrent.atomic.AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
//值 - 使用volatile修饰
private volatile int value;
原子方法:
//直接操作内存 设置新值
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//直接操作内存 设置新值并且返回旧值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// 获取当前的值,并自增,相当于线程安全版本的i++操作
public final int getAndIncrement()
// 获取当前的值,并自减,相当于线程安全版本的i--操作
public final int getAndDecrement()
// 获取当前的值,并自增,相当于线程安全版本的++i操作
public final int incrementAndGet()
// 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
boolean compareAndSet(int expect, int update)
1、基本类型
AtomicInteger、AtomicBoolean、AtomicLong
getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta)
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四个方法
// 新值可以通过传入 func 函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)