目录
1,多线程的实现
2,多线程相关讨论以及方法
3,线程状态
4,线程安全
5,wait/notify
一,多线程的实现
前情了解:
在代码中实现多线程的前提下,我们需要了解一下多线程的背景,cpu我们都知道,是电脑的大脑,电脑的计算,逻辑判断以及任务的调度都需要他,那为了提高开发的效率以及电脑的效率,我们可以提高cpu的能力,早期的cpu是单核的,也就是只有一个核心,这个时候我们如果要实现程序真正的并行(两个任务同时进行)运行的话,就得用到多核心,多核cpu通过多个核心从而调用多个进程来同时执行任务,但是进程的创建和销毁的成本很高,所以我们引入了更小的单位,也就是我们所说的线程,并且同一时刻,一个 CPU 核心只能执行一个线程。
进程:操作系统分配资源的最小单位
线程:操作系统调度执行的最小单位
例子:cpu就相当于我们干活的员工,进程就是一个工厂,线程就是一套流水线,单核cpu就相当于一个工人,多核就相当于多个工人,那肯定是多核好,可以提高效率,现在我们要提高收益,我们可以多建一些厂,也就是多进程,这个开销肯定很大,所以我们引入更轻量化的线程,在一个工厂内设置多个流水线,实现资源的共享,减小开销同时还可以提高效率
第一种方式:继承Thread类
Thread是系统提供的多线程类,里面继承了Runnable接口,也就相当于Thread是负责去执行的,然后Runnable里面的run方法是我们要执行什么,Runnable也是一个函数接口,因为里面只有一个抽象方法,所以后续我们改进就可以使用lambda表达式,在继承的时候需要重写接口的方法
//继承Thread类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread类实现的多线程");
}
}
MyThread myThread=new MyThread();
myThread.start();
第二种方式:实现Runnable接口
实现Runnable接口然后将其作为参数传给Thread
//实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口实现的多线程");
}
}
MyRunnable myRunnable=new MyRunnable();
Thread myThread2=new Thread(myRunnable);
myThread2.start();
第三种方式:匿名内部类
对于实现接口的一种改造
public static void main2(String[] args) {
//通过匿名内部类进行改造
for (int i = 0; i < 10; i++) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现Runnable接口实现的多线程");
}
});
thread.start();
}
}
第四种:lambda表达式
public static void main(String[] args) {
//通过lambda表达式进行改造
for(int i=0;i<10;i++){
Thread thread=new Thread(()->{
System.out.println("实现Runnable接口实现的多线程");
});
thread.start();
}
}
对于lambda表达式的理解,前面不是说Runnable告诉我们要执行什么嘛,但是为什么这里的lambda表达式里面并没有重写Runnable里面的run方法,那我们还知道要执行什么任务吗:因为编译器会看它是什么位置来判定它实现那个函数式接口,毕竟lambda表达式只有是函数式接口(有且仅有一个抽象方法)的时候才能使用,Thread有构造器:
,然后编译器知道这里期望传入一个Runnable类型对象,刚好我们写的lambda的()里面没有参数,对应了Runnable接口中run()也是没有参数的,所以匹配上了,所以编译器会自动判定这就是实现了run的重写
二,多线程的相关讨论以及方法
(在我们这篇博客中会在适当位置引入一些思考)
思考1:
在我们创建多线程的代码中,我们会发现Thread和Runnable并不需要导包
答:因为他们属于Java.lang里面的,这个是能被默认导包的,所以我们没有在代码中看见像是import Thread之类的
前台线程以及后台线程
前台线程:我们一般创建的默认是前台线程,前台线程就相当于一些骨干核心,当所有前台线程结束的时候,我们进程也就会结束
后台线程:也称为守护线程,后台线程的结束不会影响进程的结束,但是前台线程都结束了,后台线程也会结束
当然我们也可以在线程启动前改变种类,用seeDeamon这个方法
思考2:
对于start这个操作的理解:
当我们创建线程的时候,是重写run方法,但是正在让线程启动的而是start方法,start这个操作本质上会调用操作系统提供的api,在操作系统内部创建线程,所谓api也就是别人提前写好的方法,类以及接口,可以直接拿来使用,操作系统内部通过pcb来捕获,通过链表组织起来
pcb是进程的描述块,一个进程对应一个pcb
tcb是线程的描述块,一个线程对应一个tcb
interrupt和sleep
interrupt可以用来设置标志位,还可以用来唤醒sleep类似的阻塞方法(抛出异常)
sleep()是用来阻塞的,比如你想让这个线程等待多少秒之后再运行,单位为ms,并且你在哪个线程里面sleep,这个sleep才会在哪执行,并且他是Thread类中用static修饰的方法,只能通过Thread.sleep();的方式来使用,并且要抛异常或者try catch
public static void main1(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(200);
System.out.println("interrupt");
t.interrupt();
}

对代码进行解释:我们通过lambda表达式创建了一个线程t,里面会一直执行打印hello这个操作,我们上来首先启动t线程,然后在main线程里面执行sleep,会先阻塞200毫秒,在这个时候会先执行t线程的打印,然后main线程里面打印interrupt,但是按理来说我们预期的应该t线程还休眠800毫秒吧,应该先抛异常,那为什么还打印了一个呢,首先我们要搞清楚的就是只有当对应t线程处于sleep的时候,才会抛出异常,但是当我们main线程执行完,cpu的调度还暂时在main线程,要进入t线程还需要cpu的调度,会花去一些时间,所以当我们进入t线程的时候,sleep已经阻塞完了,需要再打印一个hello进入阻塞时候才会抛出异常,再下面就是while里面的判定一直为true,并且我们并没有退出程序的操作,所以后续还是打印hello
public static void main2(String[] args) {
Thread t=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("运行中");
}
System.out.println("结束");
});
t.start();
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t.interrupt();
}

interrupt作为标志位的展示,代码中我们先启动t线程,然后在main线程里面阻塞400毫秒,这期间不断打印运行中,程序默认最开始我们的isInterrupted为false,通过interrupt方法变为true,程序结束
再总的理解一下start调用->sleep->interrupt的全过程(我用画图来解释)
TCB线程控制块(线程的所有信息)
PCB进程控制块(进程的所有信息)

线程等待(join)
线程等待可以干预两个线程的结束顺序,像前面谈到的sleep就是一种,我们接下来要谈的是join
join代表了死等,并且谁来调用join就是要等待他执行完毕,就像t.join就是死等到t线程结束
//join方法
public static void main(String[] args) {
Thread t=new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("线程执行"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
System.out.println("主线程前面的执行");
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程后面的执行");
}

并且sleep,join以及后面会谈到的wait这三者对于interrupt的阻塞以及抛出异常的机制是一样的
获取当前线程
Thread.currentThread
因为这个方法是Thread类的static方法,所以不可以使用对象来调用,必须使用类名来调用,并且获取到当前线程之后还可以继续调用来获取当前线程的id,名字等
//获取当前线程
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getId());
System.out.println(Thread.currentThread().getPriority());//线程优先级,默认是5
System.out.println(Thread.currentThread().isDaemon());//是否是守护线程,默认是false
},"可以给线程自定义名字");
t.start();
}
思考:用this.getName也可以调用到name,那么他和我们当前调用那么的区别在哪,并且this的使用场景在哪
//static 方法 /static 类,不能直接访问普通(非 static)的成员内部类;
// 如果要在静态上下文里使用内部类,这个内部类本身必须是 static(静态内部类)。
static class myThread extends Thread{
@Override
public void run(){
System.out.println(this.getName());
System.out.println(this.getId());
}
}
//和this的区别
public static void main(String[] args) {
new Thread(){
@Override
public void run(){
System.out.println(this.getName());
System.out.println(this.getId());
}
}.start();
myThread myThread=new myThread();
myThread.start();
}
this来获取必须符合匿名内部类或者继承Thread类才可以,然后重写run方法在run方法里面使用this,不能在runnable底下使用
三,线程状态
| 状态名称 | 说明 | 触发条件 |
|---|---|---|
| NEW 新建 | 新建线程对象,没有调用start() | Thread t = new Thread();仅创建 Java 对象,操作系统没有内核 TCB,没有真正线程 |
| RUNNABLE 可运行 | 可运行。包含:正在 CPU 运行,或者等待 CPU 分配时间片(就绪) | 调用start();线程等待 IO 也处于 RUNNABLE;不区分 “运行中” 和 “就绪”,Java 全部归为 RUNNABLE |
| BLOCKED 阻塞 | 等待获取 synchronized 锁,没拿到锁就卡在这 | 竞争synchronized同步锁失败,等待监视器锁;只有synchronized才会进入 BLOCKED,Lock不会到这个状态 |
| WAITING 无限等待 | 无限等待,需要别的线程手动唤醒 | Object.wait()、Thread.join()、LockSupport.park();不会主动苏醒,必须等待另一个线程唤醒 (notify/unpark) |
| TIMED_WAITING 计时等待 | 限时等待,时间到自动醒,也可以提前唤醒 | sleep(毫秒)、Object.wait(超时)、join(超时)、LockSupport.parkNanos() |
| TERMINATED 终止 | 线程执行结束,死亡 | run () 正常执行完毕;或者线程异常结束;线程结束,内核 TCB 销毁,Java Thread 对象还在等待 GC,不能再次 start () |
四,线程安全(重点)
什么情况会引发线程安全
举个例子:
两个线程都尝试修改同一个变量
public class demo5 {
private static int count=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
//等待两个线程全部执行完毕
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
static保证两个两个线程修改的是同一个count,那按照我们的预期应该得到的结果是20000,可以实际的结果会比20000少一些,那这是为什么呢,因为我们线程的调度是随机的,我换一个执行过程的图来方便理解,其中count++这个动作可以简化为三个动作,第一:load,把内存中的数据加载到寄存器中,第二:add,把寄存器数据加一,第三:save,把寄存器中的数据写回内存

上面我只列举了两种情况,因为是随机调度,所以这三个步骤会被拆分为好多种情况组合在一起,所以几乎不可能出现全是1的情况,所以结果肯定比预期的要小一些
引起线程安全
所以经过上面的例子,我们可以总结和补充一下什么会引起线程安全
原因一:线程内部执行指令的调度顺序是随机的
原因二:多个线程针对同一个变量
原因三:修改操作不是原子的(就像count++要拆分为三个小步骤,这三个小步骤就能出现随机调度)
锁
针对第一个问题是操作系统的内核,我们控制不了,然后第二个又是我们经常需要的操作,所以我们只能针对第三个操作进行一些改进,所以我们引入了一个很关键的东西,那就是锁synchronized
锁可以将三个操作打包为一个整体,做到互斥以及独占,这是核心思想
//通过锁来解决这个线程安全问题
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
//等待两个线程全部执行完毕
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
先解释这里锁的作用以及锁的使用:锁的()里面需要填写一下object的对象,所以我们需要自己创建一下locker对象,并且两把锁里面的object对象得是同一个,这样才会产生锁竞争(阻塞,防止插队),也就是t1线程完成了count++内部的三个完整操作,才会把锁解开,可以供其他线程使用,只要进入了锁的 { 就代表进入了锁,然后到 } 就代表了出锁,这里的锁并不是防止其他线程的调度,而是防止插队,调度到其他线程之后也会因为锁已经被使用还没放出来而进行阻塞等待,那肯定就有疑惑,我们的锁加到for循环外面会怎么样:加到for循环外部会使得t1线程先完成全部的++之后再把锁让出来给t2完成全部的++,结果都是一样的,但是这就没有看到线程的并发了
死锁
比如在同一个线程对象上加两把锁,根据上面说的锁竞争就会卡住,陷入死循环
还有一个经典模型(哲学家就餐)n个线程m把锁
就是一些哲学家围在一个圆形桌子就餐,每两个人之间有一根筷子,哲学家要不在思考人生,要不就拿筷子就餐,但是当所有哲学家同时拿起左边的筷子的时候,并且哲学家都很固执,不愿意放下筷子,这样子就会陷入卡死的情况
我们来总结一下死锁出现的必要原因:1,锁是互斥的,2:锁不可被抢占,3:请求保持(拿到一把锁同时尝试获取另一把锁),4:循环等待
1和2与代码结构相关,所以我们如果想要避免死锁的话最好对3和4下手,对于3我们可以避免嵌套锁,对于4我们可以约定加锁的顺序
volatile
对于4我们引入了关键字volatile,用来解决内存可见性引起的线程安全问题以及指令重排序的问题
内存可见性
//内存可见性以及指令重排序
public class demo6 {
public static boolean flag=false;
public static void main1(String[] args) {
Thread t1=new Thread(()->{
while (!flag){
}
System.out.println("检测flag已经变为false,退出循环");
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.start();
flag=true;
}
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (!flag){
}
System.out.println("检测flag已经变为false,退出循环");
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag=true;
}
我们这里可以重点来理解一下背后的逻辑,以及这两种代码为什么会有差别
(1)这背后就会涉及到一个逻辑叫做编译器优化:就是在不改变代码逻辑的前提下,对于代码的一些优化,这是编译器背后自动会做的
(2)这就会引发一个什么问题呢,我们看main里面,我们将flag修改了但是死循环还在,依旧没有打印退出循环的那个sout,因为我们在主线程里面的修改前面设置了sleep,所以当任务开始执行的时候,会先疯狂的进入死循环,这个时候主线程里面还在sleep,所以因为好久都没有修改,所以编译器就将flag从内存放入到了寄存器当中,所以后续就算我们修改了,读到的也是寄存器当中的false,背后的逻辑就是读这个操作的时间远远大于比较以及跳转的时间,所以就将这个操作优化掉了,所以t线程里面压根看不到修改了
(3)那么上面这两种写法有什么区别呢,也就是sleep的位置不一样,那么最终的结果有区别吗,是有区别的,因为main1里面start启动之后紧接着的操作就是修改,所以循环还没来得急疯狂的跑,就已经把条件改掉了,所以根本不会死循环
指令重排序
这个也是编译器优化的,就是适当改变了一下执行的顺序
//指令重排序
public static int a=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (!flag){
}
System.out.println(a);
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
a=10;
flag=true;
}
这里我们正常肯定是打印出10,但是因为指令重排序的缘故,flag=true可能会在执行的时候安排在a=10前面,这样可能会造成循环迅速跳出,然后马上打印最开始的0,毕竟读的时间还是长一点,可能没来得及读该之后的,不过指令重排序是概率也是可能发生,所以你自己试的时候可能并不会出现
针对上面的内存可见性以及指令重排序,我们可以在变量之前加上volatile关键字防止这两个编译器优化的操作
public static volatile boolean flag=false;
在上面的代码中只需要管好flag就好了,所以只需要在flag上面加volatile,具体情况具体分析
五,wait/notify
wait和notify是一起使用的,一个进行阻塞,一个进行唤醒,因为如果想要t1先执行1,然后t2再执行2的操作,join这样的等待就不行,因为join只能影响结束顺序
wait的具体操作:1,释放obj对象对应的锁,wait是放在锁内部使用的,2:等待其他线程的通知,进入阻塞,3:当通知到达时候,从阻塞队列重新回到就绪队列,并且重新获取到锁
wait有两种,一个是有时间的,一种就是没有时间的死等
wait和sleep的异同:都可以进行阻塞,也都可以设置时间,2:wait设计是为了notify,而sleep就是阻塞对应时间,3:wait一定要搭配锁,并且wait一进来就会释放锁,4:wait也可以用interrupt唤醒,不过做好是notify
public class demo7 {
static volatile Object locker=new Object();
static volatile boolean flag=true;
static int num=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (num<10){
synchronized(locker){
while (!flag){
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (num<10){
System.out.println("t1:"+num);
num++;
flag=false;
locker.notify();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t2=new Thread(()->{
while (num<10){
synchronized(locker){
while (flag){
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (num<10){
System.out.println("t2:"+num);
num++;
flag=true;
locker.notify();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t1.start();
t2.start();
}

1342



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



