线程1——javaEE 附面题

目录

引入

进程

进程和线程的关系

线程

创建线程

[1.通过创建 Thread 子类](#1.通过创建 Thread 子类)

[2.通过实现 Runnable接口](#2.通过实现 Runnable接口)

3.子类匿名表达式

4.匿名内部类(Runnable)

启动线程

[Thread 类的常用属性(方法)](#Thread 类的常用属性(方法))

线程的状态

线程休眠

打断线程

[线程等待 join](#线程等待 join)

线程安全问题

锁synchronized()

先从使用入手

特性

wait/notify

使用

wait使用

两者搭配

面经:


引入

线程的概念离不开cpu(中央处理器),CPU 作为电脑的"脑",其算力是非常夸张的但它是如何工作的呢,在认识线程前我们先来了解一下 CPU。


计算机祖师爷冯诺依曼提出的冯诺依曼体系结构(运算器,控制器,存储设备,输入设备,输出设备)奠定了现代计算机的硬件基础。运算器,控制器便是CPU最基础,最核心的功能。

cpu 执行是很复杂的 可以简化成

  1. 读取指令 ------------------------------------------------- 指令(机器语言 )

  2. 解析指令 ------------------------------------------------- 从指令表中对应查找指令是什么意思

  3. 执行指令 ------------------------------------------------- 运算

现代 多核 cpu 下诞生了进程

进程

进程是操作系统中资源分配的基本单位。

操作系统是一个描述系统资源,通过一定数据结构组织资源的管理软件。 、

系统通过PCB 来描述进程

|--------|-----------------------------------------|
| PID | 同一台机器,同一时间,不重复 |
| 内容指针 | 内存资源分为数据/指令 操作系统可通过其找到数据/指令 |
| 文件描述符表 | 硬盘资源 打开文件可以得到一个文件描述符,打开多个就可以用数组/顺序表表示 |
| 状态 | 就绪状态 / 阻塞状态 |
| 优先级 | 依据重要性给进程分配资源 |
| 上下文 | 进程调度出 cpu 时保存一个中间状态,保证进程再调度回来时可以恢复之前的工作 |
| 记账信息 | PCB 会记录下进程在CPU上执行的次数,分配写资源给使用资源少的进程 |

进程的创建/销毁开销(销毁时间和系统资源)非常大,在创建时申请资源(大开销操作),为了提高效率降低开销引入了线程。

进程和线程的关系

1.线程是更轻量的进程。(进程太重了大开销,低效率)

2.进程包含线程,一个线程有大于等于一个线程,不能没有。

3.同一个进程上的线程共享进程的资源。

4.每个线程都可以执行独立的逻辑,并在cpu上独立调度

5.当进程已经有了,在进程上创建线程可以省去申请资源的开销。

线程

线程是系统调度执行的基本单位。

线程满足了"并发编程" 使一个服务器可以同时处理多个客户端的访问。

线程虽更轻量,多线程可以提升效率,但过犹不及。

线程过多带来的问题:

1.线程过多时,线程的创建和销毁时的开销就不可忽视了。

2.多线程环境下,多个线程对同一个变量同时进行操作。

多对一 可读不可取

一对一 可读又可取

3.线程中断会抛出异常,如果没有被捕获到,进程就会崩溃,线程会全挂掉。

创建线程

1.通过创建 Thread 子类

重写 run(); 方法,创建Thread 对象,调用start();

代码实现:

java 复制代码
class myThread extends Thread{
    public void run (){
        System.out.println("hello thread");
    }
}
public class demo1 {


    public static void main(String[] args) {
         Thread thread = new myThread();
         thread.start();
        System.out.println("hello main");


    }
}

👀输出:

通过写入无限循环观察下:

代码:

java 复制代码
class myThread extends Thread{
        @Override
        public void run() {
            while(true){
                try {
                    System.out.println("hello thread");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }



public class demo1 {

    public static void main(String[] args) {
        Thread thread = new myThread();
        thread.start();
        
        while(true){
            try {
                System.out.println("hello main");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

👀输出

观察结果可以发现 main 主线程 和 我们自己创建的 thread 线程是并行执行的,顺序由cpu调度决定,随机出现。

2.通过实现 Runnable接口

再将 runnable 实例作为参数传给Thread 构造方法

代码:

java 复制代码
class myRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread");

    }
}    
public class demo2 {
    public static void main(String[] args) {
        Runnable runnable = new myRunnable(); 
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("hello main");
    }
}

👀输出:

这样的写法分离了任务逻辑和线程管理,不依赖于具体的类,使得后续可以轻松替换任务实现,降低程序的耦合度。

3.子类匿名表达式

代码: Thread t = new Thread () {

public void run(){

}

};

java 复制代码
public class demo3 {

    public static void main(String[] args) {
        // 匿名内部类 是Thread的子类 重写了run方法
        Thread t = new Thread(){
            public void run(){
                while(true){
                    try {
                        System.out.println("hello thread");
                        Thread.sleep(1000);  //  休眠1000ms  降低打印速度方便观察
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        System.out.println("hello main");
    }

}

👀输出:

4.匿名内部类(Runnable)

代码:

Runnable runnable = new Runnable({

});

Thread t = new Thread(runnable);

java 复制代码
public class demo4 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                while(true){
                    try {
                        System.out.println("hello thread");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }; 
        Thread t = new Thread(runnable);
        t.start();

        while(true){
            try {
                System.out.println("hello main");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

👀输出:

启动线程

Thread.start(); start() 是 Thread 类的一个静态方法,可以直接用类名调用。

1.调用 start 会真正调用系统中创建线程的 api start 执行不会产生阻塞,按代码顺序立刻向下执行。

2.一个线程只能 start 一次。 start 后 线程要么是就绪,要么是阻塞 不能重新 启动 了。

3.start 执行会自动执行 run() 方法。

Thread 类的常用属性(方法)

方法类别 方法名 功能描述 补充说明
ID getId() 获取线程唯一标识符 每个线程都有一个唯一的标识符,由 JVM 分配,从 1 开始递增
名称 getName() 获取线程名称 线程创建时可以指定名称
状态 getState() 获取线程状态 返回线程当前状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)
优先级 getPriority() 获取线程优先级 线程优先级设置效果受操作系统调度机制影响,"改了不一定有用",
守护线程 isDaemon() 判断是否为守护线程 守护线程会在所有非守护线程结束后自动终止,主要用于后台支持任务
存活状态 isAlive() 判断线程是否存活 返回 false 表示线程未启动(NEW 状态)或已结束(TERMINATED 状态)
中断状态 isInterrupted() 判断线程是否被中断 不会清除中断标记,需要通过 Thread.interrupted () 静态方法清除中断标记

注:isDaemon() 是否为后台线程

后台线程: 当线程没执行完时,进程要结束,线程无法阻止当前进程结束。

前台线程: 当前线程没执行完,进程要结束要等线程执行完,这样的线程成为前台线程。

线程的状态

|--------------|---------------------------------|
| New | 创建了线程对象但还没start isAlive false |
| TERMINED | 执行完成了(run完了)但对象还在 isAlive false |
| WAITING | 死等 join 无参未设置超时时间 |
| TIME_WAITING | 有时间的等 join 设置了超时时间 |
| BLOCKED | 锁竞争产生的阻塞 |

线程休眠

sleep(时间 ms) Thread 类的静态方法 让线程休眠多少毫秒后(进入Time_WAITING)

sleep 线程进入阻塞,调度出CPU ,唤醒后变为就绪状态,但不会立即执行,等待CPU调度。

打断线程

希望线程提前结束(sleep 时提前唤醒)

1.通过变量修改

2.通过 isInterrupted 标志位

查看当前中断状态:Thread.currentThread().isInterrupted() 不清除中断标志

检查中断状态:Thread.interrupted() 清除中断标志

若线程处于休眠sleep,会抛出InterruptedException 异常,需要 catch

java 复制代码
try{
  Thread.sleep(1000);
}catch (InterruptedException e) {
  Thread.currentThread.interrupt();
}

线程等待 join

控制线程之间的执行顺序。

有两个版本的join

1.join(); 死等

2.join(超时时间) 等待到超时时间后就不等了

线程 t1 线程t2

t1.join(); t2等t1执行完

t1.join(1000) t2等待t1执行完,等了1000ms t1还没结束就不等了

⭐谁调用谁被等

线程安全问题

why:❓❓❓

  1. 【根本原因】操作系统的随即调度,抢占式执行。

  2. 操作不是原子的。 (原子的:不可再分的最小操作)

  3. 多个变量同时操作同一个变量。

  4. 内容不可见

  5. 指令重排序
    面对非原子的操作,多线程就会出现多线程做同一个操作但做的是这个操作的不同部分

怎么理解呢? 就好像把一个大象放进冰箱需要几步。(这就是非原子操作是可拆分的)

1.打开冰箱门

2.把大象放进去

3.关上冰箱门

这时候如果有多个线程同时进行把同一只大象放进冰箱的操作。就有可能线程一打开了冰箱门被调度出CPU,线程2也执行了打开冰箱门,重复开门冰箱们受不了,线程2把大象放进冰箱并关上了冰箱门然后被调度出CPU,线程一被调度回CPU读取了中间状态继续之前的操作,把大象放进去,关上冰箱门。

把一只大象放进去了两遍也就是BUG出现了,有人要问最后不还是大象在冰箱里面吗,但无效的操作消耗了资源,在这虽然没造成什么严重后果但这要是转账操作呢,同时扣了两次款呢?
how:

那怎么保证安全呢?

既然是非原子操作造成的那可以把操作打包成原子的,java中提供了synchronized 可以给操作加锁保证线程一次把该执行完的逻辑执行完,这时有其他线程来执行这个操作就会触发锁竞争,产生阻塞等待上一个执行此操作的线程执行完解锁才能拿到锁,开始执行该操作。

锁synchronized()

先从使用入手

synchronized 有两个大括号

进入第一个大括号表示锁已经加上了,

从第二个大括号出来就表示解锁了。

通过加锁操作可以把操作变成原子的。

原理:加同一把锁的线程(锁对象是相同的)会竞争同一把锁(锁竞争)没抢到的阻塞等待抢到的解锁再抢。

锁对象 Object locker = new Object();

1.锁

java 复制代码
synchronized(锁对象){
      // 操作

}

2.修饰普通方法

java 复制代码
synchronized public void 方法(){

//     this是锁对象

}

3.修饰静态方法

java 复制代码
synchronized public static void 方法(){
          // static 没有this ,所以锁对象是类对象
          // 类对象 .class
}

特性

1.互斥

当一个线程已经拥有锁的时候,该线程的锁不能被抢占。

2.可重入

当一个线程已经拥有一把锁的情况下,对于已有的这把锁可以重复加锁多次(连续加同一把锁)且不会触发死锁。

wait/notify

Object类的方法

协调线程之间执行的顺序 区别 join 控制线程间结束顺序

wait 会使线程释放锁主动阻塞等待,直到被notify 唤醒

eg:

希望t1先执行再让t2执行

使用wait主动让t2阻塞让t1先参与调度,等t1执行完用notify唤醒t2.

使用

Object object = new Object ();

object.wait(); // wait() 会阻塞 可能会抛出 InterruptedException 当执行wait时其内部会第一时间把锁放了

放锁的前提得先有一把锁,wait放锁后当前线程就会进入阻塞状态 WAITING ,等待被唤醒,被唤醒后会再重新尝试去获取之前的锁,就会引发锁竞争 BLOCKED (被唤醒了,等拿到锁就会继续执行)

所以wait 应该搭配synchronized使用

wait使用
java 复制代码
Object object = new Object();
try{
    synchronized(object){
    object.wait();
    }
}catch(InterruptedExecption e){
     
两者搭配

创建两个线程,线程t1等待,线程t2来唤醒t1

java 复制代码
import java.util.Scanner;

public class demo_notify {

    public static Object locker = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });


        Thread t2 = new Thread(() -> {
            System.out.println("请输入任意内容唤醒t1");
            Scanner scanner = new Scanner(System.in);
            scanner.next();
            synchronized (locker) { 
                locker.notify();
            }
        });


        t1.start();
        t2.start(); 

    }

}

👀输出

当多个线程都在 wait 时(同一个对象),此时 notify 会随机唤醒一个。使用 notifyAll 可以唤醒所有的。

java 复制代码
import java.util.Scanner;

public class demo_notifyAll {

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });

    Thread t2 = new Thread(() -> {
        System.out.println("t2 等待前");
        synchronized (locker) {
            try {
                locker.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("t2 等待后");
    });

    Thread t3 = new Thread(() -> {
        System.out.println("请输入任意内容唤醒t1或t2");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notify();
        }
    });

    t1.start();
    t2.start();
    t3.start();

    }

}

输出:此时t3只会唤醒t1或t2其中一个。我这里运行唤醒了t2

想同时唤醒t1,t2就需要用notifyAll

java 复制代码
import java.util.Scanner;

public class demo_notifyAll {

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });

    Thread t2 = new Thread(() -> {
        System.out.println("t2 等待前");
        synchronized (locker) {
            try {
                locker.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("t2 等待后");
    });

    Thread t3 = new Thread(() -> {
        System.out.println("请输入任意内容唤醒t1或t2");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notifyAll();
        }
    });

    t1.start();
    t2.start();
    t3.start();

    }

}

输出:

面经:

谈谈sleep 和 wait 的区别。

答:

1.wait 的设计是为了提前唤醒,超时时间只是作为Plan B。

sleep 的设计就是为了到时间唤醒,虽可用 interrupt 提前唤醒但这样的唤醒会产生异常。

2.wait 需要搭配锁使用,因为执行时会先释放锁。(避免其他线程一直拿不到锁)

sleep 就不需要搭配锁使用,当sleep 被放到synchronized中时,不会释放锁而是抱着锁睡。

多线程实用但充满陷阱未完待续。

爱是个什么东西,它太理想主义,爱有什么了不起,我充满许多怀疑

爱是个什么东西 DT

⭐❤️👍

相关推荐
一勺菠萝丶7 分钟前
PDF24 转图片出现“中间横线”的根本原因与终极解决方案(DPI 原理详解)
java
姓蔡小朋友11 分钟前
Unsafe类
java
一只专注api接口开发的技术猿25 分钟前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring
superman超哥26 分钟前
Rust 异步递归的解决方案
开发语言·后端·rust·编程语言·rust异步递归
荒诞硬汉26 分钟前
对象数组.
java·数据结构
期待のcode27 分钟前
Java虚拟机的非堆内存
java·开发语言·jvm
黎雁·泠崖28 分钟前
Java入门篇之吃透基础语法(二):变量全解析(进制+数据类型+键盘录入)
java·开发语言·intellij-idea·intellij idea
仙俊红31 分钟前
LeetCode484周赛T4
java
CC码码33 分钟前
不修改DOM的高亮黑科技,你可能还不知道
前端·javascript·面试