一、引言

  线程并发的过程中,肯定会设计到一个变量共享的概念,那么我们在多线程运行过程中,怎么保证每个先拿获取的变量信息都是最新且有序的呢?这一篇我们来专门学习一下Lock锁。

  我们先来了解几个概念:

乐观锁与悲观锁

悲观锁:

  假定会发生并发冲突,即共享资源会被某个线程更改。所以当某个线程获取共享资源时,会阻止别的线程获取共享资源。也称独占锁或者互斥锁,例如java中的synchronized同步锁。

乐观锁:

  假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。

PS:CAS相关知识戳这里~

公平锁与非公平锁

  • 公平锁的实现就是谁等待时间最长,谁就先获取锁
  • 非公平锁就是随机获取的过程,谁运气好,cpu时间片轮询到哪个线程,哪个线程就能获取锁

可重入锁与不可重入锁

不可重入锁

  若当前线程执行中已经获取了锁,如果再次获取该锁时,就会获取不到被阻塞。

可重入锁

  每一个锁关联一个线程持有者计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

二、Condition

  在使用Lock之前,我们使用的最多的同步方式应该是synchronized关键字来实现同步方式了。配合Object的wait()notify()系列方法可以实现线程的等待/通知模式

  PS:Condition的实质是通过控制线程的等待和唤醒来达到控制指定线程的功能。

  特点

  • 依赖于Lock对象,调用Lock对象的newCondition()对象创建而来
  • 可以实现等待/通知形式的线程交互模式
  • 可以有选择性的进行线程通知,唤醒指定线程

基本方法:

public interface Condition {
void await() throws InterruptedException;
boolean await(long var1, TimeUnit var3) throws InterruptedException;
long awaitNanos(long var1) throws InterruptedException;
void awaitUninterruptibly();
boolean awaitUntil(Date var1) throws InterruptedException;
void signal();
void signalAll();
}
  • await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  • await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断到达指定等待时间之前一直处于等待状态。
  • awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  • awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  • awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  • signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  • signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

使用举例:

/**
* Condition使用范例
*
* @author 有梦想的肥宅
*/
public class ConditionTest { //1、创建一个Lock对象,Condition的使用需要依赖Lock对象
public Lock lock = new ReentrantLock();
//2、使用Lock对象的newCondition()方法来生成Condition对象
public Condition condition = lock.newCondition(); //3、main方法测试Condition的作用
public static void main(String[] args) {
ConditionTest conditionTest = new ConditionTest();
//3.1、构造一个容量为2的固定线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//3.2、执行conditionWait()方法使线程进入“等待”状态
executorService.execute(new Runnable() {
@Override
public void run() {
conditionTest.conditionWait();
}
});
//3.3、conditionSignal()方法“唤醒”线程
executorService.execute(new Runnable() {
@Override
public void run() {
conditionTest.conditionSignal();
}
});
} /**
* 线程等待
*/
public void conditionWait() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "拿到锁了");
System.out.println(Thread.currentThread().getName() + "等待信号");
condition.await();//线程进入等待状态,不进入finally语句块进行锁的释放,要等待被唤醒
System.out.println(Thread.currentThread().getName() + "拿到信号");
} catch (Exception e) { } finally {
lock.unlock();
}
} /**
* 线程唤醒
*/
public void conditionSignal() {
lock.lock();
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "拿到锁了");
condition.signal();//唤醒线程
System.out.println(Thread.currentThread().getName() + "发出信号");
} catch (Exception e) { } finally {
lock.unlock();
}
} }

三、ReentrantLock可重入锁

  ReentrantLock:是一个可重入锁,且它可以设置自身非公平锁或者是公平锁

  常用方法:

  • ReentrantLock() : 创建一个ReentrantLock实例【默认非公平锁】
  • lock() : 获得锁
  • unlock() : 释放锁
/**
* ReentrantLock测试类
*
* @author 有梦想的肥宅
*/
public class ReentrantLockTest {
//全局对象lock【构造参数设置为true表示为公平锁,false或为空则默认是非公平锁】
private static Lock lock = new ReentrantLock(true); //线程方法
public static void test() {
for (int i = 0; i < 2; i++) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取了锁");
TimeUnit.MILLISECONDS.sleep(1000);//等待1秒,为了更直观地观察公平锁的机制
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} //运行方法
public static void main(String[] args) {
System.out.println("=====公平锁实例=====");
//启动一个名叫“线程A”的线程
new Thread("线程A") {
@Override
public void run() {
test();
}
}.start();
//启动一个名叫“线程B”的线程
new Thread("线程B") {
@Override
public void run() {
test();
}
}.start();
}
}

ReentrantLock与synchronized的比较

相似点

  它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,等到释放掉锁或者唤醒后才能继续获得锁。

区别

  1️⃣对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

  2️⃣便利性:Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

  3️⃣锁的细粒度和灵活度:ReenTrantLock优于Synchronized【可以指定在哪加锁和解锁】

四、ReentrantReadWriteLock可重入读写锁

  定义:ReentrantReadWriteLock是一种可重入读写锁,内部有两把锁来实现读和写的锁功能,在ReentrantLock的基础上优化了性能,但是使用起来需要更加谨慎。

  性质:

可重入

  如果你了解过synchronized关键字,一定知道他的可重入性,可重入就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。

读写分离

  我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。

锁可以降级

  线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。

锁不可升级

  线程获取读锁是不能直接升级为写入锁的。需要释放所有读取锁,才可获取写锁,我们理解了上面的概念之后,接下来我们看看如何去使用。

  使用示例:

/**
* ReentrantReadWriteLock测试类【可重入读写锁】
*
* @author 有梦想的肥宅
*/
public class ReentrantReadWriteLockTest {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);//全局可重入读写锁对象
private final Lock readLock = reentrantReadWriteLock.readLock();//读锁
private final Lock writeLock = reentrantReadWriteLock.writeLock();//写锁
private final List<String> data = new ArrayList<>();//模拟被操作的数据 /**
* 写数据的方法
* @Description 使用writeLock获取一把写锁,然后内部List写入数据,最后在finally中释放写锁。
*/
public void write() {
try {
//1、加上写锁
writeLock.lock();
//2、操作公共数据
data.add("写数据");
System.out.println("当前线程" + Thread.currentThread().getName() + "正在写数据");
//3、线程等待3秒
Thread.sleep(3000);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
//4、释放写锁
writeLock.unlock();
}
} /**
* 读数据的方法
* @Description 使用readLock获取一把读锁,然后内部List读取数据,最后再finally中释放读锁。
*/
public void read() {
try {
//1、加上写锁
writeLock.lock();
//2、读取公共数据
for (String str : data) {
System.out.println("当前线程" + Thread.currentThread().getName() + "正在读数据");
}
//3、线程等待3秒
Thread.sleep(3000);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
//4、释放读锁
readLock.unlock();
}
}
}

参考资料:

并发编程(八)Lock锁的更多相关文章

  1. [转载] java并发编程:Lock(线程锁)

    作者:海子 原文链接: http://www.cnblogs.com/dolphin0520/p/3923167.html 出处:http://www.cnblogs.com/dolphin0520/ ...

  2. 并发编程 17—— Lock

    Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...

  3. 【多线程】Java并发编程:Lock(转载)

    原文链接:http://www.cnblogs.com/dolphin0520/p/3923167.html Java并发编程:Lock 在上一篇文章中我们讲到了如何使用关键字synchronized ...

  4. Java并发编程:Lock(转)

    本文转自:http://www.cnblogs.com/dolphin0520/p/3923167.html Java并发编程:Lock 在上一篇文章中我们讲到了如何使用关键字synchronized ...

  5. 5、Java并发编程:Lock

    Java并发编程:Lock 在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.l ...

  6. Java并发编程:Concurrent锁机制解析

    Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...

  7. 【java并发编程】Lock & Condition 协调同步生产消费

    一.协调生产/消费的需求 本文内容主要想向大家介绍一下Lock结合Condition的使用方法,为了更好的理解Lock锁与Condition锁信号,我们来手写一个ArrayBlockingQueue. ...

  8. python 并发编程 多进程 互斥锁 目录

    python 并发编程 多进程 互斥锁 模拟抢票 互斥锁与join区别

  9. 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

    简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...

  10. Python之路(第三十八篇) 并发编程:进程同步锁/互斥锁、信号量、事件、队列、生产者消费者模型

    一.进程锁(同步锁/互斥锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理. 例 ...

随机推荐

  1. AWS 学习笔记之 VPC

    原文:https://ericfu.me/aws-notes-vpc/ VPC 把 VPC 想象成一个逻辑上的数据中心 包含一个 IGW (Internet Gateway)或者 Virtual Pr ...

  2. wsgi的environ变量

    The environ dictionary is required to contain these CGI environment variables, as defined by the Com ...

  3. You are using pip version 10.0.1, however version 20.2.2 is available.

    在安装第三方库时,出现如下提示: You are using pip version 10.0.1, however version 20.2.2 is available.You should co ...

  4. Vue 构造选项 - 进阶

    Directive指令:减少DOM操作的重复 Vue实例/组件用于数据绑定.事件监听.DOM更新 Vue指令主要目的就是原生DOM操作 减少重复 自定义指令 两种声明方式 方法一:声明一个全局指令 V ...

  5. Shell编程—结构化命令

    1使用if-then语句 f-then语句有如下格式. if command then commands fi bash shell的if语句会运行if后面的那个命令.如果该命令的退出状态码是0(该命 ...

  6. .NET - Task.Run vs Task.Factory.StartNew

    翻译自 Stephen Toub 2011年10月24日的博文<Task.Run vs Task.Factory.StartNew>,Stephen Toub 是微软并行计算平台团队的首席 ...

  7. C# Mongo DB 修改多层嵌套集合中的字段

    C# Mongo DB 修改嵌套集合中的字段 虽然c#的mongo 驱动很强大,而且还支持linq,但是一些复杂的操作语句还是比较困难 这里我用Bson实现功能 这是模型(我这里有多层嵌套) publ ...

  8. GitHub 热点速览 Vol.35:Let's Go,Rust 大放异彩

    摘要:语言之争,一直存在于各类社群,不论是单个编程语言的交流群,亦或是 NoSQL.云开发等技术群,总能看到"要不要换 Go"."Rust 比 C++ 更强"的 ...

  9. laravel核心Ioc容器

    laravel容器和依赖注入 啥是Ioc容器,方便我们实现依赖注入的一种实现,也就是说依赖注入不一定需要控制反转容器,只不过使用容器可能会方便些. laravel通过向容器中绑定接口的具体实现,可实现 ...

  10. 记录一道有意思的js题目

    偶然机会,在codewars上面开始做题,遇到一道有意思的题目,记录一下: 题目是这样的: In this kata, you will write a function that returns t ...