线程简介
什么是线程
OS在运行一个程序时,会为其创建一个进程。例如:在启动一个Java程序时,就会创建一个Java程序。现代OS调度的最小单位是线程(轻量级进程)。在一个进程里可以创建多个线程,这些线程拥有各自的计数器,堆栈和局部变量等属性,并且能访问共享的内存变量。处理器在这些线程上高速切换,让人感觉这些线程在同时执行。
一个Java程序从main()方法开始执行,执行main()方法的是一个名为main的线程
为什么使用多线程
(1)更多的处理器核心
线程是大多数OS调度的基本单元,一个程序作为一个进程运行,程序运行过程中能创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心。而现代处理器上的核心数量越来越多,如果是单线程程序,在运行时只能使用一个核心,那么再多的处理器核心加入也无法提升程序的执行效率;而如果程序是多线程技术,将计算逻辑分配到多个核心上,就会显著减少程序的处理时间。
(2)更快的响应时间
有时我们会编写一些较为复杂的代码,比如说:一笔订单的创建,它包括插入订单数据,生成订单,发送邮件通知卖家等业务操作。用户从点击订购按钮开始,就要等到这些操作全部完成,才能看到订购成功的结果。对于这么多的业务操作,我们可以使用多线程技术,将数据一致性不强的操作发给其他线程处理。这样就可以缩短响应时间。
线程优先级
现代OS会分出一个个时间片,而每个线程会分配若干时间片。当线程的时间片用完后,就会发生线程调度。等待下次分配。而线程优先级则决定了线程需要多或者少分配一些处理器资源的属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,在线程构建的时候可以通过setPriority(int)方法来修改优先级。优先级的范围从1~10,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操 作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较 低的优先级,确保处理器不会被独占。
/*
比较5个低优先级和5个高优先级的线程
yield()方法来让掉CPU,重新争抢。判断低优先级和高优先级的争抢强度
*/
public class Priority {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
static class MyTask implements Runnable {
private long count;
private int priority;
public MyTask(int priority) {
this.priority = priority;
}
@Override
public void run() {
while(notStart) {
Thread.yield();
}
while(notEnd) {
Thread.yield(); //正在运行的线程重新就绪,重新竞争CPU
count++;
}
}
}
public static void main(String[] args) throws Exception{
List<MyTask> taskList = new ArrayList<>();
for(int i = 0; i < 10; i++) { // 0~4的优先级为1,之后的优先级为10
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
MyTask task = new MyTask(priority);
taskList.add(task);
Thread thread = new Thread(task, "Thread:" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false; //开始计数
TimeUnit.SECONDS.sleep(10);
notEnd = false; //结束
for(MyTask task : taskList) {
System.out.println("优先级:" + task.priority + " 总计数:" + task.count);
}
}
}
本人在win10下运行,两者的结果相差还是较大。但书中的运行环境为MAC和Ubuntu,两者的输出非常相近。
这表示程序正确性不能依赖线程的优先级高低,OS可以完全不理会Java线程对优先级的设定。
线程的状态
注:Java将OS中的运行和就绪合并为运行状态;阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块时的状态,但阻塞在进入java.concurrent包中Lock接口的线程状态却是等待状态,因为该接口对于阻塞的实现均使用了LockSupport类的相关方法。
守护线程
只要当前JVM中存在一个非守护线程没有结束,守护线程就全部工作。通过调用Thread.setDaemon(true)将线程设置为守护线程。
守护线程主要被用作程序中后台调度和支持性工作。
注:
- Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
- JVM退出时,守护线程中的finally块并不一定会执行,如下面的代码所示。因此不能依靠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 {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
中断
它表示一个运行中的线程是否被其他线程进行了中断操作,其他线程通过调用该线程x的interrupt()方法对x进行中断。
线程通过方法isInterrupted()来判断是否被中断,不过如果该线程已处于终结状态,即便该线程被中断过,调用该线程对象的isInterrupted()时依旧会返 回false。同时可以调用静态方法Thread.interrupted()对当前线程中断标识位进行复位。
许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法),这些方法在抛出异常前,JVM会先将该线程的中断标识位清除,然后抛异常,此时调用isInterrupted()方法返回的是false。
安全的终止线程
除了中断以外,还可以利用一个boolean变量来控制是否要停止任务并终止线程。
public class CountThread {
private static class RunnerRunnable implements Runnable {
private long i;
private volatile boolean isRun = true;
@Override
public void run() {
while(isRun && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("i = " +i);
}
public void needStop() {
isRun = false;
}
}
public static void main(String[] args) throws InterruptedException{
RunnerRunnable rr = new RunnerRunnable();
Thread countThread = new Thread(rr, "countThread");
countThread.start();
// (1)随眠1s后通过中断来使计数线程感知到中断后结束,
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
RunnerRunnable rr2 = new RunnerRunnable();
Thread countThread2 = new Thread(rr2, "countThread");
countThread2.start();
// (2)随眠1s后通过改变isRun变量来使计数线程感知到中断后结束,
TimeUnit.SECONDS.sleep(1);
rr2.needStop();
}
}
线程间通信
volatile和synchronized关键字
java支持多个线程同时访问一个对象或对象的成员变量,由于每个线程可以拥有这个变量的拷贝(对象和成员变量分配的内存是在共享内存中的,而每个执行的线程可以拥有一份拷贝),因此线程在执行过程中,一个线程看到的变量不一定是最新的。
关键字volatile用来修饰字段,他可以告知程序任何对变量的访问都需要从共享内存中获取,而对volatile字段的改变必须同步刷新到共享内存。不过,过多的使用volatile是不必要的,因为他会降低程序执行的效率。
关键字synchronized可以修饰方法或同步块。任何一个对象都拥有自己的监视器,当这个对象被同步块或该对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器,才能进入,而没有获取到的线程会进入同步队列,状态变为阻塞状态。
等待/通知机制
等待/通知机制是指 一个线程A调用对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,执行后续操作。
需要注意的是:
- wait()、notify()和notifyAll()时需要先对调用对象加锁。
- 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的WaitQueue。
- notify()方法和notifyAll() 方法调用后,被移动的线程由WaitQueue移到 SynchronizedQueue,它的状态由WAITING变为 BLOCKED。当调用notify()或 notifAll()的线程释放锁之后,被移动的线程再次获取到锁并从wait()方法返回继续执行
Thread.join()的使用
若一个线程A调用了thread.join(),它的含义是:当前线程A等待thread终止后,才从thread.join()返回。
下面的代码,创建了10个线程类Domino ,这个Domino 线程类有一变量preThread,它是前一个线程的引用。每个线程调用前一个线程的 join()方法。
public class Join {
static class Domino implements Runnable {
private Thread preThread; //该线程类的变量preThread,它是前一个线程的引用。
public Domino(Thread preThread) {
this.preThread = preThread;
}
@Override
public void run() {
try {
preThread