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

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

synchronized

属性

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

作用

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

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

实现

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

  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多线程并发编程/锁的理解

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

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

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

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

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

  5. Java多线程系列--“JUC锁”07之 LockSupport

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

  6. Java多线程系列--“JUC锁”03之 公平锁(一)

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

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

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

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

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

  9. Java多线程-并发容器

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

随机推荐

  1. python socketserver 实现 ftp功能

    需求: 用户加密认证 允许同时多用户登录 每个用户有自己的家目录 ,且只能访问自己的家目录 对用户进行磁盘配额,每个用户的可用空间不同 允许用户在ftp server上随意切换目录 允许用户查看当前目 ...

  2. loadrunner没有告诉你的

    1.        LoadRunner之-Block l  如何在一个脚本中实现不同事务不同次数的循环呢? l  案例:假如你想在一个脚本中,实现登录执行1次,查询执行2次,插入执行3次,怎么办?录 ...

  3. Proto3使用指南

    这篇指南讲述如何使用Protocol Buffers来结构化你的Protocol Buffer数据,包括.proto文件语法以及如何从.proto文件生成你的访问类型.本文主要涵盖了proto3的语法 ...

  4. python Post 登录 cookies 和session

    def post_name(): print('\npost name') # http://pythonscraping.com/pages/files/form.html data = {'fir ...

  5. CSS 双飞翼布局

    10 Jul 2016 » CSS 双飞翼布局:总共分三栏,左侧栏Left,中间主栏Main,右侧栏Right 第一步,建立三个div,不过注意,中间Main需要加一个wrap div. 整个结构看起 ...

  6. JavaScript 执行环境以及作用域链

    执行环境(execution context,为简单起见,有时也称为"环境")是 JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们 ...

  7. HTTP协议 有这篇文章足够了

    HTTP 协议详解 HTTP(HyperText Transfer Protocol)超文本传输协议.其最初的设计目的是为了提供一种发布和接收HTML页面的方法. HTTP是一个客户端(用户)和服务端 ...

  8. Python第一周基本语句学习整理

    目录 Python第一周基本语句学习整理 一.字符串切割 二.体重单位转换 三.计算器的实现 四.猜年龄游戏 Python第一周基本语句学习整理 一.字符串切割 代码实现: s = 'luowenxi ...

  9. UVA - 10462 Is There A Second Way Left?

    题意: 给你一张无向图,让你判断三种情况:1.不是连通图(无法形成生成树)2.只能生成唯一的生成树 3.能生成的生成树不唯一(有次小生成树),这种情况要求出次小生成树的边权值和. 思路: 比较常见的次 ...

  10. mongoose .find().limit()返回undefined

    当我们使用mongoose的find方法查询数据库的同时,可以使用limit方法来指定返回数据的条数. limit方法基本语法如下所示: >db.COLLECTION_NAME.find().l ...