并发一直都是计算机编程所面临的难题,因为在多线程中,线程中拥有的数据都是独立,对于其它线程是不可见,所以在多线程编程中处理不好就会造成数据没有按照预想进行下去。
处理多线程问题我们首先想到的锁,锁大致分为排他锁和共享锁。锁只是我们为了理解而这么说的,在编译后的代码中,我们使用一些工具可以看到的是其实就是给我们加了一个monitor的监视,而进入monitor这个监视区,就只能一个线程可以进入,其它的线程会处于挂起状态,而线程一旦离开这个mointor区域其它线程才有机会被cpu调度执行,因此每次只能一个线程在monitor处理数据,执行完将数据刷新到主内存中,才不至于导致多个线程同时更新线程本地数据而没有及时刷新到主内存导致的不安全行为。
在Java中为我们提供了很多线程安全的类,特别要关注的是以下这个工具包,他就是提供给我们的并发工具库。
java.util.concurrent
我们有谈论到了并发的不安全性,这节主要是想说的,怎么在高并发多线程的环境下处理线程的流量,因为如果不考虑线程数量很可能导致cpu的资源被消耗殆尽,甚至直接导致我们系统的不可用,这是非常糟糕的,那么本节将会以介绍api和一些简单示例的形式为大家刨析限流。
Semaphore 的介绍
类 Semaphore 提供的功能是我们之前所提到的锁无法填补的,Semaphore 是 synchronize 关键字的升级版,它的只要作用就是控制线程的并发数量,这点是 synchronize 无法做到的。
jdk 对 semaphore 的描述可以理解为 一个带有计数功能并且设置线程许可数量的信号量。
Semaphore 的同步性
多线程同步的概念其实就是排着队执行一个任务,执行任务是一个一个执行的,并不是并行执行的,这样是为了保证逻辑的准确性,不会出现线程不安全的行为。
那么我们来看一个实验,Semaphore 是怎么限制线程并发数的。
package com.test.blog;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description for 程序员港湾 / 暗黑程序猫 实验例子
*/
public class SemaphoreBlogTest1 {
private static final Semaphore semaphore = new Semaphore(3);
private static final String THREAD_NAME = "Tom";
public static void main(String[] args) {
IntStream.range(0,5).mapToObj(SemaphoreBlogTest1::thread).forEach(Thread::start);
}
private static Thread thread(final int name) {
return new Thread(()-> {
try {
// 默认使用一个许可
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " start times: " + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end times: " + System.currentTimeMillis());
// 释放一个许可
semaphore.release();
},THREAD_NAME + "-" + name);
}
}
这个实验就是一个简单的 Semaphore 限制线程的例子,并发访问的时候,同时三个线程并发访问,其余线程需要等待其释放许可。acquire() 和 release() 默认分别代表得到或者释放一个许可,acquire其实是做了减法操作,底层维护一个计数功能。
在 Semaphore 构造方式 Semaphore semaphore = new Semaphore(3); 中 我们往构造器中传入 3 就代表了 并发的时候 允许三个线程同时并发执行我们的代码块,其实我们的 acquire() 和 release() 方法还可以自定义使用或者释放多少个许可的情况。
下面我们再来做个实验:
package com.test.blog;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description for 程序员港湾 / 暗黑程序猫 实验例子
*/
public class SemaphoreBlogTest1 {
private static final Semaphore semaphore = new Semaphore(4);
private static final String THREAD_NAME = "Tom";
public static void main(String[] args) {
IntStream.range(0,5).mapToObj(SemaphoreBlogTest1::thread).forEach(Thread::start);
}
private static Thread thread(final int name) {
return new Thread(()-> {
try {
semaphore.acquire(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " start times: " + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end times: " + System.currentTimeMillis());
semaphore.release(2);
},THREAD_NAME + "-" + name);
}
}
上面的代码我们更改了 Semaphore semaphore = new Semaphore(4); 将之前的 3 更改到 4 , semaphore.acquire(2); semaphore.release(2); 由之前的无参,改成 2, 代表着每次可以接收两个线程的并发访问,下面是实验的执行结果,可以证明这一点。
值得一提的是 最终的许可数目不一定和我们初始化的许可数等量的,例如我最后可以多执行或者多设置 semaphore.release() 方法都会导致不等的。
availablePermits() 和 drainPermits() 介绍
availablePermits() 常用于调试,返回当前许可数量,drainPermits() 返回当前的许可数,并将许可书置 0 。
下面我们来实验这一点:
main 代码:
package com.test.main;
import com.test.service.SemaphoreService2;
import com.test.thread.SemaphoreThread2;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description main
*/
public class SemaphoreMain2 {
public static void main(String[] args) {
SemaphoreService2 semaphoreService2 = new SemaphoreService2();
SemaphoreThread2 semaphoreThread2 = new SemaphoreThread2(semaphoreService2);
semaphoreThread2.setName("test availablePermits");
semaphoreThread2.start();
}
}
实现线程代码:
package com.test.thread;
import com.test.service.SemaphoreService2;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description semaphore thread build
*/
public class SemaphoreThread2 extends Thread {
private SemaphoreService2 semaphoreService2 ;
public SemaphoreThread2(){};
public SemaphoreThread2(SemaphoreService2 semaphoreService2) {
this.semaphoreService2 = semaphoreService2;
}
@Override
public void run() {
semaphoreService2.testSemaphoreInterrupt();
}
}
业务实现代码:
package com.test.service;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description semaphore service
*/
public class SemaphoreService2 {
Semaphore semaphore = new Semaphore(2);
public void testSemaphoreInterrupt() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行完成!!");
System.out.println("availablePermits() ->" + semaphore.availablePermits());
semaphore.release();
System.out.println("availablePermits() -> " + semaphore.availablePermits());
System.out.println("drainPermits() -> " + semaphore.drainPermits());
System.out.println("availablePermits -> " + semaphore.availablePermits());
}
}
实验执行结果:
从实验结果不难看出 availablePermits() 许可数是不断变化的,当我们调用了 drainPermits() 方法后 许可数目就被置 0 了。
Semaphore acquire() 线程可被打断和acquireUninterruptibly()不可被打断
能被线程打断的方法,可不止 semaphore acquire() 这个方法, 例如 sleep() ,wait() join() 等等方法, 都有可能被打断,那么线程被打断会做什么事情呢?线程被打断后,底层都会维护一个标识,标识这个线程被打断了 使用isInterrupted() 方法即可判断该线程释放被打断,一旦捕获到异常之后,又会擦除这个标记位,但是打断一个线程并不等于结束了线程的生命周期,仅仅是打断了当前线程的阻塞状态。
Semaphore getQueueLength() 和 hasQueueThreads() 介绍
getQueueLength() 是获取等待许可的线程数目。
hasQueueThreads() 是否有线程在等待这个许可。
Semaphore 公平和非公平信号量
semaphore 默认实现的公平信号量,我们也可以指定为非公平信号量。那么什么是公平信号量呢?公平信号量可以理解为一个先进先出的线程队列,按照一定的顺序根据信号量许可数目并发执行。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
从源码中可以看出定义 Semaphore 对象的时候 fair 为 true 的时候,即公平信号量,反之非公平信号量。
Semaphore tryAcquire() 介绍
我们在并发编程中经常会遇到 xxx() tryXxx() 这种类似的方法。其实意义都差不多,try 顾名思义就是尝试获取的意思,那么获取不到就直接失败了,所以一定是有结果的。而 xxx() 如果获取不到,就比较固执的一直阻塞,直到线程被打断或者超时报错。
Semaphore 例子
讲了这么多,那我们也要实际的动手去弄,因为好记性不如烂笔头。
笔者这个例子是模拟现实生活中实际的应用场景,例如银行柜台排队办理业务的例子,我们可以设置客户柜台,客户柜台就像是信号量,一次最多可以许可办理多少个客户,然后客户就用线程数去模拟。
main 代码:
package com.test.main;
import com.test.service.SemaphoreService;
import com.test.thread.SeamaphoreThread;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description do somethings
*/
public class SeamaphoreMain {
public static void main(String[] args) {
SemaphoreService semaphoreService = new SemaphoreService();
SeamaphoreThread[] seamaphoreThread = new SeamaphoreThread[50];
for (int i = 0 ; i < seamaphoreThread.length; i++) {
seamaphoreThread[i] = new SeamaphoreThread(semaphoreService);
}
for (int i = 0; i < seamaphoreThread.length; i++) {
seamaphoreThread[i].setName("客户"+i);
seamaphoreThread[i].start();
}
}
}
线程构造代码:
package com.test.thread;
import com.test.service.SemaphoreService;
/**
* @author xie wei hua
* @version 1.0
* @date day day up
* @description do somethings
*/
public class SeamaphoreThread extends Thread {
private SemaphoreService semaphoreService;
public SeamaphoreThread() {
}
public SeamaphoreThread(SemaphoreService semaphoreService) {
this.semaphoreService = semaphoreService;
}
@Override
public void run() {
semaphoreService.business(Thread.currentThread().getName());
}
}
业务代码:
package com.test.service;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author xie wei hua
* @version 1.0
* @date 2021/4/5
* @description
*/
public class SemaphoreService {
Semaphore semaphore = new Semaphore(1);
/**
* 可办理50个人业务
*/
private int count = 50;
/**
* 三个窗口
*/
Map<Integer, String> title = new ConcurrentHashMap<Integer, String>();
Map<Integer,Boolean> map = new ConcurrentHashMap<Integer,Boolean>(){
};
{
map.put(1,false);
map.put(2,false);
map.put(3,false);
map.put(4,false);
map.put(5,false);
map.put(6,false);
map.put(7,false);
map.put(8,false);
map.put(9,false);
map.put(10,false);
title.put(1,"窗口1");
title.put(2,"窗口2");
title.put(3,"窗口3");
title.put(4,"窗口4");
title.put(5,"窗口5");
title.put(6,"窗口6");
title.put(7,"窗口7");
title.put(8,"窗口8");
title.put(9,"窗口9");
title.put(10,"窗口10");
}
/**
* 初始化信号量
*/
Semaphore semaphoreSize = new Semaphore(10);
/**
* 办理成功减少今日任务量
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 办理业务完成通知客户办理
*/
Condition condition = lock.newCondition();
public void print(String threadName) {
try {
semaphore.acquire();
//Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName+"在"+System.currentTimeMillis()+"时刻"+"执行了");
semaphore.release();
}
/**
* 处理业务
* @param threadName
*/
public void business(String threadName) {
try {
semaphoreSize.acquire();
System.out.println("新增了人"+threadName);
realBusiness(threadName);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphoreSize.release();
}
}
/**
* 真正处理业务
* @param threadName
*/
private void realBusiness(String threadName) {
try {
if (count == 0) {
System.out.println("今日已经处理完所有用户不能再处理了!");
}
int i = -1;
lock.lock();
try {
i = chooseOneService();
map.put(i,true);
}finally {
lock.unlock();
}
String s = title.get(i);
if (s == null || s.length() == 0) {
System.out.println("窗口有误!");
return;
}
System.out.println(threadName + s + "处理业务");
Thread.sleep(new Random().nextInt(5)*1000);
lock.lock();
try {
System.out.println(threadName + "处理成功.... 还剩余:" + --count + "个人");
}finally {
lock.unlock();
}
free(i);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
/**
* 选择进入一个柜台
*/
public int chooseOneService() {
boolean isFind = false;
int id = -1;
try {
for (Map.Entry<Integer,Boolean> obj : map.entrySet()) {
if (obj.getValue().equals(Boolean.TRUE)) {
continue;
}else {
isFind = true;
id = obj.getKey();
break;
}
}
if (!isFind) {
condition.await();
chooseOneService();
}else {
return id;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
}
return -1;
}
/**
* 办理完成关闭柜台
*/
public void free(int id) {
lock.lock();
try {
map.put(id,false);
String s = title.get(id);
System.out.println(s + "已经释放=====");
condition.signalAll();
}finally {
lock.unlock();
}
}
}
程序程序结果:
从结果中可以看出:开始的时候,窗口没人,线程快速进入了队列,并且进入窗口办理业务,因为办理业务需要一定的时间,所以由于窗口数量的限制导致后面等待排队的线程相对没有一开始那种并发抢占式执行的快了。
如果觉得不错,就给笔者点个赞和关注,笔者一定会结合自身多年的IT工作经验和新的学习认知,坚持,持续输出更优质的内容。
笔者也有个人微信公众号,也存了不少编程资料,如果需要的话,请加笔者微信公众号:暗黑程序猫,回复领取资料,即可。