Java后端高频面试知识点汇总:多线程

目录

一、 线程与进程

能够理解什么是线程与进程?

面试题:线程和进程有什么区别?

 二、创建线程的方式: (3种)

(1)创建线程方式一:继承Thread类的方式:

 (2)创建线程方式二:实现Runnable接口的方法

2.1 实现Runnable接口,重写run方法

2.2 匿名内部类方式

(3)创建线程方式三:实现Callable接口(使用线程池创建)

面试题: 实现接口比继承Thread类所具有的优势:

三、线程安全问题

四、线程状态(六个)

线程状态的切换:

 实现等待唤醒机制程序:

      - 分析的等待唤醒机制程序:

面试题1: 线程中start()和run()的区别

面试题2: sleep()和wait()的区别:

五、 CAS原理

六、 AQS

未完..........

面试题


一、 线程与进程

能够理解什么是线程与进程?

进程:进程是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程 从创建、运行到消亡的过程。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个 进程;

  • 进程:其实就是应用程序的可执行单元(.exe文件)
  • 每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;

线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个 进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

  • 线程:其实就是进程的可执行单元
  • 每条线程都有独立的内存空间,一个进程可以同时运行多个线程; 

总结:

  1. 多线程并发:多条线程在同一时间段交替执行
  2. 在java中线程的调度是:抢占式调度
  3. java最少有2个线程,一条为主线程,一条为垃圾回收线程。
  4. Java 程序的进程里面至少包含两个线程,主线程也就是 main()方法线程,另外一个是垃圾回收机制线程。每 当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中 启动了一个 进程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

面试题:线程和进程有什么区别?

  1. 进程是系统资源调度的最小单位,线程是CPU调度的最小单位
  2. 每条线程都有独立的内存空间,一个进程可以包含多个线程
  3. 一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
  4. 进程在执行时拥有独立的内存单元,多个线程共享进程的内存。
  5. 进程的系统开销大于线程的开销,线程需要的系统资源较少
  6. 进程和线程的通信方式不一样。

面试题:

 二、创建线程的方式: (3种)

(1)创建线程方式一:继承Thread类的方式:

第一种继承Thread类 重写run方法

public class Demo1CreateThread extends Thread {

    public static void main(String[] args) throws InterruptedException {

        System.out.println("-----多线程创建开始-----");
        // 1.创建一个线程
        CreateThread createThread1 = new CreateThread();
        CreateThread createThread2 = new CreateThread();
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        System.out.println("-----多线程创建启动-----");
        createThread1.start();
        createThread2.start();
        System.out.println("-----多线程创建结束-----");
    }

    static class CreateThread extends Thread {
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 5; i++) {
                System.out.println(name + "打印内容是:" + i);
            }
        }
    }
}

 (2)创建线程方式二:实现Runnable接口的方法

2.1 实现Runnable接口,重写run方法

际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还 是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。

public class Demo2CreateRunnable {

    public static void main(String[] args) {
        System.out.println("-----多线程创建开始-----");
        // 1.创建线程
        CreateRunnable createRunnable = new CreateRunnable();
        Thread thread1 = new Thread(createRunnable);
        Thread thread2 = new Thread(createRunnable);
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        System.out.println("-----多线程创建启动-----");
        thread1.start();
        thread2.start();
        System.out.println("-----多线程创建结束-----");
    }

    static class CreateRunnable implements Runnable {

        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 5; i++) {
                System.out.println(name + "的内容:" + i);
            }
        }
    }
}

2.2 匿名内部类方式

 使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作

public class Demo3Runnable {

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 5; i++) {
                    System.out.println(name + "执行内容:" + i);
                }
            }
        }).start();

        new Thread(()-> {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 5; i++) {
                    System.out.println(name + "执行内容:" + i);
                }
        }).start();

        Thread.sleep(1000l);
    }
}

 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源 共享。

(3)创建线程方式三:实现Callable接口(使用线程池创建)

Callable是有返回结果的线程实现,通过Future.get()来获得线程返回的结果,get()方法会造成线程的阻塞。

public class Thread_03_CallableThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable call = new Callable() {
            @Override
            public Object call() throws Exception {
                Thread.sleep(3000);
                return "Hello Callable";
            }
        };
        // 创建固定大小的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        // 给线程池添加要执行的内容
        Future future = threadPool.submit(call);
        // 主线程打印abc, 代表着主线程与callable线程是异步线程
        System.out.println("abc");
        // 由于future.get方法会阻塞,因此必须等待Callable执行完后才会输出
        System.out.println(future.get());
        // 关闭线程池
        threadPool.shutdown();
    }
}

面试题: 实现接口比继承Thread类所具有的优势:

  1. 可以避免java中的单继承的局限性。
  2. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  3. 适合多个相同的程序代码的线程去共享同一个资源。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

三、线程安全问题

   (1)可见性问题:
        原因:一条线程对共享变量的修改,对其他线程不可见
        解决办法: volatile关键字修饰共享变量
        
   (2)有序性问题:
        原因: 编译器可能会对代码进行重排,造成数据混乱
        解决办法:volatile关键字修饰共享变量
        
    (3)原子性问题:
        共享变量原子性问题:
            原因:多条线程对共享变量的多次操作,产生了覆盖的效果
            解决办法: 原子类,同步锁,Lock锁
            
        代码块原子性问题:
            原因:一段代码,被一条线程执行,可能会被其他线程打断,从而造成数据混乱
            解决办法: 同步锁,Lock锁

四、线程状态(六个)

无限等待:

- 进入无限等待: 使用锁对象调用wait()方法
- 唤醒无限等待线程: 其他线程使用锁对象调用notify()或者notifyAll()方法
- 特点: 不会霸占cpu,也不会霸占锁对象(释放)

线程状态的切换:

 实现等待唤醒机制程序:

  •   必须使用锁对象调用wait方法,让当前线程进入无限等待状态

  •   必须使用锁对象调用notify\notifyAll方法唤醒等待线程

  •   调用wait\notfiy\notfiyAll方法的锁对象必须一致

      
- 分析的等待唤醒机制程序:

  •   线程的调度依然是抢占式调度
  •   线程进入无限等待状态,就不会霸占cpu和锁对象(释放),也不会抢占cpu和锁对象
  •   如果是在同步锁中\Lock锁中,调用sleep()方法进入计时等待,不会释放cpu和锁对象(依然占用)

面试题1: 线程中start()和run()的区别

start() :它的作用是启动一个新线程。

通过Thread类中的start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。

run() :run()就和普通的成员方法一样,可以被重复调用。

如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里。

面试题2: sleep()和wait()的区别:

sleep():sleep 方法是属于Thread 类中的方法。它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,但是在此期间线程不会释放锁,只会阻塞线程,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,高优先级和低优先级的线程都有机会执行。

wait():wait 方法是属于Object 类中的方法,wait 过程中线程会释放对象锁,只有当其他线程调用 notify()或notifyAll() 才能唤醒此线程。wait使用时必须先获取对象锁,即必须在 synchronized修饰的方法或代码块中使用,那么相应的notify方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常。

主要有四点区别:

  1. sleep()方法是Thread类的静态方法,wait()方法是Object超类的成员方法。
  2. sleep()方法导致程序暂停指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程是不会释放锁的。而调用wait()方法会释放对象锁,只有当此对象调用notify()方法后才会唤醒线程。
  3. sleep()方法可以在任何地方使用,wait()方法只能在同步方法和同步代码块中配合synchronized使用。
  4. sleep()方法需要抛出异常,wait()方法不需要。

五、 CAS原理

CAS:Compare And Swap (比较并替换)

CAS 指令需要有 3 个操作数:分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

在多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并允许再次尝试,当然也允许失败的线程放弃操作。

CAS怎么保证修改的值可见?volatile

ABA问题:

ABA问题:在CAS操作中有个经典的ABA问题?解决方式?(版本号、时间戳)

假如线程①使用CAS修改初始值为A的变量X,那么线程①会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,程序运行也不一定是正确的。

在线程①获取变量X的值A后,在执行CAS前,线程②使用CAS修改了X的值为B,然后又使用CAS修改了变量X的值为A。

所以,线程①执行CAS时X的值是A,但是这个A已经不是线程①获取时的A了,这就是ABA问题。

ABA问题的产生是因为变量的状态值产生了环形转换。

避免ABA问题:使用版本号或时间戳。给每个变量的状态值配备一个时间戳或者版本号。

六、 AQS

AQS:抽象同步队列

一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的条件队列。

Node节点

AQS是一个先进先出的双向队列,队列中元素的类型是Node类型,其中Node中的thread变量用来存放进入AQS队列中的线程。

ConditionObject

AQS中还有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。

ConditionObject变量是条件变量,每个条件变量都维护了一个条件队列(单向链表队列),其用来存放调用条件变量的await()方法后被阻塞的线程。

①当线程调用条件变量的await()方法时,必须先调用锁的lock()方法获取锁。调用await()方法后,在内部会将当前线程构造一个node节点,插入到条件队列的尾部,之后当前线程会释放获取的锁(也就是操作锁对应的state变量的值),并被阻塞挂起。

②当另外一个线程调用条件变量的signal()方法时,必须先调用锁的lock()方法获取锁,在内部会把条件队列里面对头的一个线程节点从条件队列中移除并放入AQS的阻塞队列中,等待获取锁。

state变量

被volatile修饰的state变量。

线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

①独占方式

使用独占方式时:如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。

比如:ReentrantLock,当一个线程获取了ReentrantLock的锁后,在AQS内部会使用CAS操作把state的值从0变为1,然后设置当前锁的持有者设为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把state值从1变为2,也就是设置可重入次数,而当另一个线程获取锁时发现自己不是该锁的持有者就会被放入AQS阻塞队列后挂起。

②共享方式

使用共享方式时:当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用自旋CAS方式进行获取,否则旧把当前线程放入阻塞队列。

比如:Semaphore信号量,当一个线程通过acquire()方法获取信号量时,会首先看当前信号量的个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。

未完..........

面试题

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值