Java多线程面试高配问题---线程基础知识(1)🧵

Java多线程

线程基础知识

1. 进程与线程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。
进程就是用来加载指令、管理内存、管理IO的。

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
一个进程之内可以包含一个或者多个线程。

二者对比

  • 进程是正在运行程序的实例(比如浏览器、txt文档、ppt等),进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间;(比如打开txt和idea是占用不同的内存空间);
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程);

2. 并发与并行的区别

单核CPU

  1. 单个CPU线程实际上还是串行执行的
  2. 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
  3. 微观是串行,宏观并行
  4. 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

多核CPU

每个核(core)都可以调度运行线程,这时候线程可以是并行的。

总结(现在都是多核CPU,在多核CPU下)

  1. 并发(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个
  2. 并行(parallel)是同一时间动手做(doing)多件事情的能力(4核CPU同时执行4个线程)
  3. 举例:

3. 创建线程的方式

共有四种方式可以创建线程,分别是:

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口
  • 线程池创建线程

继承Thread类

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread  run  ...");
    }

    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

实现runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("MyThread  run  ...");
    }
    public static void main(String[] args) {
        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable();

        // 创建Thread对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        // 调用start方法启动线程
        t1.start();
        t2.start();

    }
}

实现Callable接口

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return "ok";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        MyCallable mc = new MyCallable();
        // 创建FutureTask
        FutureTask<String> ft = new FutureTask<String>(mc);
        // 创建Thread对象
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);
        // 调用start方法启动线程
        t1.start();
        // 调用ft的get方法获取执行结果
        String res = ft.get();
        // 输出
        System.out.println(res);
    }
}

线程池创建线程

public class MyExecutors implements Runnable{

    @Override
    public void run() {
        System.out.println("MyThread  run  ...");
    }

    public static void main(String[] args) {
        // 创建固定大小的线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        // 提交任务,执行run
        threadPool.submit(new MyExecutors());

        // 关闭线程池
        threadPool.shutdown();
    }
}

runable和callable的区别

  • Runnable接口run方法没有返回值
  • Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

run()和start()有什么区别

  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

总结

创建线程的方式有哪些?

  • 继承Thread类
  • 实现runnable接口
  • 实现callable接口
  • 线程池创建线程(项目中使用方式)
    runnable和callable有什么区别?
  • Runnable接口run方法没有返回值
  • Callable接口call方法有返回值,需要FutureTask获取结果
  • Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
    run()和start()有什么区别?
  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

4. 线程包含哪些状态,状态直接如何变化的?

线程的状态可以参考JDK中的Thread类中的枚举State

  • NEW 尚未启动的线程的线程状态
  • RUNNABLE 可运行的线程的线程状态
  • BLOCKED 线程阻塞等待监视器锁的线程状态
  • WAITING 等待线程的线程状态
  • TIMED_WAITING 具有指定等待时间的线程状态
  • TERMINATED 已经终止的线程的线程状态(线程完成了执行)
    如下图所示,枚举类State

状态的变化过程

总结

线程包括哪些状态?

  • 新建(NEW)
  • 可运行(RUNNABLE)
  • 阻塞(BLOCKED)
  • 等待( WAITING )
  • 时间等待(TIMED_WALTING)
  • 终止(TERMINATED)

线程状态之间是如何变化的?
a. 创建线程对象是新建状态
b. 调用了start()方法转变为可执行状态
c. 线程获取到了CPU的执行权,执行结束是终止状态
d. 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态

如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

5. 新建T1,T2,T3三个线程,如何保证它们按照指定顺序执行?

join():等待线程运行结束

阻塞调用此方法的线程进入timed_waiting直到线程t执行完成后,此线程再继续执行

public class ThreadDemo01 {
    public static void main(String[] args) {
        // 创建线程:Lambda表达式定义了线程的执行内容
        Thread t1 = new Thread(()->{
            System.out.println("t1");
        });

        Thread t2 = new Thread(()->{
            try {
                t1.join(); // 加入线程t1,只有t1线程执行完毕之后,再次执行该线程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t2");
        });

        Thread t3 = new Thread(()->{
            try {
                t2.join(); // 加入线程t1,只有t1线程执行完毕之后,再次执行该线程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t3");
        });

        // 启动线程
        t3.start();
        t2.start();
        t1.start();
    }
}


测试不加join的结果

6. notify()和notifyAll()的区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个wait线程
    演示Java中多线程编程中的等待和唤醒机制,代码如下:
public class NotifyThread {

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 创建t1线程对象,并通过构造函数的参数传递了一个Runnable对象(Lambda表达式),还给线程指定了一个名称 "t1"
        Thread t1 = new Thread(() -> {
            synchronized (lock) {  // 加锁同步块,使用 lock 对象作为锁。只有一个线程可以进入这个同步块,其他线程必须等待直到锁被释放。
                System.out.println(Thread.currentThread().getName() + "...线程等待...");
                try {
                    lock.wait(); // 等待操作,它让当前线程进入等待状态,并释放 lock 锁。线程将一直等待,直到另一个线程调用相同 lock 对象的 notify() 或 notifyAll() 方法,唤醒它。
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "...线程被唤醒了...");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "...线程等待...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "...线程被唤醒了...");
            }
        }, "t2");

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

        Thread.sleep(2000);

        synchronized (lock) {
            lock.notify();
        }

    }
}

notify() 运行结果:

notifyAll() 运行结果:

7. java中wait方法和sleep方法有什么不同

共同点:

  • wait()/wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权进入阻塞状态
  • 它们都可以被interrupted方法中断。
    不同点:
  • 方法归属不同
    sleep(long)是 Thread的静态方法;
    而wait(), wait(long)都是Object的成员方法,每个对象都有;
  • 醒来时机不同
    执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来;
    wait(long)和 wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去;
    它们都可以被打断唤醒;
  • 锁特性不同(重点)
    wait方法 的调用必须先获取 wait对象的锁,使用 wait 方法则必须放在 synchronized 块里面,同样需要捕获 InterruptedException 异常,并且需要获取对象的锁。而sleep 则无此限制;
    wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用);
    而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了);

代码演示

wait方法 的调用必须先获取 wait对象的锁,配合synchronized使用,运行情况如下:

如果去掉了synchronized就会报错,运行如下:

8. 如何停止一个正在运行的线程

使用interrupt方法中断线程 示例

/**
 * @Name TwoPhaseTermination
 * @Author xiaoLi
 * @Date 2023/8/25 星期五 17:12
 * 两阶段终止模式
 * 要注意 isInterrupted()方法不会清除打断标记
 * 还有一个方法 interrupted() static的 会清除打断标记
 */
@Slf4j
public class interruptTest2 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }

}

@Slf4j
class TwoPhaseTermination {
    private Thread monitor;

    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 如果这个标记为真,代表这个程序需要自己体面了
                if (current.isInterrupted()) {
                    log.debug("自己体面~");
                    break;
                }
                try {
                    Thread.sleep(1000); // 情况1 在sleep时被打断
                    log.info("执行监控任务中..."); // 情况2 执行任务时被打断
                } catch (InterruptedException e) {
                    // 此处如果不重置,程序还会继续运行下去 重新给标记置为true
                    current.interrupt();
                    e.printStackTrace();
                }
            }
        });
        monitor.start();
    }

    public void stop() {
        if (monitor != null) {
            monitor.interrupt();
        }
    }
}

————————————————
原文链接:https://blog.csdn.net/qq_48592827/article/details/132870125

posted @   xiaolifc  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示