一、新建线程
一个 Java 程序从 main 方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 java 程序天生就是一个多线程程序,包含了:
- 分发处理发送给给 JVM 信号的线程。
- 调用对象的 finalize 方法的线程。
- 清除 Reference 的线程。
- main 线程,用户程序的入口。
那么,如何在用户程序中新建一个线程了,主要有几种方式:
- 通过继承 Thread 类,重写run方法。
- 通过实现 runable 接口。
- 通过实现 callable 接口。
public class CreateThreadDemo {
public static void main(String[] args) {
// 1、继承Thread
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("继承Thread");
super.run();
}
};
thread.start();
// 2、实现runable接口
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println("实现runable接口");
}
});
thread1.start();
// 3、实现callable接口
ExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(new Callable() {
public String call() throws Exception {
return "通过实现Callable接口";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
由于 Java 不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
实现 callable 接口,提交给 ExecutorService 返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable<V> callable)
将 callable 进行包装然后 FeatureTask 提交给 ExecutorsService。
另外由于 FeatureTask 也实现了 Runable 接口也可以利用上面第二种方式(实现 Runable 接口)来新建线程;
可以通过 Executors 将 Runable 转换成 Callable,具体方法是:Callable<T> callable(Runnable task, T result)
, Callable<Object> callable(Runnable task)
。
二、线程状态转换
此图来源于 JAVA并发编程的艺术
一书中,线程是会在不同的状态间进行转换的,Java 线程线程转换图如上图所示。线程创建之后调用 start()
方法开始运行,当调用 wait(),join(),LockSupport.lock()
方法线程会进入到 WAITING 状态,而同样的。
wait(long timeout)
sleep(long),join(long)
LockSupport.parkNanos()
LockSupport.parkUtil()
增加了超时等待的功能,也就是调用这些方法后线程会进入 TIMED_WAITING
状态,当超时等待时间到达后,线程会切换到 Runable 的状态,另外当 WAITING
和 TIMED_WAITING
状态时可以通过 Object.notify()
,Object.notifyAll()
方法使线程转换到 Runable
状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到 BLOCKED
阻塞状态,当线程获取锁时,线程进入到 Runable
状态。线程运行结束后,线程进入到 TERMINATED
状态,状态转换可以说是线程的生命周期。另外需要注意的是:
当线程进入到 synchronized 方法
或者 synchronized 代码块
时,线程切换到的是 BLOCKED 状态,而使用 java.util.concurrent.locks下lock
进行加锁的时候线程切换的是 WAITING 或者 TIMED_WAITING 状态,因为 lock 会调用 LockSupport 的方法。
三、线程状态的基本操作
3.1、interrupted
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的 interrupt()
方法对其进行中断操作,同时该线程可以调用
isInterrupted()
来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用 Thread 的静态方法
interrupted()
对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出 InterruptedException 时候,会清除中断标志位,也就是说在调用 isInterrupted 会返回 false。
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
// 线程1:睡眠线程
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000); // 睡眠1秒
} catch (InterruptedException e) {
// 关键点:当线程在 sleep 时被中断,会抛出 InterruptedException
// 并且会清除中断标志位(将标志位设为 false)
e.printStackTrace();
}
super.run();
}
};
// 线程2:死循环线程
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ; // 无限循环
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt(); // 中断睡眠线程
busyThread.interrupt(); // 中断死循环线程
while (sleepThread.isInterrupted()) ; // 这个循环实际上不会执行
// 打印两个线程的中断状态
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
输出结果:
sleepThread isInterrupted: false
busyThread isInterrupted: true
开启了两个线程分别为 sleepThread
和 BusyThread
, sleepThread 睡眠 1s,BusyThread 执行死循环。然后分别对着两个线程进行中断操作,可以看出 sleepThread 抛出 InterruptedException 后清除标志位,而 busyThread 就不会清除标志位。
另外,同样可以通过中断的方式实现线程间的简单交互,while(sleepThread.isInterrupted())
表示在Main中会持续监测 sleepThread,一旦 sleepThread 的中断标志位清零,即 sleepThread.isInterrupted()
返回为 false 时才会继续 Main 线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。
3.2、join
join 方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出。
public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)
public final void join() throws InterruptedException
Thread 类除了提供 join()
方法外,另外还提供了超时等待的方法,如果线程 threadB 在等待的时间内还没有结束的话, threadA 会在超时之后继续执行。join 方法源码关键是:
while (isAlive()) {
wait(0);
}
可以看出来当前等待对象 threadA 会一直阻塞,直到被等待对象 threadB 结束后即 isAlive()
返回false的时候才会结束 while 循环,当 threadB 退出时会调用 notifyAll()
方法通知所有的等待线程。下面用一个具体的例子来说说 join 方法的使用:
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 10; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果为:
main terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-2 terminated.
Thread-3 terminated.
Thread-4 terminated.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.
在上面的例子中一个创建了 10 个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程。
3.3、sleep
public static native void sleep(long millis)
方法显然是 Thread 的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep 方法并不会失去锁。 sleep 方法经常拿来与 Object.wait()
方法进行比价,这也是面试经常被问的地方。
sleep() VS wait()
两者主要的区别:
- 1、sleep() 方法是 Thread 的静态方法,而 wait 是 Object 实例方法。
- 2、wait() 方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而 sleep() 方法没有这个限制可以在任何地方种使用。另外,wait() 方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而 sleep() 方法只是会让出 CPU 并不会释放掉对象锁;
- 3、sleep() 方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而 wait() 方法必须等待
Object.notift/Object.notifyAll
通知后,才会离开等待池,并且再次获得 CPU 时间片才会继续执行。
3.4、yield
public static native void yield()
这是一个静态方法,一旦执行,它会是当前线程让出 CPU,但是,需要注意的是,让出的 CPU 并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了 CPU 时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。
在 Java 程序中,通过一个整型成员变量 Priority 来控制优先级,优先级的范围从 1~10 在构建线程的时候可以通过setPriority(int)
方法进行设置,默认优先级为 5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
另外需要注意的是,sleep()
和 yield()
方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep() 交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片
。yield() 方法只允许与当前线程具有相同优先级的线程能够获得释放出来的 CPU 时间片
。
四、守护线程 Daemon
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程, JIT 线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个 Java 应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述 Daemon 线程的使用。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
// 确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果为:
i am alive
finally block
i am alive
上面的例子中 daemodThread run 方法中是一个 while 死循环,会一直打印,但是当 main 线程结束后 daemonThread 就会退出所以不会出现死循环的情况。main 线程先睡眠 800ms 保证 daemonThread 能够拥有一次时间片的机会,也就是说可以正常执行一次打印 i am alive
操作和一次 finally 块中 finally block
操作。紧接着 main 线程结束后,daemonThread 退出,这个时候只打印了 i am alive
并没有打印 finnal 块中的。因此,这里需要注意的是守护线程在退出的时候并不会执行 finnaly 块中的代码,所以将释放资源等操作不要放在 finnaly 块中执行,这种操作是不安全的
。
线程可以通过 setDaemon(true)
的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于 start()
方法,否则会报异常。
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1365)
at learn.DaemonDemo.main(DaemonDemo.java:19)
这样的异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。
评论