首页 > 编程语言 >java高并发系列

java高并发系列

时间:2023-11-01 09:23:16浏览次数:31  
标签:java 中断 InterruptedException t1 并发 线程 内存 系列 方法

同步(Synchronous)和异步(Asynchronous)

同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。

如图:

 

上图中显示了同步方法调用和异步方法调用的区别。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

 

并发(Concurrency)和并行(Parallelism)

并发和并行是两个非常容易被混淆的概念。他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”,下图很好地诠释了这点。

 

死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁饥饿活锁都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程就不再活跃,也就是说它可能很难再继续往下执行了。

死锁应该是最糟糕的一种情况了(当然,其他几种情况也好不到哪里去),如下图显示了一个死锁的发生:

 

A、B、C、D四辆小车都在这种情况下都无法继续行驶了。他们彼此之间相互占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状况将永远持续下去,谁都不可能通过,死锁是一个很严重的并且应该避免和实时小心的问题,后面的文章中会做更详细的讨论。

饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。在自然界中,母鸡给雏鸟喂食很容易出现这种情况:由于雏鸟很多,食物有限,雏鸟之间的事务竞争可能非常厉害,经常抢不到事务的雏鸟有可能被饿死。线程的饥饿非常类似这种情况。此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。于死锁想必,饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。

活锁是一种非常有趣的情况。不知道大家是否遇到过这么一种场景,当你要做电梯下楼时,电梯到了,门开了,这是你正准备出去。但很不巧的是,门外一个人当着你的去路,他想进来。于是,你很礼貌地靠左走,礼让对方。同时,对方也非常礼貌的靠右走,希望礼让你。结果,你们俩就又撞上了。于是乎,你们都意识到了问题,希望尽快避让对方,你立即向右边走,同时,他立即向左边走。结果,又撞上了!不过介于人类的智慧,我相信这个动作重复两三次后,你应该可以顺利解决这个问题。因为这个时候,大家都会本能地对视,进行交流,保证这种情况不再发生。但如果这种情况发生在两个线程之间可能就不那么幸运了。如果线程智力不够。且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么久会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

 

由此可见,为了提高系统的速度,仅增加CPU处理的数量并不一定能起到有效的作用。需要从根本上修改程序的串行行为,提高系统内可并行化的模块比重,在此基础上,合理增加并行处理器数量,才能以最小的投入,得到最大的加速比。

注意:根据Amdahl定律,使用多核CPU对系统进行优化,优化的效果取决于CPU的数量,以及系统中串行化程序的比例。CPU数量越多,串行化比例越低,则优化效果越好。仅提高CPU数量而不降低程序的串行化比例,也无法提高系统的性能。

从阿姆达尔定律可以看出,程序的可并行化部分可以通过使用更多的硬件(更多的线程或CPU)运行更快。对于不可并行化的部分,只能通过优化代码来达到提速的目的。因此,你可以通过优化不可并行化部分来提高你的程序的运行速度和并行能力。你可以对不可并行化在算法上做一点改动,如果有可能,你也可以把一些移到可并行化放的部分。

 

JMM(java内存模型),由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序中数据访问一致性安全性将会受到严重挑战。如何保证一个线程可以看到正确的数据呢?这个问题看起来很白痴。对于串行程序来说,根本就是小菜一碟,如果你读取一个变量,这个变量的值是1,那么你读取到的一定是1,就是这么简单的问题在并行程序中居然变得复杂起来。事实上,如果不加控制地任由线程胡乱并行,即使原本是1的数值,你也可能读到2。因此我们需要在深入了解并行机制的前提下,再定义一种规则,保证多个线程间可以有小弟,正确地协同工作。而JMM也就是为此而生的。

 

可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。有些同学会说修改同一个变量,那肯定是可以看到的,难道线程眼盲了?

为什么会出现这种问题呢?

看一下java线程内存模型:

  • 我们定义的所有变量都储存在 主内存

  • 每个线程都有自己 独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

  • 线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写(不能越级)

  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。(同级不能相互访问)

线程需要修改一个共享变量X,需要先把X从主内存复制一份到线程的工作内存,在自己的工作内存中修改完毕之后,再从工作内存中回写到主内存。如果线程对变量的操作没有刷写回主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

共享变量可见性的实现原理:

线程A对共享变量的修改要被线程B及时看到的话,需要进过以下步骤:

1.线程A在自己的工作内存中修改变量之后,需要将变量的值刷新到主内存中 2.线程B要把主内存中变量的值更新到工作内存中

关于线程可见性的控制,可以使用volatilesynchronized来实现,后面章节会有详细介绍。

 

 

有序性

有序性指的是程序按照代码的先后顺序执行。

为了性能优化,编译器和处理器会进行指令冲排序,有时候会改变程序语句的先后顺序,比如程序。

  1. int a = 1; //1

  2. int b = 20; //2

  3. int c = a + b; //3

编译器优化后可能变成

  1. int b = 20; //1

  2. int a = 1; //2

  3. int c = a + b; //3

上面这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。

在单例模式的实现上有一种双重检验锁定的方式,代码如下:

  1. public class Singleton {

  2. static Singleton instance;

  3. static Singleton getInstance(){

  4. if (instance == null) {

  5. synchronized(Singleton.class) {

  6. if (instance == null)

  7. instance = new Singleton();

  8. }

  9. }

  10. return instance;

  11. }

  12. }

我们先看 instance=newSingleton();

未被编译器优化的操作:

  1. 指令1:分配一款内存M

  2. 指令2:在内存M上初始化Singleton对象

  3. 指令3:将M的地址赋值给instance变量

编译器优化后的操作指令:

  1. 指令1:分配一块内存S

  2. 指令2:将M的地址赋值给instance变量

  3. 指令3:在内存M上初始化Singleton对象

现在有2个线程,刚好执行的代码被编译器优化过,过程如下:

 

最终线程B获取的instance是没有初始化的,此时去使用instance可能会产生一些意想不到的错误。

现在比较好的做法就是采用静态内部内的方式实现:

  1. public class SingletonDemo {
    
    private SingletonDemo() {
    
    }
    
    private static class SingletonDemoHandler{
    
    private static SingletonDemo instance = new SingletonDemo();
    
    }
    
    public static SingletonDemo getInstance() {
    
    return SingletonDemoHandler.instance;
    
    }
    
    }
    

      

如果一个线程调用了sleep方法,一直处于休眠状态,通过变量控制,还可以中断线程么?大家可以思考一下。

此时只能使用线程提供的interrupt方法来中断线程了。

  1. public static void main(String[] args) throws InterruptedException {

  2. Thread thread1 = new Thread() {

  3. @Override

  4. public void run() {

  5. while (true) {

  6. //休眠100秒

  7. try {

  8. TimeUnit.SECONDS.sleep(100);

  9. } catch (InterruptedException e) {

  10. e.printStackTrace();

  11. }

  12. System.out.println("我要退出了!");

  13. break;

  14. }

  15. }

  16. };

  17. thread1.setName("thread1");

  18. thread1.start();

  19. TimeUnit.SECONDS.sleep(1);

  20. thread1.interrupt();

  21. }

调用interrupt()方法之后,线程的sleep方法将会抛出 InterruptedException异常。

  1. Thread thread1 = new Thread() {

  2. @Override

  3. public void run() {

  4. while (true) {

  5. //休眠100秒

  6. try {

  7. TimeUnit.SECONDS.sleep(100);

  8. } catch (InterruptedException e) {

  9. e.printStackTrace();

  10. }

  11. if (this.isInterrupted()) {

  12. System.out.println("我要退出了!");

  13. break;

  14. }

  15. }

  16. }

  17. };

运行上面的代码,发现程序无法终止。为什么?

代码需要改为:

  1. Thread thread1 = new Thread() {

  2. @Override

  3. public void run() {

  4. while (true) {

  5. //休眠100秒

  6. try {

  7. TimeUnit.SECONDS.sleep(100);

  8. } catch (InterruptedException e) {

  9. this.interrupt();

  10. e.printStackTrace();

  11. }

  12. if (this.isInterrupted()) {

  13. System.out.println("我要退出了!");

  14. break;

  15. }

  16. }

  17. }

  18. };

上面代码可以终止。

注意:sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true

 

T2调用notify方法之后,T1并不能立即继续执行,而是要等待T2释放objec投递锁之后,T1重新成功获取锁后,才能继续执行。因此最后2行日志相差了2秒(因为T2调用notify方法后休眠了2秒)。

 

总结

  1. 创建线程的2中方式:继承Thread类;实现Runnable接口

  2. 启动线程:调用线程的start()方法

  3. 终止线程:调用线程的stop()方法,方法已过时,建议不要使用

  4. 线程中断相关的方法:调用线程实例interrupt()方法将中断标志置为true;使用线程实例方法isInterrupted()获取中断标志;调用Thread的静态方法interrupted()获取线程是否被中断,此方法调用之后会清除中断标志(将中断标志置为false了)

  5. wait、notify、notifyAll方法,这块比较难理解,可以回过头去再理理

  6. 线程挂起使用线程实例方法suspend(),恢复线程使用线程实例方法resume(),这2个方法都过时了,不建议使用

  7. 等待线程结束:调用线程实例方法join()

  8. 出让cpu资源:调用线程静态方法yeild()

 

线程t1的run()方法中有个循环,通过flag来控制循环是否结束,主线程中休眠了1秒,将flag置为false,按说此时线程t1会检测到flag为false,打印“线程t1停止了”,为何和我们期望的结果不一样呢?运行上面的代码我们可以判断,t1中看到的flag一直为true,主线程将flag置为false之后,t1线程中并没有看到,所以一直死循环。

那么t1中为什么看不到被主线程修改之后的flag?

要解释这个,我们需要先了解一下java内存模型(JMM),Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

 

从上图中可以看出,线程A需要和线程B通信,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去

  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量

下面通过示意图来说明这两个步骤:

 

 

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

对JMM了解之后,我们再看看文章开头的问题,线程t1中为何看不到被主线程修改为false的flag的值,有两种可能:

  1. 主线程修改了flag之后,未将其刷新到主内存,所以t1看不到

  2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中获取flag最新的值

对于上面2种情况,有没有什么办法可以解决?

是否有这样的方法:线程中修改了工作内存中的副本之后,立即将其刷新到主内存;工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。

java帮我们提供了这样的方法,使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存

  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

 

总结

  1. java中的线程分为用户线程守护线程

  2. 程序中的所有的用户线程结束之后,不管守护线程处于什么状态,java虚拟机都会自动退出

  3. 调用线程的实例方法setDaemon()来设置线程是否是守护线程

  4. setDaemon()方法必须在线程的start()方法之前调用,在后面调用会报异常,并且不起效

  5. 线程的daemon默认值和其父线程一样

 

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:

  1. 一是存在共享数据(也称临界资源)

  2. 二是存在多条线程共同操作共享数据

因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点确实也是很重要的。

 

synchronized主要有3种使用方式

  1. 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁

  2. 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁

  3. 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

 

synchronize作用于实例方法需要注意:

  1. 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的

  2. 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法

 

程序可以正常结束了,分析一下上面代码,注意几点:

  1. main方法中调用了t.interrupt()方法,此时线程t内部的中断标志会置为true

  2. 然后会触发run()方法内部的InterruptedException异常,所以运行结果中有异常输出,上面说了,当触发InterruptedException异常时候,线程内部的中断标志又会被清除(变为false),所以在catch中又调用了this.interrupt();一次,将中断标志置为false

  3. run()方法中通过this.isInterrupted()来获取线程的中断标志,退出循环(break)

 

总结

  1. 当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,可以使用 Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)

  2. 内部有循环体,可以通过一个变量来作为一个信号控制线程是否中断,注意变量需要volatile修饰

  3. 文中的几种方式可以结合起来灵活使用控制线程的中断

 

看一下jdk中ReentrantLock的源码,2个构造方法:

  1. public ReentrantLock() {

  2. sync = new NonfairSync();

  3. }

  4.  

  5. public ReentrantLock(boolean fair) {

  6. sync = fair ? new FairSync() : new NonfairSync();

  7. }

默认构造方法创建的是非公平锁。

第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。

 

Condition常用方法

Condition接口提供的常用方法有:

和Object中wait类似的方法

  1. void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;

  2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时

  3. boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位,false:表示方法超时之后自动返回的,true:表示等待还未超时时,await方法就返回了(超时之前,被其他线程唤醒了)

  4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

  5. void awaitUninterruptibly();:当前线程进入等待状态,不会响应线程中断操作,只能通过唤醒的方式让线程继续

和Object的notify/notifyAll类似的方法

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。

  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

 

Condition.await()过程中被打断

  1. package com.itsoku.chat09;

  2.  

  3. import java.util.concurrent.TimeUnit;

  4. import java.util.concurrent.locks.Condition;

  5. import java.util.concurrent.locks.ReentrantLock;

  6.  

  7. /**

  8. * 微信公众号:路人甲Java,专注于java技术分享(带你玩转 爬虫、分布式事务、异步消息服务、任务调度、分库分表、大数据等),喜欢请关注!

  9. */

  10. public class Demo4 {

  11. static ReentrantLock lock = new ReentrantLock();

  12. static Condition condition = lock.newCondition();

  13.  

  14. public static class T1 extends Thread {

  15. @Override

  16. public void run() {

  17. lock.lock();

  18. try {

  19. condition.await();

  20. } catch (InterruptedException e) {

  21. System.out.println("中断标志:" + this.isInterrupted());

  22. e.printStackTrace();

  23. } finally {

  24. lock.unlock();

  25. }

  26. }

  27. }

  28.  

  29. public static void main(String[] args) throws InterruptedException {

  30. T1 t1 = new T1();

  31. t1.setName("t1");

  32. t1.start();

  33. TimeUnit.SECONDS.sleep(2);

  34. //给t1线程发送中断信号

  35. System.out.println("1、t1中断标志:" + t1.isInterrupted());

  36. t1.interrupt();

  37. System.out.println("2、t1中断标志:" + t1.isInterrupted());

  38. }

  39. }

输出:

  1. 1、t1中断标志:false

  2. 2、t1中断标志:true

  3. 中断标志:false

  4. java.lang.InterruptedException

  5. at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)

  6. at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)

  7. at com.itsoku.chat09.Demo4$T1.run(Demo4.java:19)

调用condition.await()之后,线程进入阻塞中,调用t1.interrupt(),给t1线程发送中断信号,await()方法内部会检测到线程中断信号,然后触发 InterruptedException异常,线程中断标志被清除。从输出结果中可以看出,线程t1中断标志的变换过程:false->true->false

 

 

LockSupport.park方法让线程等待之后,唤醒方式有2种:

  1. 调用LockSupport.unpark方法

  2. 调用等待线程的 interrupt()方法,给等待的线程发送中断信号,可以唤醒线程

 

 代码中,线程t1和t2的不同点是,t2中调用park方法传入了一个BlockerDemo对象,从上面的线程堆栈信息中,发现t2线程的堆栈信息中多了一行 -parking to waitfor<0x00000007180bfeb0>(a com.itsoku.chat10.Demo10$BlockerDemo),刚好是传入的BlockerDemo对象,park传入的这个参数可以让我们在线程堆栈信息中方便排查问题,其他暂无他用。

 

 

线程等待和唤醒的3种方式做个对比

到目前为止,已经说了3种让线程等待和唤醒的方法了

  1. 方式1:Object中的wait、notify、notifyAll方法

  2. 方式2:juc中Condition接口提供的await、signal、signalAll方法

  3. 方式3:juc中的LockSupport提供的park、unpark方法

3种方式对比:

 

 ObjectCondtionLockSupport
前置条件 需要在synchronized中运行 需要先获取Lock的锁
无限等待 支持 支持 支持
超时等待 支持 支持 支持
等待到将来某个时间返回 不支持 支持 支持
等待状态中释放锁 会释放 会释放 不会释放
唤醒方法先于等待方法执行,能否唤醒线程 可以
是否能响应线程中断
线程中断是否会清除中断标志
是否支持等待状态中不响应中断 不支持 支持 不支持

 

 

Semaphore

Semaphore(信号量)为多线程协作提供了更为强大的控制方法,前面的文章中我们学了synchronized和重入锁ReentrantLock,这2种锁一次都只能允许一个线程访问一个资源,而信号量可以控制有多少个线程可以访问特定的资源。

Semaphore主要方法

Semaphore(int permits):构造方法,参数表示许可证数量,用来创建信号量

Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量

void acquire() throws InterruptedException:从此信号量获取1个许可前线程将一直阻塞,相当于一辆车占了一个车位,此方法会响应线程中断,表示调用线程的interrupt方法,会使该方法抛出InterruptedException异常

void acquire(int permits) throws InterruptedException :和acquire()方法类似,参数表示需要获取许可的数量;比如一个大卡车要入停车场,由于车比较大,需要申请3个车位才可以停放

void acquireUninterruptibly(int permits) :和acquire(int permits) 方法类似,只是不会响应线程中断

boolean tryAcquire():尝试获取1个许可,不管是否能够获取成功,都立即返回,true表示获取成功,false表示获取失败

boolean tryAcquire(int permits):和tryAcquire(),表示尝试获取permits个许可

boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException:尝试在指定的时间内获取1个许可,获取成功返回true,指定的时间过后还是无法获取许可,返回false

boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException:和tryAcquire(long timeout, TimeUnit unit)类似,多了一个permits参数,表示尝试获取permits个许可

void release():释放一个许可,将其返回给信号量,相当于车从停车场出去时将钥匙归还给门卫

void release(int n):释放n个许可

int availablePermits():当前可用的许可数

   

程序中信号量许可数量为1,创建了3个线程获取许可,线程t1获取成功了,然后休眠100秒。其他两个线程阻塞在 semaphore.acquire();方法处,代码中对线程t2、t3发送中断信号,我们看一下Semaphore中acquire的源码:

  1. public void acquire() throws InterruptedException

这个方法会响应线程中断,主线程中对t2、t3发送中断信号之后, acquire()方法会触发 InterruptedException异常,t2、t3最终没有获取到许可,但是他们都执行了finally中的释放许可的操作,最后导致许可数量变为了2,导致许可数量增加了。所以程序中释放许可的方式有问题。需要改进一下,获取许可成功才去释放锁。

 

Semaphore内部2个方法可以提供超时获取许可的功能:

  1. public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException

  2. public boolean tryAcquire(int permits, long timeout, TimeUnit unit)

  3. throws InterruptedException

在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。

 

其他一些使用说明

  1. Semaphore默认创建的是非公平的信号量,什么意思呢?这个涉及到公平与非公平。举个例子:5个车位,允许5个车辆进去,来了100辆车,只能进去5辆,其他95在外面排队等着。里面刚好出来了1辆,此时刚好又来了10辆车,这10辆车是直接插队到其他95辆前面去,还是到95辆后面去排队呢?让新来的去排队就表示公平,直接去插队争抢第一个,就表示不公平。对于停车场,排队肯定更好一些。不过对于信号量来说不公平的效率更高一些,所以默认是不公平的。

  2. 建议阅读以下Semaphore的源码,对常用的方法有个了解,不需要都记住,用的时候也方便查询就好。

  3. 方法中带有 throwsInterruptedException声明的,表示这个方法会响应线程中断信号,什么意思?表示调用线程的 interrupt()方法后,会让这些方法触发 InterruptedException异常,即使这些方法处于阻塞状态,也会立即返回,并抛出 InterruptedException异常,线程中断信号也会被清除。

                                                   

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

标签:java,中断,InterruptedException,t1,并发,线程,内存,系列,方法
From: https://www.cnblogs.com/grhuang/p/17783273.html

相关文章

  • mac 切换java jdk版本 java8 java11
    1.  终端执行命令查看本地各版本jdk:mac通常默认安装了jdk1.8安装目录是/Library/Java/JavaVirtualMachines/cd/Library/Java/JavaVirtualMachines/ls2.上述命令列出的各版本目录名后,在全局配置文件.bash_profile中新增上面命令列出的各版本jdk,并指定当前环境变量默认......
  • linuxjava安装
    linux安装java1.8一、确认是否安装JDK,通过rpm-qa|grepjava命令查看然后https://www.oracle.com/java/technologies/downloads/#java8下载java二.进入user/local这个目录是管理员安装引用程序的目录三.进行文件上传然后把文件拖进新开的窗口里就ok了三.通过tar-......
  • java如何不创建对象就可以使用静态方法(类方法)System类等
    对象初始化一定有类加载,但是类加载不一定会对象初始化,静态方法不需要创建对象就能调用,这是因为静态方法属于类而不是对象。在Java中,静态方法是属于类的方法,而不是属于特定的对象实例的方法。当类加载到内存中时,静态方法也会加载到内存中。因此,可以直接通过类名来调用静态方法,而不......
  • java 基本数据类型和引用数据类型
    ......
  • Exception in thread "main" java.lang.NoSuchMethodError: org.springframework.util
    我的项目是springboot架构,项目启动报错如下Exceptioninthread"main"java.lang.NoSuchMethodError:org.springframework.util.Assert.isInstanceOf(Ljava/lang/Class;Ljava/lang/Object;Ljava/util/function/Supplier;)V atorg.springframework.boot.logging.logback.Logb......
  • 没闲着系列 05
    昨晚一直在下载Llama发行版ChatGLM,下到50%应该是FQ流量没有了,先放一放了。本来的打算是将issue、task的问题推给ChatGLM进行解答,看来还是先执行爬虫任务了。这会儿想画个图表示,从task、issue的问题去网上,尤其可能是stackoverflow、某dn,还有cnb去抓取相同问题。不加上这个,对git......
  • Java基础面试题收集(1)
    @目录1.一个“.Java"源文件中是否可以包括多个类(不是内部类)?有什么限制?2.Java有没有goto?3.&于&&的区别4.在Java中如何跳出当前的多重嵌套循环?5.Switch语句能否用在byte,long,String上?6.shorts1=1;s1=s1+1;有什么错?shorts1=1;s1+=1;有什么错?7.char型变量中能不能存储一个......
  • java日常记录1
    java学到框架了,发现javaSE的东西忘记的差不多了,故此挂在这里流个记录,方便将来查看,顺便巩固一下学过的知识点,文章可能有点长,我打算利用文章和代码结合的方式记录一下。顺便本文档的参考文献主要来自秦老师,当然还有自己从书本上摘抄的一下东西。java基础java自1991年,由sun公司的工......
  • java日常记录3--方法
    今天,我们来瞧瞧方法:那么什么是方法呢?Java方法是语句的集合,它们在一起执行一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法在程序中被创建,在其他地方被引用例如:System.out.println();println()是一个方法。System是系统类。out是标准输出对象。方法的......
  • 做物联网的有福了,一个开源的、企业级的物联网平台,它集成了设备管理、数据安全通信、消
     去关注、不迷路一、项目概述JetLinksIOT是一个开源的、企业级的物联网平台,它集成了设备管理、数据安全通信、消息订阅、规则引擎等一系列物联网核心能力,支持以平台适配设备的方式连接海量设备,采集设备数据上云,提供云端API,通过调用云端API实现远程控制。JetLinks物联网平台......