多线程
java 中有几种方法可以实现一个线程?
继承 Thread 类;
实现 Runnable 接口;
实现 Callable 接口通过 FutureTask 包装器来创建 Thread 线程;
使用 ExecutorService,Callable,Future 实现有返回结果的多线程(也就是使用了 ExecutorService 来管理前面的三种方式).
详情参见:
如何停止一个正在运行的线程?
1, 使用退出标志, 使线程正常退出, 也就是当 run 方法完成后线程终止.
2, 使用 stop 方法强行终止, 但是不推荐这个方法, 因为 stop 和 suspend 及 resume 一样都是过期作废的方法.
3, 使用 interrupt 方法中断线程.
参考:
notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法, 那么线程便会处于该对象的等待池中, 等待池中的线程不会去竞争该对象的锁.
当有线程调用了对象的 notifyAll()方法 (唤醒所有 wait 线程) 或 notify()方法(只随机唤醒一个 wait 线程), 被唤醒的的线程便会进入该对象的锁池中, 锁池中的线程会去竞争该对象锁. 也就是说, 调用了 notify 后只要一个线程会由等待池进入锁池, 而 notifyAll 会将该对象等待池内的所有线程移动到锁池中, 等待锁竞争.
优先级高的线程竞争到对象锁的概率大, 假若某线程没有竞争到该对象锁, 它还会留在锁池中, 唯有线程再次调用 wait()方法, 它才会重新回到等待池中. 而竞争到对象锁的线程则继续往下执行, 直到执行完了 synchronized 代码块, 它会释放掉该对象锁, 这时锁池中的线程会继续竞争该对象锁.
参考:
sleep()和 wait()有什么区别?
对于 sleep()方法, 我们首先要知道该方法是属于 Thread 类中的. 而 wait()方法, 则是属于 Object 类中的.
sleep()方法导致了程序暂停执行指定的时间, 让出 CPU 该其他线程, 但是他的监控状态依然保持者, 当指定的时间到了又会自动恢复运行状态. 在调用 sleep()方法的过程中, 线程不会释放对象锁.
当调用 wait()方法的时候, 线程会放弃对象锁, 进入等待此对象的等待锁定池, 只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备, 获取对象锁进入运行状态.
参考:
什么是 Daemon 线程? 它有什么意义?
Java 语言自己可以创建两种进程 "用户线程" 和 "守护线程"
用户线程: 就是我们平时创建的普通线程.
守护线程: 主要是用来服务用户线程.
Daemon 就是守护线程, 他的意义是:
只要当前 JVM 实例中尚存在任何一个非守护线程没有结束, 守护线程就全部工作; 只有当最后一个非守护线程结束时, 守护线程随着 JVM 一同结束工作.
Daemon 的作用是为其他线程的运行提供便利服务, 守护线程最典型的应用就是 GC (垃圾回收器), 它就是一个很称职的守护者.
参考:
java 如何实现多线程之间的通讯和协作?
参考这篇:
锁
什么是可重入锁(ReentrantLock)?
线程可以进入任何一个它已经拥有的锁所同步着的代码块.
代码设计如下:
- public class Lock{
- boolean isLocked = false;
- Thread lockedBy = null;
- int lockedCount = 0;
- public synchronized void lock()
- throws InterruptedException{
- Thread thread = Thread.currentThread();
- while(isLocked && lockedBy != thread){
- wait();
- }
- isLocked = true;
- lockedCount++;
- lockedBy = thread;
- }
- public synchronized void unlock(){
- if(Thread.currentThread() == this.lockedBy){
- lockedCount--;
- if(lockedCount == 0){
- isLocked = false;
- notify();
- }
- }
- }
- }
参考链接:
当一个线程进入某个对象的一个 synchronized 的实例方法后, 其它线程是否可进入此对象的其它方法?
如果其他方法前加了 synchronized 关键字, 就不能, 如果没加 synchronized, 则能够进去.
如果这个方法内部调用了 wait(), 则可以进入其他加 synchronized 的方法.
如果其他方法加了 synchronized 关键字, 并且没有调用 wai 方法, 则不能.
synchronized 和 java.util.concurrent.locks.Lock 的异同?
主要相同点: Lock 能完成 Synchronized 所实现的所有功能.
主要不同点: Lock 有比 Synchronized 更精确的线程予以和更好的性能. Synchronized 会自动释放锁, 但是 Lock 一定要求程序员手工释放, 并且必须在 finally 从句中释放.
乐观锁和悲观锁的理解及如何实现, 有哪些实现方式?
乐观锁是假设每次操作都不会冲突, 若是遇到冲突失败就重试直到成功; 悲观锁是让其他线程都等待, 等锁释放完了再竞争锁.
乐观锁实现方式: cas,volatile
悲观锁实现方式: synchronized,Lock
并发框架
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap()和 Hashtable 一样, 实现上在调用 map 所有方法时, 都对整个 map 进行同步. 而 ConcurrentHashMap 的实现却更加精细, 它对 map 中的所有桶加了锁. 所以, 只要有一个线程访问 map, 其他线程就无法进入 map, 而如果一个线程在访问 ConcurrentHashMap 某个桶时, 其他线程, 仍然可以对 map 执行某些操作.
所以, ConcurrentHashMap 在性能以及安全性方面, 明显比 Collections.synchronizedMap()更加有优势. 同时, 同步操作精确控制到桶, 这样, 即使在遍历 map 时, 如果其他线程试图对 map 进行数据修改, 也不会抛出 ConcurrentModificationException.
参考:
CopyOnWriteArrayList 可以用于什么应用场景?
CopyOnWriteArrayList 的特性是针对读操作, 不做处理, 和普通的 ArrayList 性能一样. 而在写操作时, 会先拷贝一份, 实现新旧版本的分离, 然后在拷贝的版本上进行修改操作, 修改完后, 将其更新至就版本中.
那么他的使用场景就是: 一个需要在多线程中操作, 并且频繁遍历. 其解决了由于长时间锁定整个数组导致的性能问题, 解决方案即写时拷贝.
另外需要注意的是 CopyOnWrite 容器只能保证数据的最终一致性, 不能保证数据的实时一致性. 所以如果你希望写入的的数据, 马上能读到, 请不要使用 CopyOnWrite 容器.
参考:
线程安全
什么叫线程安全? servlet 是线程安全吗?
线程安全就是说多线程访问同一代码, 不会产生不确定的结果.
在多线程环境中, 当各线程不共享数据的时候, 即都是私有 (private) 成员, 那么一定是线程安全的. 但这种情况并不多见, 在多数情况下需要共享数据, 这时就需要进行适当的同步控制了.
线程安全一般都涉及到 synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果.
如果你的代码所在的进程中有多个线程在同时运行, 而这些线程可能会同时运行这段代码. 如果每次运行结果和单线程运行的结果是一样的, 而且其他的变量的值也和预期的是一样的, 就是线程安全的.
Servlet 不是线程安全的, 详见: 漫画 | Servlet 属于线程安全的吗? https://www.javazhiyin.com/21242.html
同步有几种实现方法?
1. 同步方法
即有 synchronized 关键字修饰的方法.
由于 java 的每个对象都有一个内置锁, 当用此关键字修饰方法时, 内置锁会保护整个方法. 在调用该方法前, 需要获得内置锁, 否则就处于阻塞状态.
2. 同步代码块
即有 synchronized 关键字修饰的语句块.
被该关键字修饰的语句块会自动被加上内置锁, 从而实现同步.
3. 使用特殊域变量 (volatile) 实现线程同步
a.volatile 关键字为域变量的访问提供了一种免锁机制,
b. 使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c. 因此每次使用该域就要重新计算, 而不是使用寄存器中的值
d.volatile 不会提供任何原子操作, 它也不能用来修饰 final 类型的变量
4. 使用重入锁实现线程同步
在 JavaSE5.0 中新增了一个 java.util.concurrent 包来支持同步. ReentrantLock 类是可重入, 互斥, 实现了 Lock 接口的锁, 它与使用 synchronized 方法和快具有相同的基本行为和语义, 并且扩展了其能力.
5. 使用局部变量实现线程同步.
参考:
volatile 有什么用? 能否用一句话说明下 volatile 的应用场景?
作用是: 作为指令关键字, 确保本条指令不会因编译器的优化而省略, 且要求每次直接读值, 即不是从寄存器里取备份值, 而是去该地址内存存储的值.
一句话说明 volatile 的应用场景:
对变量的写操作不依赖于当前值且该变量没有包含在具有其他变量的不变式中.
请说明下 java 的内存模型.
Java 内存模型的逻辑视图
为了保证并发编程中可以满足原子性, 可见性及有序性. 有一个重要的概念, 那就是内存模型.
为了保证共享内存的正确性(可见性, 有序性, 原子性), 内存模型定义了共享内存系统中多线程程序读写操作行为的规范.
通过这些规则来规范对内存的读写操作, 从而保证指令执行的正确性. 它与处理器有关, 与缓存有关, 与并发有关, 与编译器也有关.
它解决了 CPU 多级缓存, 处理器优化, 指令重排等导致的内存访问问题, 保证了并发场景下的一致性, 原子性和有序性.
内存模型解决并发问题主要采用两种方式:
限制处理器优化
使用内存屏障
关于主内存与工作内存之间的具体交互协议, 即一个变量如何从主内存拷贝到工作内存, 如何从工作内存同步到主内存之间的实现细节, Java 内存模型定义了以下八种操作来完成:
lock(锁定): 作用于主内存的变量, 把一个变量标识为一条线程独占状态.
unlock(解锁): 作用于主内存变量, 把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定.
read(读取): 作用于主内存变量, 把一个变量值从主内存传输到线程的工作内存中, 以便随后的 load 动作使用
load(载入): 作用于工作内存的变量, 它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中.
use(使用): 作用于工作内存的变量, 把工作内存中的一个变量值传递给执行引擎, 每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作.
assign(赋值): 作用于工作内存的变量, 它把一个从执行引擎接收到的值赋值给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作.
store(存储): 作用于工作内存的变量, 把工作内存中的一个变量的值传送到主内存中, 以便随后的 write 的操作.
write(写入): 作用于主内存的变量, 它把 store 操作从工作内存中一个变量的值传送到主内存的变量中.
如果要把一个变量从主内存中复制到工作内存, 就需要按顺寻地执行 read 和 load 操作, 如果把变量从工作内存中同步回主内存中, 就要按顺序地执行 store 和 write 操作. Java 内存模型只要求上述操作必须按顺序执行, 而没有保证必须是连续执行. 也就是 read 和 load 之间, store 和 write 之间是可以插入其他指令的, 如对主内存中的变量 a,b 进行访问时, 可能的顺序是 read a,read b,load b, load a.Java 内存模型还规定了在执行上述八种基本操作时, 必须满足如下规则:
不允许 read 和 load,store 和 write 操作之一单独出现
不允许一个线程丢弃它的最近 assign 的操作, 即变量在工作内存中改变了之后必须同步到主内存中.
不允许一个线程无原因地 (没有发生过任何 assign 操作) 把数据从工作内存同步回主内存中.
一个新的变量只能在主内存中诞生, 不允许在工作内存中直接使用一个未被初始化 (load 或 assign) 的变量. 即就是对一个变量实施 use 和 store 操作之前, 必须先执行过了 assign 和 load 操作.
一个变量在同一时刻只允许一条线程对其进行 lock 操作, lock 和 unlock 必须成对出现
如果对一个变量执行 lock 操作, 将会清空工作内存中此变量的值, 在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
如果一个变量事先没有被 lock 操作锁定, 则不允许对它执行 unlock 操作; 也不允许去 unlock 一个被其他线程锁定的变量.
对一个变量执行 unlock 操作之前, 必须先把此变量同步到主内存中(执行 store 和 write 操作).
参考:
为什么代码会重排序?
直接参考:
并发容器和框架
如何让一段程序并发的执行, 并最终汇总结果?
使用 CyclicBarrier 在多个关口处将多个线程执行结果汇总, CountDownLatch 在各线程执行完毕后向总线程汇报结果.
CountDownLatch : 一个线程(或者多个), 等待另外 N 个线程完成某个事情之后才能执行.
CyclicBarrier : N 个线程相互等待, 任何一个线程完成之前, 所有的线程都必须等待.
这样应该就清楚一点了, 对于 CountDownLatch 来说, 重点是那个 "一个线程", 是它在等待, 而另外那 N 的线程在把 "某个事情" 做完之后可以继续等待, 可以终止. 而对于 CyclicBarrier 来说, 重点是那 N 个线程, 他们之间任何一个没有完成, 所有的线程都必须等待.
从 API 上理解就是 CountdownLatch 有主要配合使用两个方法 countDown()和 await(),countDown()是做事的线程用的方法, await()是等待事情完成的线程用个方法, 这两种线程是可以分开的(下面例子: CountdownLatchTest2), 当然也可以是同一组线程; CyclicBarrier 只有一个方法 await(), 指的是做事线程必须大家同时等待, 必须是同一组线程的工作.
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- /**
- * 各个线程执行完成后, 主线程做总结性工作的例子
- * @author xuexiaolei
- * @version 2017 年 11 月 02 日
- */
- public class CountdownLatchTest2 {
- private final static int THREAD_NUM = 10;
- public static void main(String[] args) {
- CountDownLatch lock = new CountDownLatch(THREAD_NUM);
- ExecutorService exec = Executors.newCachedThreadPool();
- for (int i = 0; i < THREAD_NUM; i++) {
- exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
- }
- try {
- lock.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("大家都执行完成了, 做总结性工作");
- exec.shutdown();
- }
- static class CountdownLatchTask implements Runnable{
- private final CountDownLatch lock;
- private final String threadName;
- CountdownLatchTask(CountDownLatch lock, String threadName) {
- this.lock = lock;
- this.threadName = threadName;
- }
- @Override public void run() {
- System.out.println(threadName + "执行完成");
- lock.countDown();
- }
- }
- }
CyclicBarrier 例子:
- import java.util.concurrent.*;
- /**
- *
- * @author xuexiaolei
- * @version 2017 年 11 月 02 日
- */
- public class CyclicBarrierTest {
- private final static int THREAD_NUM = 10;
- public static void main(String[] args) {
- CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() {
- @Override public void run() {
- System.out.println("这阶段大家都执行完成了, 我总结一下, 然后开始下一阶段");
- }
- });
- ExecutorService exec = Executors.newCachedThreadPool();
- for (int i = 0; i < THREAD_NUM; i++) {
- exec.submit(new CountdownLatchTask(lock, "Task-"+i));
- }
- exec.shutdown();
- }
- static class CountdownLatchTask implements Runnable{
- private final CyclicBarrier lock;
- private final String threadName;
- CountdownLatchTask(CyclicBarrier lock, String threadName) {
- this.lock = lock;
- this.threadName = threadName;
- }
- @Override public void run() {
- for (int i = 0; i < 3; i++) {
- System.out.println(threadName + "执行完成");
- try {
- lock.await();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
如何合理的配置 java 线程池? 如 CPU 密集型的任务, 基本线程池应该配置多大? IO 密集型的任务, 基本线程池应该配置多大? 用有界队列好还是无界队列好? 任务非常多的时候, 使用什么阻塞队列能获取最好的吞吐量?
虽然 Exectors 可以生成一些很常用的线程池, 但毕竟在什么情况下使用还是开发者最清楚的. 在某些自己很清楚的使用场景下, java 线程池还是推荐自己配置的. 下面是 java 线程池的配置类的参数, 我们逐一分析一下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler
corePoolSize - 池中所保存的线程数, 包括空闲线程.
maximumPoolSize - 池中允许的最大线程数.
keepAliveTime - 当线程数大于核心时, 此为终止前多余的空闲线程等待新任务的最长时间.
unit - keepAliveTime 参数的时间单位.
workQueue - 执行前用于保持任务的队列. 此队列仅保持由 execute 方法提交的 Runnable 任务. 用 BlocingQueue 的实现类都可以.
threadFactory - 执行程序创建新线程时使用的工厂. 自定义线程工厂可以做一些额外的操作, 比如统计生产的线程数等.
handler - 饱和策略, 即超出线程范围和队列容量而使执行被阻塞时所使用的处理程序. 策略有: Abort 终止并抛出异常, Discard 悄悄抛弃任务, Discard-Oldest 抛弃最老的任务策略, Caller-Runs 将任务退回给调用者策略.
至于线程池应当配置多大的问题, 一般有如下的经验设置:
1. 如果是 CPU 密集型应用, 则线程池大小设置为 N+1.
2. 如果是 IO 密集型应用, 则线程池大小设置为 2N+1.
用有界队列好还是无界队列好? 这种问题的答案肯定是视情况而定:
1. 有界队列有助于避免资源耗尽的情况发生. 但他带来了新的问题: 当队列填满后, 新的任务怎么办? 所以有界队列适用于执行比较耗资源的任务, 同时要设计好相应的饱和策略.
2. 无界队列和有界队列刚好相反, 在资源无限的情况下可以一直接收新任务. 适用于小任务, 请求和处理速度相对持平的状况.
3. 其实还有一种同步移交的队列 SynchronousQueue , 这种队列不存储任务信息, 直接将任务提交给线程池. 可以理解为容量只有 1 的有界队列, 在特殊场景下有特殊作用, 同样得设计好相应的饱和策略.
参考:
如何使用阻塞队列实现一个生产者和消费者模型? 请写代码.
下面这是一个完整的生产者消费者代码例子, 对比传统的 wait,nofity 代码, 它更易于理解.
ProducerConsumerPattern.java 如下:
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.LinkedBlockingQueue;
- import java.util.logging.Level;
- import java.util.logging.Logger;
- public class ProducerConsumerPattern {
- public static void main(String args[]){
- //Creating shared object
- BlockingQueue sharedQueue = new LinkedBlockingQueue();
- //Creating Producer and Consumer Thread
- Thread prodThread = new Thread(new Producer(sharedQueue));
- Thread consThread = new Thread(new Consumer(sharedQueue));
- //Starting producer and Consumer thread
- prodThread.start();
- consThread.start();
- }
- }
生产者, Producer.java 如下:
- class Producer implements Runnable {
- private final BlockingQueue sharedQueue;
- public Producer(BlockingQueue sharedQueue) {
- this.sharedQueue = sharedQueue;
- }
- @Override
- public void run() {
- for(int i=0; i<10; i++){
- try {
- System.out.println("Produced:" + i);
- sharedQueue.put(i);
- } catch (InterruptedException ex) {
- Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
- }
- }
- }
- }
消费者, Consumer.java 如下所示:
- class Consumer implements Runnable{
- private final BlockingQueue sharedQueue;
- public Consumer (BlockingQueue sharedQueue) {
- this.sharedQueue = sharedQueue;
- }
- @Override
- public void run() {
- while(true){
- try {
- System.out.println("Consumed:"+ sharedQueue.take());
- } catch (InterruptedException ex) {
- Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
- }
- }
- }
- }
参考:
多读少写的场景应该使用哪个并发容器, 为什么使用它? 比如你做了一个搜索引擎, 搜索引擎每次搜索前需要判断搜索关键词是否在黑名单里, 黑名单每天更新一次.
Java 中的锁
如何实现乐观锁(CAS)? 如何避免 ABA 问题?
CAS 是项乐观锁技术, 当多个线程尝试使用 CAS 同时更新同一个变量时, 只有其中一个线程能更新变量的值, 而其它线程都失败, 失败的线程并不会被挂起, 而是被告知这次竞争中失败, 并可以再次尝试.
CAS 操作包含三个操作数 -- 内存位置 (V), 预期原值(A) 和新值(B). 如果内存位置的值与预期原值相匹配, 那么处理器会自动将该位置值更新为新值. 否则, 处理器不做任何操作. 无论哪种情况, 它都会在 CAS 指令之前返回该位置的值.(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功, 而不提取当前值.)CAS 有效地说明了 "我认为位置 V 应该包含值 A; 如果包含该值, 则将 B 放到这个位置; 否则, 不要更改该位置, 只告诉我这个位置现在的值即可." 这其实和乐观锁的冲突检查 + 数据更新的原理是一样的.
这里再强调一下, 乐观锁是一种思想. CAS 是这种思想的一种实现方式.
ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A, 这时候另一个线程 two 也从内存中取出 A, 并且 two 进行了一些操作变成了 B, 然后 two 又将 V 位置的数据变成 A, 这时候线程 one 进行 CAS 操作发现内存中仍然是 A, 然后 one 操作成功. 尽管线程 one 的 CAS 操作成功, 但是不代表这个过程就是没有问题的.
解决方法: 通过版本号 (version) 的方式来解决, 每次比较要比较数据的值和版本号两项内容即可.
读写锁可以用于什么应用场景?
在多线程的环境下, 对同一份数据进行读写, 会涉及到线程安全的问题. 比如在一个线程读取数据的时候, 另外一个线程在写数据, 而导致前后数据的不一致性; 一个线程在写数据的时候, 另一个线程也在写, 同样也会导致线程前后看到的数据的不一致性.
这时候可以在读写方法中加入互斥锁, 任何时候只能允许一个线程的一个读或写操作, 而不允许其他线程的读或写操作, 这样是可以解决这样以上的问题, 但是效率却大打折扣了. 因为在真实的业务场景中, 一份数据, 读取数据的操作次数通常高于写入数据的操作, 而线程与线程间的读读操作是不涉及到线程安全的问题, 没有必要加入互斥锁, 只要在读 - 写, 写 - 写期间上锁就行了.
对于以上这种情况, 读写锁是最好的解决方案! 其中它的实现类: ReentrantReadWriteLock-- 顾名思义是可重入的读写锁, 允许多个读线程获得 ReadLock, 但只允许一个写线程获得 WriteLock
读写锁的机制:
"读 - 读" 不互斥
"读 - 写" 互斥
"写 - 写" 互斥
参考:
什么时候应该使用可重入锁?
可重入锁, 也叫做递归锁, 指的是同一线程 外层函数获得锁之后 , 内层递归函数仍然有获取该锁的代码, 但不受影响.
在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁.
参考:
什么场景下可以使用 volatile 替换 synchronized?
状态标志: 把简单地 volatile 变量作为状态标志, 来达成线程之间通讯的目的, 省去了用 synchronized 还要 wait,notify 或者 interrupt 的编码麻烦.
替换重量级锁: 如果某个变量仅是单次读或者单次写操作, 没有复合操作 (i++, 先检查后判断之类的) 就可以用 volatile 替换 synchronized.
并发工具
如何实现一个流控程序, 用于控制请求的调用次数?
- import java.util.Random;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
- /**
- * 阻塞访问的线程, 直到获取了访问令牌
- * @author xuexiaolei
- * @version 2017 年 11 月 15 日
- */
- public class FlowControl2 {
- private final static int MAX_COUNT = 10;
- private final Semaphore semaphore = new Semaphore(MAX_COUNT);
- private final ExecutorService exec = Executors.newCachedThreadPool();
- public void access(int i){
- exec.submit(new Runnable() {
- @Override public void run() {
- semaphore.acquireUninterruptibly();
- doSomething(i);
- semaphore.release();
- }
- });
- }
- public void doSomething(int i){
- try {
- Thread.sleep(new Random().nextInt(100));
- System.out.println(String.format("%s 通过线程:%s 访问成功",i,Thread.currentThread().getName()));
- } catch (InterruptedException e) {
- }
- }
- public static void main(String[] args) {
- FlowControl2 Web = new FlowControl2();
- for (int i = 0; i < 2000; i++) {
- Web.access(i);
- }
- }
- }
来源: https://www.cnblogs.com/javazhiyin/p/10118078.html