Synchronized、Volatile和ReentrantLock

各种常用的锁:

重入锁(递归锁):递归调用自己加锁方法的时候,可以再次进入该方法,优点就是相 同线程不需要在等待锁,而是可以直接进行相应操作。

不可重入锁:即使是本身,也只能进入一次方法。
悲观锁:对每次线程都认为会进行写操作,每次只让一个线程进入。(认为读少写多(synchronized实现))

乐观锁:都不加锁,在更新的时候判断是否发生改变。!CAS实现的,它是一种更新原子操作,比较当前值是否和传入值一致,一样就更新。

公平锁:大家排好队,一个一个来获得锁,但是需要排队机制,而且效率不高。

非公平锁:谁抢到谁的,但是可能有些线程一直得不到锁。效率相对高 (synchronized)。

Synchronized 的底层实现:(同步互斥)

同步互斥是一种悲观锁的并发策略,就知道加锁加锁~~~

synchronized 关键字在经过编译之后,会在同步块的前后分别形成 monitorentermonitorexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java 程序中的 synchronized 明确指明了对象参数,那就是这个对象的 reference;如果没有,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或 class 对象来作为锁对象。

在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,就把锁的计数器加 1;相应的,在执行monitorexit['eksit] 指令时将锁计数器减 1,当锁计数器为 0 时,锁就被释放。如果获取锁对象失败,当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。一旦阻塞就会导致户态和核心态切换(各种变量的保存),大大消耗cpu资源。
(阻塞相当于高速行驶的赛车需要减速停车,重新加速,消耗cpu资源)

为了线程安全还有另外的策略:

CAS(非阻塞同步): 他是一种基于冲突检查的乐观并发策略,(乐观锁:while true 一直重试)
compare and swap (变量内存地址V,旧值A,新值B),只有V的值为A时,才执行这个语句,把B的值赋给V,否则不执行。
但是可能会有ABA问题,第一次读取的时候是A,准备赋值的时候第二次读取还是A,但是在这段时间可能有其他线程把A改成B,之后又 改为A,CAS操作并不能发现这个问题。 后面引入了Atomic类通过记录比较版本号来看中途是否变动来解决这个问题!
所以一般和atomic类配合和使用。在线程数足够的情况下,cas不会造成阻塞。效率比加锁要好。

java线程阻塞的代价:(synchronized演变的原因)

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。 synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操作,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。(属于无锁并发,无阻塞并发)

介绍一下markword:

markword是java对象数据结构中的一部分,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。

synchronized锁在1.6后的演变:

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级; 这种策略是为了提高获得锁和释放锁的效率。
  • 偏向锁:同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。会消除同步,效率加快。
    始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,适用于无锁竞争的情况
    相当于门上写名字 只有一个线程进入临界区(可能会有并发问题的代码区域)
    缺点:一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作

  • 轻量级锁:它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。
    另外,轻量级锁的加锁和解锁都用到了CAS操作。(如果轻量级锁自旋到达阈值后,没有获取到锁,就会膨胀为重量级锁


    它主要是通过CAS来修改对象头锁记录自旋来实现(所以markword里存有偏向锁id,轻量级锁id等等。。)
    挂书包在门上~ 00 多个线程交替进入临界区
    在这里插入图片描述
    在这里插入图片描述
    Record为空就是重入了,重入计数+1

  • 自旋锁:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(去执行一个空循环),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。(适用于竞争不激烈的情况,在轻量级使用)自旋会一直消耗cpu资源

  • 重量级锁:多个线程同时进入临界区

Volatile(只能修饰变量,Synchronized还可以修饰类,不保证原子性)

volatile本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
volatile 是 Java 虚拟机提供的最轻量级的同步机制。当一个变量被定义为 volatile 后,它将具备几种特性:

  • 任何进程在读取的时候,都会清空本进程里面持有的共享变量的值,强制从主存里面获取;(读写屏障!!)
  • 任何进程在写入完毕的时候,都会强制将共享变量的值写入主存
  • volatile 会禁止重排序
  • volatile 实现了JMM规范的 happen-before 原则。
  • 通过内存屏障来保证可见性(写入内存,读取内存),有序性(写屏障前的代码不会排序到后面,读屏障后的代码不会到前面)
  • 解决双重检测锁问题(划分内存(如 int默认值0)–初始化对象(执行构造函数)–对象指向空间)

happen-before 原则:
若是A操作happen-beforeB操作,则A操作的执行结果对B可见,且A操作在B操作之前。
程序操作,锁操作,volatile等操作都有happen-before原则,且有传递性(A h-b B ,B h-b C 则A h-b C)。

ThreadLocal原理:(空间换时间实现线程安全)

ThreadLocal可以理解为线程本地变量,每个线程单独一份存储空间牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

它里面有一个静态内部类ThreadLocalMap,ThreadLocalMap里面又包含了一个Entry数组,Entry本身是一个弱引用(遇见就回收),key保存的是指向Threadlocald的弱引用,它具备保存key,value的能力。

但是还是存在内存泄露问题,若是entry的key被回收了,则entry里一直存在key为null,而value有值的对象,但是永远没办法访问到,只有线程结束运行才能被回收。所以每次remove掉value就ok了。

  • 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象,(所以要存其他信息,再new一个Threadlocal)
  • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value
    因为这样,所以 ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
    它是利用开放地址法解决hash冲突

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

弱引用只是解决了key的问题,value的问题想要避免内存泄露就要手动remove()掉!
最后要记住的是:
ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题

使用场景
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

ReentrantLock和Synchronized区别:

  • 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以放弃等待,执行其他任务。 tryLock(time)or interrupt()中断(设置超时时间)利用tryLock的超时时间来解决死锁问题!
  • 公平锁,二者默认的都是非公平锁,但ReentrantLock可以通过构造函数传参改变为公平锁,但是性能下降。传 true为公平锁(默认false非公平)
  • 绑定多个条件,ReentrantLock可以绑定多个condition条件对象, 用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。condition1.signal()唤醒 await()等待
  • ReentrantLock实现了Lock接口,需要手动释放锁,二者都是重入锁,Synchronized是java关键字。

AQS原理:

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
在这里插入图片描述

特点:

用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

  • getState - 获取 state 状态
  • setState - 设置 state 状态
  • compareAndSetState - cas 机制设置 state 状态
  • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

提供了基于 FIFO 的等待队列,类似于 MonitorEntryList
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

公平锁和非公平锁的实现就是因为aqs加锁的时候,一个会先去队列中(双向链表)中检查,是否为第二个(第一个是占位的节点)结点,非公平加锁的时候,就不会去检查队列,直接cas尝试获取锁。获取成功再去判断重入,state++;所以非公平锁是不排队的。

缓存更新策略:

在这里插入图片描述
在这里插入图片描述
先删除(更新)数据库的值,只要在使用数据时多查询一次就可以解决缓存不一致问题。或者加锁解决(更新数据库和刷新缓存一起操作)。

读读可以共享是因为aqs在判断为读锁时,会将所有的带读锁的节点都唤醒,直到碰见独占锁为止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值