Thread类使用详解

日升时奋斗,日落时自省 

目录

一、线程操作

1、创建

1.1、继承Thread ,重写 run

1.2、实现Runnable 接口 实现一个interface

1.3、使用匿名内部类 继承Thread

1.4、 使用匿名内部类,实现Runable

1.5、使用Lambda表达式 

1.6、线程命名的方法

2、Threah属性及方法(常见)

2.1、后台线程

2.2、线程存活

2.3、中断线程(不要理解字面意思)

2.4、等待线程

3、wait和notify方法使用

3.1、wait阻塞

3.2、wait与notify连用

3.2、扩展

Thread类

注:如果对线程没有什么了解的友友,建议去上一个博客看一下基础知识,能更够为你理解下代码提供便利(基础巩固:进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位)

Thread类可以操控线程创建与进行(java中类的创建都是一样的,不要担心,这里创建理解不了,就仅仅当做一个可以操作线程的类即可)

一、线程操作

1、创建

1.1、继承Thread ,重写 run

这里是以继承方式写的

所以我们先写一个子类 MyThread继承Thread

 有javaSE的语法基础就可以看懂这个,这里继承Thread类,run是描述了线程要做的工作

这里写一个while(true)是为了我们下面看一下线程是如何进行的,这里的代码不难理解

重写一个run方法,这一点可以理解基本就可以了

创建一个MyThread的对象,用Thread来接收如何让线程跑起来,Thread调用run方法,就可以实现啦,这里的线程是main主线程 调用的方法,我们可以来看当前跑起来,只有只能跑一个while循环,因为只有一个线程在运转,只能等run方法跑结束了,才能往下跑,跑下一个while循环
 

start 和 run之间的区别

(1)start是真正创建一个线程(从系统里创建),线程独立执行

(2)run 只是描述线程要干的活是啥,如果直接再main中调用,此时没有创建新线程,全是 main线程做事

 那如何创建一个线程呢,其实这里就用到了一个方法start方法

 注:run方法跑完了,新的线程就会自然销毁

 这些就是一个基本的创建和验证:

线程调度:“抢占式执行”,具体哪个线程先上,哪个线程后上,不确定,取决于操作系统调度器的具体实现策略。

使用应用程序层面,好像线程之间的调度顺序是“随机”的一样,内核本身并不是随机的,由于干扰因素过多,程序这一层面已经无法感知到细节,就只能认为是随机的,随机可控吗,能,但是只能进行一些有限的API进行干预

之后的线程还可以通过一个jconsole.exe程序来开我们创建的线程,双击该程序

 出现以下界面,按步骤走既可

 

 下面会显示一个不安全连接,都是我们自己的电脑,不用担心安全问题,点击既可

1.2、实现Runnable 接口 实现一个interface

 1.3、使用匿名内部类 继承Thread

1.4、 使用匿名内部类,实现Runable

 1.5、使用Lambda表达式 

优势:最简单 推荐写法

 1.6、线程命名的方法

前面在观察线程创建的时候,就看到了一个创建线程的名称为Thread-0,如果是让操作系统给你默认创建新线程的名字就是Thread-1,Thread-2,Thread-3之类的,其实也是为了我们能够辨认,当然自己写出来的更舒服,接下来说一下,自己如何给他们命名

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

上面的解释比较清晰,我们这里演示一个 创建线程对象的

这里演示:Thread(Runnable target, String name)

 这里并不冲突,main线程结束了,不是说其他线程也就结束了,因为main线程在main方法中,main方法结束后,mian线程结束,其他线程还没有跑完呢,所以我们新创建的线程还在继续跑。

解释这里的start:主线程执行了t.start之后就结束了main方法,不是进入走start方法

2、Threah属性及方法(常见)

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

这里提一下 优先级这个特殊点 :前面说线程调度是“抢占式执行”,这里虽然有一个优先级方法,但是起不到作用,直白点就是没有一点用处

2.1、后台线程

使用: setDaemon

前台线程:会阻止进程结束,前台线程的工作没有完,进程是完不了的

后台线程:不会阻止进程结束,后台线程工作没有做完,进程也是可以结束的

这里可能不太理解这两个名词是什么意思

只是用这两个名字来解释两种不同的对待进程的情况

注:代码手动创建的线程,默认都是前台的,包括main默认是前台线程,其他的jvm自带线程都是后台线程,也可以手动使用setDaemon设置成后台线程(这里也有叫守护线程都是一个东西)

 线程创建的过程:及其问题

 2.2、线程存活

使用: isAlive方法

在正在调用start之前 ,调用 t.isAlive就是 false 调用start 之后 ,isAlive 就是true;

isAlive是在判断,当前系统里的这个线程 是不是真的

 只有t在跑的时候 isAlive才是true ,也就是验证新创建线程正在运行

 2.3、中断线程(不要理解字面意思)

这里的中断就不是让线程立即停止,是通知线程,你应该要停止了,是否真的停止看线程是否愿意,取决于线程代码怎么写

实例:古代打战:军师下令 就是相当于中断通知

(1)正在打仗的时候,将在外军令有所不受  (线程代码不想终止,你通知了也没有)

(2)正在打仗的时候,军师来了通知 ,仗打完在班师回朝(线程代码想终止,但是等我这会运行完了,我在结束)

(3)正在打仗的时候,军师通知,将军唯唯诺诺,只好班师回朝,仗也不打了(线程代码设置为直接终止)

使用标志位来控制线程是否要停止,这里的flag就是一个标识

 使用Thread中方法标志位,进行

 

如果没有看懂这些的话: 可以这样理解:那这里的目的是干什么的,他就是起到了通知作用,不是真的中断,当前代码并没有设置结束,所以在中断提醒只能做一个提醒作用,在报异常之后线程仍旧会继续进行

(1)响应终止请求

 (2)忽略终止请求

 (3)响应请求,但不是现在,做好收尾工作后 

总结:外面interrupt调用中断,此时start已经开始了,isInterrupted取反以及算是一个循环条件终止,循环内部sleep被唤醒 触犯catch 捕捉异常,异常让中断条件重置为false 是否结束就看线程内部代码是怎么写的,上面分了三种情况(1、可以结束 2、可以忽略 3、能结束,但是要等一会)

 2.4、等待线程

使用: join方法

线程是随机调度的过程,等待线程,就是控制两个线程的结束的顺序(我们这里只举出了一个新线程和main线程,友友可以试试多个新线程)

 本身执行start之后,t线程和main线程就并发执行,分头行动,main继续往下执行,t也会继续执行,

这里main线程走到join 发生阻塞(阻塞也是一个常见术语 block)直到t线程run结束了,main线程从join中恢复过来,才继续往下执行(t线程在这里肯定比main线程先结束)

使用join,要看你将代码放在什么位置,进行阻塞

举出一个没有使用join的例子来对比

 实例:生活中难免会有买东西时候,你在买饭(你就是mian线程),如果有人叫你带饭,你就等买下一份饭(join线程阻塞),等待第二份买完了,你回去后main线程结束

如果没有买 第二份饭,直接回去了,main结束,但是你室友得吃饭嘛,他就去买(相当于没有join线程阻塞)main线程执行完了,t线程还在跑(你的室友还在买饭路上)

(1)只等不散

解释:就是只能等t线程结束了,main线程才能继续

public void join()等待线程结束

(2)等但是限定时间

解释:mian线程等你,但是我只等一段时间

public void join(long millis)等待线程结束,最多等 millis 毫秒

millis限定等多少毫秒就不等了,我继续向下执行

(3)等但是限定时间更精确

public void join(long millis, int nanos)同理,但可以更高精度

例如:我等你400毫秒(millis),nanos为500微秒是一个限定值

(1)也就是当nanos大于等于500微秒时,millis就加1.当nanos小于500微秒时,不改变millis的值.
(2)当millis的值为0时,只要nanos不为0,就将millis设置为1.

3、wait和notify方法使用

线程最大的问题:是抢占式执行,随机调度

我们会尽可能避开随机性的问题,控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以通过一些API让线程主动阻塞,主动放弃CPU(这里让线程阻塞的方法是wait和notify)

实例:t1,t2两个线程,t1干一半就需要t2线程接手,就可以让t2先执行为wait(阻塞等待,主动放弃CPU就绪执行)等待t1执行到了一半,再通过notify通知t2,把t2唤醒,让t2接手

join和sleep连用可以实现当前的案例吗?显然不能,join是一个“死等的状态”,sleep是一个硬时间规定,只有让t1线程执行结束,t2线程才能运行,如果想如案例一样,t1线程执行一半,t2线程开始运行,join是不能做到的,sleep是我们指定的时间,但是很难预算t1线程执行时间

3.1、wait阻塞

wait,notify,(notifyAll)这三个类都是Object类的方法

Object是java里所有类的父类,这就方便我们调用wait方法时,不用担心怎么样的对象调用不了

注:如果对线程状态不是很了解的话,可以去看下一个博客线程状态,用于理解这里 

某个线程调用wait方法,就会进入阻塞(无论是通过那个对象wait的),状态为WAITING

public static void main(String[] args) throws InterruptedException {
        Object ob=new Object();
        System.out.println("wait 之前");
        ob.wait();
        System.out.println("wait 之后");
    }

 这抛出异常之后发现,异常既然是InterruptedException,是的就是中断时的异常,这个异常,很多带有阻塞功能的方法都带,这些方法都可以被interrupt方法通过这个异常给唤醒

当期代码虽然没有编译问题了,但是还是会运行报错,接下来解释一下报错的原因

 当前附的代码是什么状态,被解锁状态,那加锁

public static void main(String[] args) throws InterruptedException {
        Object ob=new Object();
        synchronized (ob) {   //加速
            System.out.println("wait 之前");
            ob.wait();
            System.out.println("wait 之后");
        }
    }

这下就能跑了,但是跑出来的结果就只有“wait 之前”,为什么呢,因为wait是连用notify的,没有notify的通知它就会一直等下去(死等)处于WAITING状态

那其他线程可以运行吗?当然可以,虽然这里wait是阻塞了,阻塞在synchronized代码块里,这里阻塞是释放了锁的,此时其他线程是可以获取到ob这个对象的锁的

3.2、wait与notify连用

先wait等待,才有notify通知的

所以这里总结一下 wait是干什么的

(1)先释放锁(所以我们会给wait先加上锁搭配synchronized使用,就等于要先去执行带有wait的线程)

(2)进行阻塞等待

(3)收到通知之后,重新尝试获取锁,并且在获取锁后,继续往下执行

public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        Thread t1=new Thread(()->{
            System.out.println("t1 wait执行前");   //  第一步  走完后 
            try {
                synchronized (object) {//锁是作用当前对象的
                    object.wait();              //阻塞 等通知
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1 wait执行后");   //第四步  收到通知后t1执行结束打印
        });
        Thread t2=new Thread(()->{
            System.out.println("t2 notify执行前");   //第二步 t1阻塞时间后,  t2进行
            synchronized (object) {
                object.notify();                    //进行通知
            }
            System.out.println("t2 notify执行后");   //第三部t2跑结束
        });
        t1.start();  //刚刚提到 要让wait下触发,所以t1就要先触发,那再t1和t2之间阻塞一下,让t1一定预先
        Thread.sleep(500);
        t2.start();
    }

这里先解释一个问题t1.start 和t2.start 代码先后顺序不是线程调度顺序,是不确定的,所以这里加了sleep为了让t2线程进行等待

等待的原因:此处的代码 尽可能先执行 wait 后执行notify 才有意义,这里的通知只通知了一次,一旦错过,这个wait等待就算是“死等”了

举一个例子:两个网恋情人约定好了,见面的时候,男的给对面挥个手(通知notify),女的看见了就知道是你了,女的还没有来(相当于没有到见面的位置等待wait),男的挥手了,对面但是没有任何反应,因为女的还没有来(没有进行等待wait),这个手就算白灰了(通知notify没有起作用),男的走了,但是女的刚好来了(此时开始等待wait),男的都走了,这个等待也就没有意义了,因为没有通知notify了(听段子就行,别自我带入!!!)

wait刚刚说了是可以设置等待时间的,但是不同于sleep,带参数,指定了等待的最大的时间,就是说,给定最大时间等,通知不来我就走了。

注:wait更加灵活相比于sleep,所以更容易出错,使用需要更加谨慎!!!

wait和sleep的区别

其实差别还是很大的,没事具体的对比性(这里简单说)

(1)sleep难以估算时间,只能固定时间的阻塞,wait可以指定等待的最大时间,可以接收通知后执行,也可以进行过了最大时间自己就解除等待、

(2)唤醒:wait使用 notify唤醒是 正常业务逻辑 ,sleep使用 interrupt唤醒,interrupt唤醒sleep则是出异常(出问题的逻辑)

3.2、扩展

多个线程在等待对象,此时有一个线程Object.notify() ,此时是随机唤醒一个等待的线程(不知道具体是哪个)

但是,可以用多组不同的对象,将两两线程连接起来是不是就构成了一个比较好的阻塞等待,并且能够知道谁先执行,控制执行顺序(先解释基本的原理,再附一个代码进行解释)

以三个线程为例,建立两个对象用来分别连接这三个线程操作并且控制执行顺序

希望的是先执行 线程1 再 执行线程2 最后再执行线程3

(1)创建 对象obj1 ,提供给 1 2使用 (一个对象可以连接两个线程)

(2)创建 对象obj2 ,提供给 2 3使用

(3)让线程2“阻塞(wait)等待”线程1的通知notify

(4)让线程3“阻塞(wait)等待”线程2的通知notify

如果t1线程 可以打印c ,t2线程可以打印b,t3线程可以打印a,但是我们想要打印出 cba ,下面代码就够用了

代码解释: 

public static void main(String[] args) throws InterruptedException {
        Object object1=new Object();  //对象一
        Object object2=new Object();  //对象二

            Thread t1=new Thread(()->{
                System.out.print("c");
                synchronized (object1) {
                    object1.notify();        //  第三步  线程1同一个对象一通知线程2
                }
            });
            Thread t2=new Thread(()->{
                try {
                    synchronized (object1) {
                        object1.wait();     // 第二步 线程2 同一个对象一先等待
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print("b");
                synchronized (object2) {
                    object2.notify();      // 第四步 线程2 同一个对象二通知线程3
                }
            });
            Thread t3=new Thread(()->{
                try {
                    synchronized (object2) {
                        object2.wait();    // 第一步 线程23 同一个对象二 先等待
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print("a");
            });
            t3.start();         //为什么是到这执行,是因为要先加锁wait,再去接受哦通知,这个通知采用意义
            Thread.sleep(500);
            t2.start();
            Thread.sleep(500);
            t1.start();
    }

线程的顺序,上面的代码也有注释(注释调理很清晰)

为什么是先t3线程 再t2线程,最后t1线程

这里先看运行结果: 

 想要使用wait就需要 先加锁,然后在处于阻塞等待,最后才是接收通知notify

t3线程等待t2线程通知,t2线程等待t1线程通知

先给t3线程加锁并等待,再给t2线程加锁并等待

就有了先执行t3线程 再执行t2线程 最后执行t1 ,但是先打印的是t1线程,因他线程他是通知者(可以和递归思想类比)

如果执行顺序反了,就只能打印c,因为t1线程没有等待wait处理,其他线程就会一直等待“死等” ,友友们可以自己修改一下 线程的执行顺序

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值