Concurrent.util常用的工具类
1.CountDownLatch使用:
他经常用于监听某些初始化操作,等初始化执行完毕之后,通知主线程继续工作: 白老师讲zookeeper的例子,调用服务,只有等所有的初始化操作都完成之后,才可以进行使用功能
代码案例:
public class UseCountDownLatch {
public static void main(String[] args) {
// 在线程内部要使用,所以要声明为final(如果不是final,那么是用不了的)
final CountDownLatch countDown = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("进入t1线程,等待其他线程处理完成...");
countDown.await();
System.out.println("t1线程继续执行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("t2线程进行初始化操作");
Thread.sleep(3000);
System.out.println("t2线程初始化完毕,通知t1线程继续");
countDown.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("t3线程进行初始化操作");
Thread.sleep(4000);
System.out.println("t3线程初始化完毕,通知t1线程继续");
countDown.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
}
}
这段代码里面CountDownLatch初始化的数字是2,所以得等着两个CountDownLatch都释放了之后,等待着的线程才可以继续执行
CyclicBarrier使用
假设有这么一个场景:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待着.
代码案例:
public class UseCyclicBarrier {
//静态内部类 (与外部类是没有引用关系的)
static class Runner implements Runnable {
private CyclicBarrier barrier;
private String name;
public Runner(CyclicBarrier barrier, String name) {
this.barrier = barrier;
this.name = name;
}
@Override
public void run() {
try {
Thread.sleep((new Random()).nextInt(5)*1000);
System.out.println(name + " 准备OK!");
barrier.await(); // 在等着
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(name + "GO!");
}
}
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3); //如果传入的参数大于3的话,那么这三个线程都不会继续往下执行
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new Thread(new Runner(barrier, "zhangsan")));
executor.execute(new Thread(new Runner(barrier, "lisi")));
executor.execute(new Thread(new Runner(barrier, "wangwu")));
executor.shutdown();
}
}
开启了三个线程,要等这三个线程全部都准备好之后,这三个线程才会继续往下执行,否则都停在了barrier.awit()方法上了
CyclicBarrier:
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地
互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
使用的场景:多台机器去做同一个操作的时候,要求在用一个点执行操作,那么就有可能用到这个.
注意CountDownLatch和CyclicBarrier的区别:
CountDownLatch是属于一个线程等待,其他的n个线程去给我发出通知,然后,我这一个线程执行,只是针对于一个线程.
CyclicBarrier却是中几个线程都是参与阻塞的,这几个线程之后都要在同一点去执行继续的任务
Callable和Future的使用:
这个例子其实就是我们之前实现的Future模式.jdk给予我们一个实现的封装,使用非常简单.
Future模式非常适合在处理很耗时很长的业务逻辑时进行使用,可以有效地减少系统的响应时间,提高系统的吞吐量
Callable接口:Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
老师的案例代码:
public class UseFuture implements Callable<String>{
private String param;
public UseFuture(String param) {
this.param = param;
}
/* 这里是真实的业务逻辑,其执行可能很慢 */
@Override
public String call() throws Exception {
//模拟执行耗时操作
Thread.sleep(5000);
/*System.out.println(1/0);*/
String result = this.param + " | " + "处理完毕";
return result;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
String queryStr = "query";
//构造FutureTask,并且传入需要真正进行业务逻辑处理的类,该类一定是实现了Callable接口的类
FutureTask<String> futureTask1 = new FutureTask<String>(new UseFuture("query"));
FutureTask<String> futureTask2 = new FutureTask<String>(new UseFuture("QUERY"));
//创建一个固定线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
//submit和execute的区别: 第一点是submit可以传入实现Callable接口的实例对象, 第二点是submit方法有返回值
Future<?> s1 = executor.submit(futureTask1); // 任务被提交了
Future<?> s2 = executor.submit(futureTask2);
System.out.println("请求完毕");
//单独启动两个前程去执行任务
System.out.println(s1.get()); // 一定要等FutureTask处理完请求之后才能调用get()方法获取
System.out.println(s2.get()); // 返回值若为空,代表处理完成
new Thread(()->{
try {
// 要获取自己的线程执行的结果,是异步地区等待结果的返回 (主线程该怎么走就这么走,不影响)
System.out.println("数据1:" + futureTask1.get()); //get()方法是异步去获得数据的
System.out.println("数据2:" + futureTask2.get());
} catch (Exception e) {
e.printStackTrace();
} //这个get()方法是异步获取的
}).start();
try {
//这里可以做额外的数操作,也就是主程序执行其他的而业务逻辑
System.out.println("处理实际的业务逻辑....");
Thread.sleep(7000);
System.out.println("处理实际的业务逻辑 结束....");
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
仔细分析这个小程序,对比我们之前写的Future模式
信号量:
在Semaphore信号量非常适合高并发访问,在系统上线之前,要对系统的访问量进行评估,当然这个值肯定不是随便拍拍脑袋就想出来的,是经过以往的经验,数据,历年的访问量,以及推广力度进行一个合理的评估,当然评估标准不能太大也不能太小,太大的话投入的资源达不到实际效果,纯粹浪费资源,太小的话,某个时间点一个高峰值的访问量上来可以直接压垮系统
PV(page view) :
网站的总访问量,页面浏览量或点击量,用户没刷新一个就会被记录一次.
UV(unique Visitor) :
访问网站的电脑客户端为一个访客,一般来讲,时间上以00:00-24:00之内相同的ip客户端只记录一次.
QPS(query per second) :
即每秒查询数,qps很大程度上代表了系统业务上的繁忙程度,每次请求的背后,可能对应着多次磁盘I/O,多次网络请求,多个CPU时间片段等,我们通过qps可以非常
直观地了解当前系统业务情况,一旦当前qps超过所设定的预警阀值,可以考虑增加机器,对集群扩容,以免压力过大导致宕机,可以根据前期的压力测试得到的估值,
再结合后期综合运维情况,估算出阀值.
RT(response time) :
即请求的相应时间,这个指标非常关键,直接说明前端用户的体验,因此任何系统设计师都想降低rt时间.
当然还涉及cpu,内存,网络,磁盘等情况,更细节的问题很多,如select,update,delete/ps等数据库层面的统计 (ps:每秒访问的次数)
如何去解决高并发的?
1.网络端的.
2.服务端层面的.
3.解决高并发安全问题最重要的不是技术而是业务.在业务上进行严格的模块化.
4.java层面上限制流量 (一般用redis做限流得比较多)
案例代码:
public class UseSemaphore {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
//只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
//模拟20个客户端访问
for(int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
@Override
public void run() {
try {
//获取许可
semp.acquire();
System.out.println("Accessiong:" + NO + "-----" + semp.getQueueLength());
//模拟实际业务逻辑
Thread.sleep((long) (Math.random()*10000));
//f访问完之后,释放
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(run);
}
//退出线程池
executor.shutdown();
}
}
看打印结果:
Accessiong:1-----0
Accessiong:4-----0
Accessiong:0-----0
Accessiong:2-----0
Accessiong:3-----0
Accessiong:6-----14
Accessiong:7-----13
Accessiong:10-----12
Accessiong:11-----11
Accessiong:5-----10
Accessiong:8-----9
Accessiong:9-----8
Accessiong:12-----7
Accessiong:13-----6
Accessiong:14-----5
Accessiong:15-----4
Accessiong:16-----3
Accessiong:17-----2
Accessiong:18-----1
Accessiong:19-----0
最开始是5个线程可以同时进行,之后,那个线程执行完毕之后,等待着的线程抢到了时间片就去执行.
容量评估:
一般来说通过开发,运维,测试,以及业务等相关人员,综合处系统的一系列阀值,然后我们根据阀值,如qps,rt等,对系统进行有效的的变更.
一般来讲,我们进行多轮压力测试以后,可以对系统进行峰值评估,采用所谓的80/20原则,即80%的访问请求将在20%的时间内达到.这样我们可以根据系统对应的PV
计算出峰值qps.
峰值qps=(总pv*80%)/(60*60*24*20%)
锁:
在java多线程中,我们知道可以使用synchronized关键字来实现线程间的同步互斥工作,那么其实还有一个更优秀的机制去完成这个"同步互斥"工作,他就是
Lock对象,我们主要学习两种锁,重入锁和读写锁.他们具有比synchronized更为强大的功能,并且有嗅探锁定,多路分支等功能.
重入锁:
在需要进行同步的代码部分加上锁,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其他线程永远进不来的结果
一般使用lock都得有finally CopyOnWrite类底层的锁也是这样实现的
案例代码:
public class UseReentrantLock {
private Lock lock = new ReentrantLock();
public void method01() {
try {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method01");
Thread.sleep(1000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method01");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//必须要有unLock()
lock.unlock();
}
}
public void method02() {
try {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method02");
Thread.sleep(2000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method02");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//必须要有unLock()
lock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantLock url = new UseReentrantLock();
// 方法里面使用了Lock锁,一次就只能有一个线程进行操作
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
url.method01();
//url.method02();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//url.method01();
url.method02();
}
},"t2");
t1.start();
t2.start();
}
}
jdk1.8之前synchronized的性能比Lock的性能要差,但是jdk1.8之后,synchronized做了一系列优化,他的性能跟Lock差不多.Lock比synchronized要更加灵活
锁与等待/通知:
还记得我们在使用synchronized的时候,如果需要多线程间进行写作工作则需要Object的wait()和notify(),notifyAll()方法进行配合工作.
那么同样,我们在使用Lock的时候,可以使用一个新的等待/通知的类,它就是Condition.这个Condition一定是针对具体 某一把锁.也就是只有在锁的基础
上才会产生Condition
之前用synchronized的时候,有时候是需要自己new一个Object()对象出来作为锁,但是这个Object()就只是一个对象,jdk就提供了一个Lock类,里面进行了优化
案例代码:
public class UseCondition {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void method01() {
try {
lock.lock();
System.out.println("当前线程: " + Thread.currentThread().getName() + "进入等待状态...");
Thread.sleep(3000);
System.out.println("当前线程: " + Thread.currentThread().getName() + "释放锁...");
condition.await();
System.out.println("当前线程: " + Thread.currentThread().getName() + "继续执行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void method02() {
try {
lock.lock();
System.out.println("当前线程: " + Thread.currentThread().getName() + "进入等待状态...");
Thread.sleep(3000);
System.out.println("当前线程: " + Thread.currentThread().getName() + "发出唤醒...");
//并不会释放锁
condition.signal(); // 相当于 Object 的 notify (发出唤醒,但是等等这个线程走完代码释放锁之后,另一个线程才能继续执行)
//Thread.sleep(3000);
System.out.println("当前线程: " + Thread.currentThread().getName() + "继续执行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
UseCondition uc = new UseCondition();
new Thread(new Runnable() {
@Override
public void run() {
uc.method01();
}
},"t1").start();;
new Thread(new Runnable() {
@Override
public void run() {
uc.method02();
}
},"t2").start();;
}
}
多Condition:
我们可以通过一个Lock对象产生多个Condition进行多线程间的交互,非常的灵活.可以使得部分需要唤醒的线程唤醒,其他线程则继续等待通知
public class UseManyCondition {
private ReentrantLock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
public void m1(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m1等待..");
c1.await();
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m1继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m2(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m2等待..");
c1.await();
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m2继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m3(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m3等待..");
c2.await();
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m3继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m4(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
c1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m5(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseManyCondition umc = new UseManyCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
umc.m1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
umc.m2();
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
umc.m3();
}
},"t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
umc.m4();
}
},"t4");
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
umc.m5();
}
},"t5");
t1.start(); // c1
t2.start(); // c1
t3.start(); // c2
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t4.start(); // c1
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t5.start(); // c2
}
}
读写锁:
读写锁ReentrantWriteLock,其核心就是实现读写分离的锁.在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁.
之前写的synchronized,ReentrantLock时,我们知道,同一时间内只能有一个线程进行访问被锁定的代码,那么读写锁则不同,其本质是分成两个锁,即读锁,写锁.
在读锁下,多个线程可以并发地进行访问,但是在写锁的时候,只能一个一个的顺序访问.
口诀:读读共享,写写互斥,读写互斥.
public class UseReentrantReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ReadLock readLock = rwLock.readLock();
private WriteLock writeLock = rwLock.writeLock();
public void read() {
try {
readLock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write() {
try {
writeLock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
});
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
});
//两个都是读锁 能同时运行
/*t1.start();
t2.start();*/
//一个是读锁,一个是写锁,
/*t1.start();
t3.start();*/
//两个都是写锁,是互斥的
t3.start();
t4.start();
}
}