本文参考于《Java并发编程的艺术》
1、 线程简介
1.1、什么是线程?
现代操作系统调度的最小单元是线程,也叫轻量级进程
,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量
。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
1.2、为什么要使用多线程?
- 更多的处理器核心:如果该程序使用多线程技术,
将计算逻辑分配到多个处理器核心上
,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。 - 更快的响应时间:响应用户请求的线程能够尽可能快地处理完成,
缩短了响应时间
,提升了用户体验。 - 更好的编程模型:
Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决
,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。
1.3、线程优先级
1. 什么是线程优先级?
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少
,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性
。
2. 优先级高低说明
优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级
,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级
,确保处理器不会被独占。
1.4、线程状态
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,
它们要等待被其他线程
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,
在一定时间之后它们会由系统自动唤醒
。 - 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
图示说明:
1.5、线程状态转换
图示说明
线程创建之后,调用start()方法开始运行
。当线程执行wait()方法之后,线程进入等待状态
。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态
。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态
。线程在执行Runnable的run()方法之后将会进入到终止状态。
特别注意
Java将操作系统中的
运行和就绪两个状态合并称为运行状态
。
2、线程间通信
2.1、volatile和synchronized关键字
1. volatile关键字
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存
,它能保证所有线程对变量访问的可见性。过多地使用volatile是不必要的,因为它会降低程序执行的效率
。
2. synchronized关键字
关键字synchronized可以修饰方法或者以同步块的形式来进行使用
,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中
,它保证了线程对变量访问的可见性和排他性。其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器
。
3. 访问被加锁的共享对象的过程
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法
,而没有获取到监视器(执行该方法)的线程
将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
4. 对象、对象的监视器、同步队列和执行线程之间的关系
图示说明
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器
。如果获取失败,线程进入同步队列,线程状态变为BLOCKED
。当访问 Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程
, 使其重新尝试对监视器的获取。
2.2、等待/通知机制
1. 简介
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作
。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
2. 图示解析
WaitThread首先获取了对象的锁,然后调用对象的wait()方法
,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态
。由于 WaitThread释放了对象的锁, NotifyThread随后获取了对象的锁,并调用对象的notify()方法
,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态
。 NotifyThread释放了锁之后, WaitThread再次获取到锁并从 wait() 方法返回继续执行。
2.3、Thread.join()
1. Thread.join()的使用说明
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回
。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回
。
2. 代码演示
package com.xiao.test.Test;
public class Sync {
public static void main(String[] args) {
Thread pre = Thread.currentThread();
for(int i = 0; i < 10; i ++){
Thread thread = new Thread(new Threads(pre), String.valueOf(i));
thread.start();
pre = thread;
}
}
}
class Threads implements Runnable{
Thread thread;
public Threads() {
}
public Threads(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + " " + "运行结束");
}
}
结果图示
每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回
,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知
)。
2.4、ThreadLocal
1. ThreadLocal的使用说明
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构
。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。