◉◡◉ 您好,欢迎到访伊成个人站!

并发锁知识点

悲观锁:synchronized

Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。

每个对象头中分为两部分:一部分是自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。一部分是类型指针,即是对象指向它的类的元数据的指针。

而对象的锁(monitor)就在对象头中,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。

而其他想要获取该锁只能阻塞,一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,需要消耗很多处理器时间

特性:互斥锁、非公平锁、可重入、不可中断

优点:实现简单

缺点:

  • 不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大
  • 不可中断,在所有等待的线程中,synchronized无法帮你中断此任务
  • 互斥锁在获取锁失败后将进入睡眠或阻塞状态

乐观锁:CAS( compare and swap,比较并交换)

自旋锁

悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。

乐观锁的核心算法是CAS(Compare and Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。

这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

优点: 高并发性能,jdk中的并发包也大量使用基于CAS的乐观锁。

缺点:

  • 乐观锁只能保证一个共享变量的原子操作
  • 长时间自旋可能导致开销大
  • ABA问题。

CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

同步器:AQS(AbstractQueuedSynchronizer)

定义:

它为不同场景提供了实现锁及同步机制的基本框架,为同步状态的原子性管理、线程的阻塞、线程的解除阻塞及排队管理提供了一种通用的机制。

原理:

ASQ将线程封装到一个Node里面,并维护一个CHL Node FIFO队列,它是一个非阻塞的FIFO队列,也就是说在并发条件下往此队列做插入或移除操作不会阻塞,是通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入。

state

  • 独占模式state的值只能为0或1
  • 共享模式的state是可以被出事换成任意整数,一般初始值表示提供一个同时n条线程通过的管道宽度,这样一来,多条线程通过tryAcquireShared尝试将state的值减去1,成功修改state后就返回新值,只有当新值大于等于0才表示获取锁成功,拥有往下执行的权利,进入管道。在执行完毕时线程将调用tryReleaseShared尝试修改state值使之增加1。
  • 表示我已经执行完了并让出管道的通道供后面线程使用,需要说明的是与独占模式不同,由于可能存在多条线程并发释放锁,所以此处必须使用基于CAS算法的修改方法,修改成功后其他线程便可继续竞争锁。
  • 独占式:只容许一个线程通过的管道,在这种模式下线程只能逐一通过管道,任意时刻管内只能存在一条线程,这便形成了互斥效果。
  • 共享式:共享模式就是管道宽度大于1的管道,可以同时让n条管道通过,吞吐量增加但可能存在共享数据一致性问题。

阻塞唤醒三种方式:

  • suspend与resume:存在无法解决的竟态问题而被Java废弃
  • wait与notify:这两个方法必须存在于synchronized中,存在竟态条件,wait必须在notify之前执行,假如一个线程先执行notify再执行wait将可能导致一个线程永远阻塞
  • await与singal: Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法

    wait与await区别:

  • wait与notify必须配合synchronized使用,因为调用之前必须持有锁,wait会立即释放锁,notify则是同步块执行完了才释放
  • 因为Lock没有使用synchronized机制,故无法使用wait方法区操作多线程,所以使用了Condition的await来操作
  • park与unpark:由LockSupport类提供,底层调用的是Unsafe类的方法,由于park与unpark使用的是许可机制,许可最大为1,所以unpark与park操作不会累加,而且unpark可以在park之前执行,如unpark先执行,后面park将不阻塞。
  • Lock实现主要是基于AQS,而AQS实现则是基于LockSupport,所以说LockSupport更底层,所以不建议使用park和unpark去阻塞和唤醒线程

Java内部有两种锁机制:

1.synchonized

2.Lock

区别:

实现机制不同

  • synchonrized 分为两种 程序段的synchonized是通过monitor.enter monitor.exit来实现的,方法和类级别的则是通过设置实例或者类的锁字段来实现
  • Lock的实现方式则是通过AQS。AQS是一个线程的链表,负责维护线程的状态,以及线程的调度,AQS也是一个锁 保证同一时间获取AQS锁的线程只有一个,也就是下面的Nodestatus为runnning的只有一个(为什么不是同一时间运行的线程只有一个呢?线程在申请锁的时候先加入队列然后挂起,并且在公平竞争时所有的线程都会别唤醒 )

synchronized 同步锁

原理:

任何一个对象都一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。

这时如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,需要消耗很多处理器时间。如果可能,应该减少这样的切换,jvm一般会采取一些措施进行优化,例如在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象
  • javap工具查看生成的class文件信息来分析Synchronize的实现
  • 同步代码块:

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

Java对象头和monitor是实现synchronized的基础

特性:互斥锁、非公平锁、可重入、不可中断、使用简单

性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。

Lock锁实现:

ReentrantLock 重入锁

使用:

ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操作线程,await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似。

原理

核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的问题

性能和建议:性能中等,建议需要手动操作线程时使用。

ReentrantReadWriteLock 读写锁

使用

它允许多个线程读某个资源,但每次只允许一个线程来写。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。

当多个线程读取有个变量时可以使用读锁rwl.readLock().lock();,如果需要去修改某个变量时则可以上写锁rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写

与重入锁比较,其实现原理一致,但是读写锁更适合读多写少的场景,因为读读共享,而重入锁全互斥

StampedLock

时间戳锁(jdk1.8改进的读写锁)

使用

写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写,在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!

时间戳锁与读写锁比较

读锁不阻塞写锁,如果时间戳无效,则重新读取变量值。无ABA问题。

支付宝打赏 微信打赏