工作五年来的面试题目总结之-多线程

sychornized底层实现原理?

java SE 1.6对synchronized进行了各种优化,使得它在有些情况下没有那么重(陈本很高)。

作用范围有三个:普通方法、静态方法、同步代码块

  • 普通方法:锁的是当前实例对象
  • 静态方法:锁的是类对象
  • 代码块:锁的是括号中的对象

那么是怎么锁上的呢?

通过javac -p 类,获得反编译我们看到,其实就是对一个monitor对象的争夺。

但是代码块和方法有点区别

代码块:当进入方法的时候,执行monitorenter,获得monitor对象的所有权,这个时候monitor对象计数为1,此时当前线程就获得了monitor对象,也就是获得了这把锁;再次进入,如果你已经是这个锁的拥有者,计数+1;当执行完monitorexit以后,相应的计数-1,直到计数0,释放当前锁。

方法:同理,不过同步方法是隐士调用的monitorentermonitorexit。归根到底就是对monitor对象的争夺。

那么这个monitor对象中有什么特殊的呢?

其实没有什么特殊的,从代码书写你就可以看出来,每个对象都可以作为一把锁,那么synchronized锁的是什么呢?其实sychronized使用的锁就存在java对象头中。

java对象头

什么叫对象头:java对象在内存中分为对象头、实例数据、对其填充三部分。对象头又包括MarkWord、ClassPoint

对象头有多大呢?

32为虚拟机中,如果对象是数组类型,使用12个字节存储;如果不是数组类型,使用8个字节存储。

MarkWord中存储的是什么呢?

默认存储的是对象的hashcode、分代年龄、锁标志位

锁升级的过程

1.6为了减少锁获得和释放带来的性能消耗,引入偏向锁、轻量级锁

锁一共有四种状态无锁状态、偏向锁、轻量级锁、重量级锁

锁可以升级,但是不能降级无锁->偏向锁->轻量级锁->重量级锁

偏向锁

因为研究人员发现,大多数情况下,锁都是有同一线程多次获得,为了让线程获得锁的代价更低,引入偏向锁。

锁升级的过程

首次,线程访问同步代码块,并且获得锁的情况下,会在对象头栈帧中的锁记录中记录线程ID

再次,检查一下对象头中的线程ID是不是自己;是,直接获得偏向锁,执行同步代码块;不是,查看对象头锁标志位,检查获取当前锁的线程是不是偏向锁;如果没有设置锁标志位,使用CAS竞争锁,如果设置了,使用CAS将对象头偏向锁线程ID设置成自己的。如果设置成功了,当前线程获取偏向锁,执行代码。如果竞争失败了,执行偏向锁的撤销

偏向锁的撤销,需要等到全局安全点(就是这个点没有正在执行的字节码),首先、暂停拥有偏向锁的线程,检查偏向锁是否活动,不活动,对象头设置无锁;存活,拥有偏向锁的栈执行,栈中的记录和对象头要么偏向其他线程,要么无锁,或者标记该对象不适合做偏向锁,升级为轻量级锁。

轻量级锁枷锁

线程执行同步块之前,JVM在当前线程的栈帧记录中创建存储锁记录的空间,将Markword复制过来;然后使用CAS替换对象头中的线程ID,成功,获得锁,失败,根据当前线程栈帧中的锁记录自旋。

轻量级锁解锁

使用原子级操作,将当前线程中的锁记录替换回对象头中,成功,表示没有发生竞争,失败了,存在竞争,当前锁膨胀成重量级锁。释放锁会唤醒其他等待的线程。

究竟该使用sychronized还是Lock呢?

一个是JDK层面的sychronized,一个是JVM层面的Lock。具体该使用哪个应该结合具体的场景来使用,脱离具体业务都是扯淡。比如你高峰期时使用sychronized,那么过了高峰期,都是重量级锁,是不是也不合适。

线程的状态的切换

  • 创建一个线程线程处于New状态
  • 调用start方法以后进入就绪状态
  • 线程获取时间片以后进入到运行状态
  • 获取同步失败会进入阻塞状态
  • 获取到锁以后会从阻塞状态->就绪状态,在获取时间片以后变成运行状态
  • 线程运行结束会进入Terminated状态

CAS和ABA问题

CAS是一种解决线程同步问题的方式,

  • 是一种乐观锁,只有在回写操作的时候才会加锁,认为并发问题一般不会发生,写操作的时候判断变量的值是不是=预期值,如果不等于说明被改变,重新读取,如果没有改变就写回
  • 比较并写回的操作属于原子操作,由操作系统保证线程的安全性,保证线程不会中断

ABA:当前线程读取变量值A,其他线程改写B,然后在改写成A,对当前线程而言还是A,就会造成ABA问题,可能对结果没有影响,但是也需要防范。你老婆出轨以后还是你老婆吗?

sleep和yield的区别?

  • sleep不考虑线程的优先级,yield会让给比自己优先级高的线程
  • sleep会使当前线程进入阻塞状态,让出CPU(wait也让出CPU),但是不会让出锁(意思就是如果你有sychronized关键字,虽然我会阻塞,但是其他线程也得不到运行机会),yield使当前线程重新回到可执行状态
  • sleep方法会抛出InterruptedException异常,yield方法无任何异常
  • sleep有更好的移植性

wait()

  • 需要和notify()、notifyAll()方法一起使用
  • 需要在同步代码块中使用,因为是用来协调共享对象的读取,Object类的方法
  • 和sleep的区别是,wait会释放锁会使得当前线程暂停执行,加入对象等待池
  • notify并不能确切的唤醒哪个对象,由JVM确定
  • notityAll:唤醒所有等待的线程,进入就绪状态,具备争夺资格

join

  • 当前线程等待调用join的线程执行完毕在运行

volatile

特点:

  • 禁止指令重排(JVM会在对结果没有影响的前提下对代码顺序调整)
  • 保证不同线程对变量操作的内存可见性

使用了就能保证线程安全性吗?

  • volatile只能保证该变量的原子性,但是如果线程是复合操作的话并不能保证线程安全性

  • volatile 只能保证写后读的可见性,比如,A线程读取变量,进行+1写回的同时,线程B获取时间分片,线程A阻塞,线程B获取数值+1,写进去,因为回写操作只能保证其他线程在读取操作时,发现自己缓存无效,才会重新读主存的值

volatile写操作会使其他线程读缓存失效,如果其他线程在写之前已经读取,要写入的时候,是无法失效的

volatile的本质是告诉JVM寄存器,这个变量是不确定的,你要使用 就需要从内存中读取,sychronized是锁定,其他线程不可见的

volatile底层实现机制

  • 加了volatile关键字以后,汇编指令底层会在变量前加上Lock指令
  • Lock指令会引发,将当前缓存行的数据写回主内存,同时使其他线程缓存了该内存的地址无效

volatile在项目中的使用

  • 单例模式下的双重检查
  • 多线程下的循环终止

线程池

Executor线程池

如果对线程池原理不是很清楚的情况下,Executors提供了一些常用的线程池

  • newSingleThreadExecutor单一线程池,如果出现异常会再创建一个线程,保证所有任务的执行都按照任务的提交顺序进行
  • newFixedThreadPool固定大小线程池,提交一个任务就创建一个线程,直到线程最大值
  • newCachedThreadPool可缓存线程池,会回收线程,当新任务提交又会新增线程,使用SynchronousQueue作为阻塞队列,
  • newScheduledThreadPool大小无限制,周期性执行任务

线程池参数的意义

  • corePoolSize核心线程最大值
  • maximumPoolSize线程池能拥有的最多线程数
  • workQueue缓存任务的阻塞队列

三者的关系

  • 如果没有空闲的线程,并且当前运行线程小于核心线程,添加新的线程执行任务
  • 如果没有空闲的线程,并且当前运行线程数目等于核心线程,并且任务队列没有满的情况下,任务加入队列,不添加新的线程
  • 如果没有空闲的线程,并且当前运行线程数目小于最大线程数,任务队列已经满的情况下,添加新的线程执行任务
  • 如果没有空闲的线程,并且当前运行线程输入等于最大线程数,根据hanlder定制的策略执行拒绝新的任务的提交

拒绝策略

  • 直接抛出RejectedExecutionException异常
  • 由像线程池提交任务的线程进行执行
  • 抛弃最久的任务
  • 抛弃当前任务

任务队列

  • SynchronousQueue:队列中不保存任务,线程池不空闲的话,提交任务就受阻,只有有空闲,才能入禁区,无界队列
  • LinkedBlockingQueue以是有界的,也可以是无界的但在Executors中默认使用无界的
  • ArrayBlockingQueue数组组成的有界阻塞队列,FIFO
  • DelayQueue优先级队列无阻塞队列

如何知道线程池是否结束

  • shutDown():拒绝新任务的提交,等待线程执行完毕在退出线程池
  • shutDownNow():拒绝新任务的提交,立马退出线

调用isShotDown()只是返回你是否调用过shutDown方法,应该调用isTerminated()方法进行判断

通常有以下几种方式

  • 死循环中调用isTerminated()方法,判断线程是否都执行完毕
  • 使用闭锁countDownLatch(),线程执行完毕调用countDown(),调用shutDown方法后调用await方法,会等到线程执行完毕
  • 调用线程池的awaitTermination方法。

execute()和submit()的区别?

  • execute在Excutor接口中,submit在ExcutorService中
  • submit可以返回Future对象,execute没有

java中的锁

sychronized和lock的比较

  • sychronized:隐士的获取锁,但是固化了锁的获取和释放;Lock接口扩展性更好,种类更丰富,支持重入
  • Lock使用简单,可以非阻塞的获取锁,同时能响应中断、释放锁,同时可以超时获取锁,超过时间就返回

不要在try中使用lock,应该写在外面,因为如果获取锁发生异常会立刻释放锁,导致程序出现异常

队列同步器

工作原理:内部通过维护一个int变量表示同步状态,通过内置的FIFO队列完成资源获取线程的排队工作

使用方法:使用静态内部类,继承AbstarctQueuedSynchronizer,实现抽象方法

独占式获取锁与释放锁:调用tryaccqurie,线程安全的获取同步状态,如果获取失败,将当前线程构建节点,使用CAS操作的方式加入到队尾,并且通过死循环的方式获取线程的同步状态

释放锁:release方法在释放了同步状态以后,会唤醒后继节点

重入锁

ReentrantLock:支持重入,同时也支持公平、非公平(默认)

公平锁和非公平锁的区别?

  • 公平锁追求绝对的FIFO,非公平锁CAS成功就获取锁,效率更高
  • 公平锁需要大量的上下文切换,代价高昂;非公平所可能造成线程'饥饿',但是减少线程切换,保证了吞吐量
  • 公平锁就是在尝试枷锁的时候判断一下前面节点
读写锁

分离读锁写锁提高性能。写锁同一时刻所有线程都被阻塞。读锁同一时刻可以允许多个读线程访问。

出现背景

ReentrantLock虽然保证了线程安全性能,但是也浪费了一定资源,因为多个读操作是不会发生线程安全的问题的,读写锁保证多个线程读操作,同时又很好的保证了写线程的安全性

底层原理

通过高低16进行区分是读锁还是写锁,高16位:读锁,低16位:写锁

获取写锁的时候,如果读锁被获取,写锁是获取不到的,因为要保证写锁的操作对读锁可见

使用场景

更新缓存情况

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
       //获取读锁
     rwl.readLock().lock();
     if (!cacheValid) {
       // Must release read lock before acquiring write lock
       //读锁释放
       rwl.readLock().unlock();
       //获取写锁
       rwl.writeLock().lock();
       try {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         //锁降级,如果其他线程获取写锁,当前线程无感知,因此要先获取读锁,等待当前线程做完业务
         rwl.readLock().lock();
       } finally {
         //最后释放写锁
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       //使用完毕释放读锁,因为可能不走缓存,但是加上锁了
       rwl.readLock().unlock();
     }
   }
 }

锁降级

获取写锁->获取读锁->释放写锁->释放读锁

不支持锁升级

适用于读多写少的场景

LockSupport

出现背景

LockSupport提供几个带有park的方法,实现线程的通信

底层原理

调用的底层UNSAFE的park和unpark方法

使用场景

交替打印字符
    循环列表1
    输出当前字符
    unpark线程2
    park当前线程
    
    循环列表2
    输出当前字符
    unpark线程1
    park当前线程
Condition

用来替代wait,notify/notifyall的线程通知类,通过和Lock接口配合,可以使得等待通知更加灵活

使用场景

生产者消费者问题

生产能力>消费能力:生产者调用condition.await()方法,让出当前线程,等待消费能力上来,调用conditon.signal()方法,唤醒生产者

反之亦然

底层实现

等待队列、等待、通知

等待队列:Condition拥有首尾节点的引用,调用await方法以后,会将当前线程构造成节点,连接到队列,更新尾节点引用

唤醒队列

Fork/Join

Fork:将大任务进行切分成若干个子任务进行并行执行

Join:合并这些子任务结果,得到大任务的结果

框架设计

  • 1、分割任务。不停细化任务,分割出来足够小的子任务
  • 2、任务结果合并。子任务放在双端队列中,启动线程从双端队列拿任务,执行结果放在一个对立,启动线程合并执行结果,这就是工作窃取算法
并发工具类

CountDownLatch

允许一个或多个线程等待其他线程完成操作。

  • 初始化构建CountDownLatch对象的线程数
  • 线程执行完毕调用countDown方法
  • 主线程调用await方法等待

同步屏障CyclicBarrier

构建一个同步点,阻塞到达线程,直接满足条件释放。相当于countDownLatch的反操作

  • 初始化构建CyclicBarrier对象
  • 线程调用await方法,知道满足条件

使用场景

Excel中记录薪酬,可以使用CyclicBarrier,利用多线程计算每个场景,然后主线程合并计算结果

控制并发线程数的Semaphore

比如,红绿灯控制可以通行100辆车,那么第101就是红灯,但是如果前5量走了,后面是可以同行的,其实就是一个并发数的控制

可以用在数据库连接中。

  • 构造Semaphore方法,传入并发数
  • 调用accquire方法在多线程中尝试控制并发

线程分析实战

https://blog.csdn.net/luqiang81191293/article/details/106484628/

什么情况应该进行性能分析?

  • 性能调优,就是充分利用机器,使其性能最大化
  • 如果在单机CPU,不论多大压力CPU都没有办法达到100%,说明程序需要优化(这里的优化指的是你的代码没有好好利用CPU,不是说你的程序压力不大)

几种常见的性能瓶颈

  • 锁的使用不当,不相关的方法使用了同一把锁,对象锁和类锁(一不小心锁定了所有对象)
  • 锁粒度太大,不需要锁的后续代码也加上锁
    • 如果你锁住的是CPU密集计算,缩小同步代码块也不能带来性能上提升,但是也不会下降
    • 如果你锁住的是IO密集型计算,这时候CPU是空闲的,如果此时让CPU运行起来会带来性能提升
    • 总之,缩小锁住的代码块的力度总会提升性能的
  • sleep的滥用
  • 不恰当的线程模型
  • 内存泄漏,导致频发GC(内存泄漏:程序申请到内存无法释放,导致其他程序无法使用你用过的内存,内存溢出最终导致内存溢出)

线程堆栈善于分析的如下问题

  • 系统无缘无故CPU过高
  • 系统挂起,无响应
  • 系统运行越来越慢
  • 线程死锁、死循环、饿死等
  • 线程数量太多导致系统失败

你可能感兴趣的:(工作五年来的面试题目总结之-多线程)