现代操作系统在运行一个程序时,会为期创建一个进程。现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉这些线程在同时执行。
一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main()的线程。
public class MultiThread {
public static void main(String[] args) {
// 获取Java线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos)
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
}
}
结果如下:
[4]Signal Dispatcher //分发处理发送给JVM信号的线程
[3]Finalizer //调用对象finalize方法的线程
[2]Reference Handler //清楚Reference的线程
[1]main //main线程,用户程序入口
可以看到,一个Java程序的运行不仅仅是main()方法的运行,而是main()线程和多个其他线程的同时运行。
使用多线程的原因主要有以下几点。
1)更多的处理器核心
2)更快的响应时间,将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列)
3)更好的编程模型
线程优先级
现代操作系统基本采用时分的形式调度运行的程序,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程的时间片用完了就会发生线程调度,并等待这下次分配。线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
PS:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
线程的状态
Java线程在运行的生命周期中可能位于6中不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
状态名称 | 说明 |
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出Daemon线程中的finally块并不一定会执行。
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组,线程优先级,是否是Daemon线程等信息。
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name.toCharArray(); Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); 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 (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 */ tid = nextThreadID(); }
if (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 */ tid = nextThreadID(); } Thread.java的源码如上,在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon,优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。启动线程:
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
理解中断
中断可以理解为线程的一个标识位属性,他表示一个运行中的线程是否被其他线程进行来中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrput()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
许多声明抛出InterruptedException的方法(例如Thread.sleep())这些方法在抛出InterruptedException之前,Java虚拟机就先将该线程的中断标识位清楚,然后抛出InterruptedException,此时调用isInterrupted()方法会返回false。
public class Interrupted { public static void main(String[] args) throws InterruptedException { Thread sleepThread = new Thread(new SleepRunner(), "SleepThrea"); sleepThread.setDaemon(true); Thread busyThread = new Thread(new BusyRunner(), "BusyThread"); 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()); SleepUtils.second(2); } static class SleepRunner implements Runnable { @Override public void run() { // TODO Auto-generated method stub while (true) { SleepUtils.second(10); } } } static class BusyRunner implements Runnable { @Override public void run() { // TODO Auto-generated method stub while (true) { } } } }
SleepThread interrupted is false
BusyThread interrupted is true
从结果中可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除。不建议使用过期的suspend(),resume()和stop(),原因主要有:以suspend()方法为例,调用后,线程不会释放已经占有的资源(比如锁),而是占着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完全资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
恢复和暂停操作可以用等待/通知机制来替代。
安全的终止线程
中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。
以上代码通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断的将线程停止,因此这种终止线程的做法显得更加安全和优雅。public class Shutdown { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "CountThread"); countThread.start(); TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two, "CountThread"); countThread.start(); TimeUnit.SECONDS.sleep(1); two.cancel(); } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { // TODO Auto-generated method stub while (on && !Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " + i); } public void cancel() { on = false; } } }
线程间通信
线程开始运行,拥有自己的栈空间。如果多个线程能够相互配合完成工作,这将会带来巨大的价值。Java支持多个线程同时访问同一个变量或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的时加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。关键字volatile可以用来修饰变量(成员变量),就是告知程序对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。但是,过多的使用volatile是不必要的,因为它会降低程序执行的效率。关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
synchronized同步块的实现使用了monitorenter和monitorexit指令,本质上是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用是,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。 前者是生产者,后者就是消费者,这种模式隔离了做什么和怎么做。简单的实现方法是让消费者线程不断的循环检查变量是否符合预期,如下琐事while(value!=desire){Threa.sleep(1000);}doSomethind();这种方式存在如下问题:1)难以确保及时性。2)难以降低开销。Java通过内置的等待/通知机制能够很好的解决这个矛盾并实现所需的功能。等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在Object上。等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
方法名称 描述 notify() 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁 notifyAll() 通知所有等待在该对象上的线程 wait() 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁 wait(long) 超时等待一段时间,这里的参数是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 wait(long,int) 对于超时时间更细粒度的控制,可以达到纳秒
使用wait(),notify()时需要注意的细节:
1)使用wait(),notify(),notifyAll()时需要先对调用对象加锁
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
3)notify(),notifyAll()方法调用后,等待现场依旧不会从wait()返回,需要调用notify(),notifyAll()的线程释放锁之后,等待现场才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有线程全部移动到同步队列,被移动的线程状态由WAITNG变为BLOCKED。
5)从wait()方法返回的前提时获得了调用对象的锁。
等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能感知到通知线程对变量作出的修改。
管道输入/输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream,PipedInputStream,PipedReader,PipedWriter前两种面向字节,后两种面向字符。
对于Piped类型地流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流地访问会抛出异常。
Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是: 当前线程A等待thread线程终止之后才从thread.join()返回。每个线程等待前驱线程钟之后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结果通知)。重载的join()方法调用join(0);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; } } }
join()方法的逻辑结构与等待/通知经典范式一致,即加锁,循环和处理逻辑3个步骤。ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set()方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。public class Profiler { private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal() { 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 Exception { Profiler.begin(); TimeUnit.SECONDS.sleep(1); System.out.println("cost:" + Profiler.end() + "mills"); } }