精华内容
下载资源
问答
  • java多线程面试题
    千次阅读
    2021-06-23 16:01:33

    Java 线程面试问题

    在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务中多线程和并发是一个非常受欢迎的话题,特别是电子交易发展方面相关的。他们会问面试者很多令人混淆的Java线程问题。面试官只是想确信面试者有足够的Java线程与并发方面的知识,因为候选人中有很多只浮于表面。用于直接面向市场交易的高容量和低延时的电子交易系统在本质上是并发的。下面这些是我在不同时间不同地点喜欢问的Java线程问题。我没有提供答案,但只要可能我会给你线索,有些时候这些线索足够回答问题。现在引用Java5并发包关于并发工具和并发集合的问题正在增多。那些问题中ThreadLocal、Blocking Queue、Counting Semaphore和ConcurrentHashMap比较流行。

    15个Java多线程面试题及回答

    1)现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

    这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。

    2)在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

    lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。

    3)在java中wait和sleep方法的不同?

    通常会在电话面试中经常被问到的Java线程面试问题。最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

    4)用Java实现阻塞队列。

    这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用Java线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用wait()和notify()方法来实现阻塞队列,你可以要求他用最新的Java 5中的并发类来再写一次。

    5)用Java写代码来解决生产者——消费者问题。

    与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

    6)用Java编程一个会导致死锁的程序,你将怎么解决?

    这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。

    7) 什么是原子操作,Java中的原子操作是什么?

    非常简单的java线程面试问题,接下来的问题是你需要同步一个原子操作。

    8) Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?

    自从Java 5和Java内存模型改变以后,基于volatile关键字的线程问题越来越流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性。

    9) 什么是竞争条件?你怎样发现和解决竞争?

    这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验,or writing code which is free of data race or any other race condition。关于这方面最好的书是《Concurrency practices in Java》。

    10) 你将如何使用thread dump?你将如何分析Thread dump?

    在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

    11) 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

    这是另一个非常经典的java多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级Java面试的第一轮被问到。这个问题的回答应该是这样的,当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。阅读我之前写的《start与run方法的区别》这篇文章来获得更多信息。

    12) Java中你怎样唤醒一个阻塞的线程?

    这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了IO阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。

    13)在Java中CycliBarriar和CountdownLatch有什么区别?

    这个线程问题主要用来检测你是否熟悉JDK5中的并发包。这两个的区别是CyclicBarrier可以重复使用已经通过的障碍,而CountdownLatch不能重复使用。

    14) 什么是不可变对象,它对写并发应用有什么帮助?

    另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个java面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么String是不可变的。

    15) 你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?

    多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。

    补充的其它几个问题:

    1) 在java中绿色线程和本地线程区别?

    2) 线程与进程的区别?

    3) 什么是多线程中的上下文切换?

    4)死锁与活锁的区别,死锁与饥饿的区别?

    5) Java中用到的线程调度算法是什么?

    6) 在Java中什么是线程调度?

    7) 在线程中你怎么处理不可捕捉异常?

    8) 什么是线程组,为什么在Java中不推荐使用?

    9) 为什么使用Executor框架比使用应用创建和管理线程好?

    10) 在Java中Executor和Executors的区别?

    11) 如何在Windows和Linux上查找哪个线程使用的CPU时间最长?

    这只是其中的一部分,还有更多的资料以及答案

    需要学习路线视频及其文档来领取学习的小伙伴,可以转发此文关注小编,私信小编【学习】来领取!!

    最后再给大家分享一下大数据的学习路线图,希望本文能够帮助到大家的学习,也希望能够得到大家的喜欢!!

    更多相关内容
  • java多线程面试题

    2017-11-08 23:09:17
    java多线程面试经典,内含50道常被面试官问到的java多线程
  • 1. 什么是线程?...在Java Concurrency API中有哪些原⼦类(atomic classes)? 8. 什么是Executors框架? 9. 什么是阻塞队列?如何使⽤阻塞队列来实现⽣产者-消费者模型? 10. 什么是Callable和Future?
  • 面试题和答案. 有多线程, 数据库,框架题目等知识点
  • 主要介绍了15个高级Java多线程面试题及回答,翻译自国外的一篇文章,这些面试题容易混淆、较难回答,需要的朋友可以参考下吧
  • java 多线程 面试题整理(更新......)

    万次阅读 多人点赞 2021-11-30 16:23:06
    3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的...

    一、基础知识​​​​​​1、什么是线程和进程?什么是进程?进程的特点:什么是线程?区别与联系?2、什么是并行与并发?3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的关系6、什么是Callable和Future?7、什么是线程的上下文切换?8、Thread类中的start()和run()方法有什么区别?9、Java中interrupted和isInterruptedd方法的区别?10、为何stop()和suspend()方法不推荐使用10、如何停止一个正在运行的线程?(重要)i:捕捉打断标记并且直接returnii:捕捉打断标记,并且抛出异常终止程序iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记11、sleep和yield的区别?状态的区别:调度的区别:12、sleep,yield为什么是静态方法(重要)13、有三个线程T1,T2,T3,如何保证顺序执行?14、在 java中守护线程和本地线程区别15、sleep和wait的区别?16、线程创建到结束的几种状态?17、对线程优先级的理解?18、什么是后台线程?19、sleep,yiled,wait,join 对比21、Thread.sleep(0)有什么作用?二、锁知识20、什么是线程安全?21、什么是竞态条件?22、什么是临界区?22、什么是不可变对象,它对写并发应用有什么帮助?(重要)23、synchronized关键字最主要的三种使用方式1、修饰实例方法2、修饰静态方法3、修饰代码块24、讲讲你对synchronized的认识?1、无锁状态2、偏向锁状态偏向锁锁撤销:偏向锁存在的意义:偏向锁撤销的情况批量重偏向:批量撤销偏向锁3、轻量级锁锁重入:轻量级锁CAS4、自旋锁25、什么是重量级锁?为什么消耗很大?26、自旋锁的优缺点27、线程同步和互斥有几种实现方法,都是什么?(重要)28、wait的基本使用方法29、wait的相关问题(1)notify()和notifyAll()有什么区别?(2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)(3)为什么wait和notify方法要在同步块中调用?(4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?(5) join方法实现原理(6)如何实现线程间通信31、park和unpark(1)基本使用(2)先调用park再调用unpark(3)先调用unpark再调用park32、park和wait的区别33、park,wait,sleep,yield,join方法的区别34、什么是死锁,死锁发生的条件(重要)死锁的定义:死锁的四个条件:(重要,记忆)怎么预防死锁问题?怎么避免死锁问题怎么检测和解除死锁35、什么是活锁避免活锁的方法活锁与死锁的区别?36、什么是饥饿?37、什么是可重入锁?38、Reentrantlock39、ReentrantLock和Synchronized的相同点和区别(重要)40、ReentrantLock的实现原理41、lock、tryLock和lockInterruptibly的差別42Condition和Object类锁方法区别43、公平锁与非公平锁三、无锁机制(CAS,原子类)44、什么是java内存模型?45、什么是volitile?作用是什么46、什么是原子性?47、有序性48、volatile怎么保证可见性和有序性的?可见性有序性50、单例模式的双检锁是什么?51、synchronized 和 volatile 的区别是什么?为啥synchronized无法禁止指令重排,但可以保证有序性?30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)52、CAS53、synchronized与CAS的区别(重要)54、CAS的缺点(重要)1) CPU开销过大2) 不能保证多个变量的原子性如何解决CAS只能保证一个变量的原子性操作问题?3)ABA问题49、volatile 变量和 atomic 变量有什么不同?50、什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?

    一、基础知识

    1、什么是线程和进程?

    什么是进程?

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

    进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程。比如.exe文件运行,进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程

    总结:进程是把指令加载给CPU,数据加载到内存并执行的程序实例

    进程的特点:

    1、每个进程可以包括多个线程

    2、每个进程都有自己独立的内存空间,而其内部的线程可以共享这些内存空间,进程上下文切换的开销比较大,不同进程之间不共享内存

    什么是线程?

    线程是进程的一个子集,一个线程就是一个指令流的执行,线程按照一定的顺序把这些指令流交给CPU执行,就是线程的执行

    区别与联系?

    线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。

    不同的进程使用不同的内存空间,而线程共享同一进程的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

    线程作为操作系统能够进行运算调度的最小单位,进程作为资源分配的最小单位。

    线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

    2、什么是并行与并发?

    并发:操作系统的任务调度器调度多个线程轮流使用某个CPU的操作(CPU的时间片为15ms),这个过程中会发生线程的上下文切换

    1583408729416

    并行:对于多核CPU来讲,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

    1583408812725

    一般来说对于单核CPU的机器,线程执行是并发的,对于多核CPU来讲,线程执行是既有并行也有并发的

    3、什么是同步执行和异步执行

    以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步

    也就是说一个程序需要运行完了有结果了才能进行下一个线程,这样这个程序就会堵塞其他的程序,这就是同步,异步就是这个程序在运行的时候我仍然可以不管他运行别的程序

    多线程可以将同步程序变为异步的,从而增加系统资源的利用率

    比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。

    4、Java中实现多线程有几种方法?(较难)

    Thread的构造方法参数可以传入Runnable接口和FutureTask对象

    Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

    (1)继承Thread类

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

    (2)创建Thread子类的实例,即创建了线程对象。

    (3)调用线程对象的start()方法来启动该线程。

    public class MyThread extends Thread {
        public void run() {
            System.out.println("MyThread.run()");
        }
    }
    MyThread myThread1 = new MyThread();
    myThread1.start();

    (2)实现runable接口

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

    (3)调用线程对象的start()方法来启动该线程

    public class MyThread extends OtherClass implements Runnable {
        public void run() {
            System.out.println("MyThread.run()");
        }
    }
    ​
    ​

    启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:

     MyThread myThread = new MyThread(); 
     Thread thread = new Thread(myThread); 
     thread.start(); 

    //事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用 target.run() public void run() { if (target != null) { target.run(); } }

    (3)实现Callable接口(创建FutureTask(Callable)对象)

    1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    public class SomeCallable<V> extends OtherClass implements Callable<V> {
    ​
        @Override
        public V call() throws Exception {
            // TODO Auto-generated method stub
            return null;
        }
    ​
    }
    Callable<V> oneCallable = new SomeCallable<V>();   
    //由Callable<Integer>创建一个FutureTask<Integer>对象:   
    FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
    //注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
      //由FutureTask<Integer>创建一个Thread对象:   
    Thread oneThread = new Thread(oneTask);   
    oneThread.start();   
    //至此,一个线程就创建完成了。

    5、Future接口,Callable接口,FutureTask实现类的关系

    Callable接口中就一个抽象方法call(),有返回值

    Future接口中定义了关于线程状态的方法,比如打断线程执行的cancel方法,判断该线程是否被取消的isCancelled()方法,返回线程是否执行完的isDone方法,以及重要的get方法获取返回值

    FutureTask实现类实现了Future接口,并且有构造函数,参数是传入一个Callable接口, 

    以此获得返回值

    其中Future接口的get方法是阻塞方法,没有得到get的值会阻塞主线程

    package TestFutureTask;
    ​
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    ​
    public class TestMain {
        public static void main(String[] args) {
            FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
                System.out.println("futureTask开始了");
                Thread.sleep(10000);
                return 100;
            }
            );
            Thread thread = new Thread(futureTask, "thread1");
            thread.start();
            try {
                System.out.println(futureTask.get());//会阻塞主线程使得主线程不能立刻输出语句
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.println("主线程运行!");
        }
    }

    6、什么是Callable和Future?

    Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返 回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执 行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到 异步执行任务的返回值。可以认为是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable用于产生结果,Future 用于获取结果

    Futuretask类通过传入一个Callable接口创建一个有返回值的线程任务,并且其实现了Future接口,可以通过其get方法拿到这个结果

    7、什么是线程的上下文切换?

    多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

    可能有以下原因:

    • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并发的概念)

    • 垃圾回收

    • 有更高优先级的线程需要运行

    • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

    当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态

    8、Thread类中的start()和run()方法有什么区别?

    start方法是线程从就绪变为启动状态的方法,而run方法是线程启动之后需要执行的代码,如果直接调用run方法,相当于使用thread对象调用它的一个普通方法而已,调用者是线程对象,并且是在主线程中执行的。

    而start方法可以使得线程启动,之后再调用run方法便是在该线程中执行

    9、Java中interrupted和isInterruptedd方法的区别?

    一个清除一个不清除中断标记

    interrupted() 不仅返回当前Thread的中断状态,而且会清除当前Thread的中断状态**。所以如果当前Thread.interrupted()返回中断true,紧接着再call一次interrupted() 会返回“非中断false”,因为中断状态在第一次call的时候清除了。(源码中进行了操作)静态方法

    isInterrupted() 也会返回当前Thread的中断状态,但是不会主动清除当前Thread的中断状态

    10、为何stop()和suspend()方法不推荐使用

    用Thread.stop()方法来终止线程将会释放该线程对象已经锁定的所有监视器。如果以前受这些监视器保护的任何对象都处于不连贯状态,那么损坏的对象对其他线程可见,这有可能导致不安全的操作。

    suspend()方法 该方法已经遭到反对,因为它具有固有的死锁倾向。调用suspend()方法的时候,目标线程会停下来,并且不会释放锁资源,在目标线程重新开始以前,其他线程都不能访问该资源。除非被挂起的线程恢复运行。对任何其他线程来说,如果想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。

    10、如何停止一个正在运行的线程?(重要)

    (1)使用stop()来停止线程:stop()方法让线程立即停止运行, 这种暴力停止可能会破坏线程业务的原子性,不推荐使用

    (2)使用interrupt产生打断标志位来停止线程

    i:捕捉打断标记并且直接return

    由于run方法是一个void方法,可以在线程运行的时候用interrupt方法进行打断,此时产生一个打断标记位,捕捉到该标记位之后便可以优雅地结束该线程(可以直接return,也可以进行一些操作后return;)

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 500000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("线程终止, 停止for循环.");
                    return;
                }
                System.out.println("i=" + (i + 1));
            }
        }
    }
    ​
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    ​
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    ​

    ii:捕捉打断标记,并且抛出异常终止程序

    捕捉到标记位之后,扔出异常来停止该线程

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 100000; i++) {
                    if (this.isInterrupted()) {
                        System.out.println("线程终止, 停止for循环.");
                        throw new InterruptedException();
                    }
                    System.out.println("i=" + (i + 1));
                }
            } catch (InterruptedException e) {
                System.out.println("MyThread抛出InterruptedException.");
                e.printStackTrace();
            }
        }
    }
    ​
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    ​
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    ​

    需要使用throw new Exception来打断

    iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记

    当线程处于正常状态的时候,打断会产生打断的标记位,但是在线程处于sleep,join,wait,park等状态时,被打断将不会产生标记位,我们可以使用trycatch块来处理该情况,当程序被打断时,在程序catch并处理打断异常时候可以自己添加打断标记,从而设置打断标记。(两阶段终止模式)

    @Slf4j
    public class Test11 {
        public static void main(String[] args) throws InterruptedException {
            TwoParseTermination twoParseTermination = new TwoParseTermination();
            twoParseTermination.start();
            Thread.sleep(3000);  // 让监控线程执行一会儿
            twoParseTermination.stop(); // 停止监控线程
        }
    }
    ​
    ​
    @Slf4j
    class TwoParseTermination{
        Thread thread ;
        public void start(){
            thread = new Thread(()->{
                while(true){
                    if (Thread.currentThread().isInterrupted()){
                        log.debug("线程结束。。正在料理后事中");
                        break;
                    }
                    try {
                        Thread.sleep(500);
                        log.debug("正在执行监控的功能");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
        public void stop(){
            thread.interrupt();
        }
    }

    注:若程序是while循环,那么在捕捉到打断标记时,也可以用break结束循环从而结束线程

    11、sleep和yield的区别?

    状态的区别:

    调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

    调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

    调度的区别:

    调用sleep之后,该线程将进入阻塞状态,分不到CPU的时间片

    调用yield之后,该线程会让出CPU的使用权,但是任务调度器仍然可能分配给该线程时间片,从宏观上只是该线程被分配CPu的概率变低了

    12、sleep,yield为什么是静态方法(重要)

    Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。其他线程上调用这些方法是没有意义的。也就是说只有本线程才能执行休眠操作,如果sleep是成员方法,其他线程可以获得该线程的实例化对象,从而让此线程强制休眠(释放CPU的资源),这样会带来不可预估的后果。

    分析:wait,join为什么是成员方法

    join可以在其他线程中调用,因为其本身设计的意义就是其他线程等待该线程完成

    wait是本线程获取锁之后,锁对象调用的wait方法,实际上还是在本线程中使用

    sleep,yield不可以被其他线程调用!只能被自身线程调用,也是就是必须是自愿发生才可以!

    13、有三个线程T1,T2,T3,如何保证顺序执行?

    确保一个线程启动之后等待他执行完再进行下一个

    1. 1.          t1.start();
      2. •        t1.join();  
      3. •        t2.start();
      4. •        t2.join();
      5. •        t3.start();
      6. •        t3.join();

    2、现在可以用wait-notify实现线程间通信而达到顺序执行的目的

    14、在 java中守护线程和本地线程区别

    java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool);true则把该线程设置为守护线程,默认用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

    守护线程的特点是,如果一个进程中的其他用户线程全部运行完毕,那么这时守护线程也会自动结束,比如垃圾回收线程

    比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了

    15、sleep和wait的区别?

    sleep是Thread类的静态方法,在线程使用sleep方法之后会让出CPU的资源,但是不会释放锁资源

    wait方法是Object的方法,只能在同步代码块中被调用,某个线程使用锁对象的wait方法,会释放掉该线程的锁资源(同时还有CPU使用权),让其他线程去竞争

    16、线程创建到结束的几种状态?

    img

    1、线程刚被创建的时候是初始化状态New,这时候没有被分配CPU资源

    2、采用start方法之后,线程运行状态即RUNNABLE状态,这时可以被分配时间片资源进入RUNNING(RUNNING状态是包含在RUNABLE中的),也可以因为上下文切换暂时分配不到时间片资源

    3、当线程处于RUNABLE状态时,通过调用wait,join,park等方法会进入到WAITING状态,并且通过对应的唤醒操作,notify和unpark等操作(还得竞争锁成功)可以让线程从WAITING回到RUNABLE状态,join可以通过线程执行完,主线程便会变为RUNABLE

    join的底层原理是把thread对象看为一个对象锁,所以是主线程会进行wait,因此主线程会释放锁(thread锁,比较特殊)

    是主线程waiting了,而且锁是thread

    4、当线程处于RUNABLE状态时,通过调用wait(n),join(n),park(n),sleep(n)等方法会进入到TIMED_WAITING状态,可以通过等待时间结束(sleep),notify等操作回到原状态(竞争锁成功才能回到原状态)

    5、当某个线程与其他线程竞争同一把锁失败会进入BLOCKED状态,处于WAITING的线程被唤醒竞争锁失败也会进入BLOCKED状态,竞争锁成功可以回到RUNNABLE状态

    6、线程执行完毕会进入TERMINATED状态

    17、对线程优先级的理解?

    每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OSdependent)。可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级

    18、什么是后台线程?

    就是守护线程,也可以叫做精灵线程

    19、sleep,yiled,wait,join 对比

    关于join的原理和这几个方法的对比:看这里

    补充:

    1. sleep,join,yield,interrupted是Thread类中的方法

    2. wait/notify是object中的方法

    sleep 不释放锁、释放cpu join 释放锁(主线程)、抢占cpu(被调用的线程) yiled 不释放锁、释放cpu wait 释放锁、释放cpu

    sleep和yield都不会释放锁,但是会释放该线程占用的CPU资源

    对于main{

    thread.join();

    }

    会使得主线程释放锁(相当于把thread作为锁对象进行wait),thread线程会占用CPUz资源

    21、Thread.sleep(0)有什么作用?

    触发操作系统立刻重新进行一次CPU竞争,竞争的结果可能是当前线程仍然获得CPU控制权,也可能是别的线程获得CPU控制权。

    二、锁知识

    20、什么是线程安全?

    线程安全概念:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。

    21、什么是竞态条件?

    计算的正确性取决于多个线程的交替执行顺序时,就会发生竞态条件。

    多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

    竞态条件不是某种条件,而是一种问题结果

    比如对全局变量的读写操作,A线程读取变量还未执行操作时候发生了上下文切换,另一个线程读取变量(由于A并未对变量操作,所以读取的还是原来的变量)并进行了操作,之后切换到A进行了操作,这时相当于只做了A的操作,B的操作被覆盖了

    22、什么是临界区?

    一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区

    22、什么是不可变对象,它对写并发应用有什么帮助?(重要)

    答: 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(MutableObjects)。不可变对象的类即为不可变类(Immutable Class)。

    Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。

    不可变对象永远是线程安全的。

    只有满足如下状态,一个对象才是不可变的;它的状态不能在创建后再被修改;所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的逸出)。 ———————————————— 版权声明:本文为CSDN博主「Java小叮当」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:【2021最新版】Java多线程&并发面试题总结(108道题含答案解析)_程序媛小琬的博客-CSDN博客_java多线程面试题2021

    23、synchronized关键字最主要的三种使用方式

    1、修饰实例方法

     class Test{
            public synchronized void test() {
    ​
            }
        }
        //等价于
        class Test{
            public void test() {
                synchronized(this) {
    ​
                }
            }
        }

    使用方法:

    Test test = new Test();
    test.test();

    synchronized加在实例方法上,需要创建该实例方法所属类的对象,某该线程使用对象引用该方法时候,就会给该线程加上此对象锁。

    • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    2、修饰静态方法

    class Test{
            public synchronized static void test() {
            }
        }
       // 等价于
        class Test{
            public static void test() {
                synchronized(Test.class) {
    ​
                }
            }
        }

    使用方法:

    Test.test()

    修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

    如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

    package TestSynchorized;
    ​
    public class Lock {
        public  synchronized static  void test1(){
            while (true){
                System.out.println("静态方法锁");
            }
        }
        public synchronized void test2() {
                while (true){
                    System.out.println("-------------------------------------实例方法锁2");
                }
        }
            public synchronized void test3(){
                for (int i = 0; i < 10; i++) {
                    System.out.println("实例方法锁3");
                }
        }
    ​
    ​
        public static void main(String[] args) {
            Lock lock = new Lock();
            new Thread(()->{
                Lock.test1();
            }).start();
            new Thread(()->{
                lock.test2();
            }).start();
            new Thread(()->{
                lock.test3();
            }).start();
        }
    }
    ​

    访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。两个线程占用的所对象不同,当然不会发生互斥。

    但是test2和test3的同步代码块由于使用的是一把锁,所以这两个线程会发生互斥,不解决互斥的方法是,创建两个对象,分别用这两个对象调用test2和test3这样就不会使用同一把锁了

    3、修饰代码块

    修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

    Thread t1 = new Thread(() -> {
             for (int i = 0; i < 5000; i++) {
                 synchronized (room) {
                 sout("锁住代码块")
                }
            }
        }, "t1");

    synchronized 关键字底层原理属于 JVM 层面。

    总结:synchronized锁住的同步代码块在执行之前需要获取对应的对象锁

    24、讲讲你对synchronized的认识?

    java中的对象是由对象头和实例数据组成的,对象头如下:

    img

    1、无锁状态

    当一个对象没有被加锁的时候,是无锁状态,对象头中记录了其hashcode以及无锁标志位001

    2、偏向锁状态

    0是可偏向状态,1是已偏向状态

    当一个对象被某个线程加锁,当进入临界区执行代码的时候,该对象从无锁状态变为偏向锁状态,该线程使用CAS操作将线程ID写入该对象的MarkWord,该对象对象头中记录了该线程的ID以自身的偏向锁标志101

    偏向锁锁撤销:

    如果有另外一个线程A也对该对象加锁,那么会引起锁撤销流程:

    该线程检查该对象的MakWork,如果检查到线程ID不是自己A,也就是偏向别的线程B,就发生了竞争现象

    就会执行偏向锁的撤销:

    过程:

    (1)偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的,stop-the-world),到达全局安全点后,持有偏向锁的线程B也被暂停了。 (2)检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着): (3) 如果线程还存活,则检查线程是否还在执行同步代码块中的代码: (4) 如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。 (5)如果线程未存活,或线程未在执行同步代码块中的代码,将该对象设置为无锁状态,A线程再使用CAS操作使得该对象重新偏向自己

    总之:有线程竞争时,判断是不是偏向自己,不是,看看原线程是不是再执行临界区代码,不执行重偏向自己,执行,升级到轻量级锁(重偏向或者升级到重量级锁)

    偏向锁存在的意义:

    1、由于很多方法的临界区代码都只被一个线程所执行,使用偏向锁可以降低系统的开销(使用轻量级锁和重量级锁开销较大)

    2、少了轻量级锁可重入的开销(会检查线程ID是否为自己,如果是的话就不需要重新置换MArkWord了)

    偏向锁撤销的情况

    锁对象调用hashcode()方法会使得其进入无锁状态

    其他线程竞争。。。

    批量重偏向:

    批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。 批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

    ListA listA = new ArrayList();
        Thread t1 = new Thread(() - {
            for (int i = 0; i  50; i++) {
                A a = new A();
                synchronized (a) {
                    listA.add(a);
                }
            }

    当撤销的对象个数达到二十个,JVM把其余其他对象都偏向给另一个线程

    批量撤销偏向锁

    • 当 撤销偏向锁的阈值超过39以后 ,就会将整个类的对象都改为不可偏向

      因为偏向锁的作用是为了偏向某个线程,然而过多的撤销会让JVM觉得这个类的对象锁不可以再偏向了,所以再new 该对象会将这个对象锁置为不可偏向的

    3、轻量级锁

    当锁升级到轻量级锁的时候,线程会在栈内存中创建一个锁记录对象,锁记录对象包括线程地址+轻量级锁标记00,以及对象指针

    锁对象此时是无锁状态,hashcode+01

    1583755737580

    经过CAS操作之后锁记录对象中的线程地址与锁对象的对象头互换,并且锁记录对象指向该锁对象

    此时锁对象中有线程的地址+00标记位(轻量级锁)

    1583755964276

    线程与锁对象分别记录对方的信息

    锁重入:

    广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。

    比如轻量级锁,在执行某个临界区之前已经加了锁,之后在这段代码中继续加锁,就会发生锁重入

    1583756190177

    锁重入,线程栈内存会增加一条记录指向锁对象,会把锁记录地址置为null; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)

    轻量级锁CAS

    轻量级锁CAS是指线程栈内存的锁记录对象地址和锁对象MArkWord互换的过程,这个过程互换成功,说明该线程竞争到了轻量级锁,互换失败的原因有两个:

    1、锁膨胀,该线程没有竞争到轻量级锁,进入锁膨胀过程,申请Monitor,并且进入EntryList等待

    1583757586447

    Monitor的Oewner指向竞争成功的线程。

    2、锁重入机制

    因此轻量级锁有其他线程竞争时就会进入锁膨胀,竞争成功的线程成为重量级锁的OWner,失败的线程进入EntryList进行等待(BLOCKED状态)

    4、自旋锁

    是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

    主要是在重量级锁的竞争过程

    也就是说在线程0获得重量级锁执行同步代码块的时候,线程一不断地用CAS操作去攻击对象锁的MarkWord看其是不是能交换成功,如果在这个不断攻击的过程中线程0执行完了同步代码块,这时候线程1就可以获得重量级锁,就不用进入EntryList等待了,而如果攻击了多次没有效果那么就会自旋失败

    特点没有竞争成功,可以不立马进入休眠状态,而是不断地使用CAS操作与对象锁进行MarkWord进行交换,交换成功则自旋成功,自旋一定次数之后就会失败,进入EntryList休眠

    1583758113724

    自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,*多核 CPU 自旋才能发挥优势*

    25、什么是重量级锁?为什么消耗很大?

    Monitor:

    每个对象都有一个监视锁,或者叫管 程,他是为了该对象成为重量级锁对象准备的,此时锁对象的MarkWord指向该Monitor地址。

    重量级锁是轻量级锁在出现多线程竞争时膨胀得到的一种锁,其依靠锁对象的Monitor锁实现的,对于竞争到锁的线程,Monitor的Owner便是该线程,对于竞争失败的线程,经历了一定次数的自旋之后便会进入EntryList进行等待,锁对象的MarkWork地址是Monitor的地址。

    为什么重量级线程开销很大的?

    当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

    也被成为互斥锁,同步锁,悲观锁

    26、自旋锁的优缺点

    优点:在线程竞争不激烈或者临界区代码执行耗时不长的时候,自旋可以减少线程阻塞进入等待队列的操作,从而减少了操作系统挂起,唤醒线程的操作,降低系统消耗

    缺点:在线程竞争激烈或者临界区代码执行耗时长的时候,会出现自旋失败的情况,空耗CPU的资源

    27、线程同步和互斥有几种实现方法,都是什么?(重要)

    28、wait的基本使用方法

    thread1 --->
    synchronized(obj){
       while(condition is not satified){
       obj.wait;
       }
    }
    thread2--->
    sychronized(obj){
    while (condition is satified){
    obj.notify/notifyAll
    }
    }

    特点:

    当线程0获得到了锁, 成为Monitor的Owner, 但是此时它发现自己想要执行synchroized代码块的条件不满足; 此时它就调用obj.wait方法, 进入到Monitor中的WaitSet集合, 此时线程0的状态就变为WAITING。 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别: BLOCKED状态的线程是在竞争锁对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态 WAITING状态的线程是获得了对象的锁,但是自身的原因无法执行synchroized的临界区资源需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态 处于BLOCKED状态的线程会在锁被释放的时候被唤醒(包括Owner线程执行了wait,以及owner执行完临界区代码释放锁) 处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。然后它会进入到EntryList, 重新竞争锁 (此时就将锁升级为重量级锁)

    注意:**obj.wait和obj.notify方法必须是拿到该锁的线程执行才可以

    总之:BLOCKED的线程是自身竞争不到锁,进入ENtryList等待OWner释放锁并竞争

    WAITING的线程是由于自身某些执行的条件不满足,自己进行wait,等待条件满足时,会被其他线程唤醒,进入到ENTryList进行竞争锁

    29、wait的相关问题

    (1)notify()和notifyAll()有什么区别?

    notify和notifyAll都可以同一把锁唤醒处于WAITING状态的线程,并且让它们进入EntryList去竞争锁,但是notify只能随即唤醒一个线程,而notifyALl会唤醒所有的线程

    (2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)

    Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait,notify,notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象

    线程为了进入临界区(也就是同步块内),需要获得锁并等待锁可用,它们并不知道也不需要知道哪些线程持有锁,它们只需要知道当前资源是否被占用,是否可以获得锁,所以锁的持有状态应该由同步监视器来获取,而不是线程本身

    如果wait()方法定义在Thread类中,线程正在等待哪个锁就不明显了

    (3)为什么wait和notify方法要在同步块中调用?

    在同步块中调用的意义是首先获得某个对象锁,如果不在同步块中调用,notify将无法获知唤醒的是哪个锁的等待线程,wait也无法获知等待那个锁

    notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)

    (4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

    阻塞队列(BLOCKINGQUEUE)是一个在队列基础上又支持了两个附加操作的队列。

    2个附加操作:

    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程(生产者线程),直到队列不满。

    支持阻塞的移除方法:队列空时,队列会阻塞获取元素的线程(消费者线程),直到队列变为非空。

    jdk1.5之前使用简单的wait和notify实现生产者消费者模式,之后使用rentreelock的await/singal实现阻塞队列并实现生产者消费者模式

    通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。当消费者向空队列中取元素的时候会被阻塞,直到生产者生产了一个元素之后,会通知消费者当前队列可用

    (5) join方法实现原理

    public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
    ​
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    ​
            if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }

    底层是通过wait实现的,主线程将thread作为锁对象,并调用wait方法实现阻塞,当阻塞超时,或者线程执行完毕(!isAlive)死亡,主线程便会唤醒,因此这是主线程的阻塞,等待线程执行完毕,释放锁,停止阻塞

    (6)如何实现线程间通信

    wait/notify机制

    对于A线程需要等待某个条件成立在执行,对于B线程可以生产该条件

    A可以wait,b可以生产该条件之后,notify A

    31、park和unpark

    (1)基本使用

    thread1----->
    {
    LockSupport.park();
    }

    park是LockSupport的一个静态方法,它在某个线程中被调用时,会暂停该线程的执行,并且该线程会进入WAITING状态

    LockSupport.unpark(thread1);

    unpark也是LockSupport的一个静态方法

    会把被暂停的线程重新唤醒

    (2)先调用park再调用unpark

    每个线程都有自己的一个 Parker 对象(底层,由c代码实现),由三部分组成 _counter, _cond和 _mutex

    先调用park,检查cond变量如果是0,则线程进入Parker对象锁的Waiting队列(这也解释了为什么park之后是WAITING对象)

    再调用unpark对象,cond变为1,线程获得锁对象,正常运行,cond再变回0

    (3)先调用unpark再调用park

    先调用unpark,检查cond是0,设置cond为1,再调用park发现cond是1,无需打断线程执行,把cond置为0

    unpark调用时会把cond变为1,park调用时会检查cond,为0才打断运行,为1则不打断并且重新置为0

    32、park和wait的区别

    park是静态方法属于LockSupport,执行层面上是属于线程的,wait是Object的一个方法,执行层面是属于锁对象的

    park可以先调用park再调用unpark,但是wait不能先调用notify

    park唤醒的线程比较精确,而notify不精确

    33、park,wait,sleep,yield,join方法的区别

    wait和join底层都是wait,WAITING

    sleep是属于Thread的静态方法,TIMED_WAITING

    yield属于thread对象,RUNABLE

    park,WAITING

    34、什么是死锁,死锁发生的条件(重要)

    public static void main(String[] args) {
        final Object A = new Object();
        final Object B = new Object();
        
        new Thread(()->{
            synchronized (A) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
    ​
                }
            }
        }).start();
    ​
        new Thread(()->{
            synchronized (B) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
    ​
                }
            }
        }).start();
    }
    ​

    死锁的定义:

    所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

    比如上述代码,线程1需要获得线程2所持有的锁B才能释放自己所持有的锁A,线程2需要获得线程1所持有的锁A才能释放自身持有的锁B,这样线程12均会一直处于等待状态无法推进,就发生了死锁

    死锁的四个条件:(重要,记忆)

    互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

    互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。

    请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。

    非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

    循环等待条件(Circular wait):系统中若干进程组成环路,改环路中每个进程都在等待相邻进程正占用的资源。

    处理死锁问题,可以从预防,避免,检测与恢复三个方面来进行

    怎么预防死锁问题?

    1〉破坏互斥条件。*即允许进程同时访问某些资源*。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

    〈2〉破坏不可剥夺条件。*即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请*。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

    比如使用rentreelock的 tryLock方法,当一个线程尝试获得某个锁资源一段时间后,就会放弃对该资源的请求,并且主动释放之前获得的锁,通过这种方式,可以避免死锁

    〈3〉破坏请求与保持条件。*可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行*。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:

    (1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;

    (2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;

    (3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

    < 4 >破坏循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:

    (1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;

    (2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

    怎么避免死锁问题

    该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

    预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的。

    对于线程申请某个资源,如果同意该请求,会不会导致死锁的发生,如果会发生死锁,则阻塞该线程,直到直到同意该请求后系统状态仍然是安全的,否则,分配给该线程资源

    怎么检测和解除死锁

    这种方式不需要对线程的资源做任何限定,只需要要求系统发生死锁的时候能够快速的检测并解决即可

    检测死锁 首先为每个进程和每个资源指定一个唯一的号码; 然后建立资源分配表和进程等待表。 解除死锁: 当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

    剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态; 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

    35、什么是活锁

    任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。 处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

    活锁出现在两个线程 互相改变对方的结束条件,谁也无法结束。

    避免活锁的方法

    • 在线程执行时,中途给予 不同的间隔时间, 让某个线程先结束即可。

    活锁与死锁的区别?

    处于活锁的线程并没有阻塞,状态·也在不停的改变,就是因为其他线程改变其终止条件而无法终止

    处于死锁的线程互相锁住了对象所需要的资源,从而导致了死锁线程的阻塞

    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

    36、什么是饥饿?

    饥饿:如果一个线程因为 CPU 时间全部被其他线程抢走而得不到 CPU 运行时间,这种状态被称之为“饥饿”;

    二、饥饿原因

    1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。(比如使用synchronized的时候,一直有大量的线程去竞争同一个锁)

    2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。(比如某个线程设置了永远无法完成的条件进入wait状态,那么它就永远不会被唤醒)

    3. 线程在等待一个本身(在其上调用 wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

    37、什么是可重入锁?

    广义上的可重入锁指的是可重复可递归调用的锁同一线程在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

    也就是说自身线程可以重复对一个对象上锁,并且不会出现异常现象

    synchronized:

    thread---->{
    synchronized(obj){
    //外部代码块
    synchronized(obj){
    //内部代码块
    }
    }
    }

    Rentreelock

    thread--->{
       lock.lock();
       try{
       //代码
          lock.lock();
       }
    }

    38、Reentrantlock

    (1)基本使用方法

    Reentrantlock lock = new Reentrantlock();
    thread--->{
        lock.lock();
           try{
           //代码
           }finally{
             lock.unlock();
           }
    }

    创建锁对象,使用lock获取锁,执行完代码之后释放锁

    (2)可打断性

    lockinterruptly

    Reentrantlock lock = new Reentrantlock();
    thread--->{
           try{
           lock.lockinterruptly();
           //代码
           }catch(Exception e){
           
           }finally{
             lock.unlock();
           }
    }

    其获得锁的过程是可以被打断的

    main{
       thread.interrupt();
    }
    • 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败

    • 处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行

    • 可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法;

    可打断的设计目的是为了放置某个线程阻塞等待获取某个锁而导致的死锁现象,当该线程处于阻塞状态等待获得锁的时候,可以用其他线程打断它,放置死锁

    (3)锁超时(获取不到锁不会停止运行)

    lock.trylock(time)

    如果在一定时间内没有获得到锁,那么就放弃该锁,以及已经有的资源,可以用在死锁的预防里

    放弃该锁并不是说以后就不竞争锁了,只是当前放弃

    注意这里会放弃已有的资源

    (4)公平锁

    synchronized锁中,在entrylist等待的锁在竞争时不是按照先到先得来获取锁的,所以说synchronized锁时不公平的;ReentranLock锁默认是不公平的,但是可以通过设置实现公平锁。本意是为了解决之前提到的饥饿问题,但是公平锁一般没有必要,会降低并发度,使用trylock也可以实现。

    实现方式是在创建reentrantlock的时候参数设置为(true)

    (5)条件变量

    package ReentrantLock;
    ​
    import lombok.extern.slf4j.Slf4j;
    ​
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    @Slf4j(topic = "Await")
    public class Await {
        //定义一个非公平Lock
        public static final ReentrantLock lock = new ReentrantLock();
        //定义两个等待变量
        private static Condition waityanRoom = lock.newCondition();
    ​
        private static Condition waitwaimaiRoom = lock.newCondition();
    ​
        private static Boolean hasyan = false;
    ​
        private static Boolean haswaimai = false;
    ​
        //定义一个执行干活的方法
        public void Dojob() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("烟送来了吗:{}",hasyan);
                    while (!hasyan){
                        try {
                            waityanRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //有烟的时候就执行干活
                        log.info("烟来了吗{},t1开始干活吧",hasyan);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("外卖送来了吗:{}",haswaimai);
                    while (!haswaimai){
                        try {
                            waitwaimaiRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //有外卖的时候就执行干活
                        log.info("外卖来了吗{},t2开始干活吧",haswaimai);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t2");
            //剩下的是不需要等待烟或者外卖的线程
            Thread t3 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("正常员工开始干活了......");
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t3");
    ​
            Thread t4 = new Thread(() -> {
                lock.lock();
                try {
                    //先唤醒再送烟
                    waityanRoom.signal();
                    hasyan = true;
                } finally {
                    lock.unlock();
                }
            }, "t4");
    ​
            Thread t5 = new Thread(() -> {
                lock.lock();
                try {
                    //先唤醒再送烟
                    waitwaimaiRoom.signal();
                    haswaimai = true;
                } finally {
                    lock.unlock();
                }
            }, "t5");
            t3.start();
            t2.start();
            t1.start();
            Thread.sleep(1000);
            t4.start();
            Thread.sleep(1000);
            t5.start();
        }
    }
    ​

    使用条件变量可以唤醒指定的线程,而不是像notify/notifyAll随即唤醒或者唤醒所有的线程

    Reentrantlock lock;
    Condition condition1=lock.newCondition;
    Condition condition2=lock.newCondition;
    thread1--->{
       lock.lock();
       condition1.await();
    }
    thread2--->{
       lock.lock();
       condition2.await();
    }
    main--->{
       lock.lock();
       condition1.signal();
    }

    39、ReentrantLock和Synchronized的相同点和区别(重要)

    (1)相同点:

    都是同步锁,互斥锁,都是可重入锁

    两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

    (2)不同点:

    1、synchronized是关键字,是原生语言层面的互斥,需要JVM实现,Reentrantlock是API层面的互斥

    2、syn通过JVM加锁和解锁,Reen通过获得对象,并使用lock和unlock方法加锁解锁

    3、syn是JVM自动解锁,Reen是必须手动解锁,否则可能出现死锁现象,需要联合try/finally实现

    4、Reen在功能上比较丰富,可以进行获取锁中断,获取锁超时,以及设置公平锁等

    5、都可以设置条件从而让线程进入等待状态,不过REEn通过设置不同地Condition实现,可以唤醒具体的线程,而synchronized只能通过notify/notifyAll唤醒非具体特定的线程

    6、在线程竞争激烈的时候Reen的性能要比sychronized好一些,在基本没有线程竞争锁的时候,syn的性能比较好

    reentrantLock基于 AQS 实现。AQS 内部通过对 volatile 的 state 读写以及cas 操作 和在某些条件下让线程进入阻塞状态实现。

    性能:偏向锁 > 轻量级锁 > reentrantLock > 重量级锁

    为什么高并发时Reen的性能要好些

    因为在reen默认是非公平锁,在进行锁的竞争时使用的是队列首位线程CAS的操作去竞争锁,而syn

    40、ReentrantLock的实现原理

    Reen是通过AQS队列同步器实现的

    底层主要由三个组件构成:

    由votile修饰的变量state,当前获得锁的线程,以及阻塞排队队列

    img

    当某一个线程使用lock方法时,通过CAS操作查询变量state是否为0,为0代表当前锁没有被线程占有,因此可以通过CAS操作获取当前锁,否则,进入阻塞队列等待锁的释放,当持有锁的线程执行完时,通过unlock方法修改state,唤醒排队队列的第一个线程进行CAS操作,获取锁

    CAS操作:修改state=1,并把当前线程置为自身

    Reen默认是非公平锁,比如线程1执行完毕之后,本该到队列中的线程获取锁,这是如果竞争比较激烈,在队列之外出现别的线程进行CAS操作并成功,这时就是不公平的

    开启公平锁之后,队列之外的线程想要竞争该锁,首先要判断队列中是否有等待线程,如果有的话,那么该线程需要插入队列并等待队列唤醒

    也就是说公平的意思是不允许插队

    41、lock、tryLock和lockInterruptibly的差別

    lock():若lock被thread A取得,thread B会进入block状态,直到取得lock; tryLock():若当下不能取得lock,thread就会放弃,可以设置一个超时时间参数,等待多久获取不到锁就放弃; lockInterruptibly():跟lock()情況一下,但是thread B可以通过interrupt中断,放弃继续等待锁

    42Condition和Object类锁方法区别

      Condition是lock锁里面的类。

    1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效

    2. Condition 类的 signal 方法和 Object 类的 notify 方法等效

    3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

    4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

    43、公平锁与非公平锁

      公平锁指的是锁的分配机制是公平的,通常是先到先得,RenntrantLock可以在构造函数中定义公平和非公平

      非公平锁,随机、就近原则分配锁的机制,线程过来后会先自旋,尝试直接获取到锁,获取不到再去排队。非公平锁的效率要更高。

    三、无锁机制(CAS,原子类)

    44、什么是java内存模型?

    JMM 即 Java Memory Model,它从java层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在以下几个方面

    1. 原子性 - 保证指令不会受到线程上下文切换的影响

    2. 可见性 - 保证指令不会受 cpu 缓存的影响

    3. 有序性 - 保证指令不会受 cpu 指令并行优化的影响

    45、什么是volitile?作用是什么

    对于存在于主线程中的变量,线程会将其存储到自己工作内存的高速缓存中,这样线程读到的值可能不是该变量最新的值,volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值线程操作 volatile 变量都是直接操作主存

    从而保证了线程对变量的修改都是对其他线程可见的

    使用synchronized关键字也有相同的效果!在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁

    因此临界区使用的变量也是最新的

    不能保证原子性!可以保证有序性

    46、什么是原子性?

    保证指令不会受到线程上下文切换的影响,使用synchronized可以保证代码的原子性

    因为上下文切换的时候,由于互斥锁的原因,其他线程无法执行该临界区代码,所以不会发生错误

    Reentrantlock和actomic下的包都可以保证原子性

    47、有序性

    指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会对一些指令的顺序进行重新排序

    重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了

    int num = 0;
    ​
    // volatile 修饰的变量,可以禁用指令重排 volatile boolean ready = false; 可以防止变量之前的代码被重排序
    boolean ready = false; 
    // 线程1 执行此方法
    public void actor1(I_Result r) {
     if(ready) {
        r.r1 = num + num;
     } 
     else {
        r.r1 = 1;
     }
    }
    // 线程2 执行此方法
    public void actor2(I_Result r) {
     num = 2;
     ready = true;
    }
    ​

    48、volatile怎么保证可见性和有序性的?

    可见性

    lock指令 对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改

    如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期,标记位无效数据,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据。

    lock前缀指令 + MESI缓存一致性协议

    由于只有volatile修饰的变量写的时候才会发送lock指令,所以没有写指令的时候,本地缓存的数据是有效的。

    在写的时候把其他线程本地缓存的数据置为无效数据,但是其他线程此时不更改,直到其他线程要读的时候才从主存中读取最新值

    有序性

    (2)内存屏障:禁止重排序

    底层就是插入了XX内存屏障,XX内存屏障,就可以保证指令不会重排

    对于volatile修改变量的读写操作,都会加入内存屏障

    每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排

    每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排

    在这里插入图片描述

    img

    50、单例模式的双检锁是什么?

    51、synchronized 和 volatile 的区别是什么?

    • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

    • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

    为啥synchronized无法禁止指令重排,但可以保证有序性?

    由于syn是互斥锁,加了锁之后,同一时间只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的。

    30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)

    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    乐观锁的实现方式:

    1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

    2、java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

    CAS缺点:

    1. ABA问题:

    比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

    2、循环时间长开销大:

    对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

    3、只能保证一个共享变量的原子操作:

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

    52、CAS

    CAS操作的流程:

    CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

    底层通过Unsafe类提供的操作系统原生方法来实现

    CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

    假设有变量i目前的值是10,线程1想要做自增操作

    对于线程一来讲,变量的预期值是10,但是如果有其他线程2修改了内存(主存)中i的值为11,这时对于线程1来讲i就是被修改了(预期值与内存值不同),因此不能自增,需要把自身的预期值改为11,再与主存值比较是否相同(也就是判定该值是否被其他线程修改),这个过程称为自旋,直到比较成功,在进行更新(自增)

    CAS的自旋:当线程想要对某个变量进行更新操作时,需要先把自己缓存中的期望值与主存中的值进行比较(如果相同,说明没有其他线程对此变量进行修改),如果不相同,把自身缓存中的期望值更改为主存中的值,再次进行比较,这种过程叫做自旋,比较结果相同时候,自旋成功,该线程可以进行更新的操作。

    CAS:比较并更新,比较的过程就是自旋的过程,自旋成功才能够进行数值的更新

    Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

    注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

    java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:

    1. 使用原子的方式更新基本类型

      • AtomicInteger:整型原子类

      • AtomicLong:长整型原子类

      • AtomicBoolean :布尔型原子类

    原子类的包都使用了volatile来修饰变量,并且使用操作系统底层的CAS操作来进行无锁操作,保证方法的原子性

    53、synchronized与CAS的区别(重要)

    • 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

    • 对于资源竞争严重的情况,CAS自旋的概率会比较大(比如getAndAddInt方法中的do-while循环),从而浪费更多的CPU资源,效率低于synchronized。

    54、CAS的缺点(重要)

    1) CPU开销过大

    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

    当线程比较多的时候,对于某个资源竞争激烈,可能会导致一些线程的CAS一直处于自旋状态,从而白白浪费CPU资源

    2) 不能保证多个变量的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    如何解决CAS只能保证一个变量的原子性操作问题?

    使用引用类型包装需要保证原子性的变量,之后再用AtomicRefernce对该引用类型进行包装

    BankCard {
    ​
        private final String accountName;
        private final int money;
        }
    private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

    通过AtomicRefernce类中的CAS方法对该引用类型的包装属性进行原子性操作

    3)ABA问题

    什么是ABA问题?

    在线程进行自旋的时候,假设线程1的期望值是A,而此时主存存储的对象也是A,但是此时其他线程通过CAS操作将主存中的数值改成了B,再次又改回了A,这时线程1进行CAS操作,由于主存和线程1的期望值是相同的,这时线程1便认为该值没有被修改,但是实际上该值是经历了其他线程的两次修改的之后的值。

    这个过程看起来没有问题,结合实际有问题,比如剩余100有两个线程需要提取100,但是只能一个,假设线程1使用CAS提取了100,剩余0,本来线程2不应该再提取了,但是这时,线程3(老板)给打了100元过来,这时线程2便可以在提取100元,而误认为线程1没有提取到,是自己竞争到了,这与实际情况相悖,也就是说线程2被骗了!

    解决方法:使用AtomicStampedReference类,对于每个对象都采用版本号机制,线程对对象进行更新时,也会对其版本号进行更新,而CAS比较时需要比较版本号是否相同。

    或者使用锁(syn Reen)

    49、volatile 变量和 atomic 变量有什么不同?

    volatile修饰的变量可以保证其可见性和有序性,但是不能保证原子性

    atomic包下的类比如AtomicInteger等通过定义volatile的变量,并通过CAS操作实现变量读写的原子性

    50、什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?

    原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

    处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

    在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——

    Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS

    的原子操作。

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境

    下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程

    有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同

    步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和

    long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需

    要使用同步。

    java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程

    环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当

    某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像

    自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个

    线程进入,这只是一种逻辑上的理解。

    原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

    原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

    原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,

    AtomicReferenceFieldUpdater

    解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean

    来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累

    加来反映中间有没有变过)

    展开全文
  • java 多线程面试题及答案

    千次阅读 2021-09-18 09:20:33
    并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。 线程和进程的区别? 1、进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)一个程序至少有一个...

    1:并行和并发有什么区别?

    并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。并行没有对 CPU 资源的抢占;并发执行的线程需要对 CPU 资源进行抢占。
    并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。

    2:线程和进程的区别?

    1、进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)一个程序至少有一个进程,一个进程至少有一个线程。
    2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

    3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

    4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

    3:创建线程有哪几种方式

    (1):继承Thread类

    这是最直观的一种方式,让一个类继承Thread重写run方法,然后把它new出来,这便是创建了一个新线程。

     (2):实现Runnable接口

    通过实现Runnable接口的run方法,可以得到一个“可被执行的任务”,然后在new Thread的时候将这个任务传进去。

     (3):Callable+FutureTask

            1:首先让一个类实现Callable(泛型)接口的call方法,这一步是写一个“可被调用的任务”;
            2:再new一个FutureTask(”未来的任务“),同时将上一步的Callable传进去;
            3:最后new一个Thread,同时将Future传进去。
            4:请注意这种方式与上面两种方式的区别,此方式可以让你的任务返回一个返回值,类型任              你定,都是可以的。

    4:说一下 runnable 和 callable 有什么区别?

    Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
    Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

    5:线程有哪些状态? 

    线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

    创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
    就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程                       序 还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待                        或 者睡眠中回来之后,也会处于就绪状态。
    运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开                 始运行run函数当中的代码。
    阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)                  之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
    死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死                 亡的线程,无法再使用start方法令其进入就绪

    6:谈谈什么是守护线程以及做用 ?

    Java线程分为用户线程和守护线程。
    守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
    Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程

    守护线程创建的线程也是守护线程,守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题

    7:乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java里面的同步原语synchronized关键字的实现是悲观锁。

    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。在Java中j原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    乐观锁的实现方式: 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

    java中的Compare and Swap即CAS(比较和交换) ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    8:什么是CAS操作,缺点是什么?

    CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

    CAS缺点:

    ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

    循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

    只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

    9:在java中wait和sleep方法的不同?

    (1):在java.lang.Thread类中,提供了sleep(),
                 而java.lang.Object类中提供了wait(), notify()和notifyAll()方法来操作线程


    (2):sleep()可以将一个线程睡眠,参数可以指定一个时间。
                 而wait()可以将一个线程挂起,直到超时或者该线程被唤醒。 
                 wait有两种形式wait()和wait(milliseconds).

    (3):最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,               sleep通常被用于暂停执行。

    (4):sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

    (5):wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,

                 而sleep可以在任何地方使用

     synchronized(x){
          x.notify()
         //或者wait()
       }

    10:notify()和notifyAll()有什么区别?

    当一个线程进入wait之后,就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。如果没把握,建议notifyAll,防止notigy因为信号丢失而造成程序异常。

    11:为什么wait, notify 和 notifyAll这些方法不在thread类里面?

    JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

    12:怎么检测一个线程是否拥有锁?

    在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

    13:synchronized与Lock两者区别

    区别:

      1:Lock是一个接口,而Synchronized是关键字。

      2:Synchronized会自动释放锁,而Lock必须手动释放锁。

      3:Lock可以让等待锁的线程响应中断,而Synchronized不会,线程会一直等待下去。

      4:通过Lock可以知道线程有没有拿到锁,而Synchronized不能。

      5:Lock能提高多个线程读操作的效率。

      6:Synchronized能锁住类、方法和代码块,而Lock是块范围内的
     

    14:Synchronized实现原理

    synchronized可以让代码同步,所谓同步代码就是同一时刻只能一个线程执行这段代码,synchronized可以锁对象,可以锁方法,可以锁class对象。synchronized就是锁对象头,java中每个对象都有固定格式的对象头,对象头中有一个mark word,64位虚拟机中mark word有64个bit,在对象头中有两个bit是用来标志锁的,有一个bit标志是否偏向锁,还有一个bit是锁标志位,所以synchronized给对象加锁就是就是修改对象这两个锁标志位的数值,

    一个bit:   1偏向锁 ,0非偏向锁

    另一个bit:  01无锁  ,  00轻量锁   , 10重量级锁   ,11GC标记

    synchronized关键字加到static静态方法和非static静态方法区别

    synchronized关键字加到static静态方法上是给Class类上锁,简称类锁(锁的事当前类的字节码)
    而加到非static静态方法是给对象加锁

    lock实现原理

    • lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)

    • lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。lock的基本操作还是通过乐观锁来实现的

    • lock释放锁的过程:修改状态值,调整等待链表。

    • lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。

    15:JVM中哪个参数是用来控制线程的栈堆栈大小的

    答:-Xss

    16:为什么wait, notify 和 notifyAll这些方法不在thread类里面?

    JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

    17:为什么需要线程池?

    (1):减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    (2):可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

    常见线程池

    ①newSingleThreadExecutor
    单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
    ②newFixedThreadExecutor(n)
    固定数量的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
    ③newCacheThreadExecutor(推荐使用)
    可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
    ④newScheduleThreadExecutor
    大小无限制的线程池,支持定时和周期性的执行线程
     

    ThreadPoolExecutor 有哪些常用的方法?

    submit()/execute():执行线程池

    shutdown()/shutdownNow():终止线程池

    isShutdown():判断线程是否终止

    getActiveCount():正在运行的线程数

    getCorePoolSize():获取核心线程数

    getMaximumPoolSize():获取最大线程数

    getQueue():获取线程池中的任务队列

    allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程这些方法可以用来终止线程池、线程池监控等。

    说说线程池创建需要的那几个核心参数的含义

    ThreadPoolExecutor 最多包含以下七个参数:

    corePoolSize:线程池中的核心线程数

    maximumPoolSize:线程池中最大线程数

    keepAliveTime:闲置超时时间

    unit:keepAliveTime 超时时间的单位(时/分/秒等)

    workQueue:线程池中的任务队列

    threadFactory:为线程池提供创建新线程的线程工厂

    rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

    说说submit(和 execute两个方法有什么区别?

    submit() 和 execute() 都是用来执行线程池的,

    execute() 执行线程池不能有返回方法

    submit() 可以使用 Future 接收线程池执行的返回值。

    shutdownNow() 和 shutdown() 两个方法有什么区别?

    shutdownNow() 和 shutdown() 都是用来终止线程池的。

    shutdown(): 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;

    shutdownNow() :会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

    线程池的工作原理

    当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行饱和策略。

    线程池为什么需要使用(阻塞)队列?

    (1):因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

    (2):创建线程池的消耗较高。

    展开全文
  • 主要为大家分享了15个顶级Java多线程面试题,考察面试者是否有足够的Java线程与并发知识,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • Java多线程面试题(面试必备)

    万次阅读 多人点赞 2020-05-26 01:15:38
    文章目录一、多线程基础基础知识1. 并发编程1.1 并发编程的优缺点1.2 并发编程的三要素1.3 并发和并行有和区别1.4 什么是多线程多线程的优劣?2. 线程与进程2.1 什么是线程与进程2.2 线程与进程的区别2.3 用户线程...

    能力有限,初级菜🐔,多线程可是一块庞大的知识块,慢慢总结吧!

    文章目录

    一、多线程基础基础知识

    1. 并发编程

    1.1 并发编程的优缺点

    优点

    • 充分利用多核CPU的计算能力,通过并发编程的形式将多核CPU的计算能力发挥到极致,性能得到提升。
    • 方面进行业务的拆分。提高系统并发能力和性能:高并发系统的开发,并发编程会显得尤为重要,利用好多线程机制可以大大提高系统的并发能力及性能;面对复杂的业务模型,并行程序会比串行程序更适应业务需求,而并发编程更适合这种业务拆分。cai

    缺点

    • 并发编程的目的是为了提高程序的执行效率,提高程序运行速度,但并发编程并不是总能提高性能,有时还会遇到很多问题,例如:内存泄漏,线程安全,死锁等。

    1.2 并发编程的三要素

    并发编程的三要素:(也是带来线程安全所在)

    1. 原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
    2. 可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
    3. 有序性:程序的执行顺序按照代码的先后顺序

    线程安全的问题原因有:

    1. 线程切换带来的原子性问题
    2. 缓存导致的可见性问题
    3. 编译优化带来的有序性问题
    

    解决方案:

    • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    • synchronized、volatile、LOCK,可以解决可见性问题
    • Happens-Before 规则可以解决有序性问题

    1.3 并发和并行有和区别

    并发:多个任务在同一个CPU上,按照细分的时间片轮流交替执行,由于时间很短,看上去好像是同时进行的。
    并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
    串行:有n个任务,由一个线程按照顺序执行。

    1.4 什么是多线程,多线程的优劣?

    定义:多线程是指程序中包含多个流,即在一个程序中可以同时进行多个不同的线程来执行不同的任务
    优点:

    • 可以提高CPU的利用率,在多线程中,一个线程必须等待的时候,CPU可以运行其它线程而不是等待,这样就大大提高了程序的效率,也就是说单个程序可以创建多个不同的线程来完成各自的任务。
      缺点:
    • 线程也是程序,线程也需要占内存,线程也多内存也占的也多。
    • 多线程需要协调和管理,所以需要CPU跟踪线程。
    • 线程之间共享资源的访问会相互影响,必须解决禁用共享资源的问题。

    2. 线程与进程

    2.1 什么是线程与进程

    进程:内存中运行的运用程序,每个进程都有自己独立的内存空间,一个进程可以由多个线程,例如在Windows系统中,xxx.exe就是一个进程。
    线程:进程中的一个控制单元,负责当前进程中的程序执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。

    2.2 线程与进程的区别

    根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
    资源开销:每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
    包含关系:如果一个进程内有多个线程,则执行的过程不是一条线的,而是多条线(多个线程),共同完成;线程是进程的一部分,可以把线程看作是轻量级的进程。
    内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

    2.3 用户线程与守护线程

    用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程都是用户线程。
    守护(Daemon)线程:运行在后台,为其它前台线程服务,也可以说守护线程是JVM非守护线程的”佣人“,一旦所有线程都执行结束,守护线程会随着JVM一起结束运行。
    main函数就是一个用户线程,main函数启动时,同时JVM还启动了好多的守护线程,如垃圾回收线程,比较明显的区别时,用户线程结束,JVM退出,不管这个时候有没有守护线程的运行,都不会影响JVM的退出。

    2.4 什么是线程死锁

    死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
    如图,线程A拥有的资源2,线程B拥有的资源1,此时线程A和线程B都试图去拥有资源1和资源2,但是它们的🔒还在,因此就出现了死锁。
    在这里插入图片描述

    2.5 形成死锁的四个必要条件

    1. 互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
    2. 请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
    3. 不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
    4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。

    2.6 如何避免死锁

    我们只需破坏形参死锁的四个必要条件之一即可。
    破坏互斥条件:无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥
    破坏请求与保持条件:一次申请所有资源
    破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
    破坏循环等待条件:按序来申请资源。

    2.7 什么是上下文的切换

    当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。

    3. 创建线程

    3.1 创建线程的四种方式

    1. 继承Thread类
    2. 实现Runnable接口
    3. 实现Callable接口
    4. Executors工具类创建线程池

    3.2 Runnable接口和Callable接口有何区别

    相同点

    1. Runnable和Callable都是接口
    2. 都可以编写多线程程序
    3. 都采用Thread.start()启动线程

    不同点

    1. Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
    2. Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。

    :Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。

    3.2 run()方法和start()方法有和区别

    每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
    start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。

    start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。

    3.3 为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法

    这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
    如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
    小结
    调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。

    3.4 什么是Callable和Future

    Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
    Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。

    3.5 什么是FutureTask

    FutureTask是一个异步运算的任务,FutureTask里面可以可以传入Callable实现类作为参数,可以对异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。只有当结果完成之后才能取出,如果尚未完成get方法将堵塞。一个Future对象可以调用Callable和Runable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    4. 线程状态和基本操作

    4.1 线程声明周期的6种状态

    很多地方说线程有5种状态,但实际上是6中状态,可以参考Thread类的官方api

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
    

    如图:
    在这里插入图片描述
    新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
    可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
    被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
    计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
    无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
    锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。

    堵塞状态的详解:
    在这里插入图片描述

    4.2 Java用到的线程调度算法是什么?

    计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获取到CPU的使用权才能执行指令,所谓多线程的并发运行,其实从宏观上看,各线程轮流获取CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待,CPU的调度,JVM有一项任务就是负责CPU的调度,线程调度就是按照特定的机制为多个线程分配CPU的使用权。
    有两种调度模型:分时调度和抢占式调度
    分时调度就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
    抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。

    4.2 Java线程调度策略

    线程调度优先选择优先级高的运行,但是如果出现一下情况,就会终止运行(不是进入死亡状态):

    1. 线程调用了yield方法让出CPU的使用权,线程进入就绪状态。
    2. 线程调用sleep()方法,使其进入计时状态
    3. 线程由于IO受阻
    4. 另一个更高的优先级线程出现
    5. 在支持的时间片系统中,改线程的时间片用完。

    4.3 什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )

    线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
    时间分片是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或线程等待时间。

    4.4 Java线程同步和线程调度的相关方法

    1. wait():调用后线程进入无限等待状态,并释放所持对象的锁
    2. sleep():使一个线程进入休眠状态(堵塞状态),带有对象锁,是一个静态方法,需要处理InterruptException异常。
    3. notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
    4. notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。

    4.5 sleep()和wait()有什么区别

    两者都可以使线程进入等待状态

    • 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
    • 是否释放锁:sleep()不释放锁,wait()释放锁
    • 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
    • 用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。

    4.6 你是如何调用wait()方法的,使用if还是循环

    处以等待状态的线程可能会收到错误警告或伪唤醒,如果不在循环中检查等待条件,程序可能会在没有满足条件的时候退出。

    synchronized (monitor) {
        //  判断条件谓词是否得到满足
        while(!locked) {
            //  等待唤醒
            monitor.wait();
        }
        //  处理其他的业务逻辑
    }
    
    

    4.7 为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中

    Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于等待获取唤醒对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。

    有人会说,既然是线程放弃对象锁,那也可以把wait()放到Thread类中,新定义线程继承Thread类,也无需重新定义wait(),
    然而,这样做有一个很大的问题,因为一个线程可以持有多把锁,你放弃一个线程时,到底要放弃哪把锁,当然了这种设计不能不能实现,只是管理起来比较麻烦。
    综上:wait(),notify(),notifyAll()应该要被定义到Object类中。

    4.8 为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?

    wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
    如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
    如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
    由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。

    4.9 Thread的yiele方法有什么作用?

    让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。

    4.10 为什么Thread的sleep和yield是静态的?

    Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。

    4.11 线程sleep和yield方法有什么区别

    • 线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
    • sleep()方法使用时需要处理InterruptException异常,而yield没有。
    • sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。

    4.12 如何停止一个正在运行的线程?

    1. 使用stop方法终止,但是这个方法已经过期,不被推荐使用。
    2. 使用interrupt方法终止线程
    3. run方法执行结束,正常退出

    4.13 如何在两个线程间共享数据?

    两个线程之间共享变量即可实现共享数据。
    一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。

    4.14 同步代码块和同步方法怎么选?

    同步块是更好的选择,因为它不会锁着整个对象,当然你也可以然它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。
    同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
    原则:同步范围也小越好。

    4.15 什么是线程安全?Servlet是线程安全吗?

    线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
    Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
    Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
    springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
    Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
    如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。

    4.16 线程的构造方法,静态块是被哪个线程类调用的?

    线程的构造方法,静态块是被哪个线程类调用的?
    该线程在哪个类中被new出来,就是在哪个被哪个类调用,而run方法是线程类自身调用的。
    例子:mian函数中new Thread2,Thread2中new Thread1
    在这里插入图片描述
    thread1线程的构造方法,静态块是thread2线程调用的,run方法是thread1调用的。
    thread2线程的构造方法,静态块是main线程调用的,run方法是thread2调用的。

    4.17 Java中是如何保证多线程安全的?

    1. 使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
    2. 使用自动锁,synchronized锁
    3. Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法

    4.18 线程同步和线程互斥的区别

    线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
    线程互斥
    而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
    实现线程同步的方法

    1. 同步代码块:sychronized(对象){} 块
    2. 同步方法:sychronized修饰的方法
    3. 使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的ock和unlock为其加锁

    4.19 你对线程优先级有什么理解?

    每个线程都具有优先级的,一般来说,高优先级的在线程调度时会具有优先被调用权。我们可以自定义线程的优先级,但这并不能保证高优先级又在低优先级前被调用,只是说概率有点大。
    线程优先级是1-10,1代表最低,10代表最高。
    Java的线程优先级调度会委托操作系统来完成,所以与具体的操作系统优先级也有关,所以如非特别需要,一般不去修改优先级。

    4.20 谈谈你对乐观锁和悲观锁的理解?

    乐观锁:每个去拿数据的时候都认为别人不会修改,所以不会都不会上锁,但是在更新的时候会判断一下在此期间有没有去更新这个数据。所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供的类似write_condition机制,都是用的乐观锁,还有那个原子变量类,在java.util.concurrent.atomic包下
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为有人会修改,所以每次在拿数据的时候都会上锁。这样别的对象想拿到数据,那就必须堵塞,直到拿到锁。传统的关系型数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,再比如Java的同步代码块synchronized/方法用的也是悲观锁。

    展开全文
  • Java多线程面试题

    2019-05-30 01:38:24
    Java多线程面试题 博文链接:https://zccst.iteye.com/blog/1044613
  • 多线程面试题

    2019-10-13 15:42:54
    多线程面试题
  • 史上最全 Java 多线程面试题及答案.docx
  • 15个顶级Java多线程面试题及答案.pdf
  • 15个顶级Java多线程面试题答案
  • 15道面试常问的Java多线程面试题!.zip
  • 40道常问的Java多线程面试题!.zip
  • Java多线程面试题,我丝毫不慌

    万次阅读 多人点赞 2020-07-28 09:18:51
    文章目录 一、什么是多线程 一、初识多线程 1.1介绍进程 1.2回到线程 1.3进程与线程 1.4并行与并发 1.5Java实现多线程 1.5.1继承Thread,重写run方法 1.5.2实现Runnable接口,重写run方法 1.6Java实现多线程需要注意...
  • 史上最全Java多线程面试题及答案

    万次阅读 多人点赞 2018-08-20 11:17:08
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...
  • 【Java核心知识面试】-15个顶级Java多线程面试题答案
  • 15道面试常问的Java多线程面试题!.pdf
  • 40道常问的Java多线程面试题!.pdf
  • 15个顶级Java多线程面试题及回答.docx
  • 史上最全 Java 多线程面试题及答案.zip
  • 史上最全 Java 多线程面试题及答案

    万次阅读 多人点赞 2018-08-17 10:41:16
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 161,847
精华内容 64,738
关键字:

java多线程面试题