线程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

⭐❤️👍

相关推荐
王中阳Go8 小时前
面试官:“聊聊最复杂的项目?”90%的人开口就凉!我面过最牛的回答,就三句话
java·后端·面试
virtuousOne8 小时前
线程池详解
java
MOON404☾8 小时前
Rust 与 传统语言:现代系统编程的深度对比
开发语言·后端·python·rust
不吃肉的羊8 小时前
log4j2使用
java·后端
王中阳Go8 小时前
为什么很多公司都开始使用Go语言了?为啥这个话题这么炸裂?
java·后端·go
玉衡子8 小时前
三、JVM对象创建
java
先知后行。9 小时前
Reactor模型和类图设计
java·开发语言
玉衡子9 小时前
二、JVM内存模型
java
洛小豆9 小时前
为什么 Integer a = 100; 不创建新对象?从编译到运行的全流程拆解
java·后端·spring