Java多线程内容(3)

Posted by BY KiloMeter on March 1, 2019

重量级锁

synchronized

synchronized 可以把任意一个非 NULL 的对象当作锁。

1、当锁住的是实例方法时,锁住的这个类实例(this)。

2、当锁住的是静态方法时,锁住的是Class实例。

3、当所用与一个对象时,锁住的是所有以该对象为锁的代码块。

synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

Java1.6, synchronized 进行了很多的优化, 有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

自旋锁

自旋锁的原理很简单,就是如果每次线程在获取锁之后,能够在很短的时间内将锁释放给其他线程,其他线程就无需在内核态和用户态之间进行切换,只需要等待(自旋)一会持有锁的线程即可,这样就避免了线程切换的消耗。

线程自旋是需要消耗CPU的,因为在等待时就是让CPU做无用功,如果一直获取不到锁,那线程也不能无止尽地等待下去,因此需要设置一个最大自旋等待时间,如果超出这个时间,线程则会进入阻塞状态。

自适应自旋锁

自适应自旋锁是在自旋锁的基础上进行了优化,在自旋锁中,我们需要自己指定等待的时间,但是在自适应自旋锁中,不需要我们指定循环的次数,它会自身进行判断,对于一些经常拿到的锁,会多循环几次,对于基本没有拿过的锁,则循环的次数会少一些。

轻量级锁

上面讲到的synchronized、自旋锁、自适应自旋锁,都有一个特点,就是在进入方法前进行加锁操作,在方法结束后释放锁。我们都知道,加锁是需要依靠操作系统的,用户态和内核态的切换需要花费大量时间,在某些情况下,某个方法是没有其他线程来进行竞争的,如果这个时候还对该方法进行加锁和释放锁操作,势必会浪费大量时间,因此出现了轻量级锁

轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。

显然和重量级锁相比,轻量级锁使用CAS操作来改变状态所需要的开销更小

轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。

如果真的遇到了竞争,我们就会认为轻量级锁已经不适合了,我们就会把轻量级锁升级为重量级锁了

偏向锁

偏向锁是在轻量级锁的基础上,进行了更进一步地优化。Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。即很多看似有很多线程同时访问的方法,实际上只有一个线程在访问。在这种情况下,如果使用轻量级锁的话,每次进入该方法和退出该方法时,都需要使用CAS操作进行状态的修改,偏向锁优化的地方在于,在一个线程第一次进入某个同步方法时,先判断下该方法的标志位,如果标志位为空,则使用CAS操作,在该方法上标记自己的ThreadID,在退出方法的时候,不做任何修改操作,在下一次再次进入该同步方法时,判断下该方法的标志是否是自己的ThreadID,如果是的话,直接进入方法体执行即可。

偏向锁是在只有一个线程执行同步代码块时进一步地提高性能

但是如果真出现了多个线程,即其他线程发现标志位不是自己的ThreadID,这意味着偏向锁已经不适用了,这个时候偏向锁就会升级为轻量级锁

因此,偏向锁只适用于始终只有一个线程在执行一个方法的情况

什么是锁升级呢?

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

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

悲观锁

悲观锁就是悲观思想,认为写多,遇到并发写的可能搞,每次读写数据时都会对数据进行加锁,上面提到的synchronized就是悲观锁

乐观锁

乐观锁是一种乐观思想,认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁。但是在遇到更新的时候会检查有没有人去更新这个数据,采取在写时先读出版本号,加锁后进行修改(修改时检查版本号是否跟先前读取时的版本号是否一致,如果一致则进行修改,反之修改失败),上面的CAS操作也是一种更新方法,CAS方法包括了三个操作数,需要读写的内存位置、进行比较的预期原值、拟写入的值,如果该位置的值和预期原值相同,则进行修改,否则更新失败。

非公平锁

JVM 按随机、就近原则分配锁的机制则称为不公平锁, ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。 非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列 。

公平锁

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

可重入锁(递归锁)

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。JAVA中的ReentrantLock 和 synchronized 都是可重入锁。

ReentrantLock

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

  1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
  2. boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于,tryLock()只是”试图”获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
  3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 会抛出IllegalMonitorStateException异常。
  4. Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。
  5. getHoldCount() : 查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。
  6. getQueueLength():返回正等待获取此锁的线程数,比如启动10个线程,1个线程获得锁,此时返回的是9
  7. getWaitQueueLength: (Condition condition)返回等待与此锁相关的给定条件的线程数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
  8. hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
  9. hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁
  10. hasQueuedThreads(): 是否有线程等待此锁
  11. isFair(): 该锁是否公平锁
  12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
  13. isLock(): 此锁是否有任意线程占用
  14. lockInterruptibly() : 如果当前线程未被中断,获取锁
  15. tryLock() : 尝试获得锁,仅在调用时锁未被线程占用,获得锁
  16. tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

ReentrantLock和synchronized

1、ReentrantLock的lock()和unlock()方法和synchronized不同的是,ReentrantLock加锁后需要自己手动释放锁,为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。

2、ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。

ReentrantLock的中断得使用condition对象,newCondition()获取条件对象,只有持有该锁的线程能够使用该方法。

condition的方法和Object类锁方法的区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

condition的signal只能够唤醒由同一个condition对象await的线程,这就是实现唤醒指定线程的方法

Java多线程之ReentrantLock与Condition

Semaphore 信号量

Semaphore 是一种基于计数的信号量。相当于OS所学的Semaphore信号了,它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来构建一些对象池,资源池之类的, 比如数据库连接池

Semaphore 与 ReentrantLock

Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与release()方法来获得和释放临界资源。经实测, Semaphone.acquire()方法默认为可响应中断锁,与 ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。

此外, Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致。 Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。

AtomicInteger

首先说明,此处 AtomicInteger ,一个提供原子操作的Integer的类,常见的还有AtomicBoolean、 AtomicInteger、 AtomicLong、 AtomicReference 等,它们的实现原理相同,区别在与运算对象类型的不同。还可以通过 AtomicReference<V>将一个对象的所有操作转化成原子操作。

我们知道, 在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是 ReentantLock 的好几倍。

ReadWriteLock 读写锁

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

读锁

如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

写锁

如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁

Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也有具体的实现ReentrantReadWriteLock。

共享锁和独占锁

独占锁

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

共享锁

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

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

分段锁

分段锁也并非一种实际的锁,而是一种思想,ConcurrentHashMap 是学习分段锁的最好实践

锁优化

减少锁持有时间

只用在有线程安全要求的程序上加锁

减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

锁分离

最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

锁消除

锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。