JUC复习篇(二)锁的认识

锁的基本概念:

通过锁机制,能够保证在多线程环境中,在同一时间空间中,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

公平锁:

防止某个线程出现饿死现象,也就是防止某个线程分配不到CPU时间片导致指令一直无法执行。保证公平需要额外维护线程状态,更多的线程上下文切换,开销更大,吞吐量有所下降。ReentrantLock 可用设置初始化参数设置为公平锁,Synchronized默认是非公平锁并且不能变为公平锁。

非公平锁:

直接进行计算资源抢夺,容易出现线程饿死现象。但吞吐量更高,线程切换不那么频繁,谁先抢到CPU时间片即可执行。

可重入锁:

又称为递归锁,可以再次获取已经获取到的锁对象,可以一定程度的避免死锁现象。ReentrantLock和Synchronized都是可重入锁。

不可重入锁:

线程在运行中获取到锁之后,在同步代码块种再次获取这把锁将会导致死锁,不可重入锁只允许在同步代码块中被获取一次。

互斥锁:

当一个线程占用资源时,其他线程会被挂起,不会占用CPU资源,锁被释放时,CPU去调度挂起的线程,适合不会被高频操作的资源,否则频繁调度线程的效率会较低。

读写锁:

ReentrantReadWriteLock 读写锁由读锁和写锁两部分构成,读锁写锁互斥,如果只读共享资源用读锁加锁,如果需要修改共享资源则用写锁加锁。适合读多写少场景。当写锁没有被线程持有时,多个线程可以并发地持有读锁,但是当写锁被线程持有后,其他线程获取读锁和写锁的操作都会阻塞。

写优先:如果⼀直有写线程获取写锁,读线程也会被「饿死」。

读优先:一直有读线程获取读锁,那么写线程将永远获取不到,造成写线程「饿死」。

公平读写锁:

公平读写锁⽐较简单的一种方式是:⽤队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,一定程度缓解「饥死」的现象。

戳记锁:

StampedLock 是比 ReentrantReadWriteLock 更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock不同的是,StampedLock支持多个线程申请乐观读的同时,还允许一个线程申请写锁。

以下的锁不是具体锁实现,是指看待并发同步的角度

乐观锁:

可用版本号或者CAS算法实现,假定冲突的概率很低,它的工作方式是:对资源做修改时认为只有自己在做修改,修改完成后,再去校验,如果不对再修改。另外虽然叫锁,但是乐观锁全程并没有加锁,所以它也叫无锁编程。

悲观锁:

默认有多个线程对资源进行修改,每一次修改都要加锁,一般数据库本身锁的机制都是基于悲观锁的机制实现的。

分段锁:

分段锁的设计目的就是细化锁的操作,例如当操作不需要更新整个数组时,仅针对数组中的一个元素更新时,我们仅给元素加锁即可,JDK1.7中ConcurrentHashMap就是利用分段锁的形式实现并发操作,通过对Segment进行加锁实现更高的并发效率,但在JDK1.8中已经使用CAS算法代替掉了。

偏向锁:

指一段同步代码一直被一个线程访问,那么该线程就会自动获取这个锁,以降低获取锁的代价。

轻量级锁:

当前锁是偏向锁并且被另外一个线程访问时,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,不会阻塞其他线程。

重量级锁:

当前锁是轻量级锁,另一个线程自旋到一定次数还没获取到锁,轻量级锁就会升级重量级锁,会阻塞其他线程。

自旋锁:

指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。但会消耗更多的CPU资源,也就是不停的空转。一般会设置自旋次数,达到指定次数后将会挂起线程。

死锁:

死锁不是一种锁,而是一种现象,两个正在执行的线程相互需要获取对方已持有的锁才能继续执行指令时,就出现了死锁现象,导致程序卡死。无法继续向下执行。死锁可通过 jstack 指令进行排查,先通过 jps -l 指令获取到应用程序 pid 进程号。然后通过 jstack <进程号> 进行死锁排查。也可通过 jconsole 指令打开UI工具查看死锁信息。

CAS算法是什么:

CAS 全称 Compare And Swap,顾名思义,即比较与交换。该算法是一种无锁算法,可以在不加锁的前提下保证线程安全,即在没有线程被阻塞的情况下实现变量同步,因此属于非阻塞同步的范畴。

CAS算法涉及到三个操作数:内存值V、比较值A、交换值B。其具体的操作步骤是,当且仅当V等于A时,CAS算法通过原子方式用B将V替换,否则不执行任何操作。一般情况下,CAS算法是一个自旋操作,即不断的重试直到成功为止。

CAS算法有如下特点:ABA问题,存在自旋开销,只保证一个共享变量的原子操作。

  • ABA问题。若变量V初次读取时为A,在准备赋值时检查其仍为A,但这不能说明它没有被其他线程修改过,因为在这段时间它可能被改为其他值,然后又改为A,此时CAS算法就会误认为它从未修改过。该问题被称为CAS算法的ABA问题。在实际使用场景中需判断,若ABA问题对该使用场景的影响不大,可以不作处理。否则,可以采取对每次修改添加版本号标识的方式解决ABA问题,或直接采用悲观锁。
  • 存在自旋开销。在概述中已经提到了,CAS算法是一个自旋操作,拥有不撞南墙不回头的精神,在失败时会不断重试直到成功为止。但这种自旋如果长时间存在,将会对CPU造成很大的负担。这也就是所谓的自旋开销。
  • 只保证一个共享变量的原子操作。CAS算法只对单个共享变量有效,当操作跨多个共享变量时CAS算法将不能保证同步。一种解决方式是,将这些共享变量封装为一个对象之后再使用CAS算法进行处理。

底层的汇编指令:lock cmpxchgl 其中lock指定特别重要,其指令说明如下:

1、确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。‌‌2、禁止该指令与之前和之后的读和写指令重排序。‌‌3、把写缓冲区中的所有数据刷新到内存中。

A‌‌‌‌QS是什么:

Abstract Queued Synchronizer 抽象队列同步器,JUC中所有的Lock接口实现类均基于 AQS 抽象类进行实现。