当前位置: 首页 > 图灵资讯 > 技术篇> Java锁的种类

Java锁的种类

来源:图灵教育
时间:2023-06-14 09:44:40

概述

锁大概有以下名词:

其他类型的自旋锁、自旋锁、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、偏向锁、对象锁、线程锁、粗化锁、消除锁、轻量级锁、重量级锁、信号量、独家锁、共享锁、分段锁。

事实上,我们所说的锁的分类应该根据锁的特点和设计来划分。

事实上,从并发的角度来看,根据三种线程安全策略,主要内容集中在相互排斥同步,我们讨论的锁也集中在这部分,这部分锁是悲观锁;第二部分是非阻塞同步,这部分是通过CAS进行原子操作,这部分可以被视为乐观锁,实际上不是锁;第三部分是无同步解决方案,包括可重新访问代码和线程本地存储。

常见的锁

Synchronized和Lock。

其实我们真正用的锁只有两三种,只是根据设计方案和性质划分了很多。

Sychronized语义上的实现:它是一种不公平、悲观、独家、互斥、可重新进入的重量级锁。

以下两个锁都在JUC包下,是API层面的实现:

ReentrantLock:它是一种默认的非公平但可以实现公平、悲观、独家、相互排斥、重量级锁。

ReentrantReadWriteLock:它是一种默认的非公平但可以实现公平、悲观、独家写作、阅读共享、阅读和写作、重量级锁。

按性质分类

公平锁/非公平锁

公平锁是指按照申请锁的顺序获取多个线程的锁。非公平锁是指多个线程获取锁的顺序不是按照申请锁的顺序,申请的线程可能优先于先申请的线程。

可能导致优先反转或饥饿。对Java而言 就Reentrantlock而言,通过构造函数指定锁是否为公平锁,默认为非公平锁。非公平锁的优点是吞吐量大于公平锁。Synchronized也是一种不公平的锁。与Reeentrantlock不同,线程调度是通过AQS实现的,因此没有办法使其成为公平锁。

乐观锁/悲观锁

乐观锁和悲观锁不是指特定类型的锁,而是指并发同步的角度。

悲观锁认为,相应数据的并发操作肯定会发送修改,即使没有修改。因此,对于同一数据的并发操作,悲观锁采用锁的形式。悲观地认为,没有锁的并发操作肯定会有问题。

乐观锁认为,同一数据的并发操作不会被修改。在更新数据时,将尝试更新,并不断重新尝试更新数据。乐观地认为,没有锁的并发操作是无关紧要的。

从上面的描述可以看出,悲观锁适合写很多场景,乐观锁适合读很多场景,不加锁会带来很多性能提升。Java中使用悲观锁就是使用各种锁。Java中使用乐观锁是无锁编程,CAS算法经常使用。典型的例子是原子类,通过CAS自旋更新原子操作。

独享锁/共享锁

独享锁是指该锁一次只能由一个线程锁定。共享锁是指该锁可以由多个线程锁定。对应的Java Reentrantlock实际上是一个独家锁。但对于Lock的另一个实现类ReeentrantreadWritelock,它的读锁是共享锁,它的写锁是独家锁。

阅读锁的共享锁可以保证并发阅读非常高效,阅读、写作、阅读和写作过程是相互排斥的。独家锁和共享锁也通过AQS实现,通过不同的方法实现独家和共享。

Synchronized当然是独家锁。

互斥锁/读写锁

上面提到的独家锁/共享锁是一种广义的说法,互斥锁/读写锁是具体的实现。Java中互斥锁的具体实现是Reentrantlock,Java中读写锁的具体实现是ReentrantreadWritelock。

可重入锁

可重新进入锁,又称递归锁,是指在外层方法获得同一线程的锁后,进入内层方法会自动拥有锁,无需再次释放锁即可获得。

举例说明:

对Java而言 对于Reentrantlock来说,他的名字可以看出是一个可重入锁,它的名字是Reentrantlock Lock重新进入锁。对于Synchronized来说,它也是一个可重新进入锁。可重新进入锁的优点之一是可以在一定程度上避免死锁。

public sychrnozied void test() {

xxxxxx;

test2();

}

public sychronized void test2() {

yyyyy;

在上述代码中,执行test()方法需要获得当前对象作为监视器的对象锁,但该方法调用test2的同步方法,以获得同一对象作为对象锁。

说明:

如果锁具有可重入性,则调用test2时不需要再次获得当前对象的锁,可以直接进入test2进行操作。

如果锁不具有可重入性,则在调用test2时,该线程将等待当前对象锁的释放。事实上,该对象锁已被当前线程锁定,无法再次获得,并将产生死锁。

按设计方案进行分类

自旋锁/自适应锁

如果物理机器有一个以上的处理器,可以同时执行两个或两个以上的线程,我们可以让后面要求锁的线程“等一会儿”,但不要放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个忙碌的循环(旋转),这种技术被称为旋转锁。

优点:自旋等待本身可以避免线程切换的成本(线程切换,CPU最初执行A线程,由于A线程本身阻塞,CPU切换到B线程)。

缺点:自旋等待不能取代堵塞,更不用说对处理器数量的要求了。虽然自旋等待本身避免了线程切换的成本,但它也占用了处理器的时间。

自适应意味着自旋时间不再固定,而是由同一锁上的自旋时间和锁的状态决定的。

如果在同一锁对象上,自旋等待刚刚成功的活动锁,并且持有锁的线程正在运行,那么虚拟机很容易认为这种自旋很可能再次成功,然后它将允许自旋等待相对较长的时间,如100个循环。

如果自旋很少成功获得锁,那么在未来获得锁时可能会省略自旋过程,以避免浪费处理器资源。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态。而且是针对Synchronized。Java5通过引入锁升级机制实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表示的。

偏向锁是指一段同步代码已被一个线程访问,因此该线程将自动获取锁。降低获取锁的成本。

轻量级锁是指当锁偏向锁时,被另一个线程访问,偏向锁升级为轻量级锁,其他线程将尝试以旋转的形式获得锁,不会阻塞,提供性能。

重量级锁是指当锁是轻量级锁时,虽然另一个线程是自旋的,但自旋不会一直持续下去,当自旋一定次数时,没有得到锁,很容易进入堵塞,锁膨胀为重量级锁。重量级锁会阻塞其他应用程序的线程,降低性能。

分段锁

分段锁实际上是一种锁的设计,而不是一种特定的锁。对于concurenthashmap来说,并发的实现是通过分段锁实现高效的并发操作。

让我们来谈谈ConcurentHashmap分段锁的含义和设计理念。ConcurentHashmap中的分段锁称为segment,类似于Hashmap(JDK7和JDK8中Hashmap的实现)的结构,即内部有一个Entry数组,数组中的每个元素都是一个链表;同时也是一个Reentrantlock(Segment继承了Reentrantlock)。

当需要put元素时,不是锁定整个hashmap,而是通过hashcode知道它应该放在那个分段中,然后锁定这个分段。因此,当多个线程put时,只要不放在一个分段中,就可以实现真正的并行插入。

然而,在统计size时,当您获得hashmap的全球信息时,您需要获得所有的分段锁进行统计。分段锁的设计目的是细化锁的粒度。当操作不需要更新整个数组时,只对数组中的一个进行锁操作。