1.线程简介
1.1 为什么要使用多线程
- 更多的处理器核心:现在大多数计算机都比以往更加擅长并行计算
- 更快的响应时间:可以将数据一致性不强的操作派发给其他线程处理,使响应用户请求的线
程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验 - 更好的编程模型:一旦开发人员建立好了模型,稍做修改就能方便地映射到Java提供的多线程编程模型上
1.2 线程优先级
-
线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性
-
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级
-
优先级不能作为程序正确性的依赖(操作系统可以不用理会Java线程对于优先级的设定)
1.3 线程的状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
阻塞在java.concurrent包中Lock接口的线程状态却是等待状态
1.4 Daemon线程
-
主要被用作程序中后台调度以及支持性工作
-
JVM中不存在非Daemon线程的时候,Java虚拟机将会退出(即使Daemon线程有finally代码块也不会执行)
-
通过调用
Thread.setDaemon(true)
将线程设置为Daemon线程(需要在启动线程前设置)
2.启动和终止线程
2.1 构造线程
- 新构造的线程对象是由其parent线程来进行空间分配的
- child线程继承了parent是否为Daemon、优先级和加载资源的
contextClassLoader
以及可继承的ThreadLocal
,同时还会分配一个唯一的ID来标识这个child线程
2.2 启动线程
线程start()方法的含义是:当前线程(即parent线程)同步告知JVM只要线程规划器空闲,立即启动调用start()方法的线程
2.3 理解中断
-
中断状态可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作
-
其他线程通过调用某线程的interrupt()方法对其进行中断操作
-
线程通过检查自身是否被中断来进行响应,线程通过方法
isInterrupted()
来进行判断是否
被中断(true表示被中断,清除标志位表示置为false),也可以调用静态方法Thread.interrupted()
对当前线程的中断标识位复位 -
比如
Thread.sleep(longmillis)
等在声明中抛出InterruptedException
的方法,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException
2.4 过期的suspend()、resume()和stop()
- 在调用suspend()方法后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题
- stop()方法在终结一个线程时不会保证线程的资源正常释放
2.4 安全地终止线程
-
使用中断操作:
public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); // 执行该方法后,下面的isInterrupted()返回true } }
private static class Runner implements Runnable { private long i; @Override public void run() { while (!Thread.currentThread().isInterrupted()){ i++; } System.out.println("Count i = " + i); } }
-
使用标识位:
public static void main(String[] args) throws Exception { Runner two = new Runner(); countThread = new Thread(two, "CountThread"); countThread.start(); // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束 TimeUnit.SECONDS.sleep(1); two.cancel(); // 执行该方法使得on=false,进而退出while循环 }
private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on){ i++; } System.out.println("Count i = " + i); } public void cancel() { on = false; } }
3.线程间通信
3.1 volatile和synchronized关键字
-
volatile:
- 可以用来修饰字段(成员变量)
- 告知程序任何对该变量的访问均需要从共享内存中获取,而对变量的改变必须同步刷新回共享内存
- 保证所有线程对变量访问的可见性(不保证原子性,但保证一致性)
-
synchronized:
-
修饰方法或者以同步块的形式来进行使用
-
主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中
-
保证了线程对变量访问的可见性和排他性
-
使用该关键字对方法/代码块进行加锁,本质是对一个对象的监视器进行获取
-
对于同步块的实现使用了
monitorenter
和monitorexit
指令;同步方法依靠方法修饰符上的ACC_SYNCHRONIZED
来完成
-
3.2 等待/通知机制
一个线程(生产者)修改了一个对象的值,而另一个线程(消费者)感知到了变化,然后进行相应的操作。对于消费者线程需要执行以下代码判断被生产者修改的变量是否符合预期:
while (value != desire) {
Thread.sleep(1000);
}
doSomething();
但是会出现以下问题:
- 难以确保及时性:如果睡眠时间设置过大,则不能即使发现条件的变化
- 难以降低开销:如果降低睡眠的时间,消费者能更加迅速地发现条件变化,但消耗更多的处理器资源
上述问题通过内置的等待和通知机制解决
等待/通知机制:指一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作
- WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态
- 由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法(通知时不会释放对象的锁),将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态(从等待队列移到同步队列,状态也从WAITING变为BLOCKED)
- NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行(从wait()方法返回的前提是获得对象锁)
等待方(消费者)和通知方(生产者)满足以下范式:
// 消费者
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
// 生产者
synchronized(对象) {
改变条件
对象.notifyAll(); // 此时不会立即释放锁
}
3.3 管道输入/输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于它主要用于线程之间的数据传输,而传输的媒介为内存
3.4 Thread.join()的使用
如果一个线程A执行了
thread.join()
语句,表示当前线程A等待thread线程终止之后才从thread.join()
返回
3.5 ThreadLocal的使用
- ThreadLocal即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构
- 一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
4.线程应用实例
4.1 等待超时模式
当方法执行时间过长,不会“永久”阻塞调用者,而是会按照调用者的要求“按时”返回
// 对当前对象加锁
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;
}
4.2 简单的数据库连接池示例
模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null
public class ConnectionPool {
private LinkedList<Connection> pool = new LinkedList<Connection>();
public ConnectionPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(ConnectionDriver.createConnection());
}
}
}
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (pool) {
// 连接释放后需要进行通知,这样其他消费者能够感知到连接池中已经归还了一个连接
pool.addLast(connection);
pool.notifyAll();
}
}
}
// fetchConnection方法:指定在多少毫秒内超时获取连接
// 在mills内无法获取到连接,将会返回null
public Connection fetchConnection(long mills) throws InterruptedException {
synchronized (pool) {
// 完全超时
if (mills <= 0) {
while (pool.isEmpty()) {
pool.wait();
}
return pool.removeFirst();
} else {
// 等待-超时模式
long future = System.currentTimeMillis() + mills;
long remaining = mills;
while (pool.isEmpty() && remaining > 0) {
pool.wait(remaining);
remaining = future - System.currentTimeMillis();
}
Connection result = null;
if (!pool.isEmpty()) {
result = pool.removeFirst();
}
return result;
}
}
}
}
4.3 线程池技术
-
面对大量的任务递交进服务器时,如果采用一个任务一个线程的方式,则会创建大量线程,这会使操作系统频繁的进行线程上下文切换,增加系统的负载,而线程的创建和消亡都是需要耗费系统资源的,浪费系统资源
-
线程池能预先创建了若干数量的线程,并且不能由用户直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务的执行
-
线程池的本质:
- 使用了一个线程安全的工作队列连接工作者线程和客户端线程
- 客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行
- 当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程