Java 面试题(4)—— 多线程

Java实现多线程有哪几种方式。

implements Runnable, implements Callable,extends Thread


Callable和Future、FutureTask的了解。
Callable和 Future 是juc包下的接口。
Callable 可以异步执行任务,一般和 ExecutorService 的submit方法一起使用。
Future 可以监听任务是否结束,是否取消,取消任务和获取结果(阻塞操作)。
FutureTask :
实现了Runnable和Future,所以兼顾两者优点。
既可以使用ExecutorService,也可以使用Thread。
 

Callable和Future的了解。

Callable和 Future 是juc包下的接口。
Callable 可以异步执行任务,一般和 ExecutorService 的submit方法一起使用。
Future 可以监听任务是否结束,是否取消,取消任务和获取结果(阻塞操作)。
FutureTask :
实现了Runnable和Future,所以兼顾两者优点。
既可以使用ExecutorService,也可以使用Thread。

线程池的参数有哪些,在线程池创建一个线程的过程。

核心参数:
workQueue : 执行前用于保持任务的队列。
keepAliveTime: 线程数量超过核心线程数量,等待工作的空闲线程超时(以纳秒计)。
corePoolSize: 是保持活动状态的最少的核心池大小
maximumPoolSize: 最大线程池
创建方式:
newCachedThreadPool:
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool:
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool: 
创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor:
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

newWorkStealingPool:
jdk 1.8以后新出的,为了防止线程池启用过多,导致cpu占用过多导致的项目宕机。适用于执行多任务,且每个任务耗时时间较短。 上面4种方式都有可能会出现占用cpu过高的情况。

创建过程:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

volitile关键字的作用,原理。

保证内存可见性:
内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。
每次读取前必须先从主内存刷新最新的值。
每次写入后必须立即同步回主内存当中。
防止指令重排序:
volatile关键字通过“内存屏障”来防止指令被重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
(案例是 单例模式的懒加载情况下,Happens-Before内存模型的指令重排会产生两个对象)
无法保证原子性:
这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作(如count++)

https://www.cnblogs.com/monkeysayhi/p/7654460.html

synchronized关键字的用法,优缺点。

用法:
方法上、方法中、代码块,主要取决于锁的对象。
优点:
即线程之间保证同步关系。
Synchronized先天具有排他性、重入性。
jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁。
缺点:
没办法设置获取锁的等待时间,可能会导致无限期地处于阻塞状态。
对于读多写少的应用而言是很不利的,因为即使多个读者看似可以并发运行,但他们实际上还是串行的,并将最终导致并发性能的下降。

https://www.jianshu.com/p/d53bf830fa09

Lock接口有哪些实现类,使用场景是什么。

实现类 ReentrantLock,意思是“可重入锁”,可以中断获取锁操作,获取锁时候可以设置超时时间。
实现接口 ReadWriteLock 。读写锁允许多个读线程并发执行,但是不允许写线程与读线程并发执行,也不允许写线程与写线程并发执行。
ReentrantReadWriteLock 实现于 ReadWriteLock,读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁。

https://www.jianshu.com/p/4a624281235e

synchronized和lock的对比。

lock可以让尝试获得锁的其他线程只等待一定时间。而synchronized修饰的代码块执行完,才能释放锁。或线程执行发生异常,JVM会让线程自动释放锁。
读操作和写操作、写操作和写操作会发生冲突,但是读操作和读操作不发生冲突,Lock就可以允许这种可以同时进行的操作。(读写锁)
Lock可以知道线程有没有成功获取到锁。
当竞争资源非常激烈时,Lock的性能会更好。(具体场景分析)
sychronized是不可中断锁,Lock可中断。(可中断锁)
Lock可设置为公平锁,按照请求锁的顺序分配锁。(公平锁)
可以和条件变量配合使用,对共享数据的多种状态进行监控。

https://blog.csdn.net/weixin_40255793/article/details/80786249

可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。

定义:
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,
则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,
仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
原理:
每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;
当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;
而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;
当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
场景:
可重入锁主要用在线程需要多次进入临界区代码时,需要使用可重入锁。
具体的例子,比如上文中提到的一个synchronized方法需要调用另一个synchronized方法时。
读写锁:
读写锁依赖自定义同步器实现同步功能,读写状态也就是同步器的同步状态。读写锁将整形变量切分成两部分,高16位表示读,低16位表示写。
读写锁通过位运算计算各自的同步状态。
假设当前同步状态的值为c,写状态就为c&0x0000FFFF,读状态为c >>> 16(无符号位补0右移16位)。
当写状态增加1状态变为c+1,当读状态增加1时,状态编码就是c+(1 <<< 16)。
获取读锁:
ThreadLocal线程本地对象,将每个线程获取锁的次数保存到每个线程内部,这样释放锁的时候就不会影响到其它的线程。 
tryAcquireShared 失败后使用 fullTryAcquireShared cas获取锁。
首先读写锁中读状态为所有线程获取读锁的次数,由于是可重入锁,又因为每个锁获取的读锁的次数由每个锁的本地变量ThreadLocal对象去保存因此增加了读取获取的流程难度,
在每次获取读锁之前都会进行一次判断是否存在独占式写锁,如果存在独占式写锁就直接返回获取失败,进入同步队列中。
如果当前没有写锁被获取,则线程可以获取读锁,由于共享锁的存在,每次获取都会判断线程的类型,以便每个线程获取同步状态的时候都在其对应的本地变量上进行自增操作。
获取写锁:
获取写锁相比获取读锁就简单了很多,在获取读锁之前只需要判断当前是否存在读锁,如果存在读锁那么获取失败,
进而再判断获取写锁的线程是否为当前线程如果不是也就是失败否则就是重入锁在已有的状态值上进行自增。
读写锁降级:
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。
分段锁:
jdk 8以前,ConcurrentHashMap分段锁代替HashTable的独占锁。Segment继承了重入锁ReentrantLock,有了锁的功能,同时含有类似HashMap中的数组加链表结构。
jdk 8以后已经弃用分段锁了,原因由以下几点:
加入多个分段锁浪费内存空间。
生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
为了提高 GC 的效率。

https://www.cnblogs.com/wait-pigblog/p/9350569.html


悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。

乐观锁:
即认为读多写少,并发的修改少,别人每次拿到数据都不会修改,所以不会上锁。
更新的时候会校验一下这个数据有没有被更新过。具体策略是更新前先获取当前版本号,
然后和上次的版本号比较,一样再更新,如果失败则要重复读-比较-写的操作。
使用:AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
悲观锁:
悲观锁即认为写多,并发的修改多,每次拿到数据都会上锁,别人想读写这个数据就会block直到拿到锁。
使用:synchronized
CAS 优点:
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,
相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
CAS 缺点: 
1. ABA问题。即更新之前版本是A,中间更新由A->B->A,此时拿到的线程以为没更新过。
java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。
具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号或者时间戳加一,
那么A - B - A就会变成1A - 2B - 3A
2. 自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源。

https://blog.csdn.net/qq_20499001/article/details/90315061


非公平锁,公平锁的区别。


多线程执行的顺序纬度将锁分为公平锁和非公平锁。
公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。
非公平锁:加锁时直接进行一次CAS尝试获取锁,如果没有获取到,
再判断一次(tryAcquire 方法中如果锁空闲,则直接获取)没有获取到,去队尾排队。
ReentrantLock 支持两种锁。
ReentrantLock 的 公平 tryAcquire 方法注释:
Fair version of tryAcquire.  Don't grant access unless recursive call or no waiters or is first.
tryAcquire 的公平版本。除非是递归调用或没有等待者,否则不允许访问。
非公平 tryAcquire 方法注释:
Performs non-fair tryLock.  tryAcquire is implemented in subclasses, but both need nonfair try for trylock method.
执行非公平的tryLock。 tryAcquire是在子类中实现的,但两者都需要 trylock 方法的非公平尝试。
两者的区别在于 tryAcquire 中判断state = 0(锁的引用为0),非公平直接调用CAS,而公平锁要保证当前线程之前没有线程了,才可以CAS.
使用:
更多的是直接使用非公平锁;非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,
如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。

https://www.jianshu.com/p/2ada27eee90b
http://www.cnblogs.com/darrenqiao/p/9211178.html


偏向锁、轻量级锁、自旋锁、重量级锁的区别。


自旋锁:
如果持有锁的资源在很短时间内可以释放锁,其他线程就不需要进入阻塞状态,只需要等一下(自旋),
等有锁的线程释放后立即获取锁。这样避免用户线程和内核之间切换的消耗。
自旋是需要消耗cpu的,需要设置自旋的时间, 超过时间线程进入阻塞。
优点:
减少线程阻塞,对于锁竞争不激烈、持有锁时间很短的情况下,性能有很大提升,自旋
的cpu消耗会小于线程阻塞再唤醒(线程发生两次上下文切换)
缺点:
如果线程竞争资源多,而且线程持有锁的时间很长的话,自旋等待消耗的cpu要
大于线程阻塞再唤醒的消耗,造成cpu的浪费。
自旋锁时间阈值的设定:
JDK 1.6以后引入了适应性自旋锁,自旋的时间不在是固定的了,
而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
基本认为一个线程上下文切换的时间是最佳的一个时间。
JVM的优化:
如果平均负载<cpu的数量,则一直自旋
如果超过cpu数量/2个线程正在自旋,则后来线程直接阻塞
如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
如果CPU处于节电模式则停止自旋
自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
自旋时会适当放弃线程优先级之间的差异
JDK7以后自旋参数由JVM控制

偏向锁:
锁标识位01
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,
如果在运行过程中,同步锁只有一个线程访问,就会给线程加一个偏向锁,再次访问不需要CAS。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
过程:
Mark Word的偏向锁标识为01,则有偏向锁。
如果是可偏向状态,比较访问线程的ID是否指向当前线程。
如果是的话,直接同步,如果不是,会检查上一个ID指向的线程是否存活。
如果挂了,则新的线程持有偏向锁,否则的话,升级为轻量级锁(CAS替换Mark Word失败)。
偏向锁会先撤销再升级,撤销会导致STW。

轻量级锁:
锁标识位00
轻量级锁是由偏向锁升级来的。
是指两个线程交替获取锁或者另一个线程自旋阈值时间内获得锁,这种是轻量级锁。
此时Mark Word指向的是当前线程的栈帧。
如果超过阈值时间导致另一个线程进入阻塞,则轻量级锁释放升级为重量级锁。

重量级锁:
锁标识位10
重量级锁是由轻量级锁升级来的。
多个线程竞争同一个锁。
此时Mark Word指向的是持有重量级锁线程的栈帧。
重量级锁Synchronized:
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
ContentionList ——> EntryList ——> OnDeck ——> Owner ——> WaitSet ——> EntryList(wait后notify回到EntryList)

https://blog.csdn.net/zqz_zqz/article/details/70233767
https://blog.csdn.net/tian251yu/article/details/80638104

ABC三个线程如何保证顺序执行。


公平锁的方式:
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
BlockingQueue的方式:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
queue.put();
queue.poll();


线程的状态都有哪些。


新建(New):新建了一个线程对象。
就绪(Runnable):线程创建后,调用了start方法。该线程存在于"可运行线程池"中,只等待cpu的使用权。
运行(Running):就绪的线程获得cpu,运行。
阻塞(Blocked):运行的线程因为某些原因(调用wait方法)放弃cpu使用权,暂停运行,
直到线程唤醒后进入就绪状态(调用notify方法),才有机会运行。


https://www.cnblogs.com/jijijiefang/articles/7222955.html

sleep和wait的区别。


sleep是Thread类的方法,wait是Object类的方法。
sleep没有释放锁,wait方法释放了锁,被wait的线程重新进入阻塞线程池,
直到被其他线程调用notify/notifyAll后进入就绪状态。
sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。

https://www.cnblogs.com/plmnko/archive/2010/10/15/1851854.html


notify和notifyall的区别。

notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。
其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。

锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。
等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。
如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。
锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。

https://blog.csdn.net/liuzhixiong_521/article/details/86677057


ThreadLocal的了解,实现原理。

ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。
每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。

https://www.jianshu.com/p/ee8c9dccc953

发布了115 篇原创文章 · 获赞 58 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/Angry_Mills/article/details/82107312