1、张孝祥_Java 多线程与并发库高级应用【视频介绍:】Java 线程是一项非常基本和重要的技术,在偏底层和偏技术的 Java 程序中不可避免地要使用到 Java 线程技术,特别是 android 手机程序和游戏开发中,多线程成了必不可少的一项重要技术。但是,很多 Java 程序员对 Java 线程技术的了解都仅停留在初级阶段,在项目中一旦涉及到多线程时往往就表现得糟糕至极,所以,软件公司常常使用 Java 线程技术来考察面试者的基本功和判断其编码水平的高低。本套视频教程是专门为了帮助那些已经学习和了解过、但掌握得并不是很深入的人们提高 java 线程技术而讲解的,所以,Java 线程初学者学习
2、本视频教程时可能会比较吃力,可能必须耐心学习多遍才能渐入佳境,但是,你一旦掌握了其中的内容,你对 Java 线程技术的了解将会相当出众!【视频目录列表:】01. 传统线程技术回顾02. 传统定时器技术回顾03. 传统线程互斥技术04. 传统线程同步通信技术05. 线程范围内共享变量的概念与作用06. ThreadLocal 类及应用技巧07. 多个线程之间共享数据的方式探讨08. java5 原子性操作类的应用09. java5 线程并发库的应用10. Callable 与 Future 的应用11. java5 的线程锁技术12. java5 读写锁技术的妙用13. java5 条件阻塞 C
3、ondition 的应用14. java5 的 Semaphere 同步工具15. java5 的 CyclicBarrier 同步工具16. java5 的 CountDownLatch 同步工具17. java5 的 Exchanger 同步工具18. java5 阻塞队列的应用19. java5 同步集合类的应用20. 空中网挑选实习生的面试题 1 21. 空中网挑选实习生的面试题 2 22. 空中网挑选实习生的面试题 3 23. 源代码与资料01. 传统线程技术回顾传统是相对于 JDK1.5 而言的传统线程技术与 JDK1.5 的线程并发库线程就是程序的一条执行线索/线路。创建线程的两
4、种传统方式1. 创建 Thread 的子类,覆盖其中的 run 方法,运行这个子类的 start 方法即可开启线程Thread thread = new Thread() Overridepublic void run()while (true)获取当前线程对象 获取线程名字Thread.currentThread() threadObj.getName()让线程暂停,休眠,此方法会抛出中断异常InterruptedExceptionThread.sleep(毫秒值);thread.start();2. 创建 Thread 时传递一个实现 Runnable 接口的对象实例Thread thre
5、ad = new Thread(new Runnable()public void run();thread.start();问题:下边的线程运行的是 Thread 子类中的方法还是实现 Runnable 接口类的方法new Thread(b、传递实现 Runnable 接口的对象new Runnable()public void run()a、覆盖 Thread 子类 run 方法public void run().start();分析:new Thread(Runnable.run()run().start();子类 run 方法实际就是覆盖父类中的 run 方法,如果覆盖了就用子类的 r
6、un 方法,不会再找 Runnable 中的 run 方法了,所以运行的是子类中的 run 方法总结:由 Thread 类中的 run 方法源代码中看出,两种传统创建线程的方式都是在调用 Thread对象的 run 方法,如果 Thread 对象的 run 方法没有被覆盖,并且像上边的问题那样为Thread 对象传递了一个 Runnable 对象,就会调用 Runnable 对象的 run 方法。多线程并不一定会提高程序的运行效率。举例:一个人同时在三张桌子做馒头多线程下载:并不是自己电脑快了,而是抢到更多服务器资源。例:服务器为一个客户分配一个 20K 的线程下载,你用多个线程,服务器以为是
7、多个用户就分配了多个 20K 的资源给你。02. 传统定时器技术回顾传统定时器的创建:直接使用定时器类 Timera、过多长时间后炸new Timer().schedule(TimerTask 定时任务, Date time 定的时间 );b、过多长时间后炸,以后每隔多少时间再炸new Timer().schedule(TimerTask 定时任务, Long 延迟(第一次执行)时间, Long 间隔时间);TimerTask 与 Runnable 类似,有一个 run 方法Timer 是定时器对象,到时间后会触发炸弹(TimerTask )对象示例:new Timer().schedule(
8、new TimerTask()定时执行的任务public void run()SOP(“bombing”);显示计时信息while (true)SOP(new Date().getSeconds();Thread.sleep(1000);,10 定好的延迟时间,10 秒以后执行任务);问题:2 秒后炸,爆炸后每隔 3 秒再炸一次定时器 2 秒后炸,炸弹里还有定时器(每 3 秒炸一次)class MyTimerTask extends TimerTask 这就是准备用的子母弹public void run()本身就是一颗炸弹SOP(bombing);内部子弹new Timer().schedul
9、e(new MyTimerTask(), 2000);放置子母弹,2 秒后引爆new Timer().schedule(new MyTimerTask(), 2000);问题延伸:上面的问题延伸,母弹炸过后,子弹每隔 3 秒炸一次,再每隔 8 秒炸一次1、在 MyTimerTask 内部定义一个静态变量记录炸弹号,在 run 方法内将炸弹号加 1,每次产生新炸弹,号码就会加 1,根据炸弹号判断是 3 秒炸还是 8 秒炸。注意:内部类中不能声明静态变量定义一个静态变量 private static count = 0;在 run 方法内部:count=(count+1)%2 ;将定时器的时间设置
10、为:2000+2000*count2、用两个炸弹来完成,A 炸弹炸完后启动定时器安装 B 炸弹,B 炸弹炸完后也启动一个定时器安装 A 炸弹。定时器还可以设置具体时间,如某年某月某日某时可以设置周一到周五做某事,自己设置的话需要换算日期时间,可以使用开源工具 quartz 来完成。03. 传统线程互斥技术线程安全问题例子:银行转账同一个账户一边进行出账操作(自己交学费) ,另一边进行入账操作(别人给自己付款),线程不同步带来的安全问题示例:逐个字符的方式打印字符串class Outputerpublic void output(String name)int len = name.length
11、();for (int i=0; i threadData = new HashMap();public static void main(String args) 创建两个线程for (int i=0; i threadLocal = new ThreadLocal();07. 多个线程之间共享数据的方式探讨例子:卖票:多个窗口同时卖这 100 张票,票就需要多个线程共享a、如果每个线程执行的代码相同,可以使用同一个 Runnable 对象,这个对象中有共享数据。卖票就可以这样做,每个窗口都在做卖票任务,卖的票都是同一个数据。b、如果每个线程执行的代码不同,就需要使用不同的 Runnable
12、 对象,有两种方式实现Runnable 对象之间的数据共享:a)将共享数据单独封装到一个对象中,同时在对象中提供操作这些共享数据的方法,可以方便实现对共享数据各项操作的互斥和通信。b)将各个 Runnable 对象作为某个类的内部类,共享数据作为外部类的成员变量,对共享数据的操作方法也在外部类中提供,以便实现互斥和通信,内部类的 Runnable 对象调用外部类中操作共享数据的方法即可。注意:要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。08. java5 原子性操作类的应用Java5 的线程并发库java.util.co
13、ncurrent 在并发编程中很常用的实用工具类。|-locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器|-atomic 类的小工具包,支持在单个变量上解除锁的线程安全编程。可以对基本类型、数组中的基本类型、类中的基本类型等进行操作|-AtomicInteger构造方法摘要AtomicInteger() 创建具有初始值 0 的新 AtomicInteger。AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger。方法摘要int addAndGet(int delta) 以原子方式将给定值与当前值相加。boole
14、an compareAndSet(int expect, int update) 如果当前值 = 预期值,则以原子方式将该值设置为给定的更新值。int decrementAndGet() 以原子方式将当前值减 1。double doubleValue() 以 double 形式返回指定的数值。float floatValue() 以 float 形式返回指定的数值。int get() 获取当前值。int getAndAdd(int delta) 以原子方式将给定值与当前值相加。int getAndDecrement() 以原子方式将当前值减 1。int getAndIncrement() 以原
15、子方式将当前值加 1。int getAndSet(int newValue) 以原子方式设置为给定值,并返回旧值。int incrementAndGet() 以原子方式将当前值加 1。int intValue() 以 int 形式返回指定的数值。void lazySet(int newValue) 最后设置为给定值。long longValue() 以 long 形式返回指定的数值。void set(int newValue) 设置为给定值。String toString() 返回当前值的字符串表示形式。boolean weakCompareAndSet(int expect, int upd
16、ate) 如果当前值 = 预期值,则以原子方式将该设置为给定的更新值。|-AtomicIntegerArray构造方法摘要AtomicIntegerArray(int length) 创建给定长度的新 AtomicIntegerArray。AtomicIntegerArray(int array) 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。方法摘要int addAndGet(int i, int delta) 以原子方式将给定值与索引 i 的元素相加。boolean compareAndSet(int i, int expect, int
17、 update) 如果当前值 = 预期值,则以原子方式将位置 i 的元素设置为给定的更新值。int decrementAndGet(int i) 以原子方式将索引 i 的元素减 1。int get(int i) 获取位置 i 的当前值。int getAndAdd(int i, int delta) 以原子方式将给定值与索引 i 的元素相加。int getAndDecrement(int i) 以原子方式将索引 i 的元素减 1。int getAndIncrement(int i) 以原子方式将索引 i 的元素加 1。int getAndSet(int i, int newValue) 将位置
18、i 的元素以原子方式设置为给定值,并返回旧值。int incrementAndGet(int i) 以原子方式将索引 i 的元素加 1。void lazySet(int i, int newValue) 最后将位置 i 的元素设置为给定值。int length() 返回该数组的长度。void set(int i, int newValue) 将位置 i 的元素设置为给定值。String toString() 返回数组当前值的字符串表示形式。boolean weakCompareAndSet(int i, int expect, int update) 如果当前值 = 预期值,则以原子方式将位置
19、 i 的元素设置为给定的更新值。09. java5 线程并发库的应用如果没有线程池,需要在 run 方法中不停判断,还有没有任务需要执行线程池的通俗比喻:接待客户,为每个客户都安排一个工作人员,接待完成后该工作人员就废掉。服务器每收到一个客户请求就为其分配一个线程提供服务,服务结束后销毁线程,不断创建、销毁线程,影响性能。线程池:先创建多个线程放在线程池中,当有任务需要执行时,从线程池中找一个空闲线程执行任务,任务完成后,并不销毁线程,而是返回线程池,等待新的任务安排。线程池编程中,任务是提交给整个线程池的,并不是提交给某个具体的线程,而是由线程池从中挑选一个空闲线程来运行任务。一个线程同时只
20、能执行一个任务,可以同时向一个线程池提交多个任务。线程池创建方法:a、创建一个拥有固定线程数的线程池ExecutorService threadPool = Executors.newFixedThreadPool(3);b、创建一个缓存线程池 线程池中的线程数根据任务多少自动增删 动态变化ExecutorService threadPool = Executors.newCacheThreadPool();c、创建一个只有一个线程的线程池 与单线程一样 但好处是保证池子里有一个线程,当线程意外死亡,会自动产生一个替补线程,始终有一个线程存活ExecutorService threadPool
21、 = Executors.newSingleThreadExector();往线程池中添加任务threadPool.executor(Runnable)关闭线程池:threadPool.shutdown() 线程全部空闲,没有任务就关闭线程池threadPool.shutdownNow() 不管任务有没有做完,都关掉用线程池启动定时器:a、创建调度线程池,提交任务 延迟指定时间后执行任务Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,时间单位);b、创建调度线程池,提交任务, 延迟指定时间执行任务后,间隔指定时间循环执
22、行Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,间隔时间,时间单位);所有的 schedule 方法都接受 相对 延迟和周期作为参数,而不是绝对的时间或日期。将以 Date 所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的 Date 运行,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)。10. Callable 与 Future 的应用:获取一个线程的运行结果public i
23、nterface Callable返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。 Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。只有一个方法 V call() 计算结果,如果无法计算结果,则抛出一个 Exception 异常。使用方法:ExecutorService threadPool = Executors.newSingleThreadExccutor();如果不需要返回结果,就用 executor 方法 调用 submit 方法
24、返回一个 Future 对象Future future = threadPool.submit(new Callable()/接收一个 Callable 接口的实例对象覆盖 Callable 接口中的 call 方法,抛出异常public T call() throws Exceptionruturn T);获取 Future 接收的结果future。get() ;会抛出异常future.get()没有拿到结果就会一直等待Future 取得的结果类型和 Callable 返回的结果类型必须一致,通过泛型实现。Callable要通过 ExecutorService 的 submit 方法提交,返
25、回的 Future 对象可以取消任务。public interface FutureFuture 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。 方法摘要boolean cancel(boolean mayInterrupt
26、IfRunning) 试图取消对此任务的执行。V get() 如有必要,等待计算完成,然后获取其结果。V get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用) 。boolean isCancelled() 如果在任务正常完成前将其取消,则返回 true。boolean isDone() 如果任务已完成,则返回 true。public interface CompletionServiceCompletionService 用于提交一组 Callable 任务,其 take 方法返回一个已完成的 Callab
27、le任务对应的 Future 对象。好比同时种几块麦子等待收割,收割时哪块先熟先收哪块。将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。 通常,CompletionService 依赖于一个单独的 Executor 来实际执行任务,在这种情况下,CompletionService
28、只管理一个内部完成队列。ExecutorCompletionService 类提供了此方法的一个实现。CompletionService 方法摘要Future poll() 获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回 null。Future poll(long timeout, TimeUnit unit) 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则将等待指定的时间(如果有必要) 。Future submit(Callable task) 提交要执行的值返回任务,并返回表示挂起的任务结果的 Future。Future subm
29、it(Runnable task, V result) 提交要执行的 Runnable 任务,并返回一个表示任务完成的 Future,可以提取或轮询此任务。Future take() 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。ExecutorCompletionService 构造方法摘要ExecutorCompletionService(Executor executor) 使用为执行基本任务而提供的执行程序创建一个 ExecutorCompletionService,并将 LinkedBlockingQueue 作为完成队列。ExecutorComp
30、letionService(Executor executor, BlockingQueue completionQueue) 使用为执行基本任务而提供的执行程序创建一个 ExecutorCompletionService,并将所提供的队列作为其完成队列。示例:ExecutorService threadPool = Executors.newFixedThreadPool(10); /创建线程池,传递给coms用 threadPool 执行任务,执行的任务返回结果都是整数CompletionService coms = new ExecutorCompletionService(thread
31、Pool);提交 10 个任务 种麦子for (int i=0; i()public Integer call()覆盖 call 方法匿名内部类使用外部变量要用 final 修饰SOP(任务+num);Thread.sleep(new Random().nextInt(6)*1000);return num;);等待收获 割麦子for (int i=0; i getQueuedReaderThreads() 返回一个 collection,它包含可能正在等待获取读取锁的线程。protected Collection getQueuedThreads() 返回一个 collection,它包含可
32、能正在等待获取读取或写入锁的线程。protected Collection getQueuedWriterThreads() 返回一个 collection,它包含可能正在等待获取写入锁的线程。int getQueueLength() 返回等待获取读取或写入锁的线程估计数目。int getReadHoldCount() 查询当前线程在此锁上保持的重入读取锁数量。int getReadLockCount() 查询为此锁保持的读取锁数量。protected Collection getWaitingThreads(Condition condition) 返回一个 collection,它包含可能
33、正在等待与写入锁相关的给定条件的那些线程。int getWaitQueueLength(Condition condition) 返回正等待与写入锁相关的给定条件的线程估计数目。int getWriteHoldCount() 查询当前线程在此锁上保持的重入写入锁数量。boolean hasQueuedThread(Thread thread) 查询是否给定线程正在等待获取读取或写入锁。boolean hasQueuedThreads() 查询是否所有的线程正在等待获取读取或写入锁。boolean hasWaiters(Condition condition) 查询是否有些线程正在等待与写入锁有
34、关的给定条件。boolean isFair() 如果此锁将公平性设置为 ture,则返回 true。boolean isWriteLocked() 查询是否某个线程保持了写入锁。boolean isWriteLockedByCurrentThread() 查询当前线程是否保持了写入锁。ReentrantReadWriteLock.ReadLock readLock() 返回用于读取操作的锁。String toString() 返回标识此锁及其锁状态的字符串。ReentrantReadWriteLock.WriteLock writeLock() 返回用于写入操作的锁。三个线程读数据,三个线程写
35、数据示例:可以同时读,读的时候不能写,不能同时写,写的时候不能读读的时候上读锁,读完解锁;写的时候上写锁,写完解锁。注意 finally 解锁package cn.itheima;import java.util.Random;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemo/*读写所使用* 三个线程读,三个线程写*/public static void main(Strin
36、g args)/共享对象final Source source = new Source();/创建线程for (int i=0; i cache = new HashMap();取数据方法 可能有多个线程来取数据,没有数据的话又会去数据库查询,需要互斥public synchronized Object get(String key) 先查询内部存储器中有没有要的值Object value = cache.get(key);if (value=null)如果没有,就去数据库中查询,并将查到的结果存入内部存储器中value = “aaaa”; 实际代码是查询后的结果 queryDB(key)c
37、ache.put(key, value);return value;上面的代码每次只能有一个线程来查询,但只有写的时候才需要互斥,修改如下来一个读写锁ReadWriteLock rwl = new ReentrantReadWriteLock();public Object get(String key)上读锁rwl.readLock().lock();先查询内部存储器中有没有要的值Object value = cache.get(key);if (value=null)如果没有,就去数据库中查询,并将查到的结果存入内部存储器中释放读锁 上写锁rwl.readLock().unlock();r
38、wl.writeLock().lock();if (value=null)再次进行判断,防止多个写线程堵在这个地方重复写value = “aaaa”;cache.put(key, value);设置完成 释放写锁,恢复读写状态rwl.readLock().lock();rwl.writeLock().unlock();释放读锁rwl.readLock().unlock();return value; 注意:try finally 中 unlock13. java5 条件阻塞 Condition 的应用Condition 的功能类似在传统线程技术中的 Object.wait()和 Object.
39、natify()的功能,传统线程技术实现的互斥只能一个线程单独干,不能说这个线程干完了通知另一个线程来干,Condition 就是解决这个问题的,实现线程间的通信。比如 CPU 让小弟做事,小弟说我先歇着并通知大哥,大哥就开始做事。public interface ConditionCondition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set) 。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代
40、了 Object 监视器方法的使用。 Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。 作为一个示例,假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存 put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个 Condi
41、tion 实例来做到这一点。 class BoundedBuffer 阻塞队列 满了不能放,空了不能取final Lock lock = new ReentrantLock();final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object items = new Object100;int putptr, takeptr, count;public void put(Object x) throws InterruptedException
42、lock.lock();try while (count = items.length) notFull.await();itemsputptr = x; if (+putptr = items.length) putptr = 0;+count;notEmpty.signal(); finally lock.unlock();public Object take() throws InterruptedException lock.lock();try while (count = 0) notEmpty.await();Object x = itemstakeptr; if (+takep
43、tr = items.length) takeptr = 0;-count;notFull.signal();return x; finally lock.unlock(); 使用方法:Lock lock = new ReentrantLock();Condition condition = lock.newCondition();this.wait()condition.await()this.notify()condition.signal()注意:判断条件时用 while 防止虚假唤醒,等待在那里,唤醒后再进行判断,确认符合要求后再执行任务。14. java5 的 Semaphore 同
44、步工具Semaphore 可以维护当前访问自身的线程个数,并且提供了同步机制。semaphore 实现的功能类似于厕所里有 5 个坑,有 10 个人要上厕所,同时就只能有 5个人占用,当 5 个人中 的任何一个让开后,其中在等待的另外 5 个人中又有一个可以占用了。java.util.concurrent.Semaphore一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数
45、,并采取相应的行动。 Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。例如,下面的类使用信号量控制对内容池的访问: class Pool private static final int MAX_AVAILABLE = 100;private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);public Object getItem() throws InterruptedException available.acquire();return getNextAvailableItem()
46、;public void putItem(Object x) if (markAsUnused(x)available.release();/ Not a particularly efficient data structure; just for demoprotected Object items = . whatever kinds of items being managedprotected boolean used = new booleanMAX_AVAILABLE;protected synchronized Object getNextAvailableItem() for
47、 (int i = 0; i getQueuedThreads() 返回一个 collection,包含可能等待获取的线程。int getQueueLength() 返回正在等待获取的线程的估计数目。boolean hasQueuedThreads() 查询是否有线程正在等待获取。boolean isFair() 如果此信号量的公平设置为 true,则返回 true。protected void reducePermits(int reduction) 根据指定的缩减量减小可用许可的数目。void release() 释放一个许可,将其返回给信号量。void release(int permi
48、ts) 释放给定数目的许可,将其返回到信号量。String toString() 返回标识此信号量的字符串,以及信号量的状态。boolean tryAcquire() 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。boolean tryAcquire(int permits) 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。boolean tryAcquire(int permits, long timeout, TimeUnit unit) 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。boolean tryAcquire(long timeout, TimeUnit unit) 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。示例