Thread源码学习
这里我只截取了java.lang.Thread.java的部分源码—我能看懂的一部分,注释部分由我自己翻译。但受我的翻译及专业水平所限,可能错误颇多,望请勿喷。
Thread.java的部分源码即注释如下
/*
* @since JDK1.0
* 线程这个类是JDK1.0就有的,它实现了Runnable接口
*/
public class Thread implements Runnable {
// Thread是用一个枚举类型State来描述它当前所处的状态
public enum State {
// 线程还未启动
NEW,
// 就绪状态:调用了start方法后,线程的状态由NEW变为RUNNABLE
RUNNABLE,
// 阻塞状态:调用了sleep方法后,线程的状态会变为BLOCKED
BLOCKED,
// 调用了join/wait方法后,线程会处于等待状态
// 本线程调用了wait方法,会处于等待状态;其他线程调用了notify或notifyAll方法才能唤醒
// 其他线程调用了join方法,本线程会处于等待状态;只有调用join方法的线程处于terminate即死亡状态才能唤醒
WAITING,
// 定时等待状态,其在wait/join方法中传入了一个参数time,本线程会在time时间内处于定时等待状态
TIMED_WAITING,
// 终止状态,即死亡状态:run方法或main方法执行完毕,线程状态变为TERMINATED
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
// ...
}
// volatile:当前线程的状态对其他线程可见,0:线程初始化默认是NEW状态
private volatile int threadStatus = 0;
// 线程的优先级范围1-10,默认是5,设置后不可更改
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
// 返回当前线程的本地静态方法
public static native Thread currentThread();
// 告诉操作系统,该线程会让出CPU时间片,即暂停本线程的执行。
// 这个方法一般是在DEBUG测试时才会用到。
public static native void yield();
// 让当前线程的状态变为BLOCKED,持续时间为mills毫秒,也就是我们常说的阻塞状态
public static native void sleep(long millis) throws InterruptedException;
// 让当前线程的状态变为BLOCKED,持续时间为mills毫秒 + nanos纳秒
public static void sleep(long millis, int nanos)
throws InterruptedException {
// ...
}
// Thread的初始化
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
// ...
}
// Thread构造函数的默认方法,无参,默认命名为Thread-数字
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
// Thread构造函数的重载方法,可以传入一个Runnable对象
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// Thread构造函数的重载方法,可以给该线程命名
public Thread(String name) {
init(null, null, name, 0);
}
// 让线程的状态从NEW变为RUNNABLE,会让JVM去调用run()方法;
// 这个方法能确保多个线程同时运行;
// 一个线程只能调用一个start()方法
public synchronized void start() {
// ...
}
// 重写了接口Runnable的run()方法。线程在该方法中执行需要完成的功能
@Override
public void run() {
// ...
}
// 由系统来调用该方法,确保能顺利清理该线程
private void exit() {
// ...
}
// 仅仅是设置一个线程处于中断的标记,不会对线程造成任何影响
// 执行该方法后,遇到该方法调用了wait()/join()/sleep()方法时会抛出异常并清除该标记。
public void interrupt() {
// ...
}
// 测试当前线程是否有中断标记,而且该方法调用后会清除中断标记。
// 所以,如果连续成功地调用该方法两次,第二次的结果一定是false
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 测试当前线程是否调用了清除中断标记的方法,这个方法对线程的中断标记没有影响
public boolean isInterrupted() {
return isInterrupted(false);
}
// 测试这个线程是否还活着
public final native boolean isAlive();
// 设置线程的优先级
public final void setPriority(int newPriority) {
// ...
}
// 获取线程的优先级
public final int getPriority() {
return priority;
}
// 设置线程的名字
public final synchronized void setName(String name) {
// ...
}
// 获取线程的名字
public final String getName() {
return new String(name, true);
}
// 当前正在执行的线程会等待调用join(millis)的线程millis毫秒。
// 两个本来是并行的线程1、线程2。线程1调用了join()方法后,线程2会等待线程1执行了millis毫秒后再开始执行。如果join()方法不传入参数或者传入的是0,就是等线程1执行完毕后,线程2再执行
public final synchronized void join(long millis)
throws InterruptedException {
// ...
}
// 参考上个方法
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
// ...
}
// 其他线程会等待调用join方法的线程die后,再继续执行
public final void join() throws InterruptedException {
join(0);
}
// 设置是否为守护线程,只能在线程启动前调用该方法
public final void setDaemon(boolean on) {
// ...
}
// 当前进程是否为守护进程
public final boolean isDaemon() {
return daemon;
}
}
Thread源码学习总结
重要属性及其含义:
- threadStatus
threadStatus,即线程当前所处的状态。线程是用一个抽象的枚举类型State定义了其可能所处的状态。State的值:NEW(刚创建),RUNNABLE(就绪状态),BLOCKED(阻塞状态),WAITING(等待状态),TIME_WAITING(定时状态),TERMINATED(终止状态)。为了便于理解线程的状态变化,详情可见下图:
- *_PRIORITY
PRIORITY指的是线程的优先级,取值范围是1-10。新建的线程默认优先级为5。当两个线程竞争共享资源时,一般情况下优先级高的线程更可能先拿到共享资源。
- name
name,线程的名称。线程的名称默认是Thread-数字,这里的数字就是新建的线程序号。为了提高程序的可读性,一般会设置不同的name。
重要方法及其作用:
- currentThread()
获取当前正在执行的线程。
- start()
让线程的状态从NEW变为RUNNABLE,随后会让JVM去调用该线程的run()方法;每个线程只能调用一次该方法。使用start()方法启动线程,才是真正地实现了线程的异步运行。
- run()
这是Thread类的一个普通方法。在这个方法内部实现的功能,就是该线程实际要完成的功能。使用run()方法启动线程,实际线程间还是同步执行的。
- yield()
当前线程让出操作系统的CPU时间片。操作系统实际是通过将CPU时间片分发给不同的线程,然后根据这些CPU时间片的顺序串行运行其所属的线程,并根据CPU时间片的归属在不同的线程之间进行切换。这种方式运行多个线程,用于切换时间特别快,会让用户觉得这多个线程在同时运行(并发)。
以下图片参考自以操作系统的角度述说线程与进程
- sleep()
让当前线程处于阻塞状态。传入一个参数时,在参数的范围内出阻塞状态,时间过后就变为RUNNABLE状态。不传入参数时,线程停止运行,等待其他线程调用了notify()或者notifyAll()方法后才会唤醒该线程,使该线程处于RUNNABLE状态。
- join()
当前线程会等到调用join()方法的线程处于TERMINATED状态后,再去执行。常见的用法就是:主线程需要用到子线程的执行返回的结果,子线程调用join()方法后,主线程会在子线程执行完之前一直处于阻塞状态。
- interrupt(),interrupted(),isInterrupted()
interrupt()方法仅仅是设置一个线程处于中断的标记,不会对线程造成任何影响。interrupted()方法是当前线程是否有中断标记,该方法执行完后会将中断标记清除。
isInterrupted()方法是检测当前线程是否调用了interrupted()方法,该方法不会对线程中的中断标记造成影响。
- wait(),notify(),notifyAll()
这三个都是Object的方法。wait()方法与sleep()方法的效果类似,都是让当前线程处于阻塞状态。notify()方法是唤醒随机的一个处于wait状态的线程。notifyAll()方法是唤醒所有处于wait状态的线程。
案例演示
场景假设
张三、李四、王五3个人同时吃盘子里的10个包子。张三每吃一个包子要2s,李四每吃一个包子要3s,王五每吃一个包子要4s。
场景还原如下图:
过程还原如下图:
代码实现
这个小项目的源码在我的gitHub账号上
公有资源Dish.java
/**
* 盘子中记录的是此时其中还有num个foodName
* @author zhenye 2018/4/12
*/
public class Dish {
// 食物名称
private String foodName;
// 食物数目
private int num;
Dish(String foodName, int num){
this.foodName = foodName;
this.num = num;
}
public String getFoodName(){
return this.foodName;
}
public void setFoodName (String foodName) {
this.foodName = foodName;
}
public int getNum() {
return this.num;
}
public void setNum (int num) {
this.num = num;
}
}
顾客Guest.java
/**
* 记录每个顾客的信息Guest
* @author zhenye 2018/4/12
*/
public class Guest implements Runnable {
// 对于这三个顾客而言,盘子中的食物是共享资源。static修饰!!!
public static Dish dish;
// 顾客名称
private String name;
// 该顾客吃一个食物的耗时
private long time;
// 统计每个顾客吃了多少食物,初始值为0
private int count = 0;
Guest (String name, long time) {
this.name = name;
this.time = time;
}
// 场景还原
public void run() {
while (true) {
if (canGetFood()) {
eat();
}else {
System.out.println( this.name + "想从盘子中拿"+dish.getFoodName() +"吃,发现已经盘子中已经没有了,总共吃了" + this.count + "个" + dish.getFoodName());
return;
}
}
}
// 这个方法必须是同步方法!!!同一时刻只能有一个人从盘子中拿食物。
private static synchronized boolean canGetFood () {
boolean result = dish.getNum() > 0;
if (result) {
dish.setNum(dish.getNum() - 1);
System.out.println( Thread.currentThread().getName() + "拿出一个"+dish.getFoodName() +"后,此时盘子中还剩下" + dish.getNum() + "个。");
}
return result;
}
// 模拟顾客拿到食物后的进食时间
private void eat() {
try {
Thread.sleep(this.time);
this.count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类Main.java
package food;
/**
* 多线程场景还原的测试类
* @author zhenye 2018/4/12
*/
public class Main {
public static void main(String[] args) {
System.out.println("场景模拟开始:");
// 进来了三个顾客:张三,李四,王五
Guest guest1 = new Guest("张三",2000L);
Guest guest2 = new Guest("李四",3000L);
Guest guest3 = new Guest("王五",4000L);
// 他们点了盘菜:10个包子
Dish dish = new Dish("包子",10);
Guest.dish = dish;
Thread t1 = new Thread(guest1);
t1.setName("张三");
Thread t2 = new Thread(guest2);
t2.setName("李四");
Thread t3 = new Thread(guest3);
t3.setName("王五");
// start()方法启动三个线程,保证三个线程在同时运行
t1.start();
t2.start();
t3.start();
// 当前线程是main,等t1,t2,t3三个线程都处于DEAD状态后,再继续执行之后的代码。
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("场景模拟结束!");
}
}
测试效果图如下:
最终的测试结果可能会有所变化,即最后一个包子可能是张三拿到,也可能是王五拿到。但是最终的结果是符合预期的,因为盘子中的包子数目怎么测试都不会到-1,即保证了线程的安全。
总结
结合Thread源码以及案例的学习,我有了以下的体会。
- 1 多线程的优势在哪里?
我认为多线程的优势主要有两个:1. 使用多线程来还原并发场景,能极大地提高程序的可读性;2. 使用多线程处理哪些不需要考虑线程安全的资源时,能提高程序的效率。
- 2 如何保证线程安全?
线程安全,也就是保证多个线程在操作共享资源时不会出现数据错乱的情况。我的想法是:确保同一时间,只能有一个线程在操作共享资源。加锁的方式之一—案例中canGetFood()方法的被关键字synchronized修饰后,同一时刻只能有一个线程调用此方法。