文章目录
线程
为什么要使用多线程:
- 更多的处理核心
- 更快的响应时间
- 更好的编程模型
线程优先级
Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5。针对频繁阻塞的(休眠或者IO操作)的线程需要设置较高优先级,而偏重计算(需要较多的CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。但程序正确性不能依赖线程的优先级高低,因为操作系统完全可以不用理会Java线程对于优先级的设定。
线程的状态
- NEW:初始状态,线程被创建,但是还没有调用start()方法
- RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
- TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
- TERMINATED:终止状态,表示当前进程已经执行完毕
线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态,当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。
Daemon线程(守护线程)
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作,这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,可以通过调用Thread.setDarmon(tryue)将线程设置为Daemon线程。
Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。Daemon线程之中的代码不一定会全部执行,即使是finally块中的代码。所以在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
过期的suspend()、resume()和stop()
suspend()在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
安全地终止线程
线程通过中断操作和cancel()方法均可使CountThread得意终止。这种通过标识位(cancel()方法)或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法闲得更加安全和优雅。
cancel()方法:在线程类中设置一个私有volatile boolean变量并通过cancel()方法改变boolean变量的值间接控制线程的终止。
线程间通信
等待通知机制
一个线程修改了一个对象的值,另一个线程感知到了变化,然后进行相应的操作,前者是生产者,后者是消费者,在功能层面上实现了解耦,体系结构也具备了良好的伸缩性,但是如何在Java语言中实现类似的功能呢?
简单的方法是让消费者线程不断地循环检查变量是否符合预期,如下面代码所示,在while循环中设置了不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。
while(value != desire){
Thread.sleep(1000);
}
doSomething();
上面这段伪代码在条件不满足时就睡眠一段时间,这样做的目的是防止过快的“无效”尝试,这种方式看似能够实现所需的功能,但是却存在如下问题。
- 难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡得过久,就不能发现条件变化,也就是及时性难以保证。
- 难以降低开销。如果降低睡眠时间,这样消费者可以更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。
两个问题看似难以调和,然而通过Java内置的等待/通知机制能够很好地解决这个矛盾并实现所需的功能。
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上。
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wai()方法后,会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待时间长达n毫秒,如果没有通知就超时返回 |
wait(long,int) |
调用wait()、notify()、notifyAll()时需要注意的细节:
- 使用wait()、notify()、notifyAll()需要先对调用对象加锁。
- 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
- notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
- notify()方法等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态WAITING变为BLOCKED。
- 从wait()方法返回的前提是获得了调用对象的锁。
等待通知机制依赖于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到线程对变量做出的修改。
等待/通知的经典范式
等待方(消费者)遵循如下原则:
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下。
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方(生产者)遵循如下原则。
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象下的线程。
对应的伪代码如下。
synchronized(对象){
改变条件
对象.notifyAll();
}
线程应用实例
等待超时模式
开发人员经常会遇到这样的方法调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
超时等待只需要对经典范式做出非常小的改动,改动内容如下:
假设超时时间段是T,那么可以推断在当前时间now+T之后就会超时。
定义如下:
- 等待持续时间:REMANING=T
- 超时时间:FUTURE=now+T
这时仅需要wait(REMAINING)即可,在wait(REMAINING)返回之后会将执行:REMAINING=FUTURE-now。如果REMAINING小于等于0,表示已经超时,直接退出,否则将继续执行wait(REMAINING)。
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
等待超市模式就是在等待/通知范式基础上增加了超时控制,这使得该模式相比原有范式更具有灵活性,因为即使方法执行时间过长,也不会“永久”阻塞调用者,而是会按照调用者的要求“按时”返回。
一个简单的数据库连接池示例
(暂略,有空补上)
一个基于线程池技术的简单Web服务器
package com.makerhub;
import org.springframework.web.HttpRequestHandler;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import static cn.hutool.db.DbUtil.close;
public class SimpleHttpServer {
// 处理 HttpRequest 的线程池
static ThreadPool<HttpRequestHandler> threadPool = new DefaultThreadPool<HttpRequestHandler>(1);
// SimpleHttpServer 的根路径
static String basePath;
static ServerSocket serverSocket;
// 服务监听端口
static int port = 8080;
public static void setPort(int port){
if (port > 0) {
SimpleHttpServer.port = port;
}
}
public static void setBasePath(String basePath){
if (basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()){
SimpleHttpServer.basePath = basePath;
}
}
// 启动SimpleHttpServer
public static void start() throws Exception{
serverSocket = new ServerSocket(port);
Socket socket = null;
while ((socket = serverSocket.accept()) != null) {
// 接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行
threadPool.excute(new HttpRequestHandler(socket));
}
serverSocket.close();
}
static class HttpRequestHandler implements Runnable {
private Socket socket;
public HttpRequestHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
String line = null;
BufferedReader br = null;
BufferedReader reader = null;
PrintWriter out = null;
InputStream in = null;
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String header = reader.readLine();
// 由相对路径计算出绝对路径
String filePath = basePath + header.split(" ")[1];
out = new PrintWriter(socket.getOutputStream());
// 如果请求资源的后缀为jpg或者ico,则读取资源并输出
if (filePath.endsWith("jpg") || filePath.endsWith("ico")) {
in = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1){
baos.write(i);
}
byte[] array = baos.toByteArray();
out.println("HTTP/1.1 200 OK");
out.println("Server: Molly");
out.println("Content-Type: image/jpeg");
out.println("Content-Length: " + array.length);
out.println("");
socket.getOutputStream().write(array, 0, array.length);
} else {
br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
out = new PrintWriter(socket.getOutputStream());
out.println("HTTP/1.1 200 OK");
out.println("Server: Molly");
out.println("Content-Type: text/html; charset=UTF-8");
out.println("");
while ((line = br.readLine()) != null){
out.println(line);
}
}
out.flush();
} catch (Exception ex){
out.println("HTTP/1,1 500");
out.println("");
out.flush();
} finally {
close(br, in, reader, out, socket);
}
}
}
private static void close(Closeable... closeables) {
if (closeables != null) {
for (Closeable closeable : closeables){
try {
closeable.close();
} catch (Exception exception) {
}
}
}
}
}
SimpleHttpServer在建立了与客户端的连接之后,并不会处理客户端的请求,而是将其包装成HttpRequestHandler并交由线程池处理。在线程池中的Worker处理客户请求的同时,SimpleHttpServer能够继续完成后续客户端连接的建立,不会阻塞后续客户端的请求。
测试页面如下:
<html>
<head>
<title>测试页面</title>
</head>
<body align="center">
<h1>第一张图片</h1>
<img src="1.jpg" align="middle"/>
<h1>第二张图片</h1>
<img src="2.jpg" align="middle"/>
<h1>第三张图片</h1>
<img src="3.jpg" align="middle"/>
</body>
</html>
将SimpleHttpServer的根目录设定到该HTML页面所在目录,并启动SimpleHttpServer,通过Apache HTTP server benchmarking tool(版本2.3)来测试不同线程数下,SimpleHttpServer的吞吐量表现。
测试场景是5000次请求,分10个线程并发执行,测试内容主要考察响应时间(越小越好)和每秒查询的数量(越高越好),测试结果如表所示。
线程池线程数量 | 1 | 5 | 10 |
---|---|---|---|
响应时间(ms) | 0.352 | 0.246 | 0.163 |
每秒查询的数量 | 3076 | 4065 | 6123 |
测试完成时间(s) | 1.625 | 1.230 | 0.816 |
可以看到,随着线程池中线程数量的增加,SimpleHttpServer的吞吐量不断增长,响应时间不断变小,线程池的作用非常明显。
但是,线程池中数量并不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加系统的无故开销,起到相反的作用。