J2SE复习内容 - 多线程基础

在这之前应该有相应的操作系统基础知识,包括线程,进程,通信等。

系列目录:
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方法。

image.png

而调用run,就是简单的调用,按照箭头的走向,先执行main,然后调用run,run执行完了回到main,最后main执行完,其中没有线程的概念,而只是简单的类中调用。

具体start在背后做了什么,我们暂时不关注,但是明白简单的调用run只会在本线程中建立了调用栈,而不是创建新线程。

这里扩展一下,看一下Thread的join()方法,


image.png

,官方文档上面写的很清楚,Waits for this thread to die,等我死了,你再执行。

根据上图发现join的执行流程和调用run的类似,但是join确实是新开了一个子线程,大致的流程就是,主线程开始运行,新建了子线程开始运行,然后子线程调用join,主线程会等待子线程完全执行完毕,然后自己再执行。

2. 调度基础

类似于进程,线程的调度也是分为几个状态。
如图所示,我们调用start()方法后进入了就绪状态,而不是运行状态,这一点要明白。

在调度里面有两个方法需要做区分,sleep()和yield(), 两者说起来也不是很难区分,下面简单的理解一下二者的区别。

Q2: yield() 和 sleep()有什么区别?
首先两者都可以用来调度,或者说用来让出CPU,但二者的区别很明显,我们看一下源码。

yield
sleep

根据图片总结不同点
1.yield无法指定时间,而sleep可以
2.yield无异常抛出,而sleep可以抛出异常(在被别人打断睡眠的时候)。

其他的不同点:

  1. 参考上面的调度图,如果我们调用的是sleep,线程应该是从运行态进入了阻塞状态(睡眠式放弃),到时间之后继续回到就绪状态竞争CPU,而如果我们调用yield,线程应该从运行态进入就绪态(主动放弃),立刻又重新竞争CPU。

所以这样就有一个细节需要注意了,yield调用后,让出CPU的线程很有可能又获得了CPU,但这样不是缺憾,因为yield方法本身就不常用,上面的注释可以看出,该方法主要是用于测试的。

  1. 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类中还有一个方法。


image.png

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就是属于乐观锁的一种,而SychronizedReentrantLock属于悲观锁。
再比如我们说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<Integer> 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有什么区别?

  1. 首先我们要找爹,wait是Object的孩子,sleep是Thread的孩子,也就是说wait
    每个类都有,这也就是上面为什么可以调用list的wait方法的原因。

接下来看一下DOC,


就是在notify/notifyAll调用之前导致thread进入wait状态,且wait必须要持有一个对象监视器(同理notify两个方法也需要),

  1. 而当线程进入wait状态的时候,监视器(暂时理解为锁)将会被释放,但sleep不会,上面提到过(sleep是占着啥啥啥不啥啥啥)。

补充一下 notify不会释放锁,只是会发送通知进入工作状态,需要执行完后面的代码才会释放锁。

  1. 再根据上面所说的,wait会强制要求添加对象监视器,而sleep却没有要强制添加,如果wait没有对象监视器,将会抛出IllegalMonitorStateException异常

6. 总结

本文整理了一些多线程的入门知识,并且稍微深入一些问题,对一些方法的作用和一些经典例子做了整理。

文章中整理了6个问题,虽然知识点跳跃,但是联系性还是很强的,相互比较学习可以加深对线程机制的了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,943评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,426评论 1 15
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,322评论 3 87
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,707评论 12 45
  • 林炳文Evankaka原创作品。转载自http://blog.csdn.net/evankaka 本文主要讲了ja...
    ccq_inori阅读 642评论 0 4