jvm专题(4) - 【3/3】多线程-锁
  TEZNKK3IfmPf 2023年11月14日 32 0

本章内容做为多线程的最后一章主要是聊一聊“锁”,在高并发情况下影响多线程程序的性能最大的一个因素可能就是锁了,包含锁的范围、锁的类型等等,所以说锁的掌握可以说尤为重要。

不同的文章对锁的分类都不一样,本章中笔者由浅入深,先从概念开始后API实现这样的一个顺序来描述。本章内容过后,JVM专题的常用的基础知识基本全聊完了,如果后续时间允许的话笔者会新开一个专题,专门讲一个多线程的相关知识:)

老规矩,笔者先用一张图来概念下本章中涉及的所有知识点,如下图所示:

jvm专题(4) - 【3/3】多线程-锁

本小节主要讲下概念,让大家对锁的设计和使用场景有个感性的认识,一般来讲锁按不同的维度可分为:悲观、乐观;可按共享分为:独占和非独占(读写锁);排他性分为阻塞和非阻塞。

1.1、锁维度

1.1.1、乐观锁

乐观锁是一种乐观思想即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

1.1.2、悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到拿到锁。

java 中的悲观锁就是 Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁,如RetreenLock。

1.1.3、自旋锁

自旋锁原理非常简单,如果当前持有锁的线程能在很短时间内释放锁资源,则其它那些等待竞争锁的线程直接阻塞进入挂起状态,它们只需要等一等(自旋,避免内核态和用户态之间的切换)等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗,自旋时也会适当放弃线程优先级之间的差异。

但是线程自旋是需要消耗cpu的,假使一直获取不到锁那线程不能一直自旋下去占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。在1.6中可通过参数控制 -XX:+UseSpinning(开启) 和-XX:PreBlockSpin(自旋次数),到了1.7自旋由JVM控制。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,这时争用线程会停止自旋进入阻塞状态。

1.1.3.1、优缺点
  • 对于锁的竞争不激烈且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗远小于线程阻塞挂起再唤醒的操作的消耗(这些操作会导致线程发生两次上下文切换);
  • 对于锁的竞争激烈或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功同时有大量线程在竞争一个锁。当获取锁的时间很长时线程自旋的消耗有可能会大于线程阻塞挂起操作的消耗不但不能提升性能还会造成 cpu的浪费。所以这种情况下我们要关闭自旋锁(jdk1.6前需要注意);

 

1.1.3.2、自旋周期的设置

如上所述,其实自旋锁就一个设置项,即自旋周期的选择,庆幸的是在 1.6 引入了适应性自旋锁不需要人为来设置一个固定的周期值了,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化:

  • 如果平均负载小于CPU核心数则一直自旋;
  • 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞;
  • 如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞;
  • 如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差);

1.2、优先级

1.2.1、公平锁

加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。

1.2.2、非公平锁

加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。一般来讲:1、非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列;2、Java中的synchronized是非公平锁;

1.3、共享性

1.3.1、独占锁

独占锁模式下每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁则其他读线程都只能等待,这种情况下会限制不必要的并发性,因为读操作并不会影响数据的一致性。

1.3.2、共享锁

共享锁则允许多个线程同时获取锁并发访问共享资源,共享锁则是一种乐观锁,它放宽了加锁策略允许多个执行读操作的线程同时访问共享资源。

比如AQS的内部Node定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式,再比如java 的并发包中提供了 ReadWriteLock,它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

1.3.3、重入锁

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后内层递归函数仍然有获取该锁的代码(基于每线程),实现原理简单描述就是JVM通过为每个锁关联一个请求计数器和一个占有它的线程,同一个线程多次请求这个计数器会累加。直到计数器为0。锁被释放。

在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁,ReentantLock 继承接口 Lock 并实现了接口中定义的方法,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

1.3.4、分段锁

分段锁是为了提高性能而专门设计的,最好的例子就是ConcurrentHashMap的实现。在ConcurrentHashMap内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段(理论上的并发度)。所以如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根 hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行 put 操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

ConcurrentHashMap是由Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁,HashEntry 则用于存储键值对数据。
  • 一个ConcurrentHashMap里包含一个 Segment 数组,Segment 的结构和 HashMap类似是一种数组和链表结构;
  • 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;
  • 每个 Segment 守护一个HashEntry数组里的元素,当对数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

jvm专题(4) - 【3/3】多线程-锁

1.4、锁级别

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

1.4.1、重量级锁

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高。状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。

JDK 中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用,JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

1.4.2、轻量级锁

随着锁的竞争,锁可以从偏向锁升级到轻量级锁再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。所以轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

1.4.3、偏向锁

大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,可以看作是让这个线程得到了偏护。引入偏向锁是为了在多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

所以轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1.5、其它

1.5.1、CAS

CAS(Compare And Swap/Set)比较并交换,原理就是比较并交换+乐观锁+自旋的设计。CAS 算法的过程是这样:它包含 3 个参数 CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等.于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。

CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起仅是被告知失败并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理, CAS 操作即使没有锁也可以发现其他线程对当前线程的干扰并进行恰当的处理。

1.5.2、AQS

AbstractQueuedSynchronizer抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架。许多同步类实现都依赖于它,比如ReentrantLock/Semaphore/CountDownLatch。

它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。

jvm专题(4) - 【3/3】多线程-锁

AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成 abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它;
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false;
  • tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false;
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回 false。

同步器的实现是AQS的核心(state 资源状态计数),以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

二、显式锁实现

2.1、Synchronized

Synchronized是java提供的内置锁机制来保证原子性。它一般有两部分组成:锁对象的引用,以及这个锁保护的代码块synchronized(lock)。它是基于每调用的。

每个java对象都可以用作一个实现同步的锁,这些内置的锁被称作内部锁或监视器锁(锁对象)。执行线程进入synchronized块之前会自动获得锁,而在正常退出或异常退出时自动释放锁,获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。内部锁是互斥锁。会严重影响程序性能,下面的例子可以用这种方式实现但不建议。另一方面,锁不仅仅关于同步与互斥的,也是关于内存可见的,当访问一个共享的可变变量时,要求所有线程由同一个锁进行同步。

2.1.1、作用范围

  • 作用于方法时,锁住的是对象的实例(this);
  • 当作用于静态方法时,锁住的是 Class 实例,又因为 Class 的相关数据存储在永久带 PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  • synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中;

2.1.2、核心组件

  • Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
  • Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  • Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
  • Owner:当前已经获取到所资源的线程被称为 Owner;
  • !Owner:当前释放锁的线程。

2.1.3、实现原理

jvm专题(4) - 【3/3】多线程-锁

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程;
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程);
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”;
  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中;
  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的);
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源;
  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的;
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多;

从Java1.6开始synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。

2.2、ReentrantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

2.2.1、优先级

JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要否则最常用非公平锁的分配机制。

公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。

2.2.2、tryLock 和 lock 和 lockInterruptibly 的区别

  • tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false;
  • lock 能获得锁就返回 true,不能的话一直等待获得锁;
  • 如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常;

2.2.3、​​ReentrantLock 与 synchronized

  • ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被JVM自动解锁机制不同;ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作;
  • ReentrantLock相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock;

2.3、ReadWriteLock

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

ReentrantLock是一个标准的互斥锁,一次最多只有一个线程能够持有相同的ReentrantLock。但是互斥通常作为保护数据一致性的很强的加锁方式,因此过分地限制了并发性。互斥是保守的加锁策略,避免了“写/写”、“写/读”、“读/读”的重叠,只要一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行。读取ReadWriteLock锁守护的数据,必须首先获得读取的锁,当需要修改ReadWriteLock守护的数据时,必须首先获得写入的锁。

interface ReadWriteLock{
Lock read();
Lock write();
}

ReadWriteLock这个接口是一个简单的读-写锁实现,ReadWriteLock允许多种实现,这些实现可以在性能、调度保证、获取优先级、公平性、加锁主义等方面不尽相同。读-与锁的设计是用来进行性能改进的,使得特定情况下能够有更好的并发性。读取和写入锁之间的互动可以有很多种实现,比如:

  • 释放优先:当写者释放写入锁,并且读者和写者都排在队列中,应该选择哪个--读者,写者还是其它的;
  • 读者闯入:如果锁由读者获得,但是有写者正在等待,那么新到达的写者应该被授予读取的权力、还是等待。允许读者闯入到写者之前提高了并发性,但是却带来了写饥饿的风险:
  1. 重进入:读和写允许重入;
  2. 降级:如果线程持有写入的锁,它能够在不释放该锁的情况下获得读取锁么?这可能会造成写者“降级”为一个读取锁。读的级别比写要高;
  3. 升级:同降级。 这可能会造成死锁。

ReentrantReadWriteLock可以构建成公平和非公平的(默认实现),在公平的锁中,选择权交给等待时间最长的线程。如果锁由读者获取,而一个线程请求写入锁,那么不再允许读者获得读取锁,直到写者获得锁并正确释放。在非公平锁中,写者可以降级为读者,但反过来程序会造成死锁。

三、协同工具

阻塞队列在容器类中是特有的,它不仅可做为容器还可以协调生产者和消费者线程之间的控制流。Synchronizer是一个对象。它根据本身的状态调节线程的控制流,阻塞队列可以扮演一个Synchronizer角色,其它的还有信号量semaphore、关卡barrier、以及闭锁latch。还可以自己创造一个。他们都有类似的结构:封装状态,这些状态决定着线程执行到了某一点时是通过还是被迫等待,它们还提供操控状态的方法,以及高效地等待Synchronizer进入到期望状态的方法。

3.1、Latch闭锁

它可以延迟线程的进度直到线程到达终止状态。闭锁工作起来像一个大门,直到闭锁达到终点状态之前,门一直关闭,没有线程能通过,在终点状态到来的时候,门开了,允许所有线程通过,一旦闭锁到达了终点状态,就不能再改变状态了永远处于敞开状态。闭锁可以用来确保特定活动直到其他的活动完成后才发生:

  • 确保一个服务不会开始,直到它依赖的其他服务都已经开始;
  • 确保一个计算不会执行,直到它需要的资源被初始化。二元闭锁可以表示资源R已经被初始化,并且所有需要用到R的活动都在闭锁中等待;
  • 等待,直到活动的所有部分都为继续处理作好充分准备,比如多玩家的游戏;

CountDownLatch是一个灵活的闭锁实现,闭锁的状态包括一个计数器,初始化为一个正数,用来表示需要等待的事件数。countDown方法对计数器做减操作,表示一个事件发生了。而await方法等待计数器达到零,此时所有需要等待的事件都已发生,如果为非零,await会一直阻塞直到计数器为零,或等待线程中断以及超时。

这种方法可以测试线程N倍并发的情况下执行一个任务的时间。开始阀门在最后用countDown()方法减1,导致startGate.awarit()终止中断状态。所有线程开始执行。最后在endGate.countDown()减1.到0时endGate.await();终止中断状态计算响应时间。

public class TestHarness {
public long timeTasks(int nThreads, final Runnable task)
throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);

for (int i = 0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startGate.await();//等待开始阀门打开,计数器达到0
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException ignored) {
}
}
};
t.start();
}

long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
}

3.2、Semaphore计数信号量

用来控制能够同时访问某特定资源活动的数据、同时执行某一给定操作的数量、实现资源池或给一个容器限定边界。Semaphore 类中比较重要的几个方法(全是阻塞的,非阻塞可用相应tryXXX方法):

  • acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可;
  •  acquire(int permits):获取 permits 个许可;
  • release():释放许可。注意,在释放许可之前,必须先获获得许可;
  • release(int permits):释放 permits 个许可;
/*使用Semaphore为容器设置边界*/
public class BoundedHashSet <T> {
private final Set<T> set;
private final Semaphore sem;

//Semaphore管理一组虚拟的许可,通过构造函数来设置许可的数量
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}

public boolean add(T o) throws InterruptedException {
sem.acquire();//这个方法会阻塞直到有许可可用
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded)
sem.release();//使用完成后,释放许可给Semaphore
}
}

public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}

 

3.3、Barrier栅栏

栅栏类似于闭锁,不同的闭锁等待的是事件,关卡等待的是其它线程,闭锁是一次使用,而关卡是可以重置状态的。可以用于迭代计算。

CyclicBarrier允许一个给定数量的成员多次集中在一个关卡点,这在并行迭代算法中很有用,这个算法会把一个问题拆分成一系列相互独立的子问题。当线程到达关卡点时,调用await阻塞,直到所有线程都到达关卡点,如果所有线程都到达了关卡点,关卡就被成功地突破,所有的线程都被释放,关卡会重置以备下一次使用。如果对await的调用超时,或是阻塞中的线程被中断,那么关卡是失败的,所有对await未完成的调用都通过BrokenBarrierException终止。如果成功通过,为每一个线程返回一个唯一的到达索引号,可以用于下一次操作。

public class CellularAutomata {
private final Board mainBoard;
private final CyclicBarrier barrier;
private final Worker[] workers;

public CellularAutomata(Board board) {
this.mainBoard = board;
int count = Runtime.getRuntime().availableProcessors();
this.barrier = new CyclicBarrier(count,
new Runnable() {
public void run() {
mainBoard.commitNewValues();
}});
this.workers = new Worker[count];
for (int i = 0; i < count; i++)
workers[i] = new Worker(mainBoard.getSubBoard(count, i));
}

private class Worker implements Runnable {
private final Board board;

public Worker(Board board) { this.board = board; }
public void run() {
while (!board.hasConverged()) {
for (int x = 0; x < board.getMaxX(); x++)
for (int y = 0; y < board.getMaxY(); y++)
board.setNewValue(x, y, computeValue(x, y));
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}

private int computeValue(int x, int y) {
// Compute the new value that goes in (x,y)
return 10;
}
}

public void start() {
for (int i = 0; i < workers.length; i++)
new Thread(workers[i]).start();
mainBoard.waitForConvergence();
}

public static void main(String []args)
{
Board bb = (Board) new BoardImpl();
new CellularAutomata(bb).start();
}

Exchanger是关卡的另一种形式,它是一种两步关卡,在关卡点会交换数据。当两方进行的活动不对称时,这非常有用。比如当一个线程向缓冲区写入数据,这时另一个线程充当消费者使用这个数据。这些线程可以使用Exchanger进行会面,并用完整的缓冲与空缓冲进行交换,当发生交换对象时,交换为双方的对象建立了一个安全的发布。

四、锁优化思路

  • 减少锁持有时间:只用在有线程安全要求的程序上加锁
  • 减小锁粒度: 将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。 降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
  • 锁分离:最常见的锁分离就是读写锁 ReadWriteLock,读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据
  • 锁粗化:凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
  • 锁消除: 锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这 些对象的锁操作,多数是因为程序员编码不规范引起

end

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月14日 0

暂无评论

推荐阅读
TEZNKK3IfmPf