1.什么是线程
这个问题,大厂大概率都会问到(线程和进程的区别)。
现代操作系统再运行一个程序时都会为其创建一个进程,它是操作系统资源分配最小的单位,而进程可创建多个线程,线程是现代操作系统调度的最小单位。
详细见此文
Java程序是个天生的多线程。可由下面程序打印线程信息。
public class MultiThread {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
for(ThreadInfo threadInfo:threadInfos){
System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
}
}
}
为啥需要使用多线程?
- (硬件条件)随着核心数量越来越多和超线程技术的广泛应用。计算机并行计算能力更强了。
- (需要)当业务复杂时,为了更良好的用户体验,需要更快的相应速度(比如之前写那个发邮件的demo,总不能让用户傻等邮件发完再进行下一步无关操作吧)。
- (软件条件)Java为多线程编程提供了良好、考究并且一致的编程模型。
线程优先级
现代操作系统一般采用时分的形式调度运行的线程,每个线程都会分配到若干的时间片,用完了则会重新分配,优先级越高,分得的时间片越多。频繁阻塞的线程应该分配到更高的优先级,偏计算(长时间占用CPU) 的线程则设置较低的优先级。有些操作系统会忽视堆优先级的设定(比如书中的Ubuntu14.0.4,Mac OS X10.10),不过俺win10+Jdk11并不会。
public class Priority {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
public static void main(String[] args) throws InterruptedException {
List<Job> jobs = new ArrayList<>();
for(int i=0;i<10;i++){
int priorty = i<5?Thread.MAX_PRIORITY:Thread.MIN_PRIORITY;
Job job = new Job(priorty);
jobs.add(job);
Thread thread = new Thread(job,"Thread:"+i);
thread.setPriority(priorty);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
for(Job job:jobs){
System.out.println("Job Priority:"+job.priorty+"Job Count:"+job.jobCount);
}
}
private static class Job implements Runnable{
private int priorty;
private long jobCount;
public Job(int priorty) {
this.priorty = priorty;
}
@Override
public void run() {
while(notStart){
Thread.yield();
}
while (notEnd){
Thread.yield();
jobCount++;
}
}
}
}
可见差得还是挺大的。
线程的状态
状态 | 说明 |
---|---|
NEW | 初始状态,线程构建 new Thread()…之类的 |
RUNNABLE | 运行状态,就绪状态和运行状态统称为运行状态 |
BLOCKED | 阻塞状态,阻塞于锁 |
WATING | 等待状态,等待其他线程通知 |
TIME_WAITING | 超时等待,可在指定的时间内自行返回 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
//jps -l
//jstack pid
public static void main(String[] args) {
//超时等待
new Thread(new TimeWaiting(),"TimeWatingThread").start();
//等待
new Thread(new Waiting(),"WatingThread").start();
//超时等待
new Thread(new Blocked(),"BlockThread-1").start();
//阻塞
new Thread(new Blocked(),"BlockThread-2").start();
}
private static class TimeWaiting implements Runnable{
@Override
public void run() {
while(true){
SleepUtils.second(100);
}
}
}
private static class Waiting implements Runnable{
@Override
public void run() {
while(true){
synchronized (Waiting.class){
try {
Waiting.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private static class Blocked implements Runnable{
@Override
public void run() {
synchronized (Blocked.class){
while (true){
SleepUtils.second(100);
}
}
}
}
由此还可以看出,Thread.sleep()期间是不会释放锁的。
Deamon线程
是一种支持型线程,是个工具人(线程),用作程序中后台调度以及支持性工作,当JVM中不存在非Deamon线程时,JVM将会退出,所有的Deamon线程会被立即终止。可以通过设置将普通线程设置为Deamon线程,不过需要在启动前设置。
public class Deamon {
public static void main(String[] args) {
Thread thread = new Thread(new DeamonRunner(),"DeamonRunner");
thread.setDaemon(true);
thread.start();
}
private static class DeamonRunner implements Runnable{
@Override
public void run() {
try {
SleepUtils.second(10);
}finally {
System.out.println("俺好了");
}
}
}
}
线程的启动与终止
初始化
初始化源码,总之继承了父线程的是否为Deamon,优先级,加载资源的contextCloader以及可继承的ThreadLocal。以及会分配一个唯一的ID。
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();//父线程即为创建它的线程
/**
* 省略中间线程组的获取安全检查等操作
*/
this.group = g;
this.daemon = parent.isDaemon();//是否为Deamon线程
this.priority = parent.getPriority();//获取爸爸的优先级
//获取爸爸的contextClassLoader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
//
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
this.tid = nextThreadID();
}
启动线程
start()方法,由当前线程告知JVM,孩子生了(调用start方法),快来接生,如果接生婆(线程规划器)有空的话。最好给孩子起个名字,出问题了,方便调整。
中断: 线程的一个标识位属性,表示一个运行中的线程是否被其他线程中断。当前线程调用某线程的interrupt()方法时,设置中断标志位。注意会抛出中断异常的的地方都会先将线程标志位清除。
public void interrupt() {
if (this != Thread.currentThread()) {//如果不是我断我自己的话
checkAccess();//检查权限
// thread may be blocked in an I/O operation
synchronized (blockerLock) {//获取该线程的锁
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 设置标志位
b.interrupt(this);
return;
}
}
}
// set interrupt status
interrupt0();
}
suspend(),resume(),stop()
这仨是配套的方法,暂停,恢复,停止。不过由于无法保证资源的正常释放或者会造成死锁问题等原因,已被废弃。
安全的终止线程
不仅可以用中断进行停止线程,还可以使用volatile的标志位,来停止任务。
线程间的通信
在前几章提过两种通信方式,1.信号量,2.共享变量,Java就是后者。
volatile、synchronized
说到共享变量,就绕不开这俩关键字。
由于每个线程对共享变量都有一份缓存,如果都是读还好,就怕就即时的更新,大家手里的缓存就不是最新的了,怎么办呀?
使用volatile关键字啊,它使用内存屏障迫使缓存行无效,而需要去主内存读写,且不可重排序。
那如果是一系列操作后的结果呢?synchronized 啊,它保证一段代码或者一个方法的排他性和可见性。
其实这俩某种意义上是一个东西,不过volatile只保证一个变量的读和写排他性和可见性,而synchronized则保证一段代码或者方法结果的排他性和可见性。
详细描述下synchronized ,在同步代码块,则会在代码首尾分别用monitorenter、monitorexit指令来控制,而同步方法则会被ACC_SYNCHRONIZED修饰。
要想访问任意受synchronized保护的Object,必须先获取它的监视器,如果失败进入备胎池(同步队列),线程状态变成阻塞(BLOCKED),当的访问Object的前任(前驱,之前获得锁的线程)释放锁,则该释放操作唤醒被阻塞的同步队列的线程,使得它们重新尝试获取监视器。噢真是像极了爱情(不是)
等待通知机制
如果俺是面试官肯定会问为啥wait、notify、notifyAll方法都是在Object的方法而不是Thread的。
我觉得原因有
- Object是所有类的父类
- 锁的标记是在对象上的,这意味着所有对象都可以成为锁。而且获取释放锁的操作由上文所述本质上是获取它的监视器。
- 对于wait方法而言,它只有持有某个对象时对于其他线程来说才有等待的意义。
- 对于notify方法和notifyAll方法来说,它们唤醒的是持有过该对象的线程。继续执行时肯定会继续持有该对象。
就可以发现上述一系列的操作都是围绕着该对象进行的,而众所周知,Object是所有类,所以大家都能拥有该方法。
那再来个问题,为啥使用wait、notify、notifyAll方法实现等待通知机制,而不用刚才所说的设置一个 volatile共享标志位 ?
使用标志位是不是就需要不断的查询判断?这样是会浪费资源的,那可以让线程睡一段时间再来查询,这样的确是减少了一些资源的消耗,不过在睡期间如果错过了标志位修改就又凉了。所以出于及时性和资源的节约,便使用wait、notify、notifyAll方法 。(不要自己造轮子)
wait、notify、notifyAll方法的使用注意:
- 调用前提是,对调用对象加锁
- 调用wait()方法会从RUNNING变为WATING
- notidy和notifyAll调用后等待线程不会立即从wait()返回,需要notify和notifyAll释放锁之后才有机会从wait返回
- notify方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll方法则将等待队列中所有的线程全部移到同步队列中,被移动的线程状态由WAITING->BLOCKED
等待方遵循原则:
- 获取锁对象
- 如果条件不满足,调用对象wait()方法
- 条件满足后运行对应逻辑
通知方遵循原则:
- 获取对象的锁
- 改变条件
- 通知所有等待在对象上的线程
管道输入/输出流
它主要用于线程之间的数据传输,而传输的媒介是内存。
demo如下:
public class Piped {
public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
in.connect(out);//必须匹配连接
Thread printThread = new Thread(new Print(in),"PrintThread");
printThread.start();
int rece;
try{
while((rece = System.in.read())!=-1){
out.write(rece);
}
}finally {
out.close();
}
}
static class Print implements Runnable{
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive;
try{
while ((receive = in.read())!=-1){
System.out.print((char)receive);
}
}catch (IOException e){
}
}
}
}
Thread.join()
这里用到了等待通知机制。等待每个线程终止的前提是其前驱现成的终止,当前驱线程终止时才从join方法返回。
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {//不设置最长等待时间
while (isAlive()) {//只要那些家伙还活着,本线程就等
wait(0);
}
} else {
while (isAlive()) {
//双重判断,要么线程死完了,要么最长等待时间到了
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
ThreadLocal的使用
线程变量,可以当前线程为键,任意对象为值。这个结构被附带在线程上。其好处与线程绑定,与类或者方法都无关。
demo:
public class Profiler {
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>(){
@Override
protected Long initialValue() {
return System.currentTimeMillis();
}
};
public static final void begin(){
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end(){
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
public static void main(String[] args) throws InterruptedException {
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println(Profiler.end());
}
}
实例
等待超时模式
为了解决在等待限定时间内,另一个线程得到结果的情况。
之前说到的等待通知机制就不能解决这个情况,这就需要加入超时等待。
比如说join方法的后段代码。
while (isAlive()) {
//双重判断,要么线程死完了,要么最长等待时间到了
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
线程池技术
第一章就提过频繁的上下文切换可能会导致多线程并不如单线程快。线程池技术就是为了解决这个问题的,不再将线程的创建交给用户,而是预先创建若干个线程,重复使用它们。
好处是:减少频繁的创建线程与销毁线程所带来的开销,面对过量任务的提交能够平缓的劣化。
书中给出了一个线程池的例子。主要用了等待通知机制,当工作者线程要么处于工作状态,要么就是等待状态,要么就被开除了(移除工作者线程组),当添加新的工作时,通知一个工作者线程来活了,工作者线程再从工作列表中取出一个工作,干完后再进入等待状态,等待下一次唤醒。这样就做到了统一管理线程的生死。啊感觉有一丝妙啊
//工作者
class Worker implements Runnable{
private volatile boolean running = true;
@Override
public void run() {
while(running){
Job job = null;
synchronized (jobs){
while(jobs.isEmpty()){
try{
jobs.wait();
}catch (InterruptedException e){
Thread.currentThread().interrupt();
return;
}
}
job = jobs.removeFirst();
}
if(job!=null){
job.run();
}
}
}
//通知者
public void execute(Job job) {
if(job!=null){
synchronized (jobs){
jobs.addLast(job);
jobs.notify();
}
}
}