面试之并发

1.Synchronized用过吗?原理是什么?

> Synchronized是由jvm 实现的一种互斥同步的一种方式 ,如果看过 被Synchronized
> 修饰过后程序编译的字节码,会发现,修饰过的程序块 ,在编译前后 生成了一个 monitorenter和 monitorexit两个字节码指令。
> 那么这两个指令由上面含义呢 ?
> 在虚拟机执行到monitorenter指令时, 会尝试获取对象的锁,如果这个对象没有锁、或者当前线程已经拥有这个对象的锁, 把锁计数器+1> 在虚拟机执行到monitorexit的时候 ,锁计数器 -1 ,当计数器为 0的时候,锁就释放了。
> 如果获取对象失败了 ,那么当前线程就要进行阻塞等待了,直到对象锁被另一个线程释放为止。
> java中的Synchronize 通过在对象头的标记,来达到获取锁和释放锁的目的。

2.上述的锁到底是什么?如果确定对象的锁?

> 锁的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,就是要锁定和解锁的对象。我们可以使用Synchronized修饰不同的对象 ,因此、对应的对象锁就可以确定了 。
> 
> 1.如果Synchronized明确指定了锁对象,如Synchronized(paramName)Synchronized(this)等,说明加解锁对象是该对象。
> 2.如果没有明确指定,就看方法是静态 还是非静态的了 。
> 如果Synchronized修饰的是静态方法,表示对应的类对象就是锁对象。
> 如果Synchronized修饰的是非静态方法,表示这个方法就是锁对象。
> 注:当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都会阻塞,而对象里没有Syn修饰的可以正常访问

3.什么是可重入性?Synchronized是可重入锁吗?

> 可重入性事锁的基本要求 ,目的是为了解决把自己锁死的问题。
> 如果锁没有重入性 ,就会锁死自己。场景如下:
> 一个同步方法m1 , 在内部调用了 另一个同步方法 m2,  这是m2又调用了m1, 如果没有重入机制 ,  就会锁死自己,在一直等待。
> 在  问题 1 中说到的 虚拟机执行monitorenter指令的时候 , 没锁/当前线程拥有锁   就是用这种方式实现的重入性。

4.JVM对JAVA原生锁做了哪些优化?

> java6之前,Monitor 的实现 完全依赖操作系统的互斥锁实现 ,通过 java层面的线程与操作系统的原生线程形成映射关系 ,当要将一个线程进行阻塞或者唤醒都需要操作系统协助, 这就涉及到了 ,我们需要从java处理层面转向给 操作系统内核来处理,这是很消耗处理器时间的 。
> 现在JDK做了大量的优化 , 其中一种优化就是自旋锁 ,意思就是在线程进行阻塞前、先让线程自旋等一会,如果等一会就 其它线程就释放了 、那么就不需要进行系统内核操作了 。
> JDK还做了三种不同的Monitor实现
> 1)偏向锁
> 2)轻量级锁
> 3)重量级锁
> 这三种锁 使JDK得以优化Synchronized的运行,当jvm检测到不同的竞争状况,会自动切换到合适的锁实现上,这就是常说的锁升级、锁降级
> 没有竞争的情况下,默认会使用偏向锁
> (( 
> JVM 会利用CAS操作 ,在对象头上的Mark Word 部分设置线程id ,表示这个对象 偏向于这个线程、所以不涉及到真正的互斥锁,因为我们的实际业务中,很多场景,大部分对象声明周期中都是被一个线程锁定,使用偏向锁可以降低  无竞争开销
> ))
> 如果又另一个线程试图锁定 某个被标记了的,偏向锁对象 ,JVM 就会撤销偏向锁,切换到轻量级锁实现。
> 轻量级锁依赖CAS操作 MarkWord来获取锁,如果重试成功,就使用轻量级锁,否则就会进一步升级为重量级锁

5.Synchronized是公平锁吗?

> 不是!
> 非公平主要体现,是在获取锁的行为上。 它的获取锁并不是按照申请锁的时间顺序,给等待线程分配锁,每当锁被释放后, 任何一个线程都可以竞争锁,有点是这样可以提高执行性能,缺点是可能会有线程饥饿现象

6.Synchronized与重入锁ReentrantLock实现原理有什么不同?

> 首先要理解,锁实现的原理都是为了一个目的,就是让所有线程都看到一种状态或者说标记。
> Synchronized 通过在对象头设置标记实现了这个目的 ,是jvm原生锁的实现方式,而ReentrantLock以及所有基于Lock接口的实现类 ,都是通过一个volitile修饰的 int 类型的变量, 并保证每个线程都能拥有对这个变量的可见性、和原子修改 ,  本质是基于AQS框架

7.介绍一下AQS框架?

> AQS(abstractQueuedSynchronizer类) ,是一个用来构建锁 和同步器的框架 ,各种Lock 包中的锁,
> 像常用的ReentrantLock、ReadWriteLock,以及其它 如
> Semaphore、CountDownLatch,甚至早期的FutureTask等,都是基于AQS来构建的。

8.详细说明一下,Synchronized和ReentrantLock的异同.

> ReentrantLock是Lock的实现类,是一个互斥的同步锁。
> 从功能角度讲:ReentrantLock 比 Synchronized的 同步操作 更精细 ,甚至做一些 Synchronized没有的功能,如:等待可中断、带有超时的获取锁尝试、可判断是否有线程在排队等待获取锁、可以实现公平锁。
> 从释放锁角度讲:Synchronized在jvm层面实现的,不但可以通过工具监控Synchronized的锁定,并且在代码执行中出现异常,jvm可以自动释放锁,但是使用Lock不行,它是通过代码释放的。
> 从性能角度讲: Synchronized 早期实现比较低效 ,对比ReentrantLock ,大多数场景性能差不多。
> 但是在java6之后  对其进行了改进 ,在竞争不激烈时 Synchronized 性能优于 ReentrantLock;在高竞争下,Synchronized 的性能会下降几十倍 ,但ReentrantLock性能 基本不变。

9.除了ReentrantLock 你还接触过JUC中的哪些并发工具?

> 通常说的并发包(JUC)也就是java.util.concurrent及其子包,集中了 java并发的各种基础工具类,主要包括以下几个方面:
> 1 提供了CountDownLatch、CyclicBarrier、Semaphore等 ,比Synchronized更高级的 ,可以实现更加丰富多线程操作的同步结构。
> 2.提供了 ConcurrentHashMap、有序的ConcurrentSkipListMap,还有快照机制的动态数组CopyOnWriteArrayList等,各种线程安全的容器。
> 3.提供了ArrayBlockingQueue、SynchrousQueue、和针对特定场景的PriorityBlockingQueue等,各种并发队列实现。
> 4.Executor框架 ,创建各种不同类型的线程池、调度任务运行等。

10.线程池是如何实现的?

> 在java中,所谓线程池中的线程,其实就是被抽象为一个静态内部类  Worker,它基于AQS实现,存放在线程池的HashSet<worker> workers成员变量中;
> 而需要执行的任务则存放在成员变量 workQueue(BlockingQueue<Runnable>)中 ,这样整个线程池的基本实现思想就是:
> 1.从workQueue中不断取出需要执行的任务,放在Workers中进行处理

11.创建线程池的几个核心构造参数?

> 在java中线程池的创建非常灵活,可以通过配置不同的参数,创建出不同的线程池;
> corePoolSize:线程池的核心线程数
> maximunPoolSize:线程池允许的最大线程数
> keepAliveTime:超过核心线程数时,闲置线程的存活时间
> workQueue:任务执行前保存任务的队列 ,就是保存由execute方法提交的Runnable任务.

12.线程池中的线程是怎么创建的 ? 一开始随着线程池启动创建的吗?

> 当然不是,线程池默认初始化后 ,不启动worker,等待有请求时才启动。
> 每当我们通过 execute 方法添加一个任务时,线程池会做如下判断:
> 1 正在运行的线程数量 < corePoolSize ,那么马上创建线程运行这个任务。
> 2.正在运行的线程数量>= corePoolSize ,那么将这个任务放到队列.
> 3.如果队列满了,但正在执行的线程数 < maximumPoolSize,那么会创建非核心线程立刻执行这个任务。
> 4.如果队列满了,且正在运行的线程数量 >= maximumPoolSize,那么线程池会抛出异常(可配置异常处理策略)
> 当一个线程完成任务时,会从队列中取下一个任务来执行,当一个线程没事可做 ,超过keepAliveTime时,线程池会判断, 如果当前运行的线程数 > corePoolSize,那么这个线程就会被停掉。
> 所有线程池的任务完成后, 它最终会收缩到 corePoolSize的大小。

13.java 中默认实现好的线程池有哪些?

> 1.SingleThreadExecutor线程池
> 这个线程池只有一个核心线程在工作,也就是说相当于单线程串行 执行所有任务、如果这个唯一线程因为异常结束,会创建一个新的来替代它,此线程池保证所有的任务执行 是由顺序的(提交顺序)
> corePoolSize:1
> maximumPoolSize:1
> keepAliveTime:0L
> workQueue:new LinkedBlockingQueue<Runnable>, 缓存队列是无界的。
> 2.FixedThreadPool线程池
> 这个线程池是固定大小的线程池,只有核心线程。每次提交一个任务就创建一个线程,知道线程达到线程池的最大大小,达到最大值后就会保持不变, 如果某个线程因异常而终止,那么线程池会补充一个新线程。
> 该线程池多数 使用在一些稳定/固定的并发线程, 如 服务器。
> corePoolSize:nThreads
> maximumPoolSize:nThreads
> keepAliveTime:0L
> workQueue:new LinkedBlockingQueue<Runnable>,缓存队列无界
> 3.CacheThreadPool线程池
> CacheThreadPool  是无界线程池 ,如果线程池的大小 超过了处理任务所需要的线程,那么就会回收部分空闲线程(60s),当任务数增加时,它可以智能的添加新线程。
> 该线程池的大小,完全依赖于操作系统 或者 说是jvm  能够创建的最大线程数来决定。
> corePoolSize:0
> maximumPoolSize:Integer.MAX_VALUE
> keepAliveTime:60L
> workQueue:new SynchronousQueue<Runnable>,一个缓冲区为1的阻塞队列
> 4.ScheduledThreadPool线程池
> 核心线程池固定 , 大小无限的线程池。该线程池支持定时或周期性执行任务,
> corePoolSize:corePoolSize
> maximumPoolSize:Integer.MAX_VALUE
> keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
> workQueue: new DelayedWorkQueue()

14.ThreadLocal是怎么解决并发安全的?

ThreadLocal 这是 java提供的一种保存线程私有信息的机制 ,因为其在整个线程生命周期内有效,所以可以方便
的在 ,同一个线程关联的不同业务模块之间传递信息,比如事物ID、Cookie等上下文信息。
ThreadLocal 为每一个线程维护变量的副本,吧共享数据的可见范围,控制在同一个线程内, 其实现原理是在
ThreadLocal类中有一个Map,用于存储每一个线程的变量副本。

15.为什么很多人说,要慎用ThreadLocal,谈谈你的理解?

使用ThreadLocal 慎用,是因为要注意它的remove
ThreadLocal的实现是基于一个 ThreadLocalMap,在ThreadLocalMap中,它的key是一个弱引用,
通常弱引用都会配合引用队列和清理机制使用,但是!!ThreadLocal并没有。
这就意味着,它的回收依赖于显示的触发,否则就只能等 线程结束,然后回收ThreadLocalMap,
这就是很多OOM的来源, 所以一定要自己负责 remove。
注意!! 不要配合线程池使用,因为worker线程往往是不会退出的。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值