百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术流 > 正文

带你深度剖析多线程的同步控制(多线程同步机制有哪些)

citgpt 2024-08-01 13:31 8 浏览 0 评论

多线程的团队协作:同步控制

同步控制是并发程序必不可少的重要手段。之前介绍的关键字synchronized就是一种最简单的控制方法,它决定了一个线程是否可以访问临界区资源。同时,Object.wait()方法和Object.notify)方法起到了线程等待和通知的作用。这些工具对于实现复杂的多线程协作起到了重要的作用。下面我们首先将介绍关键字synchronized、Object.wait()方法和Object.notify()方法的替代品(或者说是增强版)——重入锁。

一、关键字synchronized的功能扩展:重入锁

  1. 重入锁可以完全替代关键字synchronized。在JDK
    5.0的早期版本中,重入锁的性能远远优于关键字synchronized,但从JDK 6.0开始,JDK在关键字synchronized上做了大量的优化,使得两者的性能差距并不大。
  2. 重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。下面是一段最简单的重入锁使用案例。
public class ReenterLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
@override
public void run(){
for(int j=0;j<10000000;j++){
lock.lock();
try{
i++;}finally{
lock.unlock();
  }
 }
}
public static void main (String[] args) throws 
InterruptedException {
ReenterLock tl=new ReenterLock();
Thread t1=new Thread (tl);
Thread t2=new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}}

  1. 上述代码第7~12行使用重入锁保护临界区资源i,确保多线程对i操作的安全性。从这段代码可以看到,与关键字synchronized相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远优于关键字synchronized。但值得注意的是,在退出临界区时,必须记得释放锁(代码第11行),否则,其他线程就没有机会再访问临界区了。
  2. 你可能会对重入锁的名字感到奇怪。锁为什么要加上“重入”两个字呢?从类的命名上看,Re-
    Entrant-Lock翻译成重入锁非常贴切。之所以这么叫,是因为这种锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程。上述代码的第7~12行,可以写成下面的形式:
lock.lock();lock.lock();try{
i++;}finally{
lock. unlock();lock.unlock();
}
  1. 在这种情况下,一个线程连续两次获得同一把锁是允许的。如果不允许这么操作,那么同一个线程在第2次获得锁时,将会和自己产生死锁。程序就会“卡死”在第2次申请锁的过程中。但需要注意的是,如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。如果释放锁的次数多了,那么会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。
  2. 除使用上的灵活性以外,重入锁还提供了一些高级功能。比如,重入锁可以提供中断处理的能力。

一、中断响应

  1. 对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。有些时候,这么做是非常有必要的。比如,你和朋友约好一起去打球,如果你等了半个小时朋友还没有到,你突然接到一个电话,说由于突发情况,朋友不能如约前来了,那么你一定扫兴地打道回府了。中断正是提供了一套类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须等待,可以停止工作了。这种情况对于处理死锁是有一定帮助的。
  2. 下面的代码产生了一个死锁,但得益于锁中断,我们可以很轻易地解决这个死锁。
public class IntLock implements Runnable{
public static ReentrantLock lockl = new ReentrantLock();public static ReentrantLock lock2 = new ReentrantLock();int lock;
/**
★控制加锁顺序,方便构造死锁*param lock
*/
public IntLock(int lock){
this.lock = lock;
}
@override
public void run(){
try {
if (lock == 1){
lock1. lockInterruptibly();try{
Thread.sleep(500);
}catch (InterruptedException e){1lock2. lockInterruptiblyO);
}else{
lock2 . lockInterruptibly();try{
Thread.sleep (500);
}catch (InterruptedException e){}lock1. lockInterruptiblyO);
] catch (InterruptedException e){
e.printStackTrace(;
]finally{
if(lock1.isHeldByCurrentThread ())
lockl.unlock ();
if (lock2.isHeldByCurrentThread()
lock2.unlock(;
System.out.println (Thread.currentThread ().getId()+":线程退出");
public static void main(String[] args) throws InterruptedException{
IntLock r1 = new IntLock (1);
IntLock r2= new IntLock(2);Thread tl = new Thread (r1) ;Thread t2 = new Thread(r2);t1.start();t2.start();
Thread.sleep(1000);//中断其中一个线程
t2.interrupt ();
}}

  1. 线程t1和
    t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1.因此,很容易形成t1和t2之间的相互等待。在这里,对锁的请求,统一使用lockInterruptibly(方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
  2. 在代码第47行,主线程main
    处于休眠状态,此时,这两个线程处于死锁的状态。在代码第49行,由于t2线程被中断,故t2会放弃对lock1的申请,同时释放已获得的lock2。这个操作导致t1线程可以顺利得到lock2而继续执行下去。
  3. 执行上述代码,将输出:
java.lang. InterruptedException
at java.util.concurrent. locks.AbstractQueuedSynchronizer.doAcquireInterruptibly (AbstractQueuedSynchronizer.java:898)
at java.util.concurrent. locks. AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent. locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at geym.conc.ch3.synctrl.IntLock. run (IntLock.java: 31)at java.lang. Thread.run(Thread.java:745)
9:线程退出
8:线程退出

  1. 可以看到,中断后两个线程双双退出,但真正完成工作的只有tl,而t2线程则放弃其任务直接退出,释放资源。

二、锁申请等待限时

  1. 除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。依然以约朋友打球为例,如果朋友迟迟不来,又无法联系到他,那么在等待1到2个小时后,我想大部分人都会扫兴离去。对线程来说也是这样。通常,我们无法判断为什么一个线程迟迟拿不到锁。也许是因为死锁了,也许是因为产生了饥饿。如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。我们可以使用tryLock()方法进行一次限时的等待。
  2. 下面这段代码展示了限时等待锁的使用。
public class TimeLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();@Override
public void run(){
try{
if(lock.tryLock (5,TimeUnit.SECONDS))
Thread.sleep(6000);
}else{
System.out.println ("get lock failed");
}catch (InterruptedException e){
e.printstackTrace();
}finally{if (lock.isHeldByCurrentThread0) lock.unlock();}
public static void main(String[] args){
TimeLock tl=new TimeLock();
Thread tl=new Thread(tl);Thread t2=new Thread(tl);t1.start();
t2.start();
}}
  1. 在这里,tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长为5,表示线程在这个锁请求中最多等待5秒。如果超过5秒还没有得到锁,就会返回false。如果成功获得锁,则返回true。
  2. 在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒的等待时间内获得锁,因此请求锁会失败。
  3. ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。下面演示了这种使用方式:
public class TryLock implements Runnable{
public static ReentrantLock lock1 - new ReentrantLock (;public static ReentrantLock lock2 = new ReentrantLock(;int lock;
public TeyLock (int lock){
this.lock = lock;
@override
public void run ( {
if(1ock -- 1){
while (true) [
if(lockl.tryLock0{
tryf
try {
Thread.sleep (500);
}catch (InterruptedException e)[)
if (lock2.tryLock()){
try{
System.out.println/Thread.currentThread()
.getId( +":Ny Job done"];
return;
}finally{
lock2.unlock() ;
}}}
finally {
lock1.unlock();
}
} else{
while (true) {
if(lock2.tryLock()){
try {
try {
Thread.sleep(500);
] catch (InterruptedException e){
if( lock1.tryLock(){
try {
System.out.println (Thread.currentThread()
. getId()+":My Job done");
return;
} finally {
lock1.unlock();
}
]finally-{
lock2.unlock();
}
}
}
}
}

public static void main(String[] args) throws InterruptedException {
TryLock r1 = new TryLock(1);
TryLock r2= new Trylock(2);Thread tl = new Thread(r1);Thread t2 = new Thread(r2);t1.start();
t2.start();
}
}
  1. 上述代码采用了非常容易死锁的加锁顺序。也就是先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,t2申请lock1。在一般情况下,这会导致tl和t2相互等待,从而引起死锁。
  2. 但是使用tryLock()方法后,这种情况就大大改善了。由于线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行(这里以线程同时获得lock1和 lock2两把锁,作为其可以正常执行的条件)。在同时获得 lock1和 lock2后,线程就打印出标志着任务完成的信息“My Jobdone”。
  3. 执行上述代码,等待一会儿(由于线程中包含休眠500
    毫秒的代码)。最终你还是可以欣喜地看到程序执行完毕,并产生如下输出,表示两个线程双双正常执行。

三、公平锁

  • 在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程⒉也请求了锁A。那么当锁A可用时,是线程1可以获得锁还是线程2可以获得锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个。因此不能保证其公平性。这就好比买票不排队,大家都围在售票窗口前,售票员忙得焦头烂额,也顾不及谁先谁后,随便找个人出票就完事了。而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它的构造函数如下:
  • 当参数fair为
    true时,表示锁是公平的。公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能却非常低下,因此,在默认情况下,锁是非公平的。如果没有特别的需求,则不需要使用公平锁。公平锁和非公平锁在线程调度表现上也是非常不一样的。下面的代码可以很好地突出公平锁的特点:
    在这里插入图片描述


    上述代码第﹖行指定锁是公平的。接着,由t1和t2两个线程分别请求这把锁,并且在得到锁后,进行一个控制台的输出,表示自己得到了锁。在公平锁的情况下,得到的输出通常如下所示:

    由于代码会产生大量输出,这里只截取部分进行说明。在这个输出中,很明显可以看到,两个线程基本上是交替获得锁的,几乎不会发生同一个线程连续多次获得锁的可能,从而保证了公平性。如果不使用公平锁,那么情况会完全不一样,下面是使用非公平锁时的部分输出:


    可以看到,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。对上面ReentrantLock 的几个重要方法整理如下。
  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,则返回true,失败返回false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。unlock():释放锁。

就重入锁的实现来看,它主要集中在Java层面。在重入锁的实现中,主要包含三个要素。

带你深度剖析多线程的同步控制(多线程同步机制有哪些)

  1. 第一,原子状态。原子状态使用CAS操作(在第4章进行详细讨论)来存储当前锁的状态,判断锁是否已经被别的线程持有了。
  2. 第二,等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
  3. 第三,阻塞原语park()和 unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。有关park)和unpark()的详细介绍,可以参考第3.1.7节线程阻塞工具类:LockSupport。

如果感觉小编写得不错,请素质三连:点赞+转发+关注。我会努力写出更好的作品分享给大家。更多JAVA进阶学习资料小编已打包好,可以关注私信找我领取哦


相关推荐

js中arguments详解

一、简介了解arguments这个对象之前先来认识一下javascript的一些功能:其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数...

firewall-cmd 常用命令

目录firewalldzone说明firewallzone内容说明firewall-cmd常用参数firewall-cmd常用命令常用命令 回到顶部firewalldzone...

epel-release 是什么

EPEL-release(ExtraPackagesforEnterpriseLinux)是一个软件仓库,它为企业级Linux发行版(如CentOS、RHEL等)提供额外的软件包。以下是关于E...

FullGC详解  什么是 JVM 的 GC
FullGC详解 什么是 JVM 的 GC

前言:背景:一、什么是JVM的GC?JVM(JavaVirtualMachine)。JVM是Java程序的虚拟机,是一种实现Java语言的解...

2024-10-26 08:50 citgpt

使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
跨域(CrossOrigin)

1.介绍  1)跨域问题:跨域问题是在网络中,当一个网络的运行脚本(通常时JavaScript)试图访问另一个网络的资源时,如果这两个网络的端口、协议和域名不一致时就会出现跨域问题。    通俗讲...

微服务架构和分布式架构的区别

1、含义不同微服务架构:微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并...

深入理解与应用CSS clip-path 属性
深入理解与应用CSS clip-path 属性

clip-pathclip-path是什么clip-path 是一个CSS属性,允许开发者创建一个剪切区域,从而决定元素的哪些部分可见,哪些部分会被隐...

2024-10-25 11:51 citgpt

HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
Request.ServerVariables 大全

Request.ServerVariables("Url")返回服务器地址Request.ServerVariables("Path_Info")客户端提供的路...

python操作Kafka

目录一、python操作kafka1.python使用kafka生产者2.python使用kafka消费者3.使用docker中的kafka二、python操作kafka细...

Runtime.getRuntime().exec详解

Runtime.getRuntime().exec详解概述Runtime.getRuntime().exec用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。...

promise.all详解 promise.all是干什么的
promise.all详解 promise.all是干什么的

promise.all详解promise.all中所有的请求成功了,走.then(),在.then()中能得到一个数组,数组中是每个请求resolve抛出的结果...

2024-10-24 16:21 citgpt

Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解

取消回复欢迎 发表评论: