谈谈多线程面试的一些题目及延伸吧

来谈一谈多线程的一些面试题吧!

面试官:你对多线程有了解吗?
小肥宅:有了解的
1.面试官:那你给我说说线程它有几种创建方式吧

   好的,多线程的创建方式的话,是有三种的,一种是直接继承Thread类、第二种是实现Runable接口,第三种嘛就是使用Callable和Future创建线程,其实还有一种的,就是使用线程池的方式来创建线程。第一种的话,一般就是继承了Thread之后,再去重写他的run方法就可以了。第二种的话,就是首先实现Runable接口,然后也是需要重写它的run方法,比较特殊一点的就是使用Callable来创建线程,这种方式创建线程的话是先要创建Callable接口的实现类(也就是实现Callable),然后去实现里面call()方法,在1.8开始就可以直接使用Lambda表达式来创建Callable对象了,然后可以使用Future Task类来包装Callable对象,这种方式是可以获取到子线程执行结束后的返回值得的。

  1. 面试官:那你知道Wait和Sleep的一些区别点吗?

   先说说Sleep吧,Sleep的话他是不需要锁资源的,他也不需要被唤醒,时间到了他就自己醒来了,但是Wait呢就是需要先得到锁,并且唤醒的时候需要别人来唤醒他。才能调用锁资源,而且在调用了Sleep之后,线程是不会释放掉锁的,但是Wait会释放掉锁,然后加入到等待队列里面去。还有就是sleep会让出CPU的执行时间,会强制上下文切换,wait的话就不一定。

  1. 面试官:嗯,我听你说到了线程池,那你能给我说说线程池的处理流程吗,还有你知道那些参数啊。

   线程池的话,主要就是为了降低资源消耗,提高线程利用率,降低创建和销毁线程的消耗
以及提高响应速度,任务来了,直接有线程可以使用,不用先创建再执行,还能提高线程的可管理性,线程是稀缺资源,使用线程可以统一分配调优监控。线程池的处理流程的主要就是,当有任务时,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到线程数,则创建核心线程处理任务;如果达到了核心线程数的话,线程池就会判断任务队列(workQueue)是否满了。如果没满,将任务添加到任务队列中;如果任务队列满了,线程池就判断线程数是否达到了最大线程数(maxnumPoolSize)。如果没有达到,就创建新的线程处理任务;如果这个都达到了最大的话,就会按照拒绝策略来进程处理。
同时线程他是拥有七个核心参数的,分别是:核心线程数(corePoolSize)、最大线程数(maximunPoolSize)、空闲线程存活时间(KeepAliveTime)、空闲线程存活的时间单位(unit)、工作队列(workQueue)、线程工厂(threadFactory)、拒绝策略(handler)这一些。

4.面试官:说到拒绝策略,你能说说你了解的拒绝策略吗?

拒绝策略的话主要就有四种:
DiscandPolicy:在任务已经满了的时候,他会直接将提交的任务给丢弃掉
AbortPolicy:他在拒绝任务的时候,会先抛出一个RejectedExecutionException
的运行时异常,然后我们的话可以根据他的异常进行一些其他的操作比如说重新或者放弃掉这个任务,这也是默认的拒绝策略。
DiscardQldesolicy:也是直接丢弃,不过他丢弃的是队列里面时间存活最久的。
CallerRunsPQlicy:他会将这个任务的执行权交个提交任务的线程来执行,就是谁提交的谁负责,这样的话,任务就不会被丢弃而造成损失,还有就是如果任务比较费时的话,那么这段时间内提交任务的线程会出于忙碌状态而无法继续提交任务,也起到一定的减缓任务的提交速度。

5.面试官:那你说说是怎么保证线程安全的呀

   保证线程安全的话就是加锁嘛,像Synchronized、CAS无锁操作这一些都是可以用来保证线程安全的

6.面试官:你说到CAS,那你给我说说CAS是什么。

   CAS的话就是Compare and Set,就是比较并交换,它有三个操作数,内存值、l旧的预期值、要修改的值,当且仅当预期值和内存值相同是,在将内存值修改为B并返回true,如果不同的话则证明内存值在并发的情况下被其他线程修改过了,便不做任何操作,返回false,等待下次修改。但是CAS会引发一个ABA的问题,ABA的问题的就是,比如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了—些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。针对这个问题,Java提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

7.面试官:那你说下,CAS、Synchronized、Volatile他们都有什么区别吗?

   Synchronized是悲观锁(阻塞锁),他是属于抢占式,会引起其他线程阻塞。volatile提供多线程共享变量可见性和禁止指令重排序优化,不能解决原子性,CAS是基于冲突检测的乐观锁(l阻塞锁)

8.面试官:说到Synchronized,那你对他在1.8的时候,进行的一个锁优化有了解吗?

   Synchronized他是一个不公平锁,synchronize优化之后的话是有偏向锁、轻量级锁、重量级锁这几个锁,然后其中也有个无锁,无锁的就是采用的CAS,偏向锁的话,一个对象创建后就是默认开启的,但是他的默认开启是有一定延迟的(),在对象头的Mark Work里面他们的倒数后三位是101的就可以判断他是一个偏向锁,当后三位是001的时候,他就是一个无锁的状态。然后会在Mark Work里面存储这个偏向的线程的id,当这个线程下次再来的时候,就会直接让他使用,但是如果对象被多个线程访问的时候,但是没有竞争的时候,偏向当前线程的对象是会有机会偏向其他线程的,重偏向会重置对象的线程ID,但是当撤销偏向锁的阈值超过20次以后,jvm会给这些对象加锁是重新偏向,还有就是撤销偏向超过40次后,jvm会觉得自己确实偏向错了,然后就会将整个类的所有对象变为不可偏向的,新建的也不可偏向。当有竞争的时候,synchronize的优化会自动将偏向锁升级成轻量级锁,轻量级锁的话他会在线程的锁记录(Lock Record)里面的Object reference指向锁对象,使用CAS去替换对象里面的Mark
Work,将Mark Work的值存入锁己录,并将对象里面锁标志位改成00,就代表加锁成功,如果这时候CAS失败了,就有两种情况,一种是被其他线程持有,便会进入锁膨胀过程(锁升级),另一种就是自己执行了synchronized锁重入,然后就会在添加一条Lock Record作为重入的计数,这时候要退出synchronized代码块的时候(解锁时),当取出的值是null的锁记录,就表示有重入,这个时候会将重入计数减1,当锁记录值不是null的时候,会使用CAS将Mark Work里面的值恢复给对象头,但是这个时候失败了,就说明轻量级锁已经升级成了重量级锁,然后就会按照重量级锁流程进行解锁。升级成重量级锁话是,当有其他线程想要这个锁时,便会进行自旋等待,这是锁被一个对象持有,并且也有一个线程在自旋等待时,同时又有多个线程想要获取这个锁(自旋数量超过一个),便会升级为重量级锁,重量级锁的通过monitor来控制。这个时候,这个对象头的Mark Work中就被设置执行Monitor对象的指针,刚开始的时候,Monitor里面的Owner指针是为null的,当一个线程获取到一个锁对象的时候,就会将Monitor里面Owner设置成这个线程,因为Monitor里面只能有一个Owner,这时候如果其他的线程也来获取这个锁对象的时候,就会进入到它里面的一个EntryList里面去,当当前线程执行完之后,会根据Monitor地址找到Monitor对象,将里面Owner设置成null,然后会去唤醒EntryList里面Blocked(阻塞)的线程竞争锁。然后又会进行下一次的重量级锁。

9.除了Synchronized,你还知道一些其他的锁机制吗?

   除了这个的话,我还了解到的锁机制有ReentrantLock、ReentrantReadWriteLock这一些。
ReentrantLock:他是成对出现的,try/finally,在finally里面释放锁。他是可以实现公平锁和非公平锁的,默认的话使用的非公平锁。他在加锁的时候,如果没有竞争的时候也就是加锁成功,会利用CAS将State由0变修改为1,并且将指针设置成当前线程(setExclusiveOwnerThread),当有竞争的时候,就是一个线程使用CAS尝试将state由0修改为1的时候失败了,会去尝试获取锁这时候锁已经被其他线程占用了肯定失败,就会进入addWaiter,会去创建node节点,第一次创建的时候会创建两个,第一个是null (用来占位,Dumm亚元),第二个就是我们当前的这个线程,并且这个node节点创建是一个惰性的,然后他就会进入到acquireQueued会进行死循环中不断尝试获取锁,他也不会一直的死循环,他会去判读自己是不是挨着Head的(排在第二位)是的话他会(tryAcquire)再次去尝试获取锁,如果他拿到了,便会中断当前的状态去拿锁进行任务,如果前一个线程还拿着的锁的话就会失败,就会去判断是不是要停止继续获取锁(if是一个&&,具有短路的性质),这是会进入(shouldParkAfterFailedAcquire)将前面一个node节点的wait5tatus改成-1(-1表示它有义务去唤醒后面的线程),然后会进行下一次循环,(总结就是三次自救机会:1、进行判断市调用tryAcquire2、进入acquireQueued的循环调用tryAcquire3、调用shouldParkAfterFailedAcquire(p,node)将waitStatus改成-1,第一次进去肯定是false,会进行下一次循环又一次机会)然后这时候(还是没有拿到锁的情况),再次判断需不需要停止继续获取锁的时候,因为前面已经将前置节点编程-1了所以是true,然后进入parkAndCheckInterrupt()方法里面调用park阻塞当前线程,如果有多个线程的只有最后一个的waitStatus是0,前面的都是-1。释放锁的话
ReentrantReadWriteLock:他的只有读读不互斥,读写、写读、写写都是互斥的,并且也是支持公平和非公平锁的。他的读锁是一个共享锁,写锁是一个独占锁。
他进行写读时:
有两个线程,一个线程是T1写锁,T2写锁,当读锁那到锁了,第二个线程来拿锁的时候,因为被前一个写锁,tryAcquireShared就会返回-1,表示失败(-1:失败,0:成功,后继节点不会被唤醒,正数:成功,数组表示后续有几个节点需要被唤醒,读写锁返回1.),这时候就和ReetrantLock类似,会有三次拿到锁的机会(1、进行判断市调用tryAcquire2、进入acquireQueued的循环调用tryAcquire3、调用
shouldParkAfterFailedAcquire(p,node)将waitStatus改成-1,第一次进去肯定是false,会进行下一次循环又一次机会),机会没了的话就会调用addWaiter添加节点也是和ReetrantLock类似,不过读写锁不同的是他的节点被设置为node.SHARED和Node.EXCLUSIVE表示是共享和独占,经过几次机会之后没拿到就会进行阻塞。
进行读写时:
这时候又有读写锁来了,假如写锁丕没释放锁,也是会加入到队列里面去,跟ReentrantLock一样
释放锁
如果最开始的线程((写锁)释放锁了,也是和ReentrantLock类似,先判断自己是不是老二,是的就会唤醒,如果有其他新的线程来抢,没抢过也会阻塞(非公平锁),不过读写锁会调用一个setHeadAndPropagate(node,1t)把他原本所在的位置设置成头结点时会去检查他的下一个节点是不是Shared,如果是的话就会把head的状态从-1改为O(防止unparkSuccessor被多次执行),并且去唤醒他后面一个,然后有是这样(就是唤醒所有的shared的),直到是Exclusive,就不会继续往下唤醒。唤醒写锁的时候,因为可重入,要让写锁的计数等于0的时候,才算释放完全。

10.面试官:那你知道那些安全的原子包呢?

1、原子整数-----> atomicBoolean、Atomiclnteger、AtomicLong
2、原子数组----->AtomiclIntegerArray、AtomicLongArray、AtomicReferenceArray
3、字段更新迭代器----->AtomicReferenceFiledUpdater、AtomicIntegerFiledUpdater、AtomicLongFieldUpdater只能配合volatile修饰的字段使用

11.面试官:说了这么多,那你知道线程安全的集合,ConcurrentHashMap吗?

   这个我是有一定了解的,点击查看吧

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值