Java多线程并发07——锁在Java中的实现

时间:2021-08-09 11:59:30

上一篇文章中,我们已经介绍过了各种锁,让各位对锁有了一定的了解。接下来将为各位介绍锁在Java中的实现。关注我的公众号「Java面典」了解更多 Java 相关知识点。

在 Java 中主要通过使用synchronized 、 volatile关键字,及 Lock 接口的子类 ReentrantLock 和 ReadWriteLock 等来实现加锁。

synchronized

属性

synchronized 属于独占式的悲观锁,同时属于可重入锁。

作用

synchronized 可以把任意一个非 NULL 的对象当作锁。其在不同场景下的作用范围如下:

  1. 作用于方法时,锁住的是对象的实例(this)
  2. 作用于静态方法时,锁住的是Class实例,会锁住所有调用该方法的线程。(又因为Class的相关数据存储在永久代 PermGen【Jdk1.8 则是 metaspace】,永久代是全局共享的,因此静态方法锁相当于类的一个全局锁);
  3. 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

实现

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Java多线程并发07——锁在Java中的实现

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

参考资料

volatile

属性

比 sychronized 更轻量级的同步锁

适用场景

使用 volatile 必须同时满足下面两个条件才能保证在并发环境的线程安全:

  1. 对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true);
  2. 不同的 volatile 变量之间,不能互相依赖,只有在状态真正独立于程序内其他内容时才能使用 volatile。

对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++ 这种操作的原子性,因为本质上 i++ 是读、写两次操作。

Lock

Java 中的锁都实现于 Lock 接口,主要方法有:

  1. void lock(): 用于获取锁。如果锁可用,则获取锁。 若锁不可用, 将禁用当前线程,直到取到锁;
  2. boolean tryLock():尝试获取锁。如果锁可用,则获取锁,并返回 true, 否则返回 false;

    该方法和lock()的区别在于,如果锁不可用,tryLock()不会导致当前线程被禁用。
  3. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁
  4. void unlock():释放锁。锁只能由持有者释放,如果线程并不持有锁,却执行该方法。可能导致异常的发生;
  5. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁;
  6. getHoldCount() :查询当前线程保持此锁的次数
  7. getQueueLength():返回正等待获取此锁的线程估计数。比如启动 10 个线程,1 个线程获得锁,此时返回的是 9;
  8. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10;
  9. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition)。对于指定 contidion 对象,有多少线程执行了 condition.await 方法;
  10. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  11. hasQueuedThreads():是否有线程等待此锁
  12. isFair():该锁是否公平锁
  13. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true;
  14. isLock():此锁是否有任意线程占用
  15. lockInterruptibly():如果当前线程未被中断,获取锁

tryLock 和 lock 和 lockInterruptibly 的区别

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

Condition

作用

Condition 的作用是对锁进行更精确的控制。对于同一个锁,我们可以创建多个 Condition,在不同的情况下使用不同的 Condition。

Condition 和 Object

  • 相似之处
  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效;
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效;
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效。
  • 不同处
  1. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的;
  2. Object中的 wait()、notify()、notifyAll() 方法是和 "同步锁"(synchronized关键字) 捆绑使用的;而Condition是需要与 "互斥锁"/"共享锁" 捆绑使用的。

ReentrantLock

属性

可重入锁。

特点

除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

与 synchronized 的区别

  1. ReentrantLock 需要通过方法 lock() 与 unlock() 手动进行加锁与解锁操作,而 synchronized 会 被 JVM 自动加锁、解锁;
  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。
public class MyLock {

    private Lock lock = new ReentrantLock();
// Lock lock = new ReentrantLock(true); //公平锁
// Lock lock = new ReentrantLock(false); //非公平锁
private Condition condition = lock.newCondition(); //创建 Condition public void testMethod() {
try {
lock.lock(); //lock 加锁
// 1:wait 方法等待:
//System.out.println("开始 wait");
condition.await();
// 通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
// 2:signal 方法唤醒
condition.signal(); //condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

ReadWriteLock

属性

共享锁(读-写锁)

特点

  1. 如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率;
  2. 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥(这是由 JVM 自己控制的,代码只要上好相应的锁即可)。

使用原则

  1. 如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;
  2. 如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。

CountDownLatch(线程计数器 )

作用

CountDownLatch 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
public void run() {
System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
Thread.sleep(3000);
System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
latch.countDown();
} ;
}.start();
new Thread() {
public void run() {
System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
Thread.sleep(3000);
System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
latch.countDown();
} ;
}.start();
System.out.println("等待 2 个子线程执行完毕...");
latch.await();
System.out.println("2 个子线程已经执行完毕");
System.out.println("继续执行主线程");

CyclicBarrierr(回环栅栏-等待至 barrier 状态再全部同时执行)

作用

CyclicBarrier 是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

主要方法

CyclicBarrier 中最重要的方法就是 await 方法,它有 2 个重载版本:

  1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务;
  2. public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for (int i = 0; i < N; i++)
new Writer(barrier).start();
} static class Writer extends Thread {
private CyclicBarrier cyclicBarrier; public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
} @Override
public void run() {
try {
Thread.sleep(5000); //以睡眠来模拟线程需要预定写入数据操作
System.out.println("线程" + Thread.currentThread().getName() + "写入数据完 毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务,比如数据操作");
}
}

CountDownLatch 和 CyclicBarrier 的区别

  1. CountDownLatch 的作用是允许 1 或 N 个线程等待其他线程完成执行;而 CyclicBarrier 则是允许 N 个线程相互等待;
  2. CountDownLatch 的计数器无法被重置;CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的 barrier。

Semaphore

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

实现互斥锁(计数器为 1)

我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

代码实现

它的用法如下:

// 创建一个计数阈值为 5 的信号量对象
// 只能 5 个线程同时访问
Semaphore semp = new Semaphore(5);
try { // 申请许可
semp.acquire();
try {
// 业务逻辑
} catch (Exception e) {
} finally {
// 释放许可
semp.release();
}
} catch (InterruptedException e) {
}

Semaphore 与 ReentrantLock 相似处

Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也有许多类似之处:

  1. 都需要手动加锁。通过 acquire() 与 release() 方法来获得和释放资源;
  2. Semaphone.acquire()方法默认为可响应中断锁,与 ReentrantLock.lockInterruptibly() 作用效果一致,也就是说在等待临界资源的过程中可以被 Thread.interrupt() 方法中断;
  3. Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致;
  4. Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定;
  5. 锁释放方式相同。与 ReentrantLock 一样 Semaphore 的锁释放操作也由手动进行。同时,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。

多线程与并发系列推荐

Java多线程并发06——CAS与AQS

Java多线程并发05——那么多的锁你都了解了吗

Java多线程并发04——合理使用线程池

Java多线程并发03——什么是线程上下文,线程是如何调度的

Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗

Java多线程并发01——线程的创建与终止,你会几种方式

Java多线程并发07——锁在Java中的实现的更多相关文章

  1. Java多线程并发08——锁在Java中的应用

    前两篇文章中,为各位带来了,锁的类型及锁在Java中的实现.接下来本文将为各位带来锁在Java中的应用相关知识.关注我的公众号「Java面典」了解更多 Java 相关知识点. 锁在Java中主要应用还 ...

  2. Java多线程并发编程&sol;锁的理解

    一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等 ...

  3. Java多线程并发03——在Java中线程是如何调度的

    在前两篇文章中,我们已经了解了关于线程的创建与常用方法等相关知识.接下来就来了解下,当你运行线程时,线程是如何调度的.关注我的公众号「Java面典」了解更多 Java 相关知识点. 多任务系统往往需要 ...

  4. Java多线程并发05——那么多的锁你都了解了吗

    在多线程或高并发情境中,经常会为了保证数据一致性,而引入锁机制,本文将为各位带来有关锁的基本概念讲解.关注我的公众号「Java面典」了解更多 Java 相关知识点. 根据锁的各种特性,可将锁分为以下几 ...

  5. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”07之 LockSupport

    概述 本章介绍JUC(java.util.concurrent)包中的LockSupport.内容包括:LockSupport介绍LockSupport函数列表LockSupport参考代码(基于JD ...

  6. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”03之 公平锁&lpar;一&rpar;

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  7. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”10之 CyclicBarrier原理和示例

    概要 本章介绍JUC包中的CyclicBarrier锁.内容包括:CyclicBarrier简介CyclicBarrier数据结构CyclicBarrier源码分析(基于JDK1.7.0_40)Cyc ...

  8. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...

  9. Java多线程-并发容器

    Java多线程-并发容器 在Java1.5之后,通过几个并发容器类来改进同步容器类,同步容器类是通过将容器的状态串行访问,从而实现它们的线程安全的,这样做会消弱了并发性,当多个线程并发的竞争容器锁的时 ...

随机推荐

  1. 使用 openssl 生成证书

    一.openssl 简介 目前最流行的 SSL 密码库工具官网:https://www.openssl.org/source/ 构成部分 密码算法库 密钥和证书封装管理功能 SSL通信API接口 用途 ...

  2. tomcat启动startup&period;bat一闪而过 转

    遇到很多次运行startup.bat后,一个窗口一闪而过的问题,但是从来没去纠正怎样修改配置才是正确的,现在从网上查阅的资料整理如下:tomcat在启动时,会读取环境变量的信息,需要一个CATALIN ...

  3. C&num;事件解析

    事件(event),这个词儿对于初学者来说,往往总是显得有些神秘,不易弄懂.而这些东西却往往又是编程中常用且非常重要的东西.大家都知道windows消息处理机制的重要,其实C#事件就是基于window ...

  4. &lbrack;javascript&rsqb;js修改title

    使用javascript修改title 1.这个在chrome中可以成功,在ie8中报错 <!DOCTYPE html> <html> <head> <tit ...

  5. relative、absolute和float

    relative.absolute和float   position:relative和position:absolute都可以改变元素在文档中的位置,都能激活元素的left.top.right.bo ...

  6. Web开发人员不要错过的60款用户界面设计工具(上)

    Web开发大师们,干货再次来袭!小编为大家盘点了60款功能丰富类型各异的用户界面设计工具,本系列将以上中下三篇分别为大家呈现.今天盘点的这20款工具囊括了大量界面原型设计工具,有免费的在线原型工具,有 ...

  7. 【转】国外程序员整理的Java资源大全

    Java几乎是许多程序员们的入门语言,并且也是世界上非常流行的编程语言.国外程序员Andreas Kull在其Github上整理了非常优秀的Java开发资源,推荐给大家.译文由ImportNew- 唐 ...

  8. codeforces&num;580 D&period; Kefa and Dishes(状压dp)

    题意:有n个菜,每个菜有个兴奋值,并且如果吃饭第i个菜立即吃第j个菜,那么兴奋值加ma[i][j],求吃m个菜的最大兴奋值,(n<=18) 分析:定义dp[status][last],statu ...

  9. Linux CPU信息和使用情况查看(CentOS)

    一.CPU信息查看 cat /proc/cpuinfo| grep "physical id"| sort -u | wc -l #查看是物理CPU个数,-u和uniq都是去重作用 ...

  10. Ubuntu 14&period;04 配置OpenCv 2&period;4&period;9

      安装工具 g++ 链接:http://www.cnblogs.com/LQLin168/p/6844593.html 下载OpenCv 2.4.9(官网地址):http://opencv.org/ ...