详解Java同步—线程锁和条件对象
作者:Kepler 发布时间:2023-06-01 03:28:46
线程锁和条件对象
在大多数多线程应用中,都是两个及以上线程需要共享对同一数据的存取,所以有可能出现两个线程同时访问同一个资源的情况,这种情况叫做:竞争条件。
在Java中为了解决并发的数据访问问题,一般使用锁这个概念来解决。
有几种机制防止代码收到并发访问的干扰:
1.synchronized关键字(自动创建一个锁及相关的条件)
2.ReentrantLock类+Java.util.concurrent包中的lock接口(在Java5.0的时候引入)
ReentrantLock的使用
public void Method() {
boolean flag = false;//标识条件
ReentrantLock locker = new ReentrantLock();
locker.lock();//开启线程锁
try {
//do some work...
} catch (Exception ex) {
} finally {
locker.unlock();//解锁线程
}
}
locker.lock();确保只有一个线程进入临界区,一旦一个线程进入之后,会获得锁对象,其他线程无法通过lock语句。当其他线程调用lock时,它们会被阻塞,知道第一个线程释放锁对象。
locker.unlock();解锁操作,一定要放到finally里,因为如果try语句里出了问题,锁必须被释放,否则其他线程将永远被阻塞
因为系统会随机为线程分配资源,所以在线程获得锁对象之后,可能被系统剥夺运行权,这时候其他线程来访问,但是发现有锁,进不去,只能等拿到锁对象的线程把里面的代码执行完毕后,释放锁,第二个线程才能运行。
假设说做一个银行转账的功能,线程锁操作应该定义在银行类的转账方法里,因为这样每个银行对象都有一个锁对象,两个线程访问一个银行对象的时候,那么锁以串行方式提供服务。但是,如果每个线程访问不同的银行对象,每个线程都会得到不同的锁对象,彼此之间不会冲突,所以就不会造成不必要的线程阻塞。
锁是可重入的,线程可以重复获得已经持有的锁,锁通过一个持有数量计数来跟踪对lock方法的嵌套使用。
假设说,一个线程获得锁之后,要执行A方法,但是A方法里面又调用了B方法,这时候这个线程获得了两个锁对象,当线程执行B方法的时候,也会被锁死,防止其他线程乱入,当B方法执行完毕后,锁对象变成了一个,当A方法也执行完毕的时候,锁对象变成了0个,线程释放锁。
synchronized关键字
前面我们讲了ReentrantLock锁对象的使用,但是在系统里面我们不一定要使用ReentrantLock锁,Java中还提供了一个内部的隐式锁,关键字是synchronized.
举个例子:
public synchronized void Method() {
//do some work...
}
只需要在返回值前面加上synchronized锁,就会实现上面ReentrantLock锁同样的效果.
Conditional条件对象
通常,线程拿到锁对象之后,却发现需要满足某一条件才能继续向下执行。
拿银行程序来举例子,我们需要转账方账户有足够的资金才能转出到目标账户,这时候需要用到ReentrantLock对象,因为如果我们已经完成转账方账户有足够的资金的判断之后,线程被其他线程中断,等其他线程执行完之后,转账方的钱又没有了足够的资金,这时候因为系统已经完成了判断,所以会继续向下执行,然后银行系统就会出现问题。
举例:
public void Transfer(int from, int to, double amount) {
if (Accounts[from] > amount)//系统在结束判断之后被剥夺运行权,然后账户通过网银转出所有钱,银行凉凉
DoTransfer(from, to, amount);
}
这时候我们就需要使用ReentrantLock对象了,我们修改一下代码:
public void Transfer(int from, int to, double amount) {
ReentrantLock locker = new ReentrantLock();
locker.lock();
try {
while (Accounts[from] < amount) {
//等待有足够的钱
}
DoTransfer(from, to, amount);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
}
但是这样又有了问题,当前线程获取了锁对象之后,开始执行代码,发现钱不够,进入等待状态,然后其他线程又因为锁的原因无法给该账户转账,就会一直进入等待状态。
这个问题如何解决呢?
条件对象登场!
public void Transfer(int from, int to, double amount) {
ReentrantLock locker = new ReentrantLock();
Condition sufficientFunds = locker.newCondition();//条件对象,
lock.lock();
try {
while (Accounts[from] < amount) {
sufficientFunds.await();
//等待有足够的钱
}
DoTransfer(from, to, amount);
sufficientFunds.signalAll();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
}
条件对象的关键字是:Condition,一个锁对象可以有一个或多个相关的条件对象。可以通过锁对象.newCondition方法获得一个条件对象.
一般关于条件对象的命名需要能够反映它表达的条件的名字,所以在这里我们叫他sufficientFund,表示余额充足的意思。
在进入锁之前,我们创建一个条件,然后如果金额不足,在这里调用条件对象的await方法,通知系统当前线程进入挂起状态,让其他线程执行。这样你这次调用会被锁定,然后系统可以再次调用该方法给其他账户转账,当每一次转账完成后,执行转账操作的线程在底部调用signalAll通知所有线程可以继续运行了,因为我们有可能是转足够的钱给当前账户,这时候有可能该线程会继续执行(不一定是你,是通知所有线程,如果通知的线程还是不符合条件,会继续调用await方法,并完成转账操作,然后通知其他挂起的线程。
你说为啥不直接通知当前线程?不行,可以调用signal方法只通知一个线程,但是如果这个线程操作的账户还是没钱(不是转账给这个账户的情况),那这个线程又进入等待了,这时候已经没有线程能通知其他线程了,程序死锁,所以还是用signal比较保险。
以上是使用ReentrantLock+Condition对象,那你说我要是使用synchronized隐式锁怎么办?
也可以,而且不需要
public void Transfer(int from, int to, double amount) {
while (Accounts[from] < amount) {
wait();//这个wait方法是定义在Object类里面的,可以直接用,和条件对象的await一样,挂起线程
//等待有足够的钱
}
DoTransfer(from, to, amount);
notifyAll();//通知其他挂起的线程
}
Object类里面定义了wait、notifyAll、notify方法,对应await、signalAll和signal方法,用来操作隐式锁,synchronized只能有一个条件,而ReentrantLock显式声明的锁可以用绑定多个Condition条件.
同步块
除了我们上面讲的两种获取线程锁的方式,还有另外一种机制获得锁,这种方式比较特殊,叫做同步块:
Object locker = new Object();
synchronized (locker) {
//do some work
}
//也可以直接锁当前类的对象
sychronized(this){
//do some work
}
以上代码会获得Object类型locker对象的锁,这种锁是一个特殊的锁,在上面的代码中,创建这个Object类对象只是单纯用来使用其持有的锁.
这种机制叫做同步块,应用场景也很广:有的时候,我们并不是整个一个方法都需要同步,只是方法里的部分代码块需要同步,这种情况下,我们如果将这个方法声明为synchronized,尤其是方法很大的时候,会造成很大的资源浪费。所以在这种情况下我们可以使用synchronized关键字来声明同步块:
public void Method() {
//do some work without synchronized
synchronized (this) {
//do some synchronized operation
}
}
监视器的概念
锁和条件是同步中一个很重要的工具,但是它们并不是面向对象的。多年来,Java的研究人员努力寻找一种方法,可以在不需要考虑如何加锁的情况下,就能保证多线程的安全性。最成功的的一个解决方案叫做monitor监视器,这个对象内置于每一个Object变量中,相当于一个许可证。拿到许可证就可以进行操作,没有拿到则需要阻塞等待。
监视器具有以下特性:
1.监视器是只包含私有域的类
2.每个监视器对象都有一个相关的锁
3.使用监视器对象的锁对所有的方法进行加锁(举个例子:如果调用obj.Method方法,obj对象的锁会在方法调用的时候自动获得,当方法结束或返回之后会自动释放该锁。因为所有的域都是私有的,这样可以确保一个线程在操作类对象的时候,没有其他线程可以访问里面的域)
4.该锁对象可以有任意多个相关条件
你也可以自己创建一个监视器类,只要符合以上的要求即可。
其实我们使用的synchronized关键字就是使用了monitor来实现加锁解锁,所以又被称为内部锁。因为Object类实现了监视器,所以对象又被内置于任何一个对象之中。这就是我们为什么可以使用synchronized(locker)的方式锁定一个代码块了,其实只是用到了locker对象中内置的monitor而已。每一个对象的monitor类又是唯一的,所以就是唯一的许可证,拿到许可证的线程才可以执行,执行完后释放对象的monitor才可以被其他线程获取。
举个例子:
synchronized (this) {
//do some synchronized operation
}
它在字节码文件中会被编译为:
monitorenter;//get monitor,enter the synchronized block
//do some synchronized operation
monitorexit;//leavel the synchronized block,release the monitor
死锁
虽然有了线程可以保证原子性,但是锁和条件不能解决多线程中的所有问题,举个例子:
账户1余额:200
账户2余额:300
线程1:账户1→账户2(300)
线程2:账户2→账户1(400)
因为线程1和线程2的金额都不足以进行转账,所以两个线程都阻塞了,这种状态就叫死锁(deadlock),如果所有线程死锁,程序就卡死了。
为什么倾向于使用signalAll和notifyAll方式,如果假设使用signal和notify,
锁测试和超时
线程在调用lock方法获得另一个线程持有的锁的时候,很可能发生阻塞。应该更加谨慎的申请锁,tryLock方法试图申请一个锁,如果申请成功,返回true,否则,立刻返回false,线程就会离开去做别的事,而不是被阻塞等待锁对象。
语法:
ReentrantLock locker = new ReentrantLock();
if (locker.tryLock()) {
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
} else {
//do other work
}
也可以给其指定超时参数,单位有SECONDS、MILLISECONDS、MICROSEONDS和MANOSECONDS.
ReentrantLock locker = new ReentrantLock();
if (locker.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
locker.unlock();
}
} else {
//do other work
}
lock方法不能被中断,如果一个线程在调用了lock方法后等待锁的时候被中断,中断线程在获得锁之前一直处于阻塞状态。
如果带有超时参数的tryLock方法,那么如果等待期间线程被中断,会抛出InterruptedException异常,这是一个很好的特性,允许程序打破死锁。
读/写锁
ReentrantLock类属于java.util.concurrent.locks包,这个包底下还有一个ReentrantReaderWriterLock类,如果使用多线程对数据读的操作很多,但是写的操作很少的话,可以使用这个类。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():
public void Read() {
Lock readLocker = rwl.readLock();//创建读取锁对象
readLocker.lock();//使用读取锁对象加锁
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
readLocker.unlock();
}
}
public void Write() {
Lock writeLocker = rwl.writeLock();//创建写入锁对象
writeLocker.lock();//使用写入锁对象加锁
try {
//do some work
} catch (Exception ex) {
ex.printStackTrace();
} finally {
writeLocker.unlock();
}
}
来源:https://www.cnblogs.com/Fill/p/9379776.html


猜你喜欢
- 简介在文章《GraalVM和Spring Native尝鲜,一步步让Springboot启动飞起来,66ms完成启动》中,我们介绍了如何使用
- 以前只知道@在C#中为了写文件路径的\不要加转义符而在前面加上@标识符,没想到@还有其他的作用1.忽略转义字符例如string fileNa
- 话不多说,下面来直接看示例代码具体代码:DayOfWeek4Birthday.javapackage com.gua;import java
- 本文中使用maven+eclipse搭建activiti-5.14的开发环境一、创建maven工程创建一个普通的java工程,pom文件的内
- 最近的需求有一个自动发布的功能, 需要做到每次提交都要动态的添加一个定时任务代码结构1. 配置类package com.orion.ops.
- 首次使用idea需要配置哪些东西?最近因为我的eclipse无法配置sts,于是将战场转移至idea,首次使用idea,所有的配置都得重新开
- 本文实例为大家分享了Android实现象棋游戏的具体代码,供大家参考,具体内容如下主要是实现两人对战象棋,没有实现人机对战,主要不会判断下一
- 本文实例为大家分享了java实现扫雷游戏入门程序的具体代码,供大家参考,具体内容如下分析:1.首先布一个10*10的雷阵,即二维数组map,
- 这篇文章主要介绍了Java模拟多线程实现抢票,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考
- 今天继续讲解Fragment组件的特性,主要是跟Activity的交互和生命周期的关系,我们前面已经说过Fragment是依赖于Activi
- 本教程适合新手小白,Java7之前的版本是没有内置JavaFx的,Java7-10是内置JavaFx的,但是到了Java10以后的版本,Or
- 一、使用线程的理由1、可以使用线程将代码同其他代码隔离,提高应用程序的可靠性。2、可以使用线程来简化编码。3、可以使用线程来实现并发执行。二
- 前言进入到 SpringBoot2.7 时代,有小伙伴发现有一个常用的类忽然过期了:在 Spring Security 时代,这个类可太重要
- 测试1@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECOND
- 基类:using System;using System.Collections.Generic;using System.Linq;usi
- 本文实例讲述了Android播放assets文件里视频文件相关问题。分享给大家供大家参考,具体如下:今天做了一个功能,就是播放项目工程里面的
- 前言首先,啊,先简单介绍一下优先队列的概念,学数据结构以及出入算法竞赛的相信都对队列这一数据结构十分熟悉,这是一个线性的数据结构.针对队列这
- 首先的效果图搜索到结果(这里我只是模拟数据,真正和服务器走得时候,返回来的数据都应该包含关键字的)模拟的没有搜索结果的界面具体实现在这插一句
- 前言今天,和大家分享一个开源的多功能视频播放器 — GSYVideoPlayer,支持弹幕,滤镜、水印、gif截图,片头
- 本文实例讲述了Android仿英语流利说取词放大控件的实现方法。分享给大家供大家参考,具体如下:1 取词放大控件英语流利说是一款非常帮的口语