在这之前应该有相应的操作系统基础知识,包括线程,进程,通信等。
系列目录:
J2SE复习内容 - 多线程基础
J2SE复习内容 - 多线程进阶
1. 线程基础
Java的线程相关概念定义在java.lang包下的Thread中,通过new Thread()来创建一个线程,通过start()方法启动一个线程。
同时可以使用Runnable或者Callable接口的实现来开启一个新线程,但归根结底都是调用其中的run(或者call)方法,来实现自己的逻辑。
通过上面的简单总结,会得出一个小问题,如下:
Q1: 为什么要调用start()方法,而不是直接调用run()方法?
回答这个问题,我们不如先找个例子测试一下。
先写一个Runnable
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() +"@"+i);
}
}
}
在写main方法
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable(),"MyThread");
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() +":"+i);
}
}
}
按照并行的原则来查看结果:
确实是两个线程交替执行的。
但是我们如果调用MyRunnable的run方法呢?
可以看到结果输出的是有顺序的,但是线程都是main线程。
同理我们如果这样写:
Thread t = new Thread(new MyRunnable(),"MyThread");
t.run();
也是这样的结果,那为啥会出现这种结果呢?画个简单的图就明白了。
正常start:
正常启动主线程和子线程会并行执行(宏观方面,忽略调度),由myThread来执行run方法。
而调用run,就是简单的调用,按照箭头的走向,先执行main,然后调用run,run执行完了回到main,最后main执行完,其中没有线程的概念,而只是简单的类中调用。
具体start在背后做了什么,我们暂时不关注,但是明白简单的调用run只会在本线程中建立了调用栈,而不是创建新线程。
这里扩展一下,看一下Thread的join()方法,
,官方文档上面写的很清楚,Waits for this thread to die,等我死了,你再执行。
根据上图发现join的执行流程和调用run的类似,但是join确实是新开了一个子线程,大致的流程就是,主线程开始运行,新建了子线程开始运行,然后子线程调用join,主线程会等待子线程完全执行完毕,然后自己再执行。
2. 调度基础
类似于进程,线程的调度也是分为几个状态。
如图所示,我们调用start()方法后进入了就绪状态,而不是运行状态,这一点要明白。
在调度里面有两个方法需要做区分,sleep()和yield(), 两者说起来也不是很难区分,下面简单的理解一下二者的区别。
Q2: yield() 和 sleep()有什么区别?
首先两者都可以用来调度,或者说用来让出CPU,但二者的区别很明显,我们看一下源码。
根据图片总结不同点:
1.yield无法指定时间,而sleep可以
2.yield无异常抛出,而sleep可以抛出异常(在被别人打断睡眠的时候)。
其他的不同点:
- 参考上面的调度图,如果我们调用的是sleep,线程应该是从运行态进入了阻塞状态(睡眠式放弃),到时间之后继续回到就绪状态竞争CPU,而如果我们调用yield,线程应该从运行态进入就绪态(主动放弃),立刻又重新竞争CPU。
所以这样就有一个细节需要注意了,yield调用后,让出CPU的线程很有可能又获得了CPU,但这样不是缺憾,因为yield方法本身就不常用,上面的注释可以看出,该方法主要是用于测试的。
- sleep()方法给其它线程运行时,不考虑线程的优先级;而yield()方法只会给相同优先级或更高优先级的线程运行的机会。
同时还有一点要提,参考我们上面sleep源码中的注释
The thread does not lose ownership of any monitors.
线程不丢失监视器所属权,说白了就是不会释放同步锁,也就是sleep方法调用的时候,加锁的线程还是会加锁,所以某些情境下,sleep调用并不会让出临界区,而是占着坑不XX。
接下来考虑一个常见的问题,如何停止一个线程?
Q3: 怎样安全的停止一个线程
这个问题相信很多人也已经知道答案了,简单的方法就是采用结束标志位,在讨论这个方法之前,有几种方法要探究一下。
第一种,interrupt,代码如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" : "+System.currentTimeMillis());
} catch (InterruptedException e) {
return;
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable(),"MyThread");
t.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
某些方法,如sleep会抛出InterruptedException ,所以借助Thread的interrupt方法可以致使其停止运行。
但是这个操作是危险且不合适的,借助异常机制来停止运行本身就不是很好的方法。同时要是根据isInterrupted方法来判断是否停止更危险(由于抛出异常之后的复位,这些情况下根本停不下来)
查阅API文档发现Thread类中还有一个方法。
stop方法,看起来很完美的样子,但是为什么被标注过时了呢?原因就是这个方法很危险。具体我们来看看官方怎么说。
下面是个人翻译 + 理解
stop()天生就不是一个安全的方法, 用这个方法会导致它自身所有的同步锁被释放,那么在stop调用之前所有的受同步锁保护的变量(临界区)将会产生不确定的风险,由此带来的风险是不可控的。
也就是说,stop方法会导致同步失效,不是一个安全的好办法,除此之外,更严重的是,调用了stop的线程会立马死掉,而不会管你文件是否关闭,资源是否释放等等,这样体验很不好。
同时下面也给出了一个方案,就是我们上面提到的标志位方法。
代码如下:
public class MyRunnable implements Runnable {
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" : "+System.currentTimeMillis());
} catch (InterruptedException e) {
return;
}
}
}
public void shutdown(){
flag = false;
}
}
public class Main {
public static void main(String[] args) {
MyRunnable m = new MyRunnable();
Thread t = new Thread(m,"MyThread");
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
m.shutdown();
}
}
3. 线程同步
这里的同步可能和操作系统里的同步概念不太一样,操作系统中同步是为了规定执行顺序,互斥是为了保护临界区,而Sychronized作为同步关键字,是完成了一个互斥操作,又叫做互斥 锁,但在此不深究,只知道这里面的功能对应起来就行。
而我们常见的锁机制会有这样一个分类,比如说乐观锁和悲观锁,其中CAS就是属于乐观锁的一种,而Sychronized和ReentrantLock属于悲观锁。
再比如我们说Sychronized是一种隐式锁,系统自动完成加锁解锁,ReentrantLock属于显式锁,需要手动完成加锁解锁。
再回到应用层面,Sychronized又分为语句块写法和方法体写法,等等。
但不管是乐观悲观与否,写法是如何,他们所达到的目的就是临界区互斥。
在使用Sychronized关键字的时候,也可以产生死锁,而死锁就是一种线程之间互相等待的僵局,如下面代码,m1和m2方法执行过程中,分别锁定了object1和object2,但是随后又要申请对方的锁,而此时对方又无法执行完代码释放锁,所以就处于一种尴尬的状态。
public class DeadLock {
Object object1 = new Object();
Object object2 = new Object();
public void m1() throws InterruptedException {
synchronized (object1){
Thread.sleep(1000);
synchronized (object2){
//do sth
}
}
}
public void m2() throws InterruptedException {
synchronized (object2){
Thread.sleep(1000);
synchronized (object1){
//do sth
}
}
}
}
4. 生产者消费者模型
生产者消费者模型如果在OS课程中学到过就不难理解了,因为下面的实现跟我们在OS中学习到的PV操作是一样的。
package Thread;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* 生产者消费者模型
* */
public class PCModel {
private final int MAX_LEN = 20;
private List list = new LinkedList<>();
class Producter implements Runnable{
@Override
public void run() {
produce();
}
//生产操作
private void produce() {
while (true){
synchronized (list) {
//等待
while(list.size() == MAX_LEN){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知唤醒
list.notifyAll();
//添加元素
list.add((int) Math.random() * 100);
}
}
}
}
class Consumer implements Runnable{
@Override
public void run() {
consume();
}
private void consume() {
while(true){
synchronized (list) {
//等待
while(list.size() == 0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知唤醒
list.notifyAll();
//取出元素
list.remove(list.size() - 1);
}
}
}
}
}
面试过程中,很有可能被要求手撕代码,除了上面这种最简单的,还可以使用BlockingQueue来实现,而后者需要明白,内部也是相应的阻塞原理,只需简单的调用他所提供的API即可实现。
对于上面所给出的代码有几点需要注意。
而这几点也很有意思,一起来探讨一下。
Q4 : 为什么这段代码要用While而不用if ?
while(list.size() == MAX_LEN){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
按照道理,判断一下队列已满,就进入wait()方法即可,但为什么要用while呢?理由如下:当一个线程执行了list.wait() 之后被Interrupt了,抛出了异常,如果用if执行完就会向下进行,这样的结果就不可预知,明明需要wait的情况又往下去生产了(或者消费了),就会产生系列问题,而我们用while的话,可以进行多次判断,直到某一次真正进入等待状态才可以。
Q5:为什么用notifyAll() 而不是notify() 方法?
想象一下有两个生产者的场景,可不可以某些情况下生产者notify的还是一个生产者(看似不可能),但是为了保险期间,我们使用notifyAll来随机唤醒一个线程,这样就尽可能的避免出现问题,当然很多写法仍然是notify,对于这个问题,简单思考一下即可。
5. wait和sleep
在上面我们比较了yeild和sleep的区别,下面又有一个新的问题,
Q6: wait和sleep有什么区别?
- 首先我们要找爹,wait是Object的孩子,sleep是Thread的孩子,也就是说wait
每个类都有,这也就是上面为什么可以调用list的wait方法的原因。
接下来看一下DOC,
就是在notify/notifyAll调用之前导致thread进入wait状态,且wait必须要持有一个对象监视器(同理notify两个方法也需要),
- 而当线程进入wait状态的时候,监视器(暂时理解为锁)将会被释放,但sleep不会,上面提到过(sleep是占着啥啥啥不啥啥啥)。
补充一下 notify不会释放锁,只是会发送通知进入工作状态,需要执行完后面的代码才会释放锁。
- 再根据上面所说的,wait会强制要求添加对象监视器,而sleep却没有要强制添加,如果wait没有对象监视器,将会抛出IllegalMonitorStateException异常
6. 总结
本文整理了一些多线程的入门知识,并且稍微深入一些问题,对一些方法的作用和一些经典例子做了整理。
文章中整理了6个问题,虽然知识点跳跃,但是联系性还是很强的,相互比较学习可以加深对线程机制的了解。