一,线程介绍
1. 什么是线程
- 现代操作系统在运行一个程序时,会为其创建一个进程。例如启动一个Java程序。
- 现代操作系统调度的最小单元是线程,也叫轻量级进程。
- 在一个进程里可以创建多个线程。
- 这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
- 处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
2. 为什么使用多线程
- 更多的处理器核心
- 更快的响应时间
- 更好的编程模型
3. 线程的优先级
在Java线程中,通过一个整型成员变量 private int priority
来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过 setPriority(int)
方法来修改优先级,默认优先级是5
,优先级高的线程分配时间片的数量要多于优先级低的线程。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统完全可以不用理会Java线程对于优先级的设定
4. 线程的状态
public enum State {
NEW, //新建
RUNNABLE, //就绪,运行
BLOCKED, //阻塞
WAITING, //等待
TIMED_WAITING, //定时等待
TERMINATED; //终止
}
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
线程创建之后,调用 start()
方法开始运行。当线程执行 wait()
方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的 run()
方法之后将会进入到终止状态。
注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法.
5.Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用 Thread.setDaemon(true)
将线程设置为Daemon线程。
注意 Daemon属性需要在启动线程之前设置,不能在启动线程之后设置
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
SleepUtils.second(10);
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
输出结果并没有打印 DaemonThread finally run.
二,启动和终止线程
1. 构造线程
Thread.init()
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 (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 */
tid = nextThreadID();
在上述过程中,一个新构造的线程对象是由其 parent
线程来进行空间分配的,而 child
线程继承了 parent
是否为 Daemon
、优先级和加载资源的 contextClassLoader
以及可继承的 ThreadLocal
,同时还会分配一个唯一的ID来标识这个 child
线程。
至此,一个能够运行的线程对象就初始化好了,在堆内存中等待运行.
2. 理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt()
方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法 isInterrupted()
来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()
对当前线程的中断标识位进行复位。
如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的 isInterrupted()
时依旧会返回 false
。
在一些抛出 InterruptedException
的方法(例如:Thread.sleep()
),这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用 isInterrupted()
方法将会返回 false
。
3. suspend(),resume(),stop()
弃用的方法
- suspend() :暂停线程
- resume() :恢复线程
- stop() :停止线程
不建议使用原因:
- suspend() :在线程调用后,不会释放已占用的资源(比如锁)
- stop() :在终结一个线程时不会保证线程资源的正确释放
4. 安全的终止线程
使用中断和一个boolean变量来控制是否需要停止任务并终止该线程。
public class Shutdown {
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();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
三,线程间通信
1. vlolatile和synchronized
vlolatile
Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
但是,过多的使用vlolatile是不必要的,因为他会降低程序的运行效率。
synchronized
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
public class Synchronized {
public static void main(String[] args) {
// 对Synchronized Class对象进行加锁
synchronized (Synchronized.class) {
}
// 静态同步方法,对Synchronized Class对象进行加锁
m();
}
public static synchronized void m() {
}
}
javap –v Synchronized.class
public static void main(java.lang.String[]);
// 方法修饰符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class com/murdock/books/multithread/book/Synchronized
2: dup
3: monitorenter // monitorenter:监视器进入,获取锁
4: monitorexit // monitorexit:监视器退出,释放锁
5: invokestatic #16 // Method m:()V
8: return
public static synchronized void m();
// 方法修饰符,表示: public static synchronized
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
上面class信息中,对于同步块的实现使用了 monitorenter
和 monitorexit
指令,而同步方法则是依靠方法修饰符上的 ACC_SYNCHRONIZED
来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由 synchronized
所保护对象的监视器。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
2. 等待、通知机制
等待、通知相关方法
3. 管道输入/输出流
4. join()
5. ThreadLocal
ThreadLocal
,即线程变量,是一个以 ThreadLocal
对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过 set(T)
方法来设置一个值,在当前线程下再通过 get()
方法获取到原先设置的值。
public class Profiler {
// 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
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");
}
}
Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。