Java面试题——多线程

1、进程与线程的区别?

  一个进程是一个独立运行的环境,他可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同类和程序的单一进程。线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。

2、多线程编程的好处是什么?为什么要使用多线程?

  就是为了提高系统资源的利用率。

3、用户线程与守护线程有什么区别?

  当我们在Java程序中创建一个线程,它就被称为用户线程。一个守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程执行的时候,JVM关闭程序并且退出(这里我感觉守护线程也会关闭)。一个守护线程的子线程也是守护线程。

4、创建线程的方式?如何创建一个线程?

  • 实现Runnable接口,重写run()方法(比较常用)
  • 继承Thread类,重写run()方法
  • 线程池创建线程
  • 匿名内部类创建线程(不常用)
  • 创建带返回值的Callable(不常用)

5、volatile和synchronized区别

  volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字性能要好。

  但是volatile关键字只能用于变量,而synchronized关键字还可以修饰方法和代码块。

  

  synchronized关键字在JavaSE1.6之后进行了主要包括了为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其他各种优化之后效率显著提升,实际开发中使用synchronized关键字的场景还是更多一些。

 

  多线程访问volatile关键字不会法神碰撞,而synchronized关键字可能发生阻塞。volatile关键字保证数据的可见性,但是不保证数据的原子性,synchronized关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

 

  synchronized保证三大性质:原子性、有序性、可见性

  volatile保证: 有序性、可见性、不保证原子性

6、sleep方法与wait方法有什么区别?

  1、sleep方法和wait方法都可以用来放弃CPU一定的时间暂停当前运行的线程,不同点在于:如果线程持有对某个线程的监锁,sleep方法不会释放这个对象的锁,wait方法会释放这个对象的锁,sleep必须要设定时间,而wait方法可以设定也可以不设定。

  sleep属于Thread类,wait属于Object类。

  2、另一种解释:同:都造成线程阻塞

  异:

  1. 释放锁:sleep方法没有释放锁,而wait方法释放了锁。
  2. 作用:wait通常被用于线程间通信,sleep用于暂停执行
  3. 苏醒:wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。    

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

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

8、什么是线程局部变量ThreadLocal?ThreadLocal有什么用?

  ThreadLocal采用了“以空间换时间”的方式。它为每个线程都提供了一份变量副本,因此可以同时访问而互不影响,所以肯定线程安全。

    与局部变量相比,它可以更好地解耦,因为外部设置定义这个对象。 

9、线程池中submit()和execute()方法有什么区别?

   两个方法都可以向线程池提交任务,execute()方法返回类型是void,它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象。

10、start与run的区别?

  1. start()方法来启动线程,真正实现了多线程运行。这时这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用Thread类的start()方法来启动一个线程,这时线程是出于就绪状态,并没有运行
  3. 方法run()称为线程体,它包含了要执行这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。run方法运行结束,此线程终止。然后CPU再调度其他线程。     

11、synchronnized和ReentrantLock的区别?  

  同:

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

  异:

  1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
  2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的 不可用性提供了更高的灵活性
  3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
  4. ReentrantLock 可以实现公平锁
  5. ReentrantLock 通过 Condition 可以绑定多个条件
  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻 塞,采用的是乐观并发策略
  7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言 实现。
  8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。
  9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时, 等待的线程会一直等待下去,不能够响应中断。
  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

12、线程的调度策略?

  线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

  1. 线程体中调用了yield方法让出了对cpu的占用权利
  2. 线程体中调用了sleep方法使线程进入睡眠状态
  3. 线程由于IO操作受到阻塞
  4. 另外一个更高优先级线程出现
  5. 在支持时间片的系统中,该线程的时间片用完      

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

  1. 使用退出标志(比如:return),使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用interrupt方法中断线程
  3. 使用stop方法强行终止,但是不推荐这方法,因为stop和suspend和resume一样都是过期作废的方法。   

14、什么是线程安全?

  如果你的代码在多线程和单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

  这里举例说明不安全的一个实例,就是多线程处理共享数据,比如剩余一张票时,多线程操作会造成重复卖票情况。也就是说在多线程环境中,能够正确处理多个线程之间的共享变量,使程序功能正确完成。 

15、Java中使用的是什么线程调度算法?

   有两种调度模型:分时调度模型、抢占式调度模型

   分时调度模型是指让所欲的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片。这点比较好理解。

   Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中线程的优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,一个线程用完CPU之后,操作系统会根据线程优先级,饥饿程度等数据计算出优先级并执行下一个进程。

16、Java中++操作符是线程安全的吗?

  不是,它不是原子操作,它涉及到多个指令,如变量值的读取、增加、存储回内存,这个过程可能会出现多个线程交叉运行 

17、什么是多线程的同步和异步?

  一个进程启动的多个不相干进程,他们之间相互关系为异步

  同步必须执行到底之后才能执行其他操作,而异步可以任意操作,是多个线程同时访问统一资源,等待资源访问结束

  同步的好处与弊端:

    好处:解决了线程的安全问题

    弊端:每次都有判断锁,降低了效率。

    但是在安全和效率之间,首先考虑的是安全。

18、synchronized和ReentrantLock的区别?

  synchronized:

    1. 关键字:synchronized它主要是在JVM层面实现而没有直接暴露给程序员。底层使用monitor来完成。
    2. synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待、通知机制,不需要手动释放
    3. 而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题。      

  ReentrantLock:

    1. :ReentrantLock是API层面的一个类,底层调用Unsafe的park方法加锁。比如说我们可以显示的调用Lock和unlock来实现加锁和解锁。类就更灵活,可以继承,有方法和变量。
    2. 使用condition的await和signalAll来通知。需要手动释放。
    3. 用ReentrantLock类结合Condition实例可以实现选择性通知,这个功能非常重要。可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition条件实例。比如:生产者消费者,可以指定解锁消费者线程。      

19、生产者和消费者模型的作用是什么?

  一个程序是面包工厂,开启多个线程生产面包,多个线程消费面包。每生产就wait然后notify消费者去消费。循环往复。

  1. 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率。
  2. 解耦,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要受到相互制约  

20、为什么需要等待唤醒机制?

  因为拿生产者消费者模型来说,如果只使用加锁同步Synchronized,则我们生产者在生产的时候就不会释放锁而是一直生产,而消费者不能消费。使用等待唤醒,wait()方法来让线程等待,比如生产完一个就阻塞,然后暂时释放锁给消费者。

21、什么是线程死锁?

  同时持有资源又同时申请资源。是指两个或两个以上的进程(或线程)在执行过程中,因争夺对方线程的资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推下去。

    造成死锁四个必备条件:

    • 互斥条件:线程独占资源,资源只供一个线程使用
    • 请求与保持条件:一个线程因请求资源阻塞时,对已获得的资源保持不放
    • 不剥夺条件:线程已获得的资源不能被其他线程强行剥夺,使用完毕后释放。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。            

22、如何避免线程死锁?

  1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实

  2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量

  3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时

    时限,在等待超过该时限之后变回返回一个失败信息

23、为什么要使用线程池(Executor)?(优势)

  1. 管理线程:线程池可以统一分配线程,管理和监控。
  2. 控制最大并发数:所以创建一个线程池是个更好地解决方案,因为可以更好地控制线程的数量,避免过多线程竞争。而不是无限的创建,可能导致应用的内存溢出
  3. 线程复用:可以回收再利用这些线程避免频繁的创建和销毁所带来的系统花销。从而提高资源的利用率。  

24、如何创建线程池?

  根据创建ThreadLocalPoolExecutor的方式,有三种类型的线程池:

  1. 创建只含单独线程的线程池ExecutorService service1 = Executors.newSingleThreadExecutor();   
  2. 固定数量的线程池ExecutorService service3 = Executors.newFixedThreadPool(10);用阻塞队列放任务,然后固定数量线程池执行阻塞队列中的。适用于:长期执行任务,性能好。
  3. 创建可调整数量的线程池ExecutorService service2 = Executors.newCacheThreadPool();适用于:执行短期小程序。方便扩容。   

25、线程池底层实现,七大参数(重点)

  举例可以为银行窗口和候客区

  1. corePoolSize:线程中核心线程数
  2. maxinumPoolSize:最大线程数。最大上限,当常用线程池线程满了,阻塞队列中任务也满了,还有任务过来,则这时候可以扩容,临时加班到最大上限。
  3. keepalive:多余线程存活时间。也就是临时线程存活的时间。比如:临时加班一个小时后,执行完任务,就下班。从max退回到核心线程数
  4. unit:时间单位
  5. workQueue:阻塞队列,待执行存放任务区域。
  6. threadFactory:线程工厂,一般默认。
  7. 拒绝策略:窗口最大数量满了和阻塞队列也满了,那么我们就拒绝一些任务。     

  步骤:

  • 来任务,开线程干活    
  • 大于核心线程数,放入阻塞队列
  • 队列也满了了,开线程到最大线程数
  • 最大线程数也满了,拒绝策略
  • 设置临时线程存活时间,超过时间收缩到核心线程大小

25、如何合理配置线程池?(配置线程数)

  看任务是CPU密集型还是IO密集型。

  1. 要知道自己服务器是几核的。了解自己的情况
  2. 如果是CPU密集型,大量运算,一般就是CPU+1,尽量减少切换
  3. 若IO密集型,因为CPU不是一直在执行任务,则可以尽可能多配置线程数,一般是CPU*2    

26、Java线程池中submit()和execute()方法有什么区别?

  两个方法都可以向线程池提交任务,execute()方法返回类型是void,类似于run方法,它定义在Executor接口中。而submit()方法可以返回持有结算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。

27、什么是乐观锁和悲观锁?

  悲观锁:总是假设最坏的情况,每次拿到数据都会上锁,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它线程。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

    乐观锁:总是假设最好的情况,每次拿数据都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下是否有人更新过这个数据。

    乐观锁是一种思想,CAS是这种思想的一种实现方式。CAS(compare and swap)比较和替换,如果是预期的值,则替换,如果不是,则什么都不做返回false。是非阻塞算法的一种常见实现。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,但失败的线程不会像悲观锁那样阻塞,而是被告知这次竞争中

    失败,并可以再次尝试。

    在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

  适用场景:

    像乐观锁适用于写比较少的情况下(多读情况)冲突会比较少。这样可以省去锁的开销,提高吞吐量。一般如果冲突产生比较多,多写的场景下用悲观锁比较合适。

28、什么是CAS?(了解即可)

  简单来讲就是比较,交换。CAS是这种思想的一种实现方式。CAS(compare and swap)比较和替换,如果是预期的值,则替换,如果不是,则什么都不做返回false。类似于github版本号,当我比较的时候如果一样那么我就可以提交修改,如果不一样有冲突,那么我就要重新获得主物理内存真实值。
  比如为创建一个主物理内存AtomicInteger是5,第一个线程得到副本拷贝为5,期望值是5,则可以更新为6。
  这时主物理内存为6,而第二个线程再进行比较,期望值是5,而内存值为6,就返回false,线程需要重新获取内存值。
  

  CAS底层原理
  自旋锁:使用无锁机制,性能很高。
  unsafe类:其内部方法可以像C指针一样操作内存,所有方法都用native修饰,可以直接操作系统底层资源。
  CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果

  CAS缺点
  不加锁保证一致性,只能保证一个共享变量的原子性操作。
  引出ABA问题:
  假设如下事件序列:

  线程 1 从内存位置V中取出A。
  线程 2 从位置V中取出A。
  线程 2 进行了一些操作,将B写入位置V。
  线程 2 将A再次写入位置V。
  线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
  尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。

  优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。

29、集合类不安全问题?(了解)  

  ArrayList
  vector 线程安全,数据一致性可以保证但是并发性下降,使用了synchronized。
  Arraylist,并发性上升。多线程并发add向list中写入数据操作,当一个线程写的时候,另一个线程竞争写,就会出现并发报错问题。
  解决并发问题:

  • 包一个Collections.synchronizedList(new ArrayList<>());
  • 使用CopyOnwriteArrayList()。读写分离,也就是使用读写锁,写都时候该类使用了lock锁,操作都放在lock里面。读的时候并发去读取数据。提高效率。

  Set
  HashSet底层数据结构就是HashMap,HashSet放一个数据而HashMap放两个数据,如何解释?就是调用HashMap的put方法,而key是放入的值,value设置为一个常量不管put多少个都是恒定值。

  • Collections.synchronizedSet(new HashSet<>());`
  • CopyOnwriteArraySet() 他的底层数据结构使用CopyOnwriteArrayList()

  Map
  Hashtable线程安全,但是对整个表都加了锁,范围太大。性能下降
  HashMap线程不安全。
  解决:
  使用ConcurrentHashMap
  Java8中的ConcurrentHashMap的底层结构:数组+链表+红黑树。
  每次在put()的时候我们只给链表头部元素加锁。所以我们可以只对数组中存的那个元素加锁即可。

30、公平锁与非公平锁?

  公平锁:多个线程按照申请顺序来获取锁,先来后到但是效率很低。

   非公平锁:多个线程不按照顺序来获取锁,都有可能先获得锁(也就是可以插队)。性能更好,吞吐量大。ReentrantLock默认是非公平锁,Synchronized默认也是非公平锁。

31、什么是可重入锁?

  外层方法获取一个锁后,内层方法自动获取该锁。

  ReentrantLock和Synchronized都是可重入锁

  作用:避免死锁

32、独占锁(写锁)和共享锁(读锁)?

  独占锁:该锁一次只有一个线程持有,ReentrantLock和Synchronized都是独占锁。

  共享锁:指该锁可以被多个线程持有。

  ReentrantReadWriteLock : 写锁时独占,读锁时共享。

  因为写资源必须一个线程写,其他线程不能写,而读的时候可以并发的去读,不会出现问题。

33、CyclicBarrier和CountDownLatch和Semaphore

34、什么是AQS?(了解) 

  简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器

  如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

  AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

35、什么是阻塞队列BlockingQueue?如何使用阻塞队列来实现生产者和消费者模型?

  阻塞队列:在队列为空时,获取元素的线程会等待队列中返给元素。当队列满时,放入元素的线程会等待队列可用。它是MQ消息队列核心底层原理。

  生产者消费者,阻塞队列就是仓库存储的蛋糕。

  BolckingQueue接口,当生产者试图向BlockingQueue放入元素时,如果队列满,则生产者线程被阻塞。当消费者线程试图取出元素时,如果BlockingQueue为空,则消费者线程被阻塞。利用这个特性多个线程交替向队列中存放和取出元素,实现控制线程通信。

  同步阻塞队列:单个元素阻塞队列,产生一个消费一个。

  阻塞队列常用场景就是socket客户端数据操作,线程不断将数据返给队列,然后有线程不断从队列中取出数据。

 

 

  

原文入口:https://blog.csdn.net/chongbin007/article/details/91359728  

 

  

一个小小后端的爬行痕迹
原文地址:https://www.cnblogs.com/heikedeblack/p/14763954.html