1、多线程基础
(1)sleep和yield的区别
1.sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给优先级低的线程以运行的机会,而yield()方法只会给相同优先级或者更高优先级的线程以运行机会。
2.线程执行sleep()方法后会转入阻塞状态,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。
3.sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。
(2)为什么弃用stop和suspend
第一:stop方法为什么不安全
其实stop方法天生就不安全,因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致,我们还是拿银行转账作为例子,我们还是从A账户向B账户转账500元,我们之前讨论过,这一过程分为三步,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性。
第二:suspend方法为什么被弃用
suspend被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会破换对象和强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。
假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在或者相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。这就是suspend被弃用的原因。
(3)wait方法底层原理?
当一个线程执行到wait()方法时,它就进入到waitset,同时失去了对象的锁。当它被一个notify()方法唤醒时,就会从waitset等待池中放到锁池中。一旦有机会就会重新获得锁。
(4)java中wait和sleep方法的不同?
整体的区别其实是有四个:
1.sleep是线程中的方法,但是wait是Object中的方法。
2.sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3.sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要。
4.sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
(5)我们调用start()会执行run()方法,为什么不能直接调用run()方法?
start()方法做了两件事:第一是创建新的线程,第二执行在run()方法里的代码。直接调用run方法的话,相当于普通的方法调用,不会新建线程,达不到多线程的目的。
(6)Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
(7)volatile的底层原理?不是回答内存屏障,是问你调用了什么CPU的API,在不同硬件情况下会有什么不同吗?
第一:如何保证可见性和有序性:Java的内存模型定义了8种内存间操作:
1.lock和unlock
lock把一个变量标识为一条线程独占的状态。
unlock把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定。
2.read和write
read把一个变量值从主内存传输到线程的工作内存,以便load。
write把store操作从工作内存得到的变量的值,放入主内存的变量中。
3.load和store
load把read操作从主内存得到的变量值放入工作内存的变量副本中。
store把工作内存的变量值传送到主内存,以便write。
4.use和assgin
use把工作内存变量值传递给执行引擎。
assgin将执行引擎值传递给工作内存变量值。
第二:之前的版本:
Java虚拟机通过锁的机制规定了这8中操作之间的顺序,比如read之前对该变量加锁lock,write之后对该变量释放锁unlock。
第三:之后的版本
读操作不需要加锁,写操作才会加锁,这样会提升效率,套路都是一致的:
1.降低锁粒度:只在需要加锁的时候加锁
2.事件响应机制:当有线程对共享变量有更改后,让其他线程能够感知到从而让自己本地工作内存中的缓存失效,这个思想就是CPU中的MESI缓存一致性协议:
多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
(8)Java中能创建volatile数组吗?
能,但volatile作用的范围是一个指向数组的引用,而不是整个数组。
(9)fail-fast 与 fail-safe 机制有什么区别
第一:fail-fast机制是如何检测的?
迭代器在遍历过程中是直接访问内部数据的,因此内部的数据在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个标记 “mode” ,当集合结构改变(添加删除或者修改),标记"mode"会被修改,而迭代器每次的hasNext()和next()方法都会检查该"mode"是否被改变,当检测到被修改时,抛出Concurrent Modification Exception
第二:fail-safe机制
fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException,fail-safe机制有两个问题
1.需要复制集合,产生大量的无效对象,开销大
2.无法保证读取的数据是目前原始数据结构中的数据。
(10)线程切换需要消耗哪些资源
一、线程开启所占用的空间开销
【1】Thread内核数据占用:主要有OSID(线程的ID)和Context(存放CPU寄存器相关的资源)寄存器的状态会被保存到Context中,以便下次使用。因为子线程程序和主线程程序执行主要是依赖时间片切换。通常系统分配一个时间片大约30ms,意思就是1秒中被分配至少33份,分别执行不同的线程。
【2】Thread环境块(了解)
【3】用户堆栈模式(主要部分):用户程序中的局部变量和参数传递所使用的堆栈,如果是引用类型则使用堆的空间,如果是值类型则使用栈的空间。例如在写程序中会遇到【StackOverFlowException异常:内存溢出】。典型的就是程序有死循环!不断占用堆栈空间,因为默认情况下:windows会分配1M的空间给用户模式堆栈(换句话说,一个线程分配1M的堆栈空间,用于局部变量和参数传递)
二、线程在时间的上的开销
【1】资源使用通知开销:一个程序开启通常会有很多资源调用,包括托管的,非托管的dll、exe、资源、元数据等。。。这些资源的启用都需要通知,通知是花时间的。
【2】时间片开销(主要部分):只要我们电脑的线程超过电脑的CPU处理器个数对应的线程,一定会有时间片切换。
(11)多线程中的忙循环是什么
忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
(12)什么是多线程环境下的伪共享(false sharing)
CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache的Cache Line大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。
(13)什么是线程组,为什么在Java中不推荐使用
线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃,多以价值已经大不如以前。
线程组ThreadGroup不是线程安全的,在使用过程中不能及时获取安全的信息。
(14)说出3条在Java中使用线程的最佳实践
这个问题与之前的问题类似,你可以使用上面的答案。对线程来说,你应该:
a)对线程命名
b)将线程和任务分离,使用线程池执行器来执行 Runnable 或 Callable。
c)使用线程池
(15)1000个线程同时运行,怎么防止不卡
使用线程池,提高机器性能;
(16)如何中断一个线程深入到底层
(17)suspend() 和 resume()
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。
suspend() 和 resume() 方法有很多缺点。下面将其缺点列举一二
1.当 suspend() 和 resume() 方法使用不当时极容易造成公共同步对象的独占,使其他线程无法访问公共对象资源。
写一个简单的列子直观说明该问题,如下:
2.当 suspend() 和 resume() 方法使用不当时也容易造成数据不同步的情况
2、原子包
(1)AQS原理
AQS核心思想是
(1)内部定义了一个volatile修饰的state变量,表示同步状态:一开始是0,一个线程获得锁并将state加1;使用volatile修饰,其他线程能立刻看到
(2)维护了一个双向链表结构的同步队列,来对线程排队,当有线程获取锁失败后,就被添加到队列末尾。Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和独占模式。
(2)知道unsafe包嘛?有哪些功能?为什么是unsafe的?
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着
这些方法大体可以归结为以下几类:
(1)初始化操作
(2)操作对象属性
(3)操作数组元素
(4)线程挂起和回复
(5)CAS机制
这个类是不能直接new的,使用的时候需要使用反射机制去new,有个字段是theUnSafe
3、工具类
(1)Semaphore信号量
限制访问线程的数量,当N为1时线程安全,当N大于1,线程不安全。
(2)CyclicBarrier以及和CountDownLatch的区别
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
(3)threadlocal能不能解决dateformat的同步问题
SimpleDateFormat不是线程安全的,你要么每次都new一个来用,要么做加锁来同步使用。我们为每个线程都缓存一个instance,存放在ThreadLocal里,使用的时候从ThreadLocal里取就可以了:
4、线程池
(1)Executors和ThreaPoolExecutor创建线程池的区别
第一:Executors各个方法的弊端:
newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
第二:ThreadPoolExecutor
ThreadPoolExecutor3个最重要的参数:
corePoolSize:核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize:线程池中允许存在的工作线程的最大数量
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中
keepAliveTime:线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
unit:keepAliveTime参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂
handler:线程池任务队列超过maxinumPoolSize之后的拒绝策略
第三:拒绝策略
ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
(2)线程池中submit()和execute()方法有什么区别?
接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务。
返回值:submit()方法可以返回持有计算结果的Future对象,而execute()没有
异常处理:submit()方便Exception处理
(3)线程池都有哪些状态?
显然不是的。线程池默认初始化后不启动Worker,等待有请求时才启动。
(1)RUNNING:
这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
(2)SHUTDOWN:
不接受新的线程了,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING->SHUTDOWN。
(3)STOP:
不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
(4)TIDYING:
所有的任务都销毁了,workCount为0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP->TIDYING。
(5)TERMINATED:
线程池处在TIDYING状态时,执行完terminated()之后,就会由TIDYING->TERMINATED。
(3)为什么要使用线程池?线程池创建多少合适呢?
第一:使用线程池的原因
线程的大量创建比较消耗资源,网上说一个线程的创建最少需要1M内存,而且有些线程执行时间比较短,使用线程池可以避免大量的资源消耗,而且可以复用线程,以达到节省系统资源的开销;
第二:线程池创建多少个合适呢?线程池核心线程数多少最为合适(IO密集型和CPU密集型)?
一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。
5、线程锁
(1)并发编程的锁本质到底是什么?
锁的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。
(2)synchronized为什么早期版本效率低下,又是如何改进的?
第一:效率低下原因
早期版本中,synchronized属于重量级锁,为了保证线程同步,其底层使用了monitor锁,并使用了monitorenter和monitorexit来实现线程同步,不过是依赖于底层的操作系统的MutexLock(互斥锁)来实现的,在运行时,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态(普通的线程切换是不需要的,但是这个互斥锁需要),这个状态之间的转换比较耗时,这也是为什么早期的synchronized效率低的原因。
第二:优化点:
①优化点一:锁升级
JDK1.6以后,引入了“轻量级锁”和“偏向锁”。这时候锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。对象在堆中大致可以分为三个部分,分别是对象头、实例变量和填充字节。这四个锁就存储在对象头中。
偏向锁:当线程1获取锁对象时,会在java对象头记录自己的threadID,不会主动释放锁,以后线程1再次获取锁的时候,比较threadID是否一致,如果一致直接获取;如果不一致那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,线程2设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
轻量级锁:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
不管是偏向锁还是轻量级锁,尝试获取锁的过程都是通过CAS机制来实现的。
②锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作
③锁消除
Java虚拟机监测到到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
(3)什么是可重入性,为什么说Synchronized是可重入锁?
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。比如说一个类中的同步方法B调用另一个同步方法A;调用B的时候线程获得锁,在B中调用A又要尝试获取锁;具有可重入性指的是,这次尝试可以直接获取锁;如果不具有可重入性,那么要就需要自己先释放了B,才能获取A,但是调用A的前提就是先获取B,所以造成死锁;
实现原理是在对象头加了一个线程ID进行对比(偏向锁的思想)
(4)Synchronized相比,可重入锁ReentrantLock原理有什么不同?
Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。
(5)ReentrantLock是如何实现可重入性的?
ReentrantLock内部自定义了同步器Sync,加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否
一样,一样就可重入了。
(6)CAS的缺点
(1)ABA问题
(2)循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
(3)只能保证一个共享变量的原子操作,对数组无效
(7)JDK中哪些地方用到了CAS?
1、Atomic包下的类
2、跳跃表java.util.concurrent.ConcurrentSkipListMap
ConcurrentSkipListMap采用典型的空间换取时间策略,它是一个有序的,支持高并发的Map.Put元素的时候使用了CAS
3、自旋锁
(8)为什么说synchronized是不公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后
当一个线程想获取锁时,先试图插队,插队成功直接获取锁,此时就是非公平的,插入失败那就去排队,也就是公平的;插入的机制是采用的CAS机制尝试去插入;
Synchronized采用非公平锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象(即某个线程长时间未竞争到锁)
(9)什么是竞争条件?你怎样发现和解决竞争?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。
代码审查;
(10)怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Objectobj)方法,当且仅
当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个
static方法,这意味着"某条线程"指的是当前线程。
(11)公平锁和非公平锁的实现原理
公平和非公平锁的队列都基于锁内部维护的一个双向链表和一个volatile关键字修饰的state。
首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
(12)java中的锁有哪些分类?区别是什么?
公平锁/非公平锁
可重入锁
独享锁/共享锁
互斥锁/读写锁
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
(13)线程安全有什么无锁的策略?
CAS和threadlocal
(14)Reentrantlock比synchronized有优势的原因
结合Reentrantlock类中的方法以及死锁的四个条件来谈:响应中断、非阻塞的获取锁、支持超时等
6、线程集合
(1)ThreadLocal原理
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用(生命周期和线程一样)。每次使用完ThreadLocal,都调用它的remove()方法,清除数据。Java 的 Web 项目大部分都是基于 Tomcat。每次访问都是一个新的线程,每一个线程都独享一个 ThreadLocal,我们可以在接收请求的时候 set 特定内容,在需要的时候 get 这个值。
(2)谈谈对 ConcurrentSkipListMap 的理解?
对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。
但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 。跳表的本质是同时维护了多个链表,并且链表是分层的。