1、线程基础、线程之间的共享和协作
基础概念
什么是进程和线程
进程是程序运行资源分配的最小单位
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次 运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一 个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中 必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其 他的线程共享进程所拥有的全部资源。
线程无处不在
任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异 步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都 离不开线程和并发的知识。
CPU 核心数和线程数的关系
多核心:也指单芯片多处理器(Chip Multiprocessors,简称 CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理
多线程: Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线 程同步执行并共享处理器的执行资源。
核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也就是说四核CPU 一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程 数形成 1:2 的关系
CPU 时间片轮转机制
我们平时在开发的时候,感觉并没有受 cpu 核心数的限制,想启动线程就启 动线程,哪怕是在单核 CPU 上,为什么?这是因为操作系统提供了一种 CPU 时 间片轮转机制。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
百度百科对 CPU 时间片轮转机制原理解释如下:
如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则 CPU 当即进行切换。调度程序所要做的 就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾
时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一 个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队 列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要 5ms, 再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行 进程切换。CPU 时间的 20%被浪费在了管理开销上了。
为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的 进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要 5 才能 做出响应,同样的问题在一台支持多道程序的个人计算机上也会发
结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常 是一个比较合理的折衷。
在 CPU 死机的情况下,其实大家不难发现当运行一个程序的时候把 CPU 给弄 到了 100%再不重启电脑的情况下,其实我们还是有机会把它 KⅢ掉的,我想也正是 因为这种机制的缘故。
澄清并行和并发
我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车 辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可 以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线 程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而 每个高速公路并排有多个车道。
当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。
俗话说,一心不能二用,这对计算机也一样,原则上一个 CPU 只能分配给一个 进程,以便运行这个进程。我们通常使用的计算机中只有一个 CPU,也就是说只有 一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术 相当复杂,最容易理解的是“时间片轮转进程调度算法”。
综合来说:
并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是 同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不 断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太 快,我们无法察觉到而已.
并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行.
高并发编程的意义、好处和注意事项
由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越受重视和关注。 多线程可以给程序带来如下好处。
- 充分利用 CPU 的资源
从上面的 CPU 的介绍,可以看的出来,现在市面上没有 CPU 的内核不使用多线 程并发机制的,特别是服务器还不止一个 CPU,如果还是使用单线程的技术做思路, 明显就 out 了。因为程序的基本调度单元是线程,并且一个线程也只能在一个 CPU 的一个核的一个线程跑,如果你是个 i3 的 CPU 的话,最差也是双核心 4 线程的运算 能力:如果是一个线程的程序的话,那是要浪费 3/4 的 CPU 性能:如果设计一个多线 程的程序的话,那它就可以同时在多个 CPU 的多个核的多个线程上跑,可以充分地 利用 CPU,减少 CPU 的空闲时间,发挥它的运算能力,提高并发量。
就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为 了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为 什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的 时候可以并发地去做几件事情,充分利用我们的时间,CPU 也是一样,也要充分利用。
- 加快响应用户的时间
比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个 线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。
我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应 时间若提升 1s,如果流量大的话,就能增加不少转换量。做过高性能 web 前端调优 的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域 名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网 站的响应速度。多线程,高并发真的是无处不在。
- 可以使你的代码模块化,异步化,简单化
例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分, 将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。 这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。
多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体 会它的魅力。
多线程程序需要注意事项
- 线程之间的安全性
从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就 是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、 静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多 个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
- 线程之间的死锁
为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必 须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待 获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。这只是人为设计的例子,但尽 管在运行时很难探测到,这类情况却时常发生
- 线程太多了会将服务器资源耗尽形成死机当机
线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?
某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个 线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线 程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接 池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返 回池中。资源池也称为资源库。
多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它 的危险所在。
认识 Java 里的线程
Java 程序天生就是多线程的
一个 Java 程序从main()
方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行main()
方法的是一个名称为main
的线程。
public class OnlyMain {
public static void main(String[] args) {
//Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] "
+ threadInfo.getThreadName());
}
}
}
[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的
[5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
[4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程
[3] Finalizer // 调用对象 finalize 方法的线程
[2] Reference Handler //清除 Reference 的线程
[1] main //main 线程,用户程序入口
线程的状态
Java中线程的状态分为6种:
- 初始(
NEW
):新创建了一个线程对象,但还没有调用start()
方法。 - 运行(
RUNNABLE
):Java线程中将就绪(ready
)和运行中(running
)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main
线程)调用了该对象的start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU
的使用权,此时处于就绪状态(read
y)。就绪状态的线程在获得CPU
时间片后变为运行中状态(running
)。 - 阻塞(
BLOCKED
):表示线程阻塞于锁。 - 等待(
WAITING
):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取
monitor lock
。而等待是主动的,通过调用Object.wait()
等方法进入。 - 超时等待(
TIMED_WAITING
):该状态不同于WAITING
,它无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。调用
Thread.sleep()
方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用Object.wait()
方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 - 终止(
TERMINATED
):表示该线程已经执行完毕。
状态之间的变迁如下图所示
线程的启动与中止
启动
启动线程的方式有:
- X extends Thread;,然后 X.start
- X implements Runnable;然后交给 Thread 运行
public class NewThread {
/*扩展自Thread类*/
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
// do my work;
System.out.println("I am extendec Thread");
}
}
/*实现Runnable接口*/
private static class UseRunnable implements Runnable{
@Override
public void run() {
// do my work;
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
useThread.start();
useThread.start();
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
}
}
Thread 和 Runnable 的区别
Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑) 的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
中止
-
线程自然终止
要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。 -
stop
暂停、恢复和停止操作对应在线程 Thread 的 API 就是suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方 法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资 源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方 法。 -
中断
安全的中止则是其他线程通过调用某个线程 A 的interrupt()
方法对其进行中 断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 因为 java 里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志 位是否被置为 true 来进行响应,
中断
InterruptedException
通过调用一个线程的interrupt()
来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出InterruptedException
,从而提前结束该线程。但是不能中断I/O阻塞和synchronized
锁阻塞。
对于以下代码,在main()
中启动一个线程之后再中断它,由于线程中调用了Thread.sleep()
方法,因此会抛出一个InterruptedException
,从而提前结束线程,不执行之后的语句。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
线程通过方法isInterrupted()
来进行判断是否被中断,也可以调用静态方法Thread.interrupted()
来进行判断当前线程是否被中断,不过Thread.interrupted()
会同时将中断标识位改写为 false。
isInterrupted()
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start interrupt flag = "+isInterrupted());
while (!isInterrupted()){
System.out.println(threadName + " is running");
System.out.println(threadName + " inner interrupt flag = "+isInterrupted());
}
System.out.println(threadName + " end interrupt flag = "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new UseThread();
thread.start();
Thread.sleep(20);
thread.interrupt();
}
Thread-0 start interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
......
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 end interrupt flag = true
interrupted()
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start interrupt flag = "+isInterrupted());
while (!Thread.interrupted()){
System.out.println(threadName + " is running");
System.out.println(threadName + " inner interrupt flag = "+isInterrupted());
}
System.out.println(threadName + " end interrupt flag = "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new UseThread();
thread.start();
Thread.sleep(20);
thread.interrupt();
}
Thread-0 start interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
......
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 end interrupt flag = false
如果一个线程处于了阻塞状态(如线程调用了thread.sleep
、thread.join
、thread.wai
t等),则在线程在检查中断标示时如果发现中断标示为true
,则会在这些阻塞方法调用处抛出InterruptedException
异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false
。
不建议自定义一个取消标志位来中止线程的运行。因为run
方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。这种情况下,使用中断会更好,因为:
- 一般的阻塞方法,如
sleep
等本身就支持中断的检查, - 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
中断异常处理
private static class InterruptedExceptionThread extends Thread{
@Override
public void run() {
while(!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is " +isInterrupted());
//资源释放
// interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName() +" interrupt flag is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
InterruptedExceptionThread thread = new InterruptedExceptionThread();
thread.start();
Thread.sleep(500);
thread.interrupt();
}
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 in InterruptedException interrupt flag is false
Thread-0 I am extends Thread.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.zxj.test.InterruptExample$InterruptedExceptionThread.run(InterruptExample.java:47)
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
......
可以看到发生中断异常后,线程还是回继续执行的。我们可以看到在阻塞方法捕捉到异常后的打印isInterrupted()
为false
,说明在调用interrupt()
方法,捕捉到异常后会把中断标志位又true
改为false
。如果这种情况,我们确实要中断线程怎么办呢?只需要在catch里在手动调用interrupt();
方法(打开上面代码的注释)。
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 in InterruptedException interrupt flag is false
Thread-0 I am extends Thread.
Thread-0 interrupt flag is true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.zxj.test.InterruptExample$InterruptedExceptionThread.run(InterruptExample.java:47)
对 Java 里的线程再多一点点认识
深入理解 run()和 start()
Thread
类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()
其实只是new出一个Thread
的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了start()
方法后,才实现了真正意义上的启动线程。
start()
方法让一个线程进入就绪队列等待分配cpu
,分到cpu
后才调用实现 的 run()
方法,start()
方法不能重复调用,如果重复调用会抛出异常。
而run
方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用。
其他的线程相关方法
yield()方法
使当前线程让出CPU
占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )
的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。
所有执行yield()
的线程有可能在进入到就绪状态后会被操作系统再次选中 马上又被执行。
wait()/notify()/notifyAll()
:后面会单独讲述
join 方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续 执行线程 B。
面试题
调用yield() 、sleep()、wait()、notify()
等方法对锁有何影响?
yield()
、sleep()
被调用后,都不会释放当前线程所持有的锁。
调用wait()
方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait
方法后面的代码。
调用notify()
系列方法后,对锁无影响,线程只有在syn
同步代码执行完后才会自然而然的释放锁,所以 notify()
系列方法一般都是syn同步代码的最后一行。
线程的优先级
在Java
线程中,通过一个整型成员变量priority
来控制优先级,优先级的范 围从1~10,在线程构建的时候可以通过 setPriority(int)
方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM
以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非Daemon
线程的时候,Java
虚拟机将会退出。可以通过调用Thread.setDaemon(true)
将线程设置为Daemon
线程。我们一般用不上,比如垃圾回收线程就是Daemon
线程。
public static void main(String[] args) throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
//useThread.setDaemon(true);// 设置为守护线程
useThread.start();
Thread.sleep(5);
}
private static class UseThread extends Thread{
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()+ " interrupt flag is " + isInterrupted());
} finally {
//守护线程中finally不一定起作用
System.out.println(" .............finally");
}
}
}
上面当我们没有设置守护线程时,线程是会一直运行着,但是当我们设置了守护线程时,随着main
线程执行玩,守护线程也会停止掉。
Daemon
线程被用作完成支持性工作,但是在Java
虚拟机退出时Daemon
线程中的finally
块并不一定会执行。在构建Daemon
线程时,不能依靠finally
块中 的内容来确保执行关闭或清理资源的逻辑。
线程间的共享和协作
线程间的共享
synchronized 内置锁
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线 程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量 访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class
对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class
对象,所以不同对象实例的对象锁是互不干扰的,但是 每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class
对象。类锁和对象锁之间也是互不干扰的。
错误的加锁和原因分析
但是当我们反编译这个类的 class 文件后,可以看到 i++实际是,
本质上是返回了一个新的Integer
对象。也就是每个线程实际加锁的是不同的Integer
对象。
ReentrantLock锁
ReentrantLock
,一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。(重入锁后面介 绍)
1.Lock接口
Lock
,锁对象。在Java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁,后面我们会分析)。在Lock接口出现之前,Java程序是靠synchronized
关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了Lock
接口用来实现锁的功能,它提供了与synchronized
关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized
那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized
关键字所不具备的同步特性。
Lock接口的主要方法(还有两个方法比较复杂,暂不介绍):
void lock(): 执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
boolean tryLock(): 如果锁可用,则获取锁,并立即返回true
,否则返回false.该方法和lock()
的区别在于,tryLock()
只是"试图"获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()
方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行.通常采用如下的代码形式调用tryLock()
方法:
void unlock(): 执行此方法时,当前线程将释放持有的锁.锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.Condition newCondition()
:条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()
方法,而调用后, 当前线程将缩放锁。
2.ReentrantLock的使用
关于ReentrantLock
的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
.....................
lock.lock();
//如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
} finally {
lock.unlock(); //释放锁
}
synchronized和ReentrantLock比较
1. 锁的实现
synchronized
是JVM
实现的,而ReentrantLock
是JDK
实现的。
2. 性能
新版本Java
对synchronized
进行了很多优化,例如自旋锁等,synchronized
与ReentrantLock
大致相同。
在JDK1.5中,
synchronized
是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock
对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize
加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize
的性能并不比Lock差。官方也表示,他们也更支持synchronize
,在未来的版本中还有优化余地,所以还是提倡在synchronized
能实现需求的情况下,优先考虑使用synchronized
来进行同步。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock
可中断,而synchronized
不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个ReentrantLock
可以同时绑定多个Condition
对象。
使用选择
除非需要使用ReentrantLock
的高级功能,否则优先使用synchronized
。这是因为synchronized
是JVM
实现的一种锁机制,JVM
原生地支持它,而ReentrantLock
不是所有的JDK
版本都支持。并且使用synchronized
不用担心没有释放锁而导致死锁问题,因为JVM
会确保锁的释放。
volatile,最轻量的同步机制
volatile
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。参见代码:
public class VolatileCase {
private static boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready);
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
不加volatile
时,子线程无法感知主线程修改了ready
的值,从而不会退出循环,而加了volatile
后,子线程可以感知主线程修改了ready的值,迅速退出循环。 但是volatile
不能保证数据在多个线程下同时写时的线程安全,参见代码:
public class NotSafe {
private volatile long count =0;
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
//count进行累加
public void incCount(){
count++;
}
//线程
private static class Count extends Thread{
private NotSafe simplOper;
public Count(NotSafe simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();
}
}
}
public static void main(String[] args) throws InterruptedException {
NotSafe simplOper = new NotSafe();
//启动两个线程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);
}
}
volatile 最适用的场景:一个线程写,多个线程读。
ThreadLocal 辨析
与 Synchonized 的比较
ThreadLocal
和Synchonized
都用于解决多线程并发訪问。可是ThreadLocal
与synchronized
有本质的差别。synchronized
是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal
为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
ThreadLocal的使用
ThreadLocal
类接口很简单,只有4个方法,我们先来了解一下:
void set(Object value)
设置当前线程的线程局部变量的值。
public Object get()
该方法返回当前线程所对应的线程局部变量。
public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected
的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()
或set(Object)
时才执行,并且仅执行1次。ThreadLocal
中的缺省实现直接返回一个null
。
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>()
;RESOURCE
代表一个能够存放String类型的ThreadLocal
对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
实现解析
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面先取到当前线程,然后调用getMap
方法获取对应的ThreadLocalMap
,ThreadLocalMap
是ThreadLocal
的静态内部类,然后Thread
类中有一个这样类型成员,所以getMap
是直接返回Thread
的成员。
看下ThreadLocal
的内部类ThreadLocalMap
源码:
可以看到有个Entry
内部静态类,它继承了WeakReference
,总之它记录了两个信息,一个是ThreadLocal<?>
类型,一个是Object
类型的值。getEntry
方法则是获取某个ThreadLocal
对应的值,set
方法就是更新或赋值相应的ThreadLocal
对应的值。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
回顾我们的get
方法,其实就是拿到每个线程独有的ThreadLocalMap
然后再用ThreadLocal
的当前实例,拿到Map
中的相应的Entry
,然后就可以拿到相应的值返回出去。当然,如果Map
为空,还会先进行map
的创建,初始化等工作。
引发的内存泄漏分析
预备知识
引用
Object o = new Object();
这个o
,我们可以称之为对象引用,而new Object()
我们可以称之为在内存中产生了一个对象实例。
当写下o=null
时,只是表示o
不再指向堆中object
的对象实例,不代表这个对象实例不存在了。
强引用:就是指在程序代码之中普遍存在的,类似“Object obj=new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用:是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行 第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱 引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了 WeakReference 类来实现弱引用。
虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象 实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用 来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象 实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
内存泄漏的现象
执行下面的ThreadLocalOOM
,并将堆内存大小设 置为-Xmx256m
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
new LocalVariable();
System.out.println("use local varaible");
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
我们启用一个线程池,大小固定为 5 个线程
场景 1,首先任务中不执行任何有意义的代码,当所有的任务提交执行完成 后,可以看见,我们这个应用的内存占用基本上为 25M 左右
场景 2,然后我们只简单的在每个任务中 new 出一个数组,执行完成后我们 可以看见,内存占用基本和场景 1 同
场景 3,当我们启用了 ThreadLocal 以后:
执行完成后我们可以看见,内存占用变为了 100M 左右
场景 4,于是,我们加入一行代码,再执行,看看内存情况:
可以看见,内存占用基本和场景 1 同。
这就充分说明,场景 3,当我们启用了ThreadLocal
以后确实发生了内存泄 漏。
分析
根据我们前面对ThreadLocal
的分析,我们可以知道每个Thread
维护一个 ThreadLocalMap
,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需 要存储的Object
,也就是说ThreadLocal
本身并不存储值,它只是作为一个 key 来让线程从ThreadLocalMap
获取value
。仔细观察ThreadLocalMap
,这个 map 是使用ThreadLocal
的弱引用作为Key
的,弱引用的对象在GC
时会被回收。
因此使用了ThreadLocal
后,引用链如图所示
图中的虚线表示弱引用。
这样,当把threadlocal
变量置为null
以后,没有任何强引用指向threadlocal
实例,所以threadlocal
将会被gc
回收。这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些 key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
,而这块value
永远不会被访问到了,所以存在着内存泄露。
只有当前thread
结束以后,current thread
就不会存在栈中,强引用断开,Current Thread、Map value
将全部被GC回收。最好的做法是不在需要使用ThreadLocal
变量后,都调用它的remove()
方法,清除数据。
所以回到我们前面的实验场景,场景 3 中,虽然线程池里面的任务执行完毕 了,但是线程池里面的 5 个线程会一直存在直到JVM
退出,我们set
了线程的 localVariable
变量后没有调用localVariable.remove()
方法,导致线程池里面的 5 个 线程的threadLocals
变量里面的new LocalVariable()
实例没有被释放。
其实考察ThreadLocal
的实现,我们可以看见,无论是get()
、set()
在某些时 候,调用了expungeStaleEntry
方法用来清除Entry
中Key
为null
的Value
,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()
方法中显式调用了expungeStaleEntry
方法。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:引用ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
的对象实例不会 被回收,导致Entry
内存泄漏。
key 使用弱引用:引用的ThreadLocal
的对象被回收了,由于 ThreadLocalMap 持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
的对象实例也会被 回收。value
在下一次ThreadLocalMap
调用set
,get
,remove
都有机会被回收。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
总结
JVM利用设置ThreadLocalMap
的Key为弱引用,来避免内存泄露。
JVM利用调用remove、get、set
方法的时候,回收弱引用。
当ThreadLocal
存储很多Key为null的Entry的时候,而不再去调用remove、 get、set
方法,那么将导致内存泄漏。
使用线程池+ThreadLocal时要小心,因为这种情况下,线程是一直在不断的 重复运行的,从而也就造成了value可能造成累积的情况。
错误使用 ThreadLocal 导致线程不安全
public class ThreadLocalUnsafe implements Runnable {
public static Number number = new Number(0);
public void run() {
//每个线程计数加一
number.setNum(number.getNum()+1);
//将其存储到ThreadLocal中
value.set(number);
SleepTools.ms(2);
//输出num值
System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
}
运行后的结果为
如果我们加入SleepTools.ms(2);
会看的更明显
为什么每个线程都输出5?难道他们没有独自保存自己的Number
副本吗?为什么其他线程还是能够修改这个值?仔细考察ThreadLocal
和Thead
的代码,我们发现ThreadLocalMap
中保存的其实是对象的一个引用,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是同一Number
对象的引用,在线程睡眠的时候,其他线程将num
变量进行了修改,而修改的对象Number
的实例是同一份,因此它们最终输出的结果是相同的。
而上面的程序要正常的工作,应该的用法是让每个线程中的ThreadLocal
都应该持有一个新的Number
对象。
- 去掉成员变量
Number
的static
public Number number = new Number(0);
- 在创建
ThreadLocal
时给个初始值
public static ThreadLocal<Number> value = new ThreadLocal<Number>(){
@Override
protected Number initialValue() {
return new Number(0);
}
};
线程间的协作
线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在while
循环中设置不满足的条件,如果条件满足则退出while
循环,从而完成消费者的工作。却存在如下问题:
- 难以确保及时性。
- 难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。
等待/通知机制
是指一个线程A调用了对象O的wait()
方法进入等待状态,而另一个线程B调用了对象O的notify()
或者notifyAll()
方法,线程A收到通知后从对象O的wait()
方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()
和notify/notifyAll()
的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
notify():
通知一个在对象上等待的线程,使其从wait
方法返回,而返回的前提是该线程 获取到了对象的锁,没有获得锁的线程重新进入WAITING
状态。
notifyAll():
通知所有等待在该对象上的线程
wait()
调用该方法的线程进入WAITING
状态,只有等待另外线程的通知或被中断 才会返回.需要注意,调用wait()
方法后,会释放对象的锁
wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有 通知就超时返回
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式
等待方遵循如下原则。
- 获取对象的锁。
- 如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
-
条件满足则执行对应的逻辑。
通知方遵循如下原则。
- 获得对象的锁。
- 改变条件。
-
通知所有等待在对象上的线程。
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进入wait()
方法后,当前线程释放锁,在从wait()
返回前,线程与其他线程竞争重新获得锁,执行notify()
系列方法的线程退出调用了notifyAll
的synchronized
代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized
代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
notify 和 notifyAll 应该用谁
尽可能用notifyall()
,谨慎使用notify()
,因为notify()
只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
等待超时模式实现一个连接池
调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
假设等待时间段是T,那么可以推断出在当前时间now+T
之后就会超时
等待持续时间:REMAINING=T
。
超时时间:FUTURE=now+T
。
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
// 当超时大于 0 并且 result 返回值不满足要求
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
DBPool.java
public class DBPool {
private static LinkedList<Connection> pool = new LinkedList<Connection>();
public DBPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
}
public void releaseConnection(Connection connection) {
if (connection != null) {
//TODO
}
}
// 在mills内无法获取到连接,将会返回null
public Connection fetchConnection(long mills) throws InterruptedException {
//TODO
return null;
}
}
DBPoolTest.java
public class DBPoolTest {
static DBPool pool = new DBPool(10);
// 控制器:控制main线程将会等待所有Woker结束后才能继续执行
static CountDownLatch end;
public static void main(String[] args) throws Exception {
// 线程数量
int threadCount = 50;
end = new CountDownLatch(threadCount);
int count = 20;//每个线程的操作次数
AtomicInteger got = new AtomicInteger();//计数器:统计可以拿到连接的线程
AtomicInteger notGot = new AtomicInteger();//计数器:统计没有拿到连接的线程
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new Worker(count, got, notGot),
"worker_"+i);
thread.start();
}
end.await();// main线程在此处等待
System.out.println("总共尝试了: " + (threadCount * count));
System.out.println("拿到连接的次数: " + got);
System.out.println("没能连接的次数: " + notGot);
}
static class Worker implements Runnable {
int count;
AtomicInteger got;
AtomicInteger notGot;
public Worker(int count, AtomicInteger got,
AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
public void run() {
while (count > 0) {
try {
// 从线程池中获取连接,如果1000ms内无法获取到,将会返回null
// 分别统计连接获取的数量got和未获取到的数量notGot
Connection connection = pool.fetchConnection(1000);
if (connection != null) {
try {
connection.createStatement();
connection.commit();
} finally {
pool.releaseConnection(connection);
got.incrementAndGet();
}
} else {
notGot.incrementAndGet();
System.out.println(Thread.currentThread().getName()
+"等待超时!");
}
} catch (Exception ex) {
} finally {
count--;
}
}
end.countDown();
}
}
}
客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。
它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用fetchConnection(long)
方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用releaseConnection(Connection)
方法将连接放回线程池