
日升时奋斗,日落时自省
目录
1.2、实现Runnable 接口 实现一个interface
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属性及方法(常见)
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | 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处理,其他线程就会一直等待“死等” ,友友们可以自己修改一下 线程的执行顺序
1万+

被折叠的 条评论
为什么被折叠?



