1、线程简介
1.1、什么是线程
现代操作系统中运行一个程序,会为他创建一个进程。而每一个进程中又可以创建许多个线程。现代操作系统中线程是最小的调度单元。
两者关系:一个线程只属于一个进程,而一个进程可以拥有多个线程。线程是一个轻量级的进程。
一个Java程序从main方法开始执行,然后按照既定的顺序执行代码,看上去没有其他线程参与。但实际上Java程序天生就是多线程程序。我们可以通过JMX来查看一个普通的Java程序包括哪些线程。示例代码如下:
public class MultiThread {public static void main(String[] args) {ThreadMXBean threadMXBean= ManagementFactory.getThreadMXBean();ThreadInfo[] threadInfos=threadMXBean.dumpAllThreads(true,true);for (ThreadInfo threadInfo : threadInfos) {System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName());}}
}
上面代码执行结果如下:
从上图可以看到,一个Java程序运行不仅是main方法的运行,而是main进程和一些其他进程的同时运行。
1.2、为什么要使用多线程
- 更多的处理器核心:现代计算机大多是多核处理器,一个程序作为一个进程在处理器上运行。程序运行过程中可以创建多个线程,而一个线程只能在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么在多的处理器核心加入也不会提升程序的运行效率。相反如果采用多线程技术,那么运行在多核处理器上就会显著提升运行效率。
- 更快的相应时间:
- 更好的编程模型
**
1.3、线程优先级
在《Java并发编程的艺术》的一书中,作者的实验代码的结果得出的结论是线程的优先级不能作为程序正确性的依赖,操作系统完全可以不用理会Java代码对优先级的设置(作者环境为jdk1.7+Mac OS X10.10)。但是我自己跑出的结果可以看到代码中设置的优先级是有用的(本人环境jdk1.8+win10)。实例代码如下:
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 priority=i<5?Thread.MIN_PRIORITY:Thread.MAX_PRIORITY;Job job=new Job(priority);jobs.add(job);Thread thread=new Thread(job,"Thread:"+i);thread.setPriority(priority);thread.start();}notStart=false;TimeUnit.SECONDS.sleep(10);notEnd=false;for (Job job:jobs){System.out.println("Job priority:"+job.priority+" count:"+job.jobCount);}}static class Job implements Runnable{private int priority;private long jobCount;Job(int priority){this.priority=priority;}@Overridepublic void run() {while (notStart){Thread.yield();}while (notEnd){Thread.yield();jobCount++;}}}
}
本人跑出的结果为:
从上图可以看到优先级为10的结果和优先级为1的结果差距是挺大的。至于为什么会和作者的结论不一样,尚未搞清楚。
1.4、线程的状态
线程在运行过程中可能处于以下6种状态中的某一个,在给定的一个时刻,线程只能处于其中的一个状态。
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还未调用start方法 |
RUNNABLE | 运行状态,线程调用start方法后,Java线程将操作系统中的就绪和运行两种状态笼统的成为运行中 |
BLOCKED | 阻塞状态,表示线程阻塞与锁 |
WAITING | 等待状态,进入该状态的线程需要等待其他线程做出一些特定的动作(通知,中断) |
TIME_WAITING | 超时等待,表示线程等待指定的时间之后自动唤醒,继续执行 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
我们可以通过jstack命令来查看线程的执行状态。示例代码如下:
public class ThreadState {//改该程一直睡眠static class TimeWaiting implements Runnable{@Overridepublic void run() {while (true){try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}//该线程一直等待static class Waiting implements Runnable{@Overridepublic void run() {while (true){synchronized (Waiting.class){try {Waiting.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}static class Blocked implements Runnable{@Overridepublic void run() {synchronized (Blocked.class){while (true){try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}}public static void main(String[] args) {new Thread(new TimeWaiting(),"TimeWaitingThread").start();new Thread(new Waiting(),"WaitingThread").start();new Thread(new Blocked(),"BlockedThread-1").start();new Thread(new Blocked(),"BlockedThread-2").start();}
}
1、先通过jps命令来查看线程pid
2、通过jstack pid来查看线程的状态
java线程变迁图如下:
从上图可以看到,当线程被创建之后,执行start方法之后线程处于运行状态。当线程执行wait方法之后,线程处于等待状态,这时此线程需要其他线程唤醒才能继续执行(notify和notifyAll)。而超时等待状态是在等待状态的基础上加了等待超时机制,也就是说线程在等待指定的时间后,就会自动回到执行状态,不需要其他线程唤醒。当线程调用同步方法时,在没有获取到锁的情况下,线程会被阻塞住,此时线程处于阻塞状态。当线程执行run方法之后,就处于终止状态了。
阻塞状态是线程阻塞在进入由synchronize修饰的方法或者代码块时的状态。但是阻塞状态在java.concurrent包中的Lock接口上的状态是等待状态。那是因为java.concurrent包中Lock接口对阻塞的实现均使用了LockSupport类中的方法。
1.5、Daemon线程
Daemon线程是一种支持型线程,主要用作程序中后台调度以及支持性工作。在JVM中,当不存在非Daemon线程时,JVM将会退出。
Daemon线程时支持型线程,当JVM退出时,Daemon线程中的finally块并不一定会执行。实例代码如下:
public class Daemon {static class DaemonRunner implements Runnable{@Overridepublic void run() {try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}finally {System.out.println("DaemonThread finally run.");}}}public static void main(String[] args) {Thread thread=new Thread(new DaemonRunner(),"DaemonThread");thread.setDaemon(true);thread.start();}
}
如果是Daemon线程执行结果如下:
非Daemon线程执行结果如下:
PS:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
2、线程的启动和终止
2.1、线程的启动
线程的启动通过调用线程的start方法来启动。start方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start方法的线程。
PS:启动一个线程前,最好设置线程的名称,这样,在使用jstack排查程序问题时,就会给开发人员提供一些提示。自定义线程最好能够起个别名。
2.2、线程的中断
中断可以理解为线程的一个标识位属性。他表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否中观来进行响应,线程通过isInterrupted来进行是否中断判断,也可以调用静态方法Thread.interrupted对当前线程的中断标识位进行复位。但是如果线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted时,依旧会返回false。
在Java的许多API中,有许多抛出InterruptedException的方法,这些方法在抛出InterruptedException之前,JVM会先将线程中的中断标识位清除,这是调用该线程对象的isInterrupted时,依旧会返回false。
示例代码如下:
public class Interrupted {static class SleepRunner implements Runnable{@Overridepublic void run() {while (true){try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}static class BusyRunner implements Runnable{@Overridepublic void run() {while (true){}}}public static void main(String[] args) throws InterruptedException {Thread sleepThread=new Thread(new SleepRunner(),"SleepRunner");
// sleepThread.setDaemon(true);Thread busyThread=new Thread(new BusyRunner(),"BusyRunner");
// busyThread.setDaemon(true);sleepThread.start();busyThread.start();TimeUnit.SECONDS.sleep(5);sleepThread.interrupt();busyThread.interrupt();System.out.println("SleepThread interrupted is"+ sleepThread.isInterrupted());System.out.println("BusyThread interrupted is"+ busyThread.isInterrupted());TimeUnit.SECONDS.sleep(2);}
}
运行结果如下:
从结果中可以看到抛出异常的SleepThread线程在抛出后,其标识位被清楚了返回的是false,而一直运行的BusyThread线程返回是true。
2.3、过期的suspend,resume,stop
1、suspend():暂定线程
2、resume():恢复线程
3、stop():终止线程
过期的原因:以suspend方法为例,在调用后,线程不会释放占用的资源(比如锁),而是占用着资源进行睡眠,这时就有可能导致死锁。同样stop方法在终止线程时,不会保证线程的资源正常释放,通常是没有给予线程完成资源释放的机会。
2.4、安全的终止线程
中断是一种简便的线程间的交互方式,而这种方式最适合用来取消或停止任务。当然还可以用共享变量来终止任务。示例代码如下:
public class Shutdown {static class Runner implements Runnable{private long i;private volatile boolean on =true;@Overridepublic void run() {while (on && !Thread.currentThread().isInterrupted()){i++;}System.out.println(i);}public void cancel(){on=false;}}public static void main(String[] args) throws InterruptedException {Runner one=new Runner();Thread thread=new Thread(one,"one");thread.start();TimeUnit.SECONDS.sleep(1);thread.interrupt();Runner weo=new Runner();Thread thread1=new Thread(weo,"weo");thread1.start();TimeUnit.SECONDS.sleep(1);weo.cancel();}
}
运行结果如下:
在示例中,main方法中通过中断操作和cancel方法均可使线程终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源而不是武断的将线程终止。
3、线程间的通信
3.1、volatile和synchronize
java允许多线程同时访问同一个共享变量。由于每一个线程都有这个变量的拷贝(JMM协议),所以线程中看到的变量不一定是最新的。
关键字volatile可以用来修饰变量,就是用来告知程序对这个变量的访问要从内存中取,对这个变量的修改要立即写到刷新到内存中,这样其他线程每次看到的都是最新的数据。volatile详情
关键字synchronize可以修饰方法或者代码块,他用来保证同一时刻只能有一个线程能进入方法或者同步代码块中。他保证了线程对变量访问的排他性和可见性。synchronize详情
3.2、等待/通知机制
一个线程修改了一个值,另一个线程感知到变化,然后进行相关的操作。整个过程开始于一个线程,而最终执行与另一个线程。前者是生产者,后者就是消费者。这种结构在功能层面上实现了解耦,体系结构具有良好的伸缩性。那么在java中要如何实现呢?
在java中简单的办法就是让消费者线程循环检查变量是否符合预期。如下面的伪代码,在while循环中设置不满足的条件,如果条件允许则退出while循环,从而完成消费的工作。示例代码如下:
while(value != desire){Thread.sleep(1000);
}
doSomeThing();
上面这段代码,条件不足时就睡眠一段时间,这样做的目的是防止过快的无效尝试,浪费CPU的资源。这种方式看似能解决问题,但是却存在以下问题:
- 难以确保及时性
- 难以降低开销
对于以上问题,Java中内置了等待/通知机制。等待/通知相关方法是任意Java对象都具备的,因为其定义在所有对象的超类java.lang.Object上。相关方法描述如下:
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到该对象的锁。 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或者中断才能返回。在调用wait方法后,会释放对象的锁。 |
wait(long) | 超时等待一段时间,参数是毫秒,也就是等待长达n毫秒后,会没有通知就可以超时返回。调用该方法的线程会进入TIMED_WAITING状态 |
wait(long,int) | 对于超时等待更细粒度的控制,可以达到纳秒 |
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程基于对象O来完成交互。对于wait,notify,notifyAll的使用示例代码如下:
public class WaitNotify {static boolean flag=true;static final Object lock=new Object();static class Wait implements Runnable{@Overridepublic void run() {synchronized (lock){while (flag){System.out.println(Thread.currentThread()+" flag is true.wait "+ LocalDateTime.now());try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread()+" flag is false. running "+LocalDateTime.now());}}}static class Notify implements Runnable{@Overridepublic void run() {synchronized (lock){System.out.println(Thread.currentThread()+" hold lock. notify "+LocalDateTime.now());lock.notify();flag=false;try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (lock){System.out.println(Thread.currentThread()+" hold lock again. sleep"+LocalDateTime.now());try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) throws InterruptedException {Thread waitThread=new Thread(new Wait(),"WaitThread");waitThread.start();TimeUnit.SECONDS.sleep(1);Thread notify=new Thread(new Notify(),"NotifyThread");notify.start();}
}
执行结果如下:
从上述的例子中,有以下使用wait,notify,notifyall的细节:
- 调用wait、notify、notifyAll需要先对调用对象加锁
- 调用wait方法之后,线程状态由RUNNING变为WAITING,并将当前线程放到对象上的等待队列中。
- notify、notifyAll方法调用之后,等待线程依旧不会从wait返回,需要调用notify、notifyAll的线程释放锁之后,等待线程才有机会从wait返回。
- notify方法将等待队列中的一个线程从等待队列移动到同步队列中,notifyAll方法则是将等待队列中所有的线程全部移动到同步队列中。被移动的线程从WAITING变为BLOCKED。
- 从wait方法返回的前提是从对象获取到锁。
上述代码运行过程如下:
在图中,首先WaitThread获取到对象的锁,然后调用wait方法,从而放弃了锁,并进入等待队列中。由于WaitThread释放了锁,NotifyThead线程获取到对象的锁,并调用了线程的notify方法,将WaitThread从等待队列中转移到同步队列中,此时WaitThread线程的状态变为阻塞状态,在NotifyThread线程释放锁之后,WaitThread再次获取到锁,并从wait方法返回继续执行。
3.3、等待/通知经典范式
我们可以从上述示例代码WaitNotify中提炼出等待/通知的经典范式。改范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方(消费者)遵循的原则如下:
- 获取对象的锁
- 如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件
- 条件满足继续执行对应的逻辑
对应的伪代码如下:
synchronize(对象){while(条件不满足){对象.wait();}对应的处理逻辑
}
通知方(生产者)遵循的原则如下:
- 获取对应的锁
- 改变条件
- 通知所有等待在对象上的线程
对应的伪代码如下:
synchronize(对象){改变条件;对象.notifyAll();
}
4、管道输入/输出流
管道输入/输出流和普通输入/输出流或者网络输入/输出流的区别在于,管道输入输出流主要用于线程间的数据传输,而传输的媒介就是内存。
管道输入输出流的实现主要分为PipedOutputStream、PipedInputStream、PipedReader、PipedWriter,前两种面向字节,后两种面向字符。示例代码如下:
public class Piped {static class Print implements Runnable{private PipedReader in;public Print(PipedReader in){this.in=in;}@Overridepublic void run() {int receive=0;while (true) {try {if ((receive=in.read())!=-1){System.out.print((char)receive);}} catch (IOException e) {e.printStackTrace();}}}}public static void main(String[] args) throws IOException {PipedWriter out=new PipedWriter();PipedReader in=new PipedReader();out.connect(in);Thread printThread=new Thread(new Print(in),"PrintThread");printThread.start();int receive=0;while (true) {try {if ((receive=System.in.read())!=-1) {out.write(receive);}} catch (IOException e) {e.printStackTrace();}}}
}
执行结果如下:
在示例中,创建PringThread线程用来接受main方法的输入,任何main方法线程的输入均通过PipedWriter写入,而PringThread线程的另一端通过PipedReader将内容读出并打印。
对于pip流必须先进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,那么对于流的访问会抛出异常。
5、Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义就是线程A等待线程thread执行完之后从thread.join()返回,并继续执行自己的代码。线程Thread除了提供join()方法外,还有join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在指定的超时时间内没有终止,那么将会从thread.join方法中返回。示例代码如下:
public class Join {static class Domino implements Runnable{private Thread thread;public Domino(Thread thread){this.thread=thread;}@Overridepublic void run() {try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" terminate.");}}public static void main(String[] args) throws InterruptedException {Thread pre=Thread.currentThread();for (int i=0;i<10;i++){Thread thread=new Thread(new Domino(pre),String.valueOf(i));thread.start();pre=thread;}TimeUnit.SECONDS.sleep(5);System.out.println(Thread.currentThread().getName() + " terminate.");}
}
执行结果如下:
从上述输出可以看到,每个线程的终止,都依赖于前一个线程的终止。
6、ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的值。
可以通过set(T)方法来设置一个值,在当前线程下在通过get()方法获取到原先设置的值。示例代码如下:
public class Profiler {private static final ThreadLocal<Long> TIME_THREAD_LOCAL=new ThreadLocal<Long>(){protected Long init(){return System.currentTimeMillis();}};public static void begin(){TIME_THREAD_LOCAL.set(System.currentTimeMillis());}public static long end(){return System.currentTimeMillis()-TIME_THREAD_LOCAL.get();}public static void main(String[] args) throws InterruptedException {Profiler.begin();TimeUnit.SECONDS.sleep(1);System.out.println("Cost: "+Profiler.end()+" mills");}
}
运行结果如下:
ThreadLocal结构