在面试过程中,我相信大家都被问到过线程的问题,在网上看了一下,知识很分散,我想在自己学习的时候归纳总结写个笔记,希望帮助到自己的同时能帮助到大家。
线程的基本概念
- 进程:进程是指正在运行的程序。当一个程序进入内存运行,就变成了一个进程,进程是出于运行过程中的程序,并且具有一定的独立性。(例如使用电脑时,可以一边使用微信,一边使用QQ,一边打开网页。。。等等)
- 线程:线程是进程中的一个执行单元,负责当前进程中的执行,一个进程中至少有一个线程。一个程序运行至少有一个进程,而一个进程中可以有多个线程执行程序。(例如使用QQ的时候,可以一边跟好友聊天,一边跟另外的好友视频。。。等等)
线程的生命周期
摘自API
- NEW (至今尚未启动的线程处于这种状态。)
- RUNNABLE (正在 Java 虚拟机中执行的线程处于这种状态。)
- BLOCKED (受阻塞并等待某个监视器锁的线程处于这种状态。)
- WAITING (无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。)
- TIMED_WAITING (等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。)
- TERMINATED (已退出的线程处于这种状态。)
如上图,当创建new Thread 及 Thread的子类的时候,线程进入NEW 新建状态,调用线程的start()方法,线程具有CPU执行权,当该线程分配到CPU时进入 RUNNABLE 运行状态,当CPU没有执行到的时候进入BLOCKED 受阻塞状态,运行结束或者调用stop()方法时候,线程进入TERMINATED 死亡状态。在线程执行时,调用sleep()方法,传入毫秒参数,线程进入TIMED_WAITING休眠状态,时间结束,重新获得CPU执行权,CUP执行则到运行状态,反之受阻塞状态。另外,如果调用Object的wait()方法,线程进入WAITING 等待状态,只有调用Object的notify()或notifyAll()方法才能重新获得CPU执行权,CUP执行则到运行状态,反之受阻塞状态。值得注意的是在给定时间点上,一个线程只能处于一种状态。
-------
线程的使用
一、线程的创建
创建新执行线程有两种方法。
以循环输出1~50为例
- 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
public class TestThread {
public static void main(String[] args) {
SubThread su = new SubThread();
su.start();
Stream.iterate(0, n -> n + 1).limit(50).forEach(i -> System.out.println("main--->" + i));
}
public static class SubThread extends Thread {
@Override
public void run() {
Stream.iterate(0, n -> n + 1).limit(50).forEach(i -> System.out.println("SubThread--->" + i));
}
}
}
console->
从控制台输出中可以明显看出,两个线程同时执行,同时输出。
- 创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
public class RunnbaleDemo implements Runnable {
@Override
public void run() {
Stream.iterate(0, n -> n + 1).limit(50).forEach(i -> System.out.println("Runnable--->" + i));
}
}
public class TestRunnable {
public static void main(String[] args) {
RunnbaleDemo rab = new RunnbaleDemo();
Thread th = new Thread(rab);
th.start();
Stream.iterate(0, n -> n + 1).limit(50).forEach(i -> System.out.println("main--->" + i));
}
}
console->
第二种创建使用线程的方法也是很明显的看出两条线程同时执行,一起进行输出。
- 扩展再说一个使用lambda的匿名内部类启动。
public class TestRunnable {
public static void main(String[] args) {
// RunnbaleDemo rab = new RunnbaleDemo();
// Thread th = new Thread(rab);
// th.start();
new Thread(() -> Stream.iterate(0, n -> n + 1).limit(50)
.forEach(i -> System.out.println("Runnable--->" + i))).start();
Stream.iterate(0, n -> n + 1).limit(50).forEach(i -> System.out.println("main--->" + i));
}
}
既然两种方法都可以实现线程,为什么有两种方法呢,实现Runnable接口跟集成Thread创建线程有什么区别呢?
我们都知道,接口可以多实现,而继承只能单继承,所以实现Runnable接口,避免了继承Thread单继承的局限性。而且实现Runnable接口的方式更加符合面向对象的特点,线程分为两部分,一部分是线程对象,一部分是线程任务。继承Thread类,线程对象跟线程任务耦合在一起,一旦创建了Thread的子类,既是线程对象,又有线程任务,而实现Runnable接口,将线程任务单独分离出来封装成一个对象,类型就是Runnable接口类型,Runnable接口对线程的对象和任务进行了解耦,所以我们通过使用实现Runnable接口的方式使用线程。
二、线程的优先级
线程的执行是一种抢占CUP执行权的方式,我们可以设置这种优先级别来抢占更多的优先时间,但就算如此,真正的执行权还在CUP手中,CUP执行具有随机性,所以就算设置了线程的优先级,也不能一定保证优先级高的就能更多的执行。
Thread源码中定义了三个int类型的优先级
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
优先权从小到大 1-> 5-> 10 ,我们用代码来测试一下。
public class TestRunnable {
public static void main(String[] args) {
Thread one = new Thread(() -> Stream.iterate(0, n -> n + 1).limit(10)
.forEach(i -> System.out.println("One--->" + i)));
Thread two = new Thread(() -> Stream.iterate(0, n -> n + 1).limit(10)
.forEach(i -> System.out.println("Two--->" + i)));
one.setPriority(Thread.MIN_PRIORITY);
one.start();
two.start();
}
}
console->
正如上面提到的CPU的执行具有很强的随机性,所以也会出现One执行的多的情况。
三、线程的休眠和唤醒、让步
线程的生命周期中,我们提到了休眠状态,那么我们来写个例子从代码中理解休眠和唤醒。
首先 休眠方法,调用Thread的sleep()方法,我们一起看看sleep()方法,摘自API
- static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
- static void sleep(long millis, int nanos)
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
--------题外话--------
第二个方法,虽说可以加上指定的纳秒数,不过仔细阅读源码会发现纳秒级别的压根不存在。
对传入的nanos 进行判断,大于500微秒,毫秒加一,不到500微秒,则不改变毫秒值。
对传入的nanos 进行判断,传入毫秒值为0,nanos不为0,毫秒值0++,设置为1。
--------言归正传--------
用代码测试一下sleep()方法
public class TestRunnable {
public static void main(String[] args) {
Thread one = new Thread(() -> {
System.out.println("好困,想睡觉~~");
try {
System.out.println("开始睡觉了~~");
sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡醒了~~~");
});
one.start();
}
}
注意console的输出语句,开始睡觉后会停留10秒才会输出睡醒。
唤醒方法:
public void interrupt() 中断线程。
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
代码测试interrupt()方法
public class TestRunnable {
public static void main(String[] args) {
Thread one = new Thread(() -> {
System.out.println("好困,想睡觉~~");
try {
System.out.println("开始睡觉了~~");
sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡醒了~~~");
});
one.start();
one.interrupt();
}
}
console->
确实没有停留10秒,也正如API中所提到的,会抛出InterruptedException 中断异常,从而提早的结束它的休眠状态。
线程的让步方法 :
static void yield() :暂停当前正在执行的线程对象,并执行其他线程。
代码测试yield()方法:
public class ThreadDemo {
public static void main(String[] args) {
Thread th1 = new Thread(() -> {
for (int i = 0; i < 21; i++) {
System.out.println("one--->" + i);
if (i % 5 == 0) {
Thread.yield();
}
}
});
Thread th2 = new Thread(() -> {
for (int i = 0; i < 21; i++) {
System.out.println("two--->" + i);
}
});
th1.start();
th2.start();
}
}
console->
仔细观察会发现,每当 线程一输出到 5或5的倍数 的时候,调用暂停方法,此时CPU立马切换到另外一个线程,如此反复。
四、JOIN
摘自JPA
void join() 等待该线程终止。
void join(long millis) 等待该线程终止的时间最长为 millis 毫秒。
void join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
通俗易懂就是,T1线程调用了该方法,其他线程就得等到T1线程执行完毕才能执行。
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
// 创建新线程
Thread t0 = new Thread(new MyRunnable());
// 开启新线程
t0.start();
// 调用线程join方法
t0.join();
// 输出1到50
Stream.iterate(1, n -> n + 1).limit(50)
.forEach(n -> System.out.println(Thread.currentThread().getName() + ":" + n));
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
// 输出1到50
Stream.iterate(1, n -> n + 1).limit(50)
.forEach(n -> System.out.println(Thread.currentThread().getName() + ":" + n));
}
}
}
查看console会发现,永远都是新开启的线程先输出结束,才会输出main方法的输出,如果注释掉join()方法,就变成多线程一起执行。