【八股文】JUC

1.为什么要使用并发编程

2.并发编程有什么缺点

3.创建线程的四种方式

4.线程的状态有哪些

5.并发编程三要素是什么?在java程序中怎么保证多线程的运行安全

6.synchronized

7.volatile

8.Lock

9.线程池

10.Semaphore(信号量)

11.CountDownLatch(倒计时器)

12.CyclicBarrier(循环栅栏)

13.原子类

14.ThreadLocal

1.为什么要使用并发编程

  • 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
  • 方便进行业务拆分,提升系统并发能力和性能

2.并发编程有什么缺点

内存泄露,上下文切换,线程安全,死锁等等

什么是上下文切换

多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为了每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换

3.创建线程的四种方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 使用Executor工具类创建线程池

runnable和callable的区别

1)runnable的run方法无返回值,callable的call方法提供返回值用来表示任务运行的结果

2)runnable提供run方法,无法通过throws抛出异常,异常必须在run方法内部处理。callable提供call方法,直接抛出异常。

4.线程的状态有哪些

  • 新建(new):新创建了一个线程对象
  • 就绪(runnable):线程对象创建后,当调用线程对象的start()方法,该线程可能处于运行状态,可能处于就绪状态,等待被线程调度选中,获取cpu使用权
  • 运行(running):线程获取CPU权限执行
  • 阻塞(blocked):阻塞包含三种情况:等待阻塞,同步阻塞,其他阻塞

等待阻塞:wait(),进入等待队列,需要notify唤醒(但notify唤醒的线程是随机的)

同步阻塞:获取同步锁失败进入阻塞

其他阻塞:sleep(),join(),发出IO请求

  • 消亡(terminated):线程执行结束

5.并发编程三要素是什么?在java程序中怎么保证多线程的运行安全

三要素:

  • 原子性:一个或多个操作要么全部执行成功要么全部执行失败
  • 可见性:一个线程对共享变量的修改,另一个线程能看见
  • 有序性:程序执行的顺序按照代码的先后顺序执行

出现线程安全问题的原因:

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

解决办法:

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

6.synchronized

synchronized是Java的一个关键字,是一个内部锁。它可以使用在方法和方法块上,表示同步方法和同步代码块。在多线程环境下,同步方法和同步代码块在同一时刻只允许有一个线程执行,其他线程都在等待获取锁。

synchronized是如何保证三要素的:

  • 原子性:锁通过互斥来保障原子性,临界区代码只能被一个线程执行
  • 可见性:synchronized内部锁通过写线程冲刷处理器缓存和读线程刷新处理器缓存保证可见性
  • 有序性:保障了原子性和可见性,即可保障有序性

synchronized底层实现原理

同步代码块:通过monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指向同步代码块的结束位置,当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么java中任意对象可以作为锁的原因)的持有权。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1。相应的执行了monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

方法:ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized锁升级的过程

在jdk1.6后java对synchronized锁进行了升级过程,主要包含了偏向锁,轻量级锁和重量级锁,主要是针对对象头MarkWord的变化

1)偏向锁

经过大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程就会自动获取锁。降低获取锁的代价。

2)轻量级锁

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

3)重量级锁

重量级锁是指当锁是轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

4)自旋锁

解释一下自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少了线程上下文切换的消耗,缺点是循环会消耗CPU

7.volatile

volatile关键字是一种轻量级的锁,可以保证可见性和有序性,但是不能保证原子性。

volatile如何保证了可见性和有序性?

  • 变量可见性:当一个线程改编了线程共享变量的值,其他线程能够立即得知这个修改的新值

  • 禁止重排序

重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能的提高并发度。Java编译器会在生成指令系列时在适当的位置插入内存屏障来禁止重排序。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

解释一下内存屏障:

内存屏障在不同层面代表的含义不同

四个基本规则加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外的部分,程序都可能被重排序

而这里的volatile,写操作一定会发生在读操作之前,也即保证读的数据是最新的。

8.Lock

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并且在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体来说Lock是synchronized的扩展版,Lock提供了无条件,可轮询的(tryLock方法),定时的(tryLock带参方法),可中断的(lockInterruptibly),可多条件队列的(new Condition方法)锁操作。另外Lock的实现类基本都支持非公平锁和公平锁。synchronized只支持非公平锁。

ReentrantLock是如何实现可重入性的?

ReentrantLock 内部自定义了同步器 Sync,在加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,检查当前维护的那个线程 ID 和当前请求的线程 ID 是否 一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。

synchronzied和ReentrantLock的区别

  • synchronzied是关键字,ReentrantLock是接口
  • synchronzied执行完自动释放锁,ReentrantLock需要手动释放锁
  • synchronzied是非公平锁,ReentrantLock默认是非公平锁,但可以设置为公平锁
  • synchronzied会无限期等待获取锁,ReentrantLock可中断

什么是AQS

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

9.线程池

1.为什么使用线程池

  • 节省资源:通过重复利用已创建的线程降低创建和销毁造成的损耗
  • 提高响应速度:当任务达到时,不需要等到线程创建就能立即执行
  • 提高可管理型:统一管理线程

2.线程池执行原理

任务被提交到线程池,如果任务需要的线程数量 < 核心线程数量,则直接执行即可。如果超过了核心线程,则放入队列中等待,如果队列也满了,才开始创建非核心线程执行任务,如果这个也满了,就会使用拒绝策略

3.线程池参数有哪些

  • corePoolSize:核心线程数,作用就是即使没有任务,核心线程也创建好了随时等待任务
  • maximumPoolSize:最大线程数,线程池能创建线程的最大数量
  • keepAliveTime:非核心线程空闲后,如果等了keepAliveTime还没有任务,则线程退出
  • TimeUnit:时间单位
  • ThreadFactory:线程创建工厂
  • blockingQueue:提交的任务将会被放在这里
  • RejectedExecutionHandler:拒绝策略

4.线程池类型,使用场景

  • FixedThreadPool:固定线程数的线程池
  • SingleThreadExecutor:单核
  • CachedThreadPool:跟据需要创建新线程的线程池
  • ScheduledThreadPool:在给定的延迟后运行任务,或者定期执行任务

5.怎么判断线程池的任务是否执行完了

1)使用线程池的原生函数isTerminated()来判断,如果全部完成返回true,否则返回false

2)使用重入锁,维持一个公共计数,所有的任务维持一个计数器,当任务完成时计数器+1(这里要加锁),当计数器的值等于任务数时,所有的任务执行完毕

3)使用CountDownLatch,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值-1

4)submit向线程池提交任务,使用Future判断任务执行状态,通过future.isDone()方法可以知道任务是否执行完毕

10.Semaphore(信号量)

允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

11.CountDownLatch(倒计时器)

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

12.CyclicBarrier(循环栅栏)

协调多个线程同步执行

13.原子类

是指一个操作是不可中断的,即便是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

14.ThreadLocal

1.概念

线程本地变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本(相当于自己线程的私有数据),而不会影响其他线程

2.原理

每个线程都有一个ThreadLocalMap(ThreadLocal内部类),Map中元素的键为ThreadLocal,而值对应线程的变量副本。

3.内存泄漏的原因

每个Thread都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。GC的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用Thread对象节省资源,这也就导致了Thread对象的生命周期比较长,这样便一直存在一条强引用链的关系:Thread --> ThreadLocalMap-->Entry-->Value,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

4.使用场景

每个线程需要有自己单独的实例,比如javaweb中的session

15.Thread类的常用方法

  • start():启动当前线程
  • run():将创建的线程要执行的操作写入
  • currentThread():静态方法,返回执行当前代码的线程
  • getName():获取当前线程的名字
  • setName():设置当前线程的名字
  • yield():释放当前cpu的执行权(让运行状态回到就绪状态)
  • join():在线程a中调用线程b的join(),此时a就进入阻塞状态,直到b完全执行完之后,线程a才会结束阻塞状态
  • stop():强制结束,已过时
  • sleep():睡眠
  • isAlive():判断当前线程是否存活

非Thread但相关的方法

  • wait():执行后进入等待队列
  • notify():唤醒
  • notifyAll():唤醒全部

#Java##八股文##JUC#
八股文合集 文章被收录于专栏

本专栏是我总结的八股大全

全部评论

相关推荐

11-09 16:03
江南大学 Java
什么时候才能拿到offer啊,太难了今年,已经后悔考研了,就一个华勤的意向书被晾了半个月了,焦虑的一批。面了一个多小时,问了很多,记得不是很清楚了,只能凭印象记录点了。面试总体评价:面试官人还挺好的,虽然中途换了一个人,但是气氛还是比较轻松的,不过我还是紧张到发抖就是了,面试的问题除了八股文之外,有很多一些平时写代码不会注意的小细节问题(背八股文的话是根本不会注意到的问题),或者可能就是代码写太少导致的。1.&nbsp;自我介绍2.&nbsp;是不是没有安卓开发经验,(是的),那就以JAVA基础为主来考察。3.&nbsp;了解JAVA的哪些内容?(JAVA集合,Spring,JUC等)。4.&nbsp;Synchronized原理是什么?5.&nbsp;了解设计模式吗?手撕一个单例(共享屏幕,看着你写,中途面试官说有事情,然后换了一个面试官继续,写了一个双重检测锁,然后跟面试官说最安全的单例应该用枚举类来实现)。6.&nbsp;继续多线程的问题,大概讲讲JUC有哪些内容7.&nbsp;项目中用到锁了吗(虽然确实深入学了下JUC但是鼠鼠一来没有实习,二来项目也没做多少,天天被压榨的写煞笔论文,老实说用的不多),然后问JAVA中有哪些锁,有什么区别,JUC中的并发安全的集合类有哪些,ConcurrentHashMap实现原理等等。8.&nbsp;线程池有哪些参数,项目中怎么用的9.&nbsp;线程池怎么用,参数设置有哪些参考因素(IO密集,CPU密集)10.&nbsp;然后出个题,问核心线程有多少个,非核心线程多少个。(事后发现回答错了,是阻塞队列满了才会创建非核心线程,想紫砂了,JUC学了大半个月,啃了好多源码,倒在这种基本问题上了)11.&nbsp;一个线程用Synchronized获取到了锁,然后在这个线程中又new了一个线程去获取锁,这种情况能调用成功吗(因为前面说了Synchronized与ReetrantLock的不可重入的区别,我说应该不行,因为这样不就死锁了吗)12.&nbsp;什么情况下会内存泄漏,问的很细,还问了栈溢出算内存泄漏吗(事后发现栈溢出、内存泄漏、内存溢出是不同的,基础漏洞太多了)13.&nbsp;然后又问了异常相关的问题,然后又问栈溢出是异常吗,那他能被捕获吗?(我想了一会,他又补充到,他是异常吧,我顺势嗯了一声,然后他又说那他可以被捕获吗,我又嗯了一下,他就笑了,仔细一想发现完全被他带到错误的答案了,不过还是自己太菜了。)14.&nbsp;JAVA垃圾回收算法15.&nbsp;循环依赖了还能回收吗16.&nbsp;JAVA有哪些引用类型(知识盲区了,只记得一点点)17.&nbsp;问了Redis有哪些作用场景18.&nbsp;MySQL的视图有什么优点19.&nbsp;MySQL慢查询优化20.&nbsp;http与https的区别21.&nbsp;问leetcode刷的多不多,我说还好,然后就让我写leetcode第一题。。。我哪记得第一题是啥,然后就跟我说是两数之和。然后就用hashMap写了。22.&nbsp;反问:技术栈是JAVA还是kotlin。kotlin更多JAVA也有。23.&nbsp;反问:评价。评价是基础还不错原理都能说出来,但是细节把握的不好(光背八股文的缺点,研究生几年确实没怎么写代码,天天炼丹写论文,感觉还不如本科自己,这时候就该骂导师了,还在pua,“发好文章就有好工作啦,要培养科学思维,不然一辈子打工人”,想鲨了他的心都有了)最后面试官说不出意外后面应该还有一面。也不知道是过了还是没过,等了2天了,秋招到现在还是0offer,焦虑的一批。最近也是彻底放弃JAVA后端转安卓了,感觉安卓的岗位门槛还是第一点,面试给的多一些。
查看21道真题和解析
点赞 评论 收藏
分享
评论
10
67
分享
牛客网
牛客企业服务