JUC第四讲:Java中的锁

news2024/12/27 13:20:33

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文是JUC第4讲,旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

文章目录

    • 前言
      • 锁的分类
    • Action1:讲讲你知道的一些锁?
    • 1、乐观锁 VS 悲观锁
    • 2、自旋锁 VS 适应性自旋锁
    • 3、无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
    • 4、公平锁 VS 非公平锁
    • 5、可重入锁 VS 非可重入锁
    • 6、独享锁(排他锁) VS 共享锁
    • 7、Java中的锁
      • 7.1、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
      • 7.2、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?
      • 7.3、偏向锁、轻量级锁、重量级锁分别对应了什么使用场景?
      • 7.4、自旋发生在哪个阶段?
      • 7.5、为什么要设计自旋操作?
      • 7.6、自适应自旋是如何体现自适应的?
      • 7.7、Java读写锁是啥?读写锁设计主要解决什么问题?
      • 7.8、Java 同步机制有哪些
    • 结语
    • 参考资料

前言

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:

img

锁的分类

  • 从乐观和悲观的角度可分为乐观锁和悲观锁,

    • 乐观锁

      • 乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作。Java中的乐观锁大部分是通过CAS(Compare And Swap,比较和交换)操作实现的,CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态。
      • 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了 乐观锁的一种实现方式 CAS 实现的。
    • 悲观锁

      • 悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生
      • 乐观锁:(锁的粒度小)假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。
      • 行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现
    • 适应场景

      • 乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的 时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
      • 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适
  • 获取资源的公平性角度可分为公平锁和非公平锁

    • 公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

      • 在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
    • 非公平锁 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

      • 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

      • 非公平锁的优点在于吞吐量比公平锁大。

    • Lock是支持公平锁的,synchronized不支持公平锁。

    • 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁

  • 是否共享资源的角度 可分为共享锁和独占锁

    • 独占锁:指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Synchronized 而言都是独占锁。
    • 共享锁:指该锁可被多个线程所持有。
      • 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。
      • ReentrantReadWriteLock读锁是共享锁,其写锁是独占锁
      • 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
  • 锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。下面将详细介绍这些锁

    • 偏向锁 --》轻量级锁 --》自旋锁、自适应自旋、锁消除、锁粗化。
    • 自旋锁 – JVM
      • 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
      • 自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁
        • 《深入理解Java虚拟机》第二版

Action1:讲讲你知道的一些锁?

  • 公平锁和非公平锁,可重入锁(可递归)和非可重入锁,独享锁和共享锁,互斥锁 / 读写锁,乐观锁 / 悲观锁,自旋锁…

  • 然后根据锁提出的,讲讲synchronized和ReentractLock的原理和区别

  • 从硬件层面讲到java层面,举出了区别和应用场景,说道了哪些框架和源码用了

1、乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

img

根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:

// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
	// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
    lock.lock();
    // 操作同步资源
    lock.unlock();
}

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

通过调用方式示例,我们可以发现 悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?具体可以参看 JUC第八讲:JUC原子类:CAS,Unsafe和原子类详解。

2、自旋锁 VS 适应性自旋锁

在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

img

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

自旋锁相关可以看 JUC第四讲:关键字: synchronized详解 第四节:自旋锁与自适应自旋锁

3、无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

总结而言: 偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞

img

相关可以看 JUC第四讲:关键字: synchronized详解

4、公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。

img

如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:

img

更多请参看 JUC第十讲:JUC锁: ReentrantLock详解。

5、可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。

还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

img

但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。

img

之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

img

更多请参看:

  • JUC第五讲:关键字: synchronized详解
  • JUC第八讲:JUC锁: LockSupport详解
  • JUC第九讲:JUC锁: 锁核心类AQS详解
  • JUC第十讲:JUC锁: ReentrantLock详解

6、独享锁(排他锁) VS 共享锁

独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

下图为ReentrantReadWriteLock的部分源码:

img

我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的,所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

更多请参看 JUC第十讲:JUC锁: ReentrantReadWriteLock详解

7、Java中的锁

7.1、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?

  • 重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。

  • 这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大

  • 而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销

7.2、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?

  • 偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword ,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。

    • markword 详见这篇文章:
    • CAS详解见这篇文章: JUC第四讲:Java中的锁/CAS原理与案例分析
  • 如果确定同步代码块会被多个线程访问或者竞争较大,可以通过 -XX:-UseBiasedLocking 参数关闭偏向锁。

7.3、偏向锁、轻量级锁、重量级锁分别对应了什么使用场景?

  • 1)偏向锁

    • 适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。
    • 但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。
    • Action:什么是重偏向?
  • 2)轻量级锁

    • 适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级会重量级锁。有同学可能会说,不是有自旋,请继续往下看。
  • 3)重量级锁

    • 适用于多个线程同时获取锁。
    • Action:只有重量级锁才会有自旋操作

7.4、自旋发生在哪个阶段?

自旋锁代码验证

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");
        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {
						// 摸鱼
        }
    }

    public void myUnLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        // 自己用完了后,把 atomicReference 变成 null
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }
    
		public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        // 启动t1线程,开始操作
        new Thread(() -> {
            // 开始占有锁
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();

        // 让main线程暂停1秒,使得t1线程,先执行
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {
            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();
        }, "t2").start();
		}
}

输出结果

t1	 come in 
t2	 come in 
t1	 invoked myUnlock()
t2	 invoked myUnlock()

自旋发生在重量级锁阶段。

网上99.99%的说法,自旋都是发生在轻量级锁阶段,但是实际看了源码(JDK8)之后,并不是这样的。

  • 轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。

  • 而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。

7.5、为什么要设计自旋操作?

因为重量级锁的挂起开销太大。

  • 一般来说,同步代码块内的代码应该很快就执行结束,这时候竞争锁的线程自旋一段时间是很容易拿到锁的,这样就可以节省了重量级锁挂起的开销。

7.6、自适应自旋是如何体现自适应的?

自适应自旋锁有自旋次数限制,范围在:1000~5000。

  • 如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。
  • 所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。
  • 相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。

7.7、Java读写锁是啥?读写锁设计主要解决什么问题?

是否共享资源的角度可分为共享锁和独占锁

  • 独占锁:指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Synchronized 而言都是独占锁。

  • 共享锁:指该锁可被多个线程所持有。

    • 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。
    • ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁
    • 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
  • 实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "\t 写入完成");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
    }
}

public class ReadWriteWithoutLockDemo {
    public static void main(String[] args) {
          MyCache myCache = new MyCache();
          // 线程操作资源类,5个线程写
          for (int i = 0; i < 5; i++) {
              final int tempInt = i;
              new Thread(() -> {
                  myCache.put(tempInt + "", tempInt +  "");
              }, String.valueOf(i)).start();
          }

          // 线程操作资源类, 5个线程读
          for (int i = 0; i < 5; i++) {
              final int tempInt = i;
              new Thread(() -> {
                  myCache.get(tempInt + "");
              }, String.valueOf(i)).start();
          }
    }
}

输出结果:

0	 正在写入:0
1	 正在写入:1
3	 正在写入:3
2	 正在写入:2
4	 正在写入:4
0	 正在读取:
1	 正在读取:
2	 正在读取:
4	 正在读取:
3	 正在读取:
1	 写入完成
4	 写入完成
0	 写入完成
2	 写入完成
3	 写入完成
3	 读取完成:3
0	 读取完成:0
2	 读取完成:2
1	 读取完成:null
4	 读取完成:null

看到有些线程读取到 null,可用 ReentrantReadWriteLock 解决

package com.lun.concurrency;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache2 {

    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

  	public void put(String key, Object value) {
        // 创建一个写锁
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 写锁 释放
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        // 使用读锁
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 读锁释放
            rwLock.readLock().unlock();
        }
    }

    public void clean() {
        map.clear();
    }
}

public class ReadWriteWithLockDemo {
    public static void main(String[] args) {
        MyCache2 myCache = new MyCache2();
        // 线程操作资源类,5个线程写
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

        // 线程操作资源类, 5个线程读
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

1	 正在写入:1
1	 写入完成
2	 正在写入:2
2	 写入完成
3	 正在写入:3
3	 写入完成
5	 正在写入:5
5	 写入完成
4	 正在写入:4
4	 写入完成
2	 正在读取:
3	 正在读取:
1	 正在读取:
5	 正在读取:
4	 正在读取:
3	 读取完成:3
2	 读取完成:2
1	 读取完成:1
5	 读取完成:5
4	 读取完成:4

7.8、Java 同步机制有哪些

阻塞同步

  • 1、synchronized 关键字,这个相信大家很了解,最好能理解其中的原理

  • 2、Lock 接口及其实现类,如 ReentrantLock.ReadLock 和 ReentrantReadWriteLock.WriteLock

非阻塞同步

  • 1、CAS
  • 2、AtomicInteger

无同步方案

  • 要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

  • 1、各类工具类

    • 信号量(Semaphore):是一种计数器,用来保护一个或者多个共享资源的访问,它是并发编程的一种基础工具,大多数编程语言都提供这个机制,这也是操作系统中经常提到的

    • CountDownLatch:是 Java 语言提供的同步辅助类,在完成一组正在其他线程中执行的操作之前,他允许线程一直等待

    • CyclicBarrier:也是 java 语言提供的同步辅助类,他允许多个线程在某一个集合点处进行相互等待;

    • Phaser:也是 java 语言提供的同步辅助类,他把并发任务分成多个阶段运行,在开始下一阶段之前,当前阶段中所有的线程都必须执行完成,JAVA7 才有的特性。

    • Exchanger:他提供了两个线程之间的数据交换点。

  • 2、线程本地存储

具体可以看这篇文章:JUC第二讲:Java并发理论基础:Java内存模型(JMM)与线程 第7节

结语

本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。

其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。

参考资料

  • 《Java并发编程艺术》
  • Java中的锁
  • Java CAS 原理剖析
  • Java并发——关键字synchronized解析
  • Java synchronized原理总结
  • 聊聊并发(二)——Java SE1.6中的Synchronized
  • 深入理解读写锁—ReadWriteLock源码分析
  • 【JUC】JDK1.8源码分析之ReentrantReadWriteLock
  • Java多线程(十)之ReentrantReadWriteLock深入分析
  • Java–读写锁的实现原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1024474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ROBOGUIDE教程:三维模型创建功能介绍与操作方法

目录 概述 模型创建方法 模型属性设置 其他类型模型创建 资源文件夹创建 安全围栏模型创建 概述 ROBOGUIDE软件除了可以从外部导入模型外&#xff0c;软件自身也可以创建一些简易的三维模型&#xff0c;在对FANUC机器人虚拟仿真或离线编程时&#xff0c;可以利用这个功能…

SOLIDWORKS Composer反转关键帧实现产品安装过程

SOLIDWORKS Composer 是一款被用来制作交互式产品说明书的工具&#xff0c;可以帮助我们对产品设定精确的机构动画&#xff0c;并能根据材质生成一定细节的渲染图像。 今天我们主要向大家讲解的是&#xff0c;利用SOLIDWORKS Composer关键帧反转实现产品动态的安装。 一般情况下…

springcloud3 分布式事务解决方案seata之SAGA模式7

一 saga模式 1.1 saga Saga模式是SEATA提供的长事务解决方案&#xff0c;在Saga模式中&#xff0c;业务流程中每个参与者都提交本地事务&#xff0c;当出现某一个参与者失败则补偿前面已经成功的参与者&#xff0c;一阶段正向服务和二阶段补偿服务都由业务开发实现。 分布式…

VmWare16+Ubuntu安装教程

文章目录 前言一、前期软件和系统镜像准备二、VmWare16安装三、Ubuntu安装&#xff08;1&#xff09;下载Ubuntu镜像&#xff08;2&#xff09;打开VmWare16&#xff0c;点击创建新的虚拟机&#xff08;3&#xff09;选择典型&#xff0c;下一步&#xff08;4&#xff09;选择刚…

【lesson7】git的介绍及使用

文章目录 什么是gitgit的历史git使用在gitee上创建仓库git clone HTTPS地址git add .git add 文件名git commit “日志”git pushgit loggit rm 文件名git statusgit pull 什么是git git是版本控制器&#xff0c;那么什么是版本控制器呢&#xff1f; 下面讲个故事为大家讲解一…

Linux内核源码分析 (B.6)从内核源码看 slab 内存池的创建初始化流程

Linux内核源码分析 (B.6)从内核源码看 slab 内存池的创建初始化流程 文章目录 Linux内核源码分析 (B.6)从内核源码看 slab 内存池的创建初始化流程1\. \_\_kmem\_cache\_alias1.1 find\_mergeable 查找可被复用的 slab cache1.2 calculate\_alignment 综合计算出一个合理的对齐…

Linux内嵌汇编

文章目录 前言一、内嵌汇编二、内嵌汇编示例三、不使用printf实现打印四、INT 80H总结 前言 本篇文章我们来讲讲内嵌汇编的概念和教大家如何来编写内嵌汇编的代码。 一、内嵌汇编 内嵌汇编&#xff08;Inline Assembly&#xff09;是将汇编代码嵌入到高级语言中的一种编码技…

qsort库函数的使用

目录 1.认识qsort函数 2.qsort 排序整型数据 3.qsort排序字符型数据 4.qsort排序浮点型数据、 5.qsort排序结构体数据 6.总结 1.认识qsort函数 注&#xff1a;以上信息来源于cplusplus官网 翻译以上信息如下&#xff1a; qsort函数是c语言标准库中基于快速排序算法实现的一…

如何快速走出网站沙盒期(关于优化百度SEO提升排名)

网站沙盒期是指新建立的网站在百度搜索引擎中无法获得好的排名&#xff0c;甚至被完全忽略的现象。这个现象往往发生在新建立的网站上&#xff0c;因为百度需要时间来评估网站的质量和内容。蘑菇号www.mooogu.cn 为了快速走出网站沙盒期&#xff0c;需要优化百度SEO。以下是5个…

Redis的softMinEvictableIdleTimeMillis和minEvictableIdleTimeMillis参数

背景&#xff1a; Redis的softMinEvictableIdleTimeMillis&#xff0c;minEvictableIdleTimeMillis是一个令人疑惑两个参数&#xff0c;特别是当它和minIdle组合起来时就更难理解了&#xff0c;本文就来梳理下他们的之间的关系 softMinEvictableIdleTimeMillis&#xff0c;mi…

如何正确安装封箱机胶带

胶带是封箱机与开箱机最重要的耗材&#xff0c;在使用的过程中安装胶带卷是必不可少的工作项目&#xff0c;但很多用户使用很久了&#xff0c;安装的胶带仍然不理想&#xff0c;下面就和您分享一下如何正确安装封箱机胶带。 1、带卷安装要牢靠。封箱机胶带卷的固定是靠3个卡簧来…

Tesla P40千元级大显卡主机装机实践

序 随着chatglm2-6b,llama-7b等模型的开源&#xff0c;早就想要在消费级显卡&#xff0c;独立体验一下部署大模型的感觉&#xff0c;尽管在一些商用云平台上部署实践过&#xff0c;总是觉得缺少点什么&#xff0c;而且它们是按小时收费。 尤其是阿里的通义千问7B开源&#xff…

PYQT制作动态时钟

所有代码&#xff1a; import sys from PyQt5.QtCore import Qt, QTimer, QRect from PyQt5.QtGui import QPixmap, QTransform, QPainter, QImage from PyQt5.QtWidgets import QApplication, QLabel from PyQt5 import uic import newdef adder():global iglobal angle_s, a…

【AAAI2023】Spatial-Spectral Transformer for Hyperspectral Image Denoising

论文&#xff1a;https://readpaper.com/paper/4694783227240398849 代码&#xff1a;https://github.com/MyuLi/SST 1、总体介绍 高光谱图像&#xff08;HSI&#xff09;去噪是后续HSI应用的关键预处理过程&#xff0c;但是基于CNN的方法需要在计算效率与非局部特征建模能力之…

如何使用ArcGIS Pro将等高线转DEM

通常情况下&#xff0c;我们拿到的等高线数据一般都是CAD格式&#xff0c;如果要制作三维地形模型&#xff0c;使用栅格格式的DEM数据是更好的选择&#xff0c;这里就为大家介绍一下如何使用ArcGIS Pro将等高线转DEM&#xff0c;希望能对你有所帮助。 创建TIN 在工具箱中选择“…

Redis学习 - 了解Redis(三)

1. 什么是缓存击穿、缓存穿透、缓存雪崩&#xff1f; 1.1 缓存穿透问题 先来看一个常见的缓存使用方式&#xff1a;读请求来了&#xff0c;先查下缓存&#xff0c;缓存有值命中&#xff0c;就直接返回&#xff1b;缓存没命中&#xff0c;就去查数据库&#xff0c;然后把数据库…

读书笔记-《ON JAVA 中文版》-摘要25[第二十二章 枚举]

文章目录 第二十二章 枚举1. 基本功能1.1 基本 enum 特性 2. 方法添加2.1 方法添加2.2 覆盖 enum 的方法 3 switch 语句中的 enum4. values 方法的神秘之处5. 实现而非继承6. 随机选择7. 使用接口组织枚举8. 使用 EnumSet 替代 Flags9. 使用 EnumMap10. 常量特定方法11. 本章小…

pcl--第六节 3D特征描述子

特征描述子 Feature Descriptor 是每个特征点独特的身份认证同一空间点在不同视角的特征点具有高度相似的描述子不同特征点的描述子差异性尽量大通常描述子是一个具有固定长度的向量 描述子可以分为以下几种类型&#xff1a;基于不变性的描述子、基于直方图的描述子、二进制描…

上万条童话故事儿童故事ACCESS\EXCEL数据库

虽然已经有《7千多儿童故事网ACCESS\EXCEL数据库》这种记录数的童话故事类数据&#xff0c;但是遇到了好采集的就总想采集下来&#xff0c;后续有时间或有需求可以再做合并等操作。 分类情况统计为&#xff1a; 儿童故事&#xff1a;儿童小故事&#xff08;1895&#xff09;、…

Cobra眼睛蛇-强大的Golang CLI框架,快速上手的脚手架搭建项目工具,详细安装和使用

Cobra眼睛蛇-强大的Golang CLI框架&#xff0c;快速上手的脚手架搭建项目工具&#xff0c;详细安装和使用。 阅读过k8s源码的同学&#xff0c;应该都知道k8s Scheduler、kubeadm、kubelet等核心组件的命令行交互全都是通过spf13写的Cobra库来实现。本文就来介绍下Cobra的相关概…