悲观锁与乐观锁
悲观锁与乐观锁是一种广义上的概念 体现了看待线程同步的不同角度
对于同一个数据的并发操作 悲观锁认为自己在使用数据的时候一定会有别的线程来修改 因此在获取数据之前先加锁 确保数据不会被别的线程修改 在java中 synchronized关键字和Lock的实现类 都是悲观锁的体现
乐观锁则认为 自己使用数据时 不会有别的线程修改数据 不会添加锁 只是在更新数据的时候去判断之前有没有其他线程修改数据。如果这个数据没有被更新 当前线程就会成功写入数据 如果数据已经被更改 则根据不同的方式执行不同的操作(报错或重试)
乐观锁通过无锁编程实现 最常采用CAS算法 compareandset 比较并交换 java原子类中的递增操作就通过cas实现
悲观锁适合写操作多的场景 先加锁可以保证写操作时的数据正确
乐观锁适合读操作多的场景 不加锁的特点能够使其读操作的性能 大幅提升
CAS全程 Compare And Swap(Set) 比较并交换 是一种无锁算法 在不使用锁(没有被线程阻塞)的情况下实现多线程之间的变量同步
CAS涉及三个操作数
-
需要读写的内存值V
-
进行比较的值A
-
要写入的新值B
当且仅当 比较值A 和内存值V一致时 CAS通过原子方式 用新值B去更新内存V的值(比较和更新是一个原子操作)否则不会执行任何操作 一般情况下 更新是一个不断重试的操作
jdk提供的原子类即是通过CAS方式实现了共享数据的非阻塞同步
unsage: 获取并操作内存中的数据
valueOffset: 存储在value在AtomicInteger中的偏移量
value: 存储AtomicInteger的int值 需要借助volatile关键字保证其可见性
incrementAndGet()的源码
// ------------------------- JDK 8 ------------------------- // AtomicInteger 自增方法 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // Unsafe.class 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; } // ------------------------- OpenJDK 8 ------------------------- // Unsafe.java public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
getIntVolatile(o,offset) 根据对象和便宜量 获取内存中的值
!compareAndSwapInt(o, offset, v, v + delta) 根据对象和偏移量获取内存中的值 和之前读取的v进行比较 如果此时内存中的值和v一致 则修改内存中的值 并结束循环返回内存中的值
如果不一致 则自旋重试
假设线程A和线程B同时执行getAndAddInt操作(分别跑在不同的CPU上)
AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的 value 为3,根据JMM模型,线程A和线程B各自持有一份价值为3的副本,分别存储在各自的工作内存 线程A通过getIntVolatile(var1 , var2) 拿到value值3,这是线程A被挂起(该线程失去CPU执行权) 线程B也通过getIntVolatile(var1, var2)方法获取到value值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt方法,比较内存的值也是3,成功修改内存值为4,线程B打完收工,一切OK 这是线程A恢复,执行CAS方法,比较发现自己手里的数字3和主内存中的数字4不一致,说明该值已经被其它线程抢先一步修改过了,那么A线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行do while 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。 Unsafe类 + CAS思想: 也就是自旋,自我旋转
CAS虽然很高校 但是存在三大问题
1.ABA问题
CAS在操作时会检查内存值是否发生变化,没有发生变化时才能成功修改内存值,但如果内存值原来是A后来变为B最后又变回A,那么CAS检查时并不会发现值在这个过程中的变化,但实际上是有变化的,对于某些业务场景来说ABA问题是无法接受的。 解决ABA问题的方法:给变量添加版本号,每次修改变量后版本号也+1,比对的时候,只有在变量值和版本号都相同的时候才能修改成功。 AtomicStampedReference类(带有时间戳的原子引用)通过添加stamp可以解决aba问题
2.循环时间长的时候开销大
CAS操作如果长时间不成功,会导致其一直自旋,给cpu带来非常大的开销
3.只能保证一个变量的原子操作
对一个共享变量进行操作时 CAS能保证原子性,但是对于多个共享变量的操作,CAS无法保证原子性
从jdk1.5之后 提供了AtomicReference引用原子类,来保证引用对象之间操作的原子性,可以把多个变量放在一个对象里进行CAS操作
synchronize关键字
通常我们称synchronize锁是一种重量级锁,因为在互斥状态下,没有得到锁的线程会被挂起,“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
synchronize使用的锁是存储在对象头中的
对象头
对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
在运行期间 MarkWord中存储的数据会随着锁标志位的变化而变化
Monitor
Monitor通常被描述为一个同步工具或一种同步机制,通常被描述为一个对象。每个java对象都会关联一个Monitor对象,称为Monitor锁或者内部锁
Monitor是线程私有的数据结构 每个线程都会有一个可用Monitorrecord列表 同时还有一个全局的可用列表。
每一个被锁(重量级锁)住的对象都会和一个Monitor关联,同时Monitor中的owner字段会指向持有这把锁的线程
无锁 偏向锁 轻量级锁 重量级锁
-
无锁
无锁状态下 没有对资源做锁定 所有的线程都能访问并修改同一个资源 但同时只有一个线程能够修改成功
无锁的特点是修改操作在循环内进行,线程会不断尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环重试。如果有多个线程修改同一个值,必定会有一个线程先修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理即是无锁的实现,无锁无法替代有锁,但在某些场合下,性能是非常高的。
-
偏向锁
偏向锁是指一段同步代码一直被同一个线程访问 那么该线程会自动获取到锁 降低获取锁的代价
在大多数情况 锁总是由同一个线程获得,不存在线程竞争,所以出现了偏向锁,其目的就是在只有一个线程执行同步代码时能够提高性能
当一个线程访问同步代码并获取锁时 会在markword中存储锁偏向线程的id 在线程进入和退出同步代码时 不再需要通过CAS操作获得和释放锁,而是检测markword中是否存储着指向当前线程的线程id偏向锁。 引入偏向锁是为了在无多线程竞争的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的释放和获取会执行多次CAS原子指令,而偏向锁只用在置换ThreadId时依赖一次原子指令
偏向锁不会主动释放 偏向锁只会在有其他线程竞争偏向锁时才会释放锁,并不会主动释放锁。偏向锁的撤销需要等待全局安全点(在这个时间点上没有字节码正在执行),它会暂停拥有偏向锁的线程执行 判断锁对象是否处于锁定状态。撤销后偏向锁变为01无锁状态或00轻量级锁状态
-
轻量级锁
当锁是偏向锁时 被另外的线程访问 偏向锁就会升级成轻量级锁 其他线程会通过自旋的形势尝试获取锁 不会阻塞 从而提高性能
轻量级锁上锁解锁流程:
当代码进入同步块时 虚拟机会在当前线程的栈帧中建立一个名为Lock Record(锁记录)的空间,用于存储锁对象目前markword中的信息,然后拷贝markword中的信息到Lock Record中
拷贝成功之后 虚拟机将使用CAS操作尝试将对象的MarkWord信息更改为指向Lock Record的指针 并将Lock Record中的指针指向对象的MarkWord
如果这个操作成功了 则表示当前线程获取到了锁 锁记录中存储着对象头信息 和指向对象头的指针 对象头中存储着锁记录的指针 并将表示位改为00 表示轻量级锁
如果CAS失败 可能出现两种情况 锁重入 和 有其他已持有轻量级锁
虚拟机会首先检查markword是否指向当前线程的栈帧 如果是就说明当前线程已持有锁 可以直接执行同步代码(可重入锁)
否则说明存在多线程竞争
若当前只有一个线程等待 则该线程通过自旋进行等待。但当自旋超过一定次数 或者有第三个线程来争抢锁 轻量级锁会升级成重量级锁
-
重量级锁
重量级锁的实现需要对象关联Monitor对象 锁的标志位变为10 并且markword中存储Monitor对象的地址
重量级锁的上锁流程
Thread-1 操作轻量级锁 CAS失败 进入锁升级过程
为Object对象申请重量级锁 即对象会关联Monitor对象 MarkWord中会存放指向Monitor的指针
同时Thread-1 会进入EntryList 进入Blocked状态
当Thread-0执行完同步方法之后 按Monitor地址找到Monitor对象 之后设置Owner设置成null 唤醒EntryList中阻塞的线程 争抢锁