一,线程的生命周期
线程五大生命周期:
- 新建:就是创建一个线程,如:new Thread()
- 就绪:进入就绪等待状态,如:start()执行就进入就绪状态
- 运行:当处理器分配时间碎片给当前线程,该线程就执行任务
- 堵塞:堵塞指当前线程不执行,也没有死亡,处于一个休眠状态,进行激活或者休眠时间到期才继续执行
- 死亡:线程消亡
① 新建
- new关键字创建了一个线程之后,该线程就处于新建状态
- JVM为线程分配内存,初始化成员变量值
② 就绪
- 当线程对象调用了start()方法之后,该线程处于就绪状态
- JVM为线程创建方法栈和程序计算器,等待线程调度器调度
③ 运行
- 就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
④ 堵塞
在那种情况下,线程将会进入堵塞:
- 线程调用sleep()方法主动放弃cpu资源进入休眠
- 线程调用了一个堵塞式IO方法(如new Scanner(System.in) 输入),在该方法返回之前,该线程被堵塞
- 线程试图获得一个同步锁(synchronized),但该锁被其他线程持有,需要等待释放
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将线程挂起,但该方法容易导致死锁,已被弃用
⑤ 死亡
线程以如下三种方式结束就处于死亡状态:
- run()或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的exeption或error
- 调用该线程stop()方法来结束该线程,该方法容易导致死锁,已被弃用
二,线程安全问题
一,线程安全问题的概述和演示
啥是线程安全问题:如果有多个线程同时运行同一个实现了Runnable接口的类,程序每一次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的,反之则是线程不安全的
一,线程安全问题演示(抢票)
线程安全问题演示,抢票
① runnable实现
public class ticket implements Runnable{ //抢票数 private int num; //剩余票数 private int count=50; @Override public void run() { while(true) { if(count<=0) { break; } num++; count--; try { //模拟网络延迟 Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("用户"+Thread.currentThread().getName()+":抢到第"+num+"张票,剩余"+count+"张票!"); } } }
② 用户抢票
public class ticket_main { public static void main(String[] args) { ticket ticket = new ticket(); //三个用户抢票 Thread wql = new Thread(ticket,"wql"); Thread fq = new Thread(ticket,"fq"); Thread lj = new Thread(ticket,"lj"); wql.start(); fq.start(); lj.start(); } }
③ 结果(出现线程安全问题)
出现安全问题产生的结果:
- 相同的票被卖出多次
- 卖出不存在的票,比如0票
二,线程的安全问题的分析
线程安全问题都是由全局变量及其静态变量等共享变量数据引起的,假如每一个线程对全局变量,静态变量只读,不写一般来说是没有线程安全问题的,若有多个线程同步执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全
线程安全问题的根本原因(重点):
- 多个线程操作共享的数据
- 操作共享数据的线程代码有多条
- 多个线程对共享的数据都具有写操作
通过线程安全问题产生的原因可以避免这个问题
二,解决线程安全问题
要解决线程安全问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源完成对应的操作,这样保证了数据的同步性,解决了线程不安全的现象
为了保证每一个线程都能正常执行共享资源操作,java引入了7种线程同步机制
7种线程同步机制:
- 同步方法(synchronized)
- 同步代码块(synchronized)
- 同步锁(ReenreantLock)
- 特殊变量(volatile)
- 局部变量(ThreadLocal)
- 堵塞队列(LinkedBlockingQueue)
- 原子变量(Atomic)
一,synchronized同步代码块
同步代码块:synchronized关键字可以用于方法中的某一个区域中,表示只对这个区域的资源实行互斥访问
格式:synchronized(同步锁){需要同步操作的代码}
同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
- 锁对象可以是容易类型
- 多个线程要使用同一把锁
注:在任何时候最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等待锁被释放(BLOCKED)
① 在原基础上加上同步代码块
public class ticket implements Runnable{ //抢票数 private int num; //剩余票数 private int count=50; @Override public void run() { while(true) { //同步代码块 synchronized(this) { if(count<=0) { break; } num++; count--; try { //模拟网络延迟 Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("用户"+Thread.currentThread().getName()+":抢到第"+num+"张票,剩余"+count+"张票!"); } } } }
② 测试结果
二,synchronized同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等待
格式:public synchronized void method(){可能会产生线程安全问题的代码}
synchronized同步代码块和同步方法使用同步锁是有区别的:
- 同步代码块:同步锁可以自己指定,可以为this当前对象,也可以为new Object等
- 同步方法:同步锁不能自定义,但有两种情况(static和非static)
同步锁是谁:
- 对于非static方法,同步锁就是this
- 对于static方法,同步锁就是当前方法所在类的字节码对象(类名.class)
三,同步锁(RennreantLock)
同步锁使用的是第三方开发的完善java线程安全机制的包(java.util.concurrent),它比synchronized更好
这里使用同步锁只是这个包下的一种锁(重入锁RennreantLock),concurrent包提供并发编程的绝大多数解决方案,详细可以去学习
同步锁:java.util.concurrent.locks.Lock机制提供的比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的概念Lock全都有,此外后者的功能更强大
同步锁方法:
- lock():加同步锁
- unlock():释放同步锁
① 加lock锁
public class ticket implements Runnable{ //抢票数 private int num; //剩余票数 private int count=50; //同步锁 Lock lock = new ReentrantLock(true);//true表示为公平锁(多个线程有公平的执行权) @Override public void run() { while(true) { lock.lock(); try { if(count<=0) { break; } num++; count--; try { //模拟网络延迟 Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("用户"+Thread.currentThread().getName()+":抢到第"+num+"张票,剩余"+count+"张票!"); }finally{ lock.unlock(); }} }}
② 测试
四,synchronized和lock的区别
- synchronized是java内置关键字在jvm层面,lock是个java类
- synchronized无法判断是否获取锁的状态,Lock可以获取到锁并得到状态
- synchronized会自动释放锁,Lock锁需要在finally中手动释放锁,否则会造成死锁
- 有synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2等待,如果线程1堵塞,线程2也会堵塞;而lock锁可以重入不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束
- synchronized的锁可以重入,不可判断,非公平;而lock锁可重入,可判断,可公平(两者均可)
- lock锁适合大量并发的同步问题,synchronized锁使用代码少量并发
三,线程死锁
一,死锁的概念和产生的条件
一,死锁的概念
多线程以及多线程改善了系统资源的利用率并提供了系统的处理能力,然而并发执行也带来了新的问题--死锁
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程将永远处于死循环中
二,死锁产生的条件
死锁的产生有四个必要条件,只要发生系统死锁,这些条件必然成立,而只要上述条件之一不满足就不会发生死
四个必要条件:
- 互斥条件(或排它条件)
- 不可剥夺条件
- 占有并等待条件
- 循环等待条
1,互斥条件:
进程要求对所分配的资源进行排他控制,即在一段时间内资源仅为一个进程占用,此时若有其他进程请求该资源,则请求进程只能等待
2,不可剥夺条件:
进程所获得的资源在未使用完毕之前,不能被其他线程强行剥夺,即只能由获取该资源的线程自己进行释放(只能是主动释放)
3,占用并等待条件:
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时线程进入堵塞状态,但对自己已获得的资源保持不释放
4,循环等待条件:
存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个进程所请求,即存在一个处于等待状态的进程集合{P1,P2,…Pn},其中P1等待的资源被P(i+1)占用,Pn等待的资源被P0占用,如图
图1就出现死锁 图2有pk请求就不会存在死锁但也出现了循环
二,死锁的演示
演示步骤:
- 有两个对象锁obj1、obj2
- 两个线程wql、fq
- wql的执行顺序是obj1-->obj2, fq的执行顺序是obj2-->obj1
- 出现状态 wql持有 obj1 锁,等待 obj2 锁; fq 持有 obj2 锁,等待 obj1 锁
- 出现死锁
①
public class killlock_1 implements Runnable{ Object obj1; Object obj2; public killlock_1(Object obj1, Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj2) try { System.out.println(Thread.currentThread().getName()+": "+"获取了资源obj2"); Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } synchronized(obj1) { System.out.println(Thread.currentThread().getName()+": "+"获取了资源obj2和obj1"); } } }
②
public class killlock_2 implements Runnable{ Object obj1; Object obj2; public killlock_2(Object obj1, Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj1) { try { System.out.println(Thread.currentThread().getName()+": "+"获取了资源obj1"); Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } synchronized(obj2) { System.out.println(Thread.currentThread().getName()+": "+"获取了资源obj1和obj2"); } } }
③
public class ticket_main { public static void main(String[] args) { Object obj1 = new Object(); Object obj2 = new Object(); killlock_1 kill1 = new killlock_1(obj1, obj2); killlock_2 kill2 = new killlock_2(obj1, obj2); Thread wql = new Thread(kill1,"wql"); Thread fq = new Thread(kill2,"fq"); wql.start(); fq.start(); } }
三,死锁的处理
死锁的处理包括多方面:
- 预防死锁:提供设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生
- 避免死锁:在资源的动态分配过程中,用某些方法去防止系统进入不安全状态,从而避免死锁的发生
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测,当死锁被检测后采取适当措施加以清除
- 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来
一,死锁的预防
预防死锁至少破坏产生死锁的四个必要条件之一,严格的避免死锁的产生
1,破坏互斥条件(不可取)
互斥条件是无法破坏的,因此在死锁的预防里主要是破坏其他几个必要条件
2,破坏占用并等待条件
破坏占用并等条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他其他资源,即要阻止进程在持有资源的同时申请其他资源
- 方法一:一次性分配资源,即创建进程时,要求它申请所需要的全部资源,系统或满足其所有要求,或什么也不分配
- 方法二:要求每一个进程提出新的资源申请前,释放它所占用的资源,这样,一个进程在需要资源时,需要先把它占用的资源释放掉,然后才能提出资源申请,即使它可能很快又要用到资源
3,破坏不可抢占条件
破坏不可抢占条件就是允许对资源的实行抢夺
- 方法一:如果占有某些资源的一个进程进行进一步的资源请求被拒绝,则该进程必须释放它最初占用的资源,如果有必要,可再次请求这些资源和另外的资源
- 方法二:如果一个进程请求当前被另一个进程占用的一个占用,则操作系统可以抢占另一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,才能预防死锁
4,破坏循环等待条件
破坏循环等待条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须安装资源的编号顺序提出,这样做就能保证系统不出现死锁
二,死锁的避免
避免死锁这种方式不严格限制死锁的必要条件存在,因为即使死锁的必要条件存在,也不一定发生死锁
死锁的避免有四种方式:
- 有序资源分配法
- 银行家算法(经典)
- 顺序加锁
- 限时加锁
一,有序资源分配法
算法实现的步骤:
- 必须为所有资源统一编号,例如:磁盘为1,打印机为2,传真机为3
- 同类资源必须一次申请完,例如:打印机和传真机一般为同一个机器,必须同时申请
- 不同类资源必须按顺序申请
例如:有两个进程P1和P2,有两个资源R1和R2
P1请求资源:R1,R2
P2请求资源:R1,R2
这样的顺序申请就避免了环路条件,避免了死锁的产生
二,银行家算法
银行家算法是一个避免死锁的著名算法,是由艾兹个.迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法,它以银行贷款系统的分配策略为基础,判断并保证系统的安全运行,流程图如下
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法
设进程i提出请求RRQUEST [i],则银行家算法按如下规则进行判断:
- 如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错
- 如果REQUBST [i]<= AVAILABLE[i],则转(3);否则,等待
- 系统试探分配资源,修改相关数据WAILABLE [1]--REQUEST [i]: //可用资源数-请求资源数ALLOCATION [ ]+-REQUEST[i]: //已分配资源数+请求资源数NED [ ]-REDQUEST [1] ;//需要资源数-请求资源数
- 系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待
三,顺序加锁
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生
例如:以下两个线程就会死锁
①Thread 1
lock A (when C locked) lock B (when C locked) wait for C
②Thread 2
wait for A wait for B lock C (when A locked)
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
例如:
lock A lock B lock C
②Thread 2
wait for A wait for B wait for C
按照顺序加锁是一种有效的死锁预防机制,但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,且有序后并发量也变小,只适合于特定场景
四,限时加锁
限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退释放所有已经获得的锁,然后等待一段随机时间重试
这种方式的缺点:
- 当线程数量少时,该种方式可避免死锁,当线程数量过多,这些线程的加锁时间相同的概念很高,可能导致超时后重试的死循环
- java中不能对synchronized同步块设置超时时间,需要创建一个自定义锁,或所有java.util.concurrent并发包
三,死锁的检测
预防和避免死锁系统的开销大且并不能充分的利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复
死锁检测数据结构:
检测的步骤:
- 寻找一个结束标记的线程P1,对于它而言R矩阵的第1行向量小于等于A
- 如果找到了这样的一个线程,执行该线程,然后将C矩阵的第i行向量加到A中,标记该线程,并转到第1步
- 如果没有这样的线程,那么算法终止
- 算法结束时,所有没有标记过的线程都是死锁线程
四,死锁的恢复
① 利用抢占恢复
临时将某个资源从它的当前所属进程移动到另一个进程,这种做法很可能需要人工干预,主要做法是否可行取决于资源本身的特性
② 利用回滚恢复
周期性的将进程的状态进行备份,当发现进程进入死锁后,根据备份将进程复位到一个更早的还没有取得所需资源的状态,接着把这些资源分配到其他死锁进程
③ 通过杀死进程恢复
最直接简单的方式是杀死一个或若干个进程
四,线程通信
需要线程通信的原因:多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按照我们的规律执行线程,此时就需要线程之间协调通信
线程的通信方式:有四种
- 休眠唤醒方式:Object的wait,notify,notifyAll,Condition的await,signal,signalAll
- CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行,例:上课场景,老师需要等待全部同学都在时才上课
- CyclicBarrier:一组线程等待至某个状态之后再全部同时执行,例:跑步比赛,需要等待所有运动员都准备好才开始
- Semaphore:用于控制对某组资源的访问权限
注:休眠唤醒方式,java有两套实现,一个是原生java提供的Object,一个是java.util.concurrent中的Condition提供的
一,休眠唤醒方式
一,Object休眠唤醒
方法:
- wait():休眠状态
- notify():唤醒
- notify():唤醒所有
注:object的休眠和唤醒方法在使用时都需要加synchronized锁
例:从0开始,当是奇数时,奇数线程打印,偶数线程等待,当是偶数时,偶数线程打印,奇数线程等待
① 奇偶打印方法
public class object_comm { //要打印的数 int num=1; //锁对象 private Object obj = new Object(); //打印奇数方法 public void odd() { synchronized(obj) { while(num<=10) { if(num%2==1) { System.out.println("奇数:"+num); num++; obj.notify(); }else { try { obj.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }} }}} //打印偶数 public void even() { synchronized(obj) { while(num<=10) { if(num%2==0) { System.out.println("偶数:"+num); num++; obj.notify(); }else { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
②测试
public class object_comm_main { public static void main(String[] args) { object_comm h = new object_comm(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { h.odd(); } }); Thread thread2 = new Thread(()->{ h.even(); }); thread1.start(); thread2.start(); } }
③ 结果
二,Condition休眠唤醒
方法:
- await():休眠
- signal():唤醒
- signalall():唤醒全部
例:从0开始,当是奇数时,奇数线程打印,偶数线程等待,当是偶数时,偶数线程打印,奇数线程等待
① 奇偶打印方法
public class condition_comm { //要打印的数 int num=1; //lock锁对象 Lock locks = new ReentrantLock(); //condition对象 Condition condition = locks.newCondition(); //打印奇数方法 public void odd() { locks.lock(); try { while(num<=10) { if(num%2==1) { System.out.println("奇数:"+num); num++; condition.signal(); }else { try { condition.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }} }}finally { locks.unlock(); } } //打印偶数 public void even() { locks.lock(); try { while(num<=10) { if(num%2==0) { System.out.println("偶数:"+num); num++; condition.signal(); }else { try { condition.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }finally { locks.unlock(); } } }
②测试
public class condition_main { public static void main(String[] args) { object_comm h = new object_comm(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { h.odd(); } }); Thread thread2 = new Thread(()->{ h.even(); }); thread1.start(); thread2.start(); } }
③ 结果
三,object和condition休眠唤醒的区别
- object wait()必须在synchronized(同步锁)下使用
- object wait()必须提供notify()方法唤醒
- condition await()必须和lock(互斥锁/同步锁)配合使用
- condition await()必须通过signal()方法进行唤醒
二,CountDownLatch方式
- CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下
- CountDownLatch能够使一个线程等待其他线程完成各自的工作后再执行
- CountDownLatch是通过一个计数器来实现的,计算器的初始值为线程的数量
每当一个线程完成了自己的任务后,计算器的值就会减少,当计算器值到达0时,它表示所有的线程已经完成了任务,然后再闭锁上等待的线程就可以恢复执行任务
例:老师上课,老师需要等待所有同学都到才开始上课
①
public class countdownlatch_comm { //创建CountDownLatch对象并设置等待的线程数,为4个学生 CountDownLatch countdownlatch = new CountDownLatch(4); public void ra() { //1,获取学生名称 String name = Thread.currentThread().getName(); //2,学生到达教室,准备上课 System.out.println(name+"到达教室准备上课!"); //3,准备时间 try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //4,准备完毕,countdownlatch减一 System.out.println(name+"到达教室准备完毕!"); countdownlatch.countDown(); } public void rc() { //1,获取老师线程名称 String name = Thread.currentThread().getName(); //2,老师等待所有学生完毕 System.out.println(name+"等待所有学生准备完毕!"); //3,调用CountDownlatch的await方法等待其他线程完毕 try { countdownlatch.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //4,学生全部准备完毕开始上课 System.out.println(name+"老师上课!"); } }
② 测试
public class countdownlatch_main { public static void main(String[] args) { countdownlatch_comm countdown = new countdownlatch_comm(); //学生线程 Thread thread1 = new Thread(()->{countdown.ra();},"fq"); Thread thread2 = new Thread(()->{countdown.ra();},"wql"); Thread thread3 = new Thread(()->{countdown.ra();},"luoqin"); Thread thread4 = new Thread(()->{countdown.ra();},"lzj"); //老师线程 Thread thread5 = new Thread(()->{countdown.rc();},"---"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); } }
③ 结果
三,CyclicBarrier方式
- CyclicBarrier是在java1.5被引入的,存在于java.util.concurrent包中
- CycliBarrier实现让一组线程等待至某一个状态之后再全部同时执行
- CycliBarrier底层是基于ReentrantLock和Condition实现
例:现在有一场奔跑比赛,只有等三个运动员全部准备完毕才进行开始
①CycliBarrier
public class cyclibarrier_comm { //创建CyclicBarrier指定为3个线程,只有三个线程都到位,才会执行最终结果 CyclicBarrier cycli = new CyclicBarrier(3); public void start() { String name = Thread.currentThread().getName(); System.out.println(name+"正在准备!"); try { //判断三个线程是否都到位,否则休眠 cycli.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (BrokenBarrierException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("准备完毕!"); } }
②测试
public class cyclibarrier_main { public static void main(String[] args) { cyclibarrier_comm test = new cyclibarrier_comm(); Thread thread1 = new Thread(()->{test.start();}); Thread thread2 = new Thread(()->{test.start();}); Thread thread3 = new Thread(()->{test.start();}); thread1.start(); thread2.start(); thread3.start(); } }
③结果
四,Semaphore方式
Semaphore和CountDownLatch一样都是在java1.5被引入的,存在于java.util.concurrent包下
semaphore用于控制对某组资源的访问权限
重要方法:
- acquire():获取资源
- release():释放资源
例:现在有一组机器总共有3个,每一次操作机器只能一个人工
①Semaphore
public class semaphore_comm { //工人数 int worker=10; //机器数通过Semaphore指定 Semaphore sem = new Semaphore(3); public void use() { try { //1,工人获取机器 sem.acquire(); //2,个人获取到机器开始工作 String name = Thread.currentThread().getName(); System.out.println(name+"获取到机器开始工作"); //3,设置工作时间,模拟工人工作 Thread.sleep(100); //4,工作完毕释放机器 System.out.println(name+"工作完成!"); sem.release(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
②测试
public static void main(String[] args) { semaphore_comm sem = new semaphore_comm(); //模拟10个工人(10线程) for(int a=1;a<=10;a++) { new Thread(()->{sem.use();}).start(); } } }
③结果
Comments | NOTHING
Warning: Undefined variable $return_smiles in /www/wwwroot/wql_luoqin_ltd/wp-content/themes/Sakura/functions.php on line 1109