多线程基础面试题

发布于:2022-12-22 ⋅ 阅读:(510) ⋅ 点赞:(0)

多线程

1.并发、并⾏、串⾏之间的区别

  1. 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
  2. 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
  3. 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏

2.并发的三⼤特性:有序性 ,原子性,可见性

原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。 关键字:synchronized

可见性:当一个线程修改了共享变量的值,其他线程能够看到修改的值。关键字:volatile、synchronized、final

有序性:即程序执行的顺序按照代码的先后顺序执行。关键字:volatile、synchronized

记住这三句话:指令重排导致了有序性;线程切换导致了原子性;缓存导致了可见性 。

3.Java死锁如何避免?

造成死锁的⼏个原因:

  1. ⼀个资源每次只能被⼀个线程使⽤
  2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
  3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
  4. 若⼲线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决

4.如何理解volatile关键字

保证被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共 享变量的值,新值总是可以被其他线程⽴即得知。

禁⽌指令重排序优化

5.为什么⽤线程池?解释下线程池参数?

1、降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。

2、提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。

3、提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。

  • corePoolSize 代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不 会消除,⽽是⼀种常驻线程
  • maxinumPoolSize 代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程 数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最⼤线程数
  • keepAliveTime 、 unit 表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程 不会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间
  • workQueue ⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部 放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程
  • ThreadFactory 实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创 建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选 择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚
  • Handler 任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池 后,这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续 想线程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处 理新提交的任务时,这是也就拒绝

6.线程池的底层⼯作原理

img

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

CAS:CAS是一种乐观锁

CAS缺点?

1、循环时间开销大(如果CAS失败,会一直尝试)

2、只能保证一个共享变量的原子操作。(对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用加锁来保证)

3、存在ABA问题

原子类AtomicInteger类ABA问题及解决方案

1、ABA问题是怎么产生的?

当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新的值U之前。另外两个线程已经连续修改了两次变量V的值,使得该值又恢复为旧的值,这样我们就无法正确判断这个变量是否已经被修改过

2、ABA问题的解决方案:

使用版本号的方式解决ABA问题.

AtomicStampedReference:是一个带有时间戳的对象引用,在每次修改后,不仅会设置新的值,还会记录修改的时间

AtomicMarkableReference:维护的是一个Boolean值的标识,这种方式并不能完全防止ABA问题的发生,只能减少ABA发生的概率

锁的类型

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高。每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞(block)直到拿到锁。

java中的悲观锁就是Synchronize、AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁,如:ReentrantLock。

锁的状态无锁、偏向锁、轻量级锁、重量级锁

在这里插入图片描述

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

sleep()、wait()、join()、yield()之间的的区别

锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则 其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线 程得到后会进⼊就绪队列进⾏等待cpu资源分配。

等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只 有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀ 个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中

  1. sleep 是 Thread 类的静态本地⽅法,wait 则是 Object 类的本地⽅法。

  2. sleep⽅法不会释放lock,但是wait会释放,⽽且会加⼊到等待队列中。

  3. sleep⽅法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字

  4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别⼈中断)。

  5. sleep ⼀般⽤于当前线程休眠,或者轮循暂停操作,wait 则多⽤于多线程之间的通信。

  6. sleep 会让出 CPU 执⾏时间且强制上下⽂切换,⽽ wait 则不⼀定,wait 后可能还是有机会重新竞 争到锁继续执⾏的。 sleep()、wait()、join()、yield()之间的的区别

  7. yield()执⾏后线程直接进⼊就绪状态,⻢上释放了cpu的执⾏权,但是依然保留了cpu的执⾏资 格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏

  8. join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队 列,直到线程A结束或中断线程

Thread和Runable的区别

Thread和Runnable的实质是继承关系,没有可⽐性。⽆论使⽤Runnable还是Thread,都会new Thread,然后执⾏run⽅法。⽤法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执⾏⼀个任务,那就实现runnable。

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的 值
  3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把 设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强 引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
  4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)
    img

使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。

ThreadLocal内存泄露原因,如何避免

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据

将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

ReentrantLock中的公平锁和⾮公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,

它们的区别在于:线程在使 ⽤lock()⽅法加锁时,

  • 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,
  • 如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。

不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤ CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release() ⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。

Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

一、JAVA如何开启线程?怎么保证线程安全?

进程和线程的区别:进程是操作系统进行资源的最小单元。线程是操作系统进行任务分配的最小单元,线程隶属于进程。

如何开启进程?1.继承Thread类,重写run方法。2.实现Runable接口,实现run方法。3.实现Callable接口,实现call方法。通过FuterTask创建一个线程。获取到线程的执行的返回值,4.通过线程池来开启线程。

怎么保证线程安全?加锁: 1、JMM提供的锁,也就是Synchronized关键字。2、JDK提供的各种锁Lock。

二、Volatile和Synchronized有什么区别? Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?

1、Synchronized关键字,用来加锁。Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。

2、不能。Volatile关键字只能保证线程可见性,不能保证原子性。

3、Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

三、JAVA线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

1、JAVA的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。

在这里插入图片描述

在这里插入图片描述

2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。|

在这里插入图片描述

四、谈谈你对AQS的理解。AQS如何实现可重入锁?

1.AQS是一个JAVA线程同步的框架,是JDK中很多锁工具的核心实现框架。

2.在AQS中,维护了一个信号量state和一个线程组成的双向队列链表,其中,这个线程队列,就是用来给线程排队的,而state就是一个红绿灯,用来控制线程队列的排队或者放行的。在不同的处境下有不同的意义。
在这里插入图片描述

3.在可重入锁这个场景下,state就用来表示加锁的次数,0标识无锁,每加一次锁,state就加1,每释放锁,state就减一

五、有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错进行?

CountDownLatch, CvlicBarrier, Semaphore。

//保证三个线程同时执行
public static void main(String[] args) throws InterruptedException {
    int size=3;
    ThreadSalfDemo threadSalfDemo=new ThreadSalfDemo();
    CountDownLatch countDownLatch=new CountDownLatch(1);
    for (int i = 0; i < size; i++) {
        new Thread(()->{
            try {
                countDownLatch.await();
                System.out.println(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();
    }
    Thread.sleep(500);
    countDownLatch.countDown();
}
//利用volatile来保证线程之间的数据可见性
public static volatile int ticket=1;
public static void main(String[] args) {

    Thread t1=new Thread(()->{
        while (true){
            if (ticket==1){
                try {
                    Thread.sleep(100);
                    for (int i = 0; i < 10; i++) {
                        System.out.println("a"+i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket=2;
                return;
            }
        }
    });

    Thread t2=new Thread(()->{
        while (true){
            if (ticket==2){
                try {
                    Thread.sleep(100);
                    for (int i = 0; i < 10; i++) {
                        System.out.println("b"+i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket=3;
                return;
            }
        }
    });

    Thread t3=new Thread(()->{
        while (true){
            if (ticket==3){
                try {
                    Thread.sleep(100);
                    for (int i = 0; i < 10; i++) {
                        System.out.println("c"+i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket=1;
                return;
            }
        }
    });
    t1.start();
    t2.start();
    t3.start();
}

如果你提交任务时,线程池队列已满,这时会发生什么

1.如果使用的无界队列,那么可以继续提交任务时没关系的
⒉.如果使用的有界队列,提交任务时,如果队列满了,如果核心线程数没有达到上限,那么则增加线程,如果线程数已经达到了最大值,则使用拒绝策略进行拒绝

如何查看线程死锁

1.可以通过jstack命令来进行查看,jstack命令中会显示发生了死锁的线程
⒉.或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

1、查询是否锁表
show OPEN TABLES where In_use > 0;
2、查询进程
show processlist;|
3、查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKs;
4、查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

线程之间如何进行通讯的

1.线程之间可以通过共享内存或基于网络来进行通信
⒉如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒

3.像Java中的wait()、notify()就是阻塞和唤醒
4.通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式

ABLES where In_use > 0;
2、查询进程
show processlist;|
3、查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKs;
4、查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;


### 线程之间如何进行通讯的

1.线程之间可以通过共享内存或基于网络来进行通信
⒉如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒

3.像Java中的wait()、notify()就是阻塞和唤醒
4.通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到