多线程对于共享变量访问带来的安全性问题
数据结果与预期不一致性的问题
问题描述
一个自增的程序,当两个线程操作时,可能读取的时同一个数据,再将其自增1,最终返回值只增加了1,实际结果应该增加2。
解决方法
增加锁,达到数据安全的目的,不允许读正在被修改的数据,只允许读没有被修改状态的数据。
锁的认识
Java提供的加锁方法就是Synchroinzed关键字
背景
在多线程并发编程中synchronized一直是元老级角色,很 多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对 synchronized进行了各种优化之后,有些情况下它就并不 那么重,Java SE 1.6中为了减少获得锁和释放锁带来的性 能消耗而引入的偏向锁和轻量级锁
加锁的方式
修饰实例方法,作用于当前实例加锁,进入同步代码前 要获得当前实例的锁
静态方法,作用于当前类对象加锁,进入同步代码前要 获得当前类对象的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同 步代码库前要获得给定对象的锁,不同的修饰类型,代表锁的控制粒度
synchronized锁是如何存储
观察 synchronized的整个语法发现,synchronized(lock)是基于 lock 这个对象的生命周期来控制锁粒度的
对象在内存中的布局
在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分 为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头信息
分代年龄
锁标识
是否为偏向锁
偏向锁的时间戳 等
为什么每个对象都可以实现锁
每个对象都继承自Object,Object在 JVM 内部都有一个 native 的 C++对象 oop/oopDesc进行对应,包含锁的相关信息,所以每个对象都拥有各自的锁的相关信息
OOP(Ordinary Object Point) 指的是普通对象指针, Klass 用来描述对象实例的具体类型 。 Hotspot 采用 instanceOopDesc 和 arrayOopDesc 来描述对象头, arrayOopDesc 对象用来描述数组类型
锁的状态
无锁
偏向锁
当一个线程访问加了同步锁的代码块时,会在对象头中存 储当前线程的ID,后续这个线程进入和退出这段加了同步 锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等 表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,这个过程简称CAS 。
轻量级锁
轻量级锁在加锁过程中,用到了自旋锁 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗cpu的,就相当于 在执行一个啥也没有的for循环。 所以,轻量级锁适用于那些同步代码块执行的很快的场景, 这样,线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗CPU资源。默认情况下自旋的次数是10次, 可以通过 preBlockSpin来修改
在JDK1.6之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。
轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过 CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的 MarkWord 中,如果成功表示没有竞争。如果失败,表示 当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁
重量级锁
当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起 阻塞来等待被唤醒了。监视器会监听重量级锁的同步队列,线程的锁出队列后执行一个新的线程。
CAS
compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
补充
在我们的应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过jvm参数 UseBiasedLocking 来设置开启或关闭偏向锁
锁的状态图表
偏向锁的获得与撤销流程图
轻量级锁的获得与膨胀流程图
相关文章: