一.线程概念
在java中,并发编程是一个相当重要的话题,通过使用线程我们可以发挥多处理器的强大计算能力,可以构建多任务的应用从而提升体验,而并发编程的基础就是线程。
jdk从1.0时代就引入了线程相关的类(Thread,Runnable等),在jdk1.5中又引入了java.util.concurrent包,提供了更多便捷的并发操作。
今天我们就来走近线程这位java家族的老朋友,了解他的日常。
提到线程,就不能不提进程,在操作系统运行过程中,进程和线程都是为了便于进行多任务处理而存在的,其中进程有独立的内存空间以及相关资源(如文件句柄等),而线程是在进程内部更小的工作单元,同一个进程中的线程共享资源,有一个类比可以用来解释进程和线程:
我们的计算机就像是一座工厂,时刻都在运行,而工厂中又有很多车间。
由于工厂的电力有限,一段时间内,只能供应一个车间运行,当一个车间运行的时候,其他车间就必须停工。
进程就好比是工厂里的车间。
一个车间里,有很多工人,车间里的空间和资源(比如空调、洗手间等)是车间里的工人共享的。
每个工人就相当于进程中的线程。
简单总结一下:进程是CPU资源分配的最小单元,线程是CPU调度的最小单元。
二.线程生命周期
我们今天的主角是线程,下面来看一下线程的生命周期。
如果查看Thread类的源码,我们就会发现,Thread有如下几种状态:
public enum State {
NEW, // 新创建
RUNNABLE, // 可以被调度
BLOCKED, // 被阻塞
WAITING, // 等待
TIMED_WAITING, // 限时等待
TERMINATED; // 结束
}
各个状态间的流转关系如下图所示:
线程生命周期下面我们通过一段代码来感受一下线程的状态流转:
public class ThreadLifeCycle {
// 定义一个线程
static class MyThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start to work:" + Thread.currentThread().getState());
// 线程执行过程中需要调用一个同步的方法
work();
}
}
// 同步方法,将造成线程阻塞
public static synchronized void work() {
try {
// 线程进入后等待2秒钟
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread job1 = new MyThread();
MyThread job2 = new MyThread();
Thread thread1 = new Thread(job1, "thread1");
Thread thread2 = new Thread(job1, "thread2");
System.out.println("thread1 befor start:" + thread1.getState());
System.out.println("thread2 befor start:" + thread2.getState());
thread1.start();
thread2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 after wait 100ms:" + thread1.getState());
System.out.println("thread2 after wait 100ms:" + thread2.getState());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 after 5000ms:" + thread1.getState());
System.out.println("thread2 after 5000ms:" + thread2.getState());
}
}
运行结果如下:
thread1 befor start:NEW
thread2 befor start:NEW
thread1 start to work:RUNNABLE
thread2 start to work:RUNNABLE
thread1 after wait 100ms:TIMED_WAITING
thread2 after wait 100ms:BLOCKED
thread1 after 5000ms:TERMINATED
thread2 after 5000ms:TERMINATED
可以看到:
- 在线程刚刚创建,调用start方法以前,两个线程的状态均为NEW;
- 在调用线程的start方法后,线程的状态变为RUNNABLE
- 在主线程等待100ms后,线程1处于TIMED_WAITING状态,这是由于线程1先开始执行,并获得了进入work方法的锁;而线程2由于无法获得锁,只能处于BLOCKED状态
- 5秒钟后,两个线程都已经执行完成了,处于TERMINATED状态
三.线程的调度
由于java是平台无关的,而线程调度这种事情都是操作系统级别的,因此具体的线程调度策略取决于具体的操作系统。
当前,主流的操作系统均使用抢占式的,基于线程优先级的调度策略。这里有两个关键词:抢占式、优先级。
抢占式:所有的线程争夺CPU使用权,CPU按照一定算法来给所有线程分配时间片,一个线程执行完自己的时间片后需要让出CPU执行权。
优先级:所有的线程均有优先级,优先级高的线程将优先被执行。
我们在创建线程时,可以通过setPriority
方法给线程设置优先级。设置完优先级后,jvm在执行时,将把这个优先级设置为操作系统的线程优先级,供cpu调度。
Thread类中定义了有关优先级的常量:
// 最低优先级
public final static int MIN_PRIORITY = 1;
// 默认优先级
public final static int NORM_PRIORITY = 5;
// 最高优先级
public final static int MAX_PRIORITY = 10;
下面我们通过程序来感受一下线程的优先级:
本人的电脑是4核,win7系统。为了能使线程出现等待和调度,我们下面将启动8个工作线程。
public class ThreadPriority {
static class MyThread extends Thread {
private volatile boolean running = true;
private volatile Random random = new Random();
public MyThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
int count = 0;
// 不停的执行正弦预算,直到接到结束指令
while (running) {
Math.sin(random.nextDouble());
count++;
}
System.out.println(threadName + " run " + count + " times");
}
public void shutDown() {
running = false;
}
}
public static void main(String[] args) {
// 给主线程设置为最高优先级,以使其他线程启动后,主线程能够执行后续操所
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
List<MyThread> list = new ArrayList<MyThread>();
for (int i = 0; i < 8; i++) {
MyThread thread = new MyThread( "thread" + i);
thread.setPriority(5);
list.add(thread);
}
for (int i = 0; i < 8; i++) {
list.get(i).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 8; i++) {
list.get(i).shutDown();
}
}
}
第一次,我们将所有线程的优先级都设置为5,执行结果如下:
thread3 run 63641301 times
thread5 run 73277080 times
thread4 run 65114449 times
thread2 run 65464405 times
thread0 run 66025262 times
thread7 run 63567566 times
thread6 run 97058244 times
thread1 run 72544080 times
然后,我们修改一下线程的优先级:
……
for (int i = 0; i < 8; i++) {
MyThread thread = new MyThread( "thread" + i);
// 不同的线程优先级不同
thread.setPriority(i + 1);
list.add(thread);
}
……
执行结果如下:
thread6 run 154709310 times
thread7 run 145893865 times
thread4 run 147268692 times
thread5 run 108303568 times
thread2 run 19743170 times
thread0 run 16955990 times
thread3 run 15611271 times
thread1 run 0 times
可以发现,4,5,6,7四个线程执行的次数比0,1,2,3要多一个数量级,甚至线程1没有被分到时间片。另外也可以看出,cpu并不是完全严格的按照优先级来进行调度,指定优先级只能给cpu一个参考,低优先级的未必得不到执行,但是大体还是按照高优先级先被执行的原则的。
四.线程间的协作
在了解了线程的调度机制后,我们知道,一个线程在什么时候被调度是不确定的,即便是优先级较高的线程,也只是原则上比优先级低的线程优先执行而已,优先多少也是不确定的。
但是有时,我们需要针对不同的线程设定一些执行的规则,例如一个线程执行完成后再执行另一个线程等等,因此就需要建立线程间的协作机制。
1.wait、notify、notifyAll
这三个方法是Object类中的基础方法,不知道大家有没有疑问,反正我看到这的时候有一个疑惑:这三个方法都是用于线程间协作的,为什么不定义在Thread类中,而要定义在Object类中呢?
让我们先来了解一下这三个方法的使用,稍后再来回答这个问题。
直接上代码:
public class ThreadCooperation {
public synchronized void testWait() {
System.out.println(Thread.currentThread().getName() +" start");
try {
// 使进入该方法的线程处于等待状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +" end");
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
cooperation.testWait();
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after 1000ms,notify");
synchronized (cooperation) {
cooperation.notify();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after another 1000ms,notify all");
synchronized (cooperation) {
cooperation.notifyAll();
}
}
}
在上面代码中,我们创建了3个线程,每个线程执行时会进入等待状态,然后等待主线程唤醒他们。
执行结果如下:
thread0 start
thread2 start
thread1 start
after 1000ms,notify
thread0 end
after another 1000ms,notify all
thread1 end
thread2 end
在调用notify方法时,thread0被唤醒,在调用notifyAll方法时,剩余的两个线程被唤醒。除此之外,我们也看到,当一个线程处于wait时,该线程会释放锁,以便其他线程获取,否则,再输出"thread0 start"后,就不会输出"thread2 start"和"thread1 start"了。
下面我们来回答本节提出的问题:为什么wait、notify、notifyAll方法被定义在Object类中,而不是Thread类?
因为等待,是指线程在某一个资源(对象)上等待,如我们上面的程序,线程可以在任何对象上等待;唤醒也是类似,线程在对象上等待,自然应该在同一个对象上被唤醒。因此这三个方法被定义在Object类。
2. sleep、yield、join
这三个方法都是被定义在Thread类中的。显然,他们都是指线程的某种行为。
(1)sleep方法是让当前线程休眠一段时间,即让出cpu让其他线程执行,但是它并不会释放锁。通过代码来看一下:
public class ThreadCooperation {
public synchronized void testSleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
cooperation.testSleep();
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
}
}
上面代码中,我们仍然创建3个线程,每个线程中都需要调用testSleep方法,一旦某个线程进入testSleep方法后,就会进入休眠状态,其他线程可以执行,但是由于第一个线程正在休眠,而且没有释放锁,所以其他线程只能等待。
执行结果如下:
thread1 start
thread0 start
thread2 start
thread1 end
thread2 end
thread0 end
可以看到,程序会先输出:
thread1 start
thread0 start
thread2 start
然后每隔一秒再输出后面的结果。说明在一个线程休眠时,其他线程得到了执行,但是被阻塞在了testSleep方法上。
(2)yield方法使当前线程变为等待执行状态,让出CPU以便其他线程执行,但是不能保证当前线程被立刻暂停,也不能保证暂定多久,甚至如果该线程的优先级高,可能刚刚被暂停,又重新获得执行。所以yield方法的行为是不甚明确的,不可靠的。
同样来看一段代码:
public class ThreadCooperation {
public synchronized void testYield() {
System.out.println(Thread.currentThread().getName() +" testYield start");
Thread.yield();
System.out.println(Thread.currentThread().getName() +" testYield end");
}
static class MyThread implements Runnable {
private ThreadCooperation cooperation;
public MyThread(ThreadCooperation cooperation) {
this.cooperation = cooperation;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
cooperation.testYield();
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
ThreadCooperation cooperation = new ThreadCooperation();
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(cooperation), "thread" + i).start();
}
}
}
某一次执行的结果如下:
thread0 start
thread1 start
thread0 testYield start
thread2 start
thread0 testYield end
thread0 end
thread2 testYield start
thread2 testYield end
thread2 end
thread1 testYield start
thread1 testYield end
thread1 end
可以看到在输出“thread0 testYield start”之后,输出了“thread2 start”,说明thread0让出了cpu。但让出cpu并不意味着会让出锁,其他线程虽然可以执行,但是需要等待先进入testYield方法的线程执行完才能进入。
(3)join方法使父线程等待子线程执行完成后才能执行。jdk源码中对jon方法的注释是:“Waits for this thread to die”,也就是需要等待执行join方法的线程执行完。
看一下下面的代码:
public class ThreadCooperation {
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" start");
System.out.println(Thread.currentThread().getName() +" end");
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new MyThread(), "thread" + i).start();
}
System.out.println("main thread run");
}
}
某一次执行结果如下:
main thread run
thread0 start
thread1 start
thread1 end
thread0 end
thread2 start
thread2 end
可以看到主线程先执行了。如果我们想让子线程先执行完,再继续执行主线程,就可以使用join方法了:
……
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<Thread>();
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new MyThread(), "thread" + i);
threadList.add(thread);
thread.start();
}
try {
// 主线程需要等待thread1执行完
threadList.get(1).join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread run");
}
典型的执行结果如下:
thread0 start
thread2 start
thread2 end
thread1 start
thread1 end
main thread run
thread0 end
从中我们可以看出与不使用join方法的区别。
下面总结一下wait、notify、sleep、yield、join这几个方法:
- wait、notify方法定义在Object类中,sleep、yield、join方法定义在Thread类中。
- sleep、yield方法是静态方法,而join方法是实例方法。
- wait、sleep、yield三个方法都可以时当前线程让出cpu,但其中wait方法必须在同步块中调用,而其他两个不用;wait方法针对某个对象,使线程在该对象上等待;而其他两个方法都是针对线程的;wait方法会让当前线程释放锁,而其他两个方法不会。wait方法和sleep方法都可以指定等待的时间,而yield不可以,调用yield后其行为是不可控的。
参考资料:
- 进程与线程的一个简单解释
- 浅析Java的线程调度策略
- Java多线程优先级的一些测试
- 为什么wait,notify和notifyall定义在Object中
- Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
- Java中Wait、Sleep和Yield方法的区别
本我已迁移至我的博客:http://ipenge.com/37241.html
网友评论