100道Java并发和多线程基础⾯试题⼤集合

100道Java并发和多线程基础⾯试题⼤集合(含解答),这波⾯
试稳了~
100个问题汇总
1、多线程有什么⽤?
⼀个可能在很多⼈看来很扯淡的⼀个问题:我会⽤多线程就好了,还管它有什么⽤?在我看来,这个回答更扯淡。所谓"知其然知其所以
然",“会⽤"只是"知其然”,“为什么⽤"才是"知其所以然”,只有达到"知其然知其所以然"的程度才可以说是把⼀个知识点运⽤⾃如。OK,下
⾯说说我对这个问题的看法:
(1)发挥多核CPU的优势
随着⼯业的进步,现在的笔记本、台式机乃⾄商⽤的应⽤服务器⾄少也都是双核的,4核、8核甚⾄16核的也都不少见,如果是单线程的程
序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同⼀时间处理器只会处理
⼀段逻辑,只不过线程之间切换得⽐较快,看着像多个线程"同时"运⾏罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑
同时⼯作,多线程,可以真正发挥出多核CPU的优势来,达到充分利⽤CPU的⽬的。
(2)防⽌阻塞
从程序运⾏效率的⾓度来看,单核CPU不但不会发挥出多线程的优势,反⽽会因为在单核CPU上运⾏多线程导致线程上下⽂的切换,⽽降
低程序整体的效率。但是单核CPU我们还是要应⽤多线程,就是为了防⽌阻塞。试想,如果单核CPU使⽤单线程,那么只要这个线程阻塞
了,⽐⽅说远程读取某个数据吧,对端迟迟未返回⼜没有设置超时时间,那么你的整个程序在数据返回回来之前就停⽌运⾏了。多线程可以
防⽌这个问题,多条线程同时运⾏,哪怕⼀条线程的代码执⾏读取数据阻塞,也不会影响其它任务的执⾏。
(3)便于建模
这是另外⼀个没有这么明显的优点了。假设有⼀个⼤的任务A,单线程编程,那么就要考虑很多,建⽴整个程序模型⽐较⿇烦。但是如果把
这个⼤的任务A分解成⼏个⼩任务,任务B、任务C、任务D,分别建⽴程序模型,并通过多线程分别运⾏这⼏个任务,那就简单很多了。
2、创建线程的⽅式
⽐较常见的⼀个问题了,⼀般就是两种:
(1)继承Thread类
(2)实现Runnable接⼝
⾄于哪个好,不⽤说肯定是后者好,因为实现接⼝的⽅式⽐继承类的⽅式更灵活,也能减少程序之间的耦合度,⾯向接⼝编程也是设计模式
6⼤原则的核⼼。
3、start()⽅法和run()⽅法的区别
只有调⽤了start()⽅法,才会表现出多线程的特性,不同线程的run()⽅法⾥⾯的代码交替执⾏。如果只是调⽤run()⽅法,那么代码还是同步
执⾏的,必须等待⼀个线程的run()⽅法⾥⾯的代码全部执⾏完毕之后,另外⼀个线程才可以执⾏其run()⽅法⾥⾯的代码。
4、Runnable接⼝和Callable接⼝的区别
有点深的问题了,也看出⼀个Java程序员学习知识的⼴度。
Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;Callable接⼝中的call()⽅法是有返回
值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
这其实是很有⽤的⼀个特性,因为多线程相⽐单线程更难、更复杂的⼀个重要原因就是因为多线程充满着未知性,某条线程是否执⾏了?某
条线程执⾏了多久?某条线程执⾏的时候我们期望的数据是否已经赋值完毕?⽆法得知,我们能做的只是等待这条多线程的任务执⾏完毕⽽
已。⽽Callable+Future/FutureTask却可以获取多线程运⾏的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,
真的是⾮常有⽤。
5、CyclicBarrier和CountDownLatch的区别
两个看上去有点像的类,都在java.util.concurrent下,都可以⽤来表⽰代码运⾏到某个点上,⼆者的区别在于:
(1)CyclicBarrier的某个线程运⾏到某个点上之后,该线程即停⽌运⾏,直到所有的线程都到达了这个点,所有线程才重新运⾏;
CountDownLatch则不是,某线程运⾏到某个点上之后,只是给某个数值-1⽽已,该线程继续运⾏
(2)CyclicBarrier只能唤起⼀个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重⽤,CountDownLatch不可重⽤,计数值为0该CountDownLatch就不可再⽤了
6、volatile关键字的作⽤
⼀个⾮常重要的问题,是每个学习、应⽤多线程的Java程序员都必须掌握的。理解volatile关键字的作⽤的前提是要理解Java内存模型,这
⾥就不讲Java内存模型了,可以参见第31点,volatile关键字的作⽤主要有两个:
(1)多线程主要围绕可见性和原⼦性两个特性⽽展开,使⽤volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到
volatile变量,⼀定是最新的数据
(2)代码底层执⾏不像我们看到的⾼级语⾔----Java程序这么简单,它的执⾏是Java代码–>字节码–>根据字节码执⾏对应的C/C++代码–

C/C++代码被编译成汇编语⾔–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进⾏重排序,多线程下可能会出现⼀些
意想不到的问题。使⽤volatile则会对禁⽌语义重排序,当然这也⼀定程度上降低了代码执⾏效率
从实践⾓度⽽⾔,volatile的⼀个重要作⽤就是和CAS结合,保证了原⼦性,详细的可以参见java.util.concurrent.atomic包下的类,⽐如
AtomicInteger。
7、什么是线程安全
⼜是⼀个理论的问题,各式各样的答案有很多,我给出⼀个个⼈认为解释地最好的:如果你的代码在多线程下执⾏和在单线程下执⾏永远都
能获得⼀样的结果,那么你的代码就是线程安全的。
这个问题有值得⼀提的地⽅,就是线程安全也是有⼏个级别的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何⼀个线程都改变不了它们的值,要改变除⾮新创建⼀个,因此这些不可变对象不需
要任何同步⼿段就可以直接在多线程环境下使⽤
(2)绝对线程安全
不管运⾏时环境如何,调⽤者都不需要额外的同步措施。要做到这⼀点通常需要付出许多额外的代价,Java中标注⾃⼰是线程安全的类,实
际上绝⼤多数都不是线程安全的,不过绝对线程安全的类,Java中也有,⽐⽅说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove⽅法都是原⼦操作,不会被打断,但也仅限于此,如果
有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机
制。
(4)线程⾮安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程⾮安全的类
8、Java中如何获取到线程dump⽂件
死循环、死锁、阻塞、页⾯打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两
步:
(1)获取到线程的pid,可以通过使⽤jps命令,在Linux环境下还可以使⽤ps -ef | grep java
(2)打印线程堆栈,可以通过使⽤jstack pid命令,在Linux环境下还可以使⽤kill -3 pid
另外提⼀点,Thread类提供了⼀个getStackTrace()⽅法也可以⽤于获取线程堆栈。这是⼀个实例⽅法,因此此⽅法是和具体线程实例绑定
的,每次获取获取到的是具体某个线程当前运⾏的堆栈,
9、⼀个线程如果出现了运⾏时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停⽌执⾏了。另外重要的⼀点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视
器会被⽴即释放
10、如何在两个线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进⾏唤起和等待,⽐⽅说阻塞队列BlockingQueue
就是为线程之间共享数据⽽设计的
11、sleep⽅法和wait⽅法有什么区别
这个问题常问,sleep⽅法和wait⽅法都可以⽤来放弃CPU⼀定的时间,不同点在于如果线程持有某个对象的监视器,sleep⽅法不会放弃这
个对象的监视器,wait⽅法会放弃这个对象的监视器
12、⽣产者消费者模型的作⽤是什么
这个问题很理论,但是很重要:
(1)通过平衡⽣产者的⽣产能⼒和消费者的消费能⼒来提升整个系统的运⾏效率,这是⽣产者消费者模型最重要的作⽤
(2)解耦,这是⽣产者消费者模型附带的作⽤,解耦意味着⽣产者和消费者之间的联系少,联系越少越可以独⾃发展⽽不需要收到相互的
制约
13、ThreadLocal有什么⽤
简单说ThreadLocal就是⼀种以空间换时间的做法,在每个Thread⾥⾯维护了⼀个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据
进⾏隔离,数据不共享,⾃然就没有线程安全⽅⾯的问题了
14、为什么wait()⽅法和notify()/notifyAll()⽅法要在同步块中被调⽤
这是JDK强制的,wait()⽅法和notify()/notifyAll()⽅法在调⽤前都必须先获得对象的锁
15、wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器时有什么区别
wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器的时候的区别在于:
wait()⽅法⽴即释放对象监视器,notify()/notifyAll()⽅法则会等待线程剩余代码执⾏完毕才会放弃对象监视器。
16、为什么要使⽤线程池
避免频繁地创建和销毁线程,达到线程对象的重⽤。另外,使⽤线程池还可以根据项⽬灵活地控制并发的数⽬。
17、怎么检测⼀个线程是否持有对象监视器
我也是在⽹上看到⼀道多线程⾯试题才知道有⽅法可以判断某个线程是否持有对象监视器:Thread类提供了⼀个holdsLock(Object obj)⽅
法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是⼀个static⽅法,这意味着"某条线程"指的是当前线程。
18、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while⼀样的关键字,ReentrantLock是类,这是⼆者的本质区别。既然ReentrantLock是类,那么它就提供
了⽐
synchronized更多更灵活的特性,可以被继承、可以有⽅法、可以有各种各样的类变量,ReentrantLock⽐synchronized的扩展性体现在⼏
点上:
(1)ReentrantLock可以对获取锁的等待时间进⾏设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,⼆者的锁机制其实也是不⼀样的。ReentrantLock底层调⽤的是Unsafe的park⽅法加锁,synchronized操作的应该是对象头中mark
word,这点我不能确定。
19、ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的⼤⼩,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是
ConcurrentHashMap对Hashtable的最⼤优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?
20、ReadWriteLock是什么
⾸先明确⼀下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使⽤ReentrantLock,可能本⾝是为了防⽌线程A在写
数据、线程B在读数据造成的数据不⼀致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但
是还是加锁了,降低了程序的性能。
因为这个,才诞⽣了读写锁ReadWriteLock。ReadWriteLock是⼀个读写锁接⼝,ReentrantReadWriteLock是ReadWriteLock接⼝的⼀个具
体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的
性能。
21、FutureTask是什么
这个其实前⾯有提到过,FutureTask表⽰⼀个异步运算的任务。FutureTask⾥⾯可以传⼊⼀个Callable的具体实现类,可以对这个异步运算
的任务的结果进⾏等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接⼝的实现类,所以FutureTask
也可以放⼊线程池中。
22、Linux环境下如何查找哪个线程使⽤CPU最长
这是⼀个⽐较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
(1)获取项⽬的pid,jps或者ps -ef | grep java,这个前⾯有讲过
(2)top -H -p pid,顺序不能改变
这样就可以打印出当前的项⽬,每条线程占⽤CPU时间的百分⽐。注意这⾥打出的是LWP,也就是操作系统原⽣线程的线程号,我笔记本
⼭没有部署Linux环境下的Java⼯程,因此没有办法截图演⽰,⽹友朋友们如果公司是使⽤Linux环境部署项⽬的话,可以尝试⼀下。
使⽤"top -H -p pid"+"jps pid"可以很容易地找到某条占⽤CPU⾼的线程的线程堆栈,从⽽定位占⽤CPU⾼的原因,⼀般是因为不当的代码操
作导致了死循环。
最后提⼀点,"top -H -p pid"打出来的LWP是⼗进制的,"jps pid"打出来的本地线程号是⼗六进制的,转换⼀下,就能定位到占⽤CPU⾼的线
程的当前线程堆栈了。
23、Java编程写⼀个会导致死锁的程序
第⼀次看到这个题⽬,觉得这是⼀个⾮常好的问题。很多⼈都知道死锁是怎么⼀回事⼉:线程A和线程B相互等待对⽅持有的锁导致程序⽆
限死循环下去。当然也仅限于此了,问⼀下怎么写⼀个死锁的程序就不知道了,这种情况说⽩了就是不懂什么是死锁,懂⼀个理论就完事⼉
了,实践中碰到死锁的问题基本上是看不出来的。
真正理解什么是死锁,这个问题其实不难,⼏个步骤:
(1)两个线程⾥⾯分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
(2)线程1的run()⽅法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的
对象锁。这么做主要是为了防⽌线程1启动⼀下⼦就连续获得了lock1和lock2两个对象的对象锁
(3)线程2的run)(⽅法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2
肯定是要等待线程1释放lock1的对象锁的
这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时⼀个死锁就形成了。代码
就不写了,占的篇幅有点多,Java多线程7:死锁这篇⽂章⾥⾯有,就是上⾯步骤的代码实现。
24、怎么唤醒⼀个阻塞的线程
如果线程是因为调⽤了wait()、sleep()或者join()⽅法⽽导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程
遇到了IO阻塞,⽆能为⼒,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
25、不可变对象对多线程有什么帮助
前⾯有提到过的⼀个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进⾏额外的同步⼿段,提升了代码执⾏效率。
26、什么是多线程的上下⽂切换
多线程的上下⽂切换是指CPU控制权由⼀个已经正在运⾏的线程切换到另外⼀个就绪并等待获取CPU执⾏权的线程的过程。
27、如果你提交任务时,线程池队列已满,这时会发⽣什么
这⾥区分⼀下:

  1. 如果使⽤的是⽆界队列LinkedBlockingQueue,也就是⽆界队列的话,没关系,继续添加任务到阻塞队列中等待执⾏,因为
    LinkedBlockingQueue可以近乎认为是⼀个⽆穷⼤的队列,可以⽆限存放任务
  2. 如果使⽤的是有界队列⽐如ArrayBlockingQueue,任务⾸先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据
    maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使⽤拒绝策略
    RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
    28、Java中⽤到的线程调度算法是什么
    抢占式。⼀个线程⽤完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出⼀个总的优先级并分配下⼀个时间⽚给某个线程执
    ⾏。
    29、Thread.sleep(0)的作⽤是什么
    这个问题和上⾯那个问题是相关的,我就连在⼀起了。由于Java采⽤抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控
    制权的情况,为了让某些优先级⽐较低的线程也能获取到CPU控制权,可以使⽤Thread.sleep(0)⼿动触发⼀次操作系统分配时间⽚的操作,
    这也是平衡CPU控制权的⼀种操作。
    30、什么是⾃旋
    很多synchronized⾥⾯的代码只是⼀些很简单的代码,执⾏时间⾮常快,此时等待的线程都加锁可能是⼀种不太值得的操作,因为线程阻塞
    涉及到⽤户态和内核态切换的问题。既然synchronized⾥⾯的代码执⾏得⾮常快,不妨让等待锁的线程不要被阻塞,⽽是在synchronized的
    边界做忙循环,这就是⾃旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是⼀种更好的策略。
    31、什么是Java内存模型
    Java内存模型定义了⼀种多线程访问Java内存的规范。Java内存模型要完整讲不是这⾥⼏句话能说清楚的,我简单总结⼀下Java内存模型
    的⼏部分内容:
    (1)Java内存模型将内存分为了主内存和⼯作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程⽤到这些
    主内存中的变量的时候,会读⼀次主内存中的变量,并让这些内存在⾃⼰的⼯作内存中有⼀份拷贝,运⾏⾃⼰线程代码的时候,⽤到这些变
    量,操作的都是⾃⼰⼯作内存中的那⼀份。在线程代码执⾏完毕之后,会将最新的值更新到主内存中去
    (2)定义了⼏个原⼦操作,⽤于操作主内存和⼯作内存中的变量
    (3)定义了volatile变量的使⽤规则
    (4)happens-before,即先⾏发⽣原则,定义了操作A必然先⾏发⽣于操作B的⼀些规则,⽐如在同⼀个线程内控制流前⾯的代码⼀定先⾏
    发⽣于控制流后⾯的代码、⼀个释放锁unlock的动作⼀定先⾏发⽣于后⾯对于同⼀个锁进⾏锁定lock的动作等等,只要符合这些规则,则不
    需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码⼀定是线程⾮安全的
    32、什么是CAS
    CAS,全称为Compare and Swap,即⽐较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值
    V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS⼀定要volatile变量配合,这样才能保证每次拿到的变
    量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是⼀个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
    33、什么是乐观锁和悲观锁
    (1)乐观锁:就像它的名字⼀样,对于并发间操作产⽣的线程安全问题持乐观状态,乐观锁认为竞争不总是会发⽣,因此它不需要持有
    锁,将⽐较-替换这两个动作作为⼀个原⼦操作尝试去修改内存中的变量,如果失败则表⽰发⽣冲突,那么就应该有相应的重试逻辑。
    (2)悲观锁:还是像它的名字⼀样,对于并发间操作产⽣的线程安全问题持悲观状态,悲观锁认为竞争总是会发⽣,因此每次对某资源进
    ⾏操作时,都会持有⼀个独占的锁,就像synchronized,不管三七⼆⼗⼀,直接上了锁就操作资源了。
    34、什么是AQS
    简单说⼀下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
    如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核⼼了,ReentrantLock、CountDownLatch、Semaphore等
    等都⽤到了它。AQS实际上以双向队列的形式连接所有的Entry,⽐⽅说ReentrantLock,所有等待的线程都被放在⼀个Entry中并连成双向队
    列,前⾯⼀个线程使⽤ReentrantLock好了,则双向队列实际上的第⼀个Entry开始运⾏。
    AQS定义了对双向队列所有的操作,⽽只开放了tryLock和tryRelease⽅法给开发者使⽤,开发者可以根据⾃⼰的实现重写tryLock和
    tryRelease⽅法,以实现⾃⼰的并发功能。
    35、单例模式的线程安全性
    ⽼⽣常谈的问题了,⾸先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建⼀次出来。单例模式有很多种的写
    法,我总结⼀下:
    (1)饿汉式单例模式的写法:线程安全
    (2)懒汉式单例模式的写法:⾮线程安全
    (3)双检锁单例模式的写法:线程安全
    36、Semaphore有什么作⽤
    Semaphore就是⼀个信号量,它的作⽤是限制某段代码块的并发数。
    Semaphore有⼀个构造函数,可以传⼊⼀个int型整数n,表⽰某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线
    程执⾏完毕这段代码块,下⼀个线程再进⼊。由此可以看出如果Semaphore构造函数中传⼊的int型整数n=1,相当于变成了⼀个
    synchronized了。
    37、Hashtable的size()⽅法中明明只有⼀条语句"return count",为什么还要做同步?
    这是我之前的⼀个困惑,不知道⼤家有没有想过这个问题。某个⽅法中如果有多条语句,并且都在操作同⼀个类变量,那么在多线程环境下
    不加锁,势必会引发线程安全问题,这很好理解,但是size()⽅法明明只有⼀条语句,为什么还要加锁?
    关于这个问题,在慢慢地⼯作、学习中,有了理解,主要原因有两点:
    (1)同⼀时间只能有⼀条线程执⾏固定类的同步⽅法,但是对于类的⾮同步⽅法,可以多条线程同时访问。所以,这样就有问题了,可能
    线程A在执⾏Hashtable的put⽅法添加数据,线程B则可以正常调⽤size()⽅法读取Hashtable中当前元素的个数,那读取到的值可能不是最
    新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size⼀定是不准确的。⽽给
    size()⽅法加了同步之后,意味着线程B调⽤size()⽅法只有在线程A调⽤put⽅法完毕之后才可以调⽤,这样就保证了线程安全性
    (2)CPU执⾏代码,执⾏的不是Java代码,这点很关键,⼀定得记住。Java代码最终是被翻译成机器码执⾏的,机器码才是真正可以和硬
    件电路交互的代码。即使你看到Java代码只有⼀⾏,甚⾄你看到Java代码编译之后⽣成的字节码也只有⼀⾏,也不意味着对于底层来说这句
    语句的操作只有⼀个。⼀句"return count"假设被翻译成了三句汇编语句执⾏,⼀句汇编语句和其机器码做对应,完全可能执⾏完第⼀句,线
    程就切换了。
    38、线程类的构造⽅法、静态块是被哪个线程调⽤的
    这是⼀个⾮常刁钻和狡猾的问题。请记住:线程类的构造⽅法、静态块是被new这个线程类所在的线程所调⽤的,⽽run⽅法⾥⾯的代码才
    是被线程⾃⾝所调⽤的。
    如果说上⾯的说法让你感到困惑,那么我举个例⼦,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
    (1)Thread2的构造⽅法、静态块是main线程调⽤的,Thread2的run()⽅法是Thread2⾃⼰调⽤的
    (2)Thread1的构造⽅法、静态块是Thread2调⽤的,Thread1的run()⽅法是Thread1⾃⼰调⽤的
    39、同步⽅法和同步块,哪个是更好的选择
    同步块,这意味着同步块之外的代码是异步执⾏的,这⽐同步整个⽅法更提升代码的效率。请知道⼀条原则:同步的范围越⼩越好。
    借着这⼀条,我额外提⼀点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着⼀种叫做锁粗化的优化⽅法,这种⽅法就是把同步
    范围变⼤。这是有⽤的,⽐⽅说StringBuffer,它是⼀个线程安全的类,⾃然最常⽤的append()⽅法是⼀个同步⽅法,我们写代码的时候会反
    复append字符串,这意味着要进⾏反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和⽤户态
    之间进⾏切换,因此Java虚拟机会将多次append⽅法调⽤的代码进⾏⼀个锁粗化的操作,将多次的append的操作扩展到append⽅法的头
    尾,变成⼀个⼤的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执⾏的效率。
    40、⾼并发、任务执⾏时间短的业务怎样使⽤线程池?并发不⾼、任务执⾏时间长的业务怎样使⽤线程
    池?并发⾼、业务执⾏时间长的业务怎样使⽤线程池?
    这是我在并发编程⽹上看到的⼀个问题,把这个问题放在最后⼀个,希望每个⼈都能看到并且思考⼀下,因为这个问题⾮常好、⾮常实际、
    ⾮常专业。关于这个问题,个⼈看法是:
    (1)⾼并发、任务执⾏时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下⽂的切换
    (2)并发不⾼、任务执⾏时间长的业务要区分开看:
    a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占⽤CPU,所以不要让所有的CPU闲下来,可以加⼤线程
    池中的线程数⽬,让CPU处理更多的业务
    b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)⼀样吧,线程池中的线程数设置得少⼀些,减
    少线程上下⽂的切换
    (3)并发⾼、业务执⾏时间长,解决这种类型任务的关键不在于线程池⽽在于整体架构的设计,看看这些业务⾥⾯某些数据是否能做缓存
    是第⼀步,增加服务器是第⼆步,⾄于线程池的设置,设置参考(2)。
    最后,业务执⾏时间长的问题,也可能需要分析⼀下,看看能不能使⽤中间件对任务进⾏拆分和解耦。
    41、为什么使⽤Executor框架?
    每次执⾏任务创建线程 new Thread()⽐较消耗性能,创建⼀个线程是⽐较耗时、耗资源的。
    调⽤ new Thread()创建的线程缺乏管理,被称为野线程,⽽且可以⽆限制的创建,线程之间的相互竞争会导致过多占⽤系统资源⽽导致系统
    瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
    接使⽤new Thread() 启动的线程不利于扩展,⽐如定时执⾏、定期执⾏、定时定期执⾏、线程中断等都不便实现。
    42、在Java中Executor和Executors的区别?
    Executors ⼯具类的不同⽅法按照我们的需求创建了不同的线程池,来满⾜业务的需求。
    Executor 接⼝对象能执⾏我们的线程任务。ExecutorService接⼝继承了Executor接⼝并进⾏了扩展,提供了更多的⽅法我们能获得任务执
    ⾏的状态并且可以获取任务的返回值。
    使⽤ThreadPoolExecutor 可以创建⾃定义线程池。Future 表⽰异步计算的结果,他提供了检查计算是否完成的⽅法,以等待计算的完成,
    并可以使⽤get()⽅法获取计算的结果。
    43、什么是原⼦操作?在Java Concurrency API中有哪些原⼦类(atomic classes)?
    原⼦操作(atomic operation)意为”不可被中断的⼀个或⼀系列操作” 。处理器使⽤基于对缓存加锁或总线加锁的⽅式来实现多处理器之间的
    原⼦操作。
    在Java中可以通过锁和循环CAS的⽅式来实现原⼦操作。CAS操作——Compare & Set,或是 Compare & Swap,现在⼏乎所有的CPU指
    令都⽀持CAS的原⼦操作。
    原⼦操作是指⼀个不受其他操作影响的操作任务单元。原⼦操作是在多线程环境下避免数据不⼀致必须的⼿段。
    int++并不是⼀个原⼦操作,所以当⼀个线程读取它的值并加1时,另外⼀个线程有可能会读到之前的值,这就会引发错误。
    为了解决这个问题,必须保证增加操作是原⼦的,在JDK1.5之前我们可以使⽤同步技术来做到这⼀点。到
    JDK1.5,java.util.concurrent.atomic包提供了int和long类型的原⼦包装类,它们可以⾃动的保证对于他们的操作是原⼦的并且不需要使⽤同
    步。
    java.util.concurrent这个包⾥⾯提供了⼀组原⼦类。其基本的特性就是在多线程环境下,当有多个线程同时执⾏这些类的实例包含的⽅法
    时,具有排他性。
    即当某个线程进⼊⽅法,执⾏其中的指令时,不会被其他线程打断,⽽别的线程就像⾃旋锁⼀样,⼀直等到该⽅法执⾏完成,才由JVM从等
    待队列中选择⼀个另⼀个线程进⼊,这只是⼀种逻辑上的理解。
    原⼦类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
    原⼦数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
    原⼦属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
    解决ABA问题的原⼦类:AtomicMarkableReference(通过引⼊⼀个boolean来反映中间有没有变过),AtomicStampedReference(通过引
    ⼊⼀个int来累加来反映中间有没有变过)
    44、Java Concurrency API中的Lock接⼝(Lock interface)是什么?对⽐同步它有什么优势?
    Lock接⼝⽐同步⽅法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以⽀持多个相关类的
    条件对象。
    它的优势有:
    可以使锁更公平
    可以让线程尝试获取锁,并在⽆法获取锁的时候⽴即返回或者等待⼀段时间
    可以在不同的范围,以不同的顺序获取和释放锁
    整体上来说Lock是synchronized的扩展版,Lock提供了⽆条件的、可轮询的(tryLock⽅法)、定时的(tryLock带参⽅法)、可中断的
    (lockInterruptibly)、可多条件队列的(newCondition⽅法)锁操作。
    另外Lock的实现类基本都⽀持⾮公平锁(默认)和公平锁,synchronized只⽀持⾮公平锁,当然,在⼤部分情况下,⾮公平锁是⾼效的选择。
    45、什么是Executors框架?
    Executor框架是⼀个根据⼀组执⾏策略调⽤,调度,执⾏和控制的异步任务的框架。
    ⽆限制的创建线程会引起应⽤程序内存溢出。所以创建⼀个线程池是个更好的的解决⽅案,因为可以限制线程的数量并且可以回收再利⽤这
    些线程。利⽤Executors框架可以⾮常⽅便的创建⼀个线程池。
    46、什么是阻塞队列?阻塞队列的实现原理是什么?如何使⽤阻塞队列来实现⽣产者-消费者模型?
    阻塞队列(BlockingQueue)是⼀个⽀持两个附加操作的队列。
    这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为⾮空。当队列满时,存储元素的线程会等待队列可⽤。
    阻塞队列常⽤于⽣产者和消费者的场景,⽣产者是往队列⾥添加元素的线程,消费者是从队列⾥拿元素的线程。阻塞队列就是⽣产者存放元
    素的容器,⽽消费者也只从容器⾥拿元素。
    JDK7提供了7个阻塞队列。分别是:
    ArrayBlockingQueue :⼀个由数组结构组成的有界阻塞队列。
    LinkedBlockingQueue :⼀个由链表结构组成的有界阻塞队列。
    PriorityBlockingQueue :⼀个⽀持优先级排序的⽆界阻塞队列。
    DelayQueue:⼀个使⽤优先级队列实现的⽆界阻塞队列。
    SynchronousQueue:⼀个不存储元素的阻塞队列。
    LinkedTransferQueue:⼀个由链表结构组成的⽆界阻塞队列。
    LinkedBlockingDeque:⼀个由链表结构组成的双向阻塞队列。
    Java 5之前实现同步存取时,可以使⽤普通的⼀个集合,然后在使⽤线程的协作和线程同步可以实现⽣产者,消费者模式,主要的技术就是
    ⽤好,wait ,notify,notifyAll,sychronized这些关键字。⽽在java 5之后,可以使⽤阻塞队列来实现,此⽅式⼤⼤简少了代码量,使得多线程编
    程更加容易,安全⽅⾯也有保障。
    BlockingQueue接⼝是Queue的⼦接⼝,它的主要⽤途并不是作为容器,⽽是作为线程同步的的⼯具,因此他具有⼀个很明显的特性,当⽣
    产者线程试图向BlockingQueue放⼊元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出⼀个元素时,如果队列为空,则该
    线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放⼊元素,取出元素,它可以很好的控制线程
    之间的通信。
    阻塞队列使⽤最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放⼊队列,然后解析线程不断从队列取数据解
    析。
    47、什么是Callable和Future?
    Callable接⼝类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且⽆法抛出返回结果的异常,⽽Callable功能更强
    ⼤⼀些,被线程执⾏后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执⾏任务的返回值。可以认为是带有
    回调的Runnable。
    Future接⼝表⽰异步任务,是还没有完成的任务给出的未来结果。所以说Callable⽤于产⽣结果,Future⽤于获取结果。
    48、什么是FutureTask?使⽤ExecutorService启动任务。
    在Java并发程序中FutureTask表⽰⼀个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等⽅法。只有当运算
    完成的时候结果才能取回,如果运算尚未完成get⽅法将会阻塞。
    ⼀个FutureTask对象可以对调⽤了Callable和Runnable的对象进⾏包装,由于FutureTask也是调⽤了Runnable接⼝所以它可以提交给
    Executor来执⾏。
    49、什么是并发容器的实现?
    何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调⽤同步容器的⽅法,它们将会串⾏执⾏。⽐如
    Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等⽅法返回的容器。
    可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的⽅式就是将它们的状态封装起来,并在需
    要同步的⽅法上加上关键字synchronized。
    并发容器使⽤了与同步容器完全不同的加锁策略来提供更⾼的并发性和伸缩性,例如在ConcurrentHashMap中采⽤了⼀种粒度更细的加锁机
    制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执⾏读操作的线程和写操作的线程也可以并发的访问
    map,同时允许⼀定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更⾼的吞吐量。
    50、多线程同步和互斥有⼏种实现⽅法,都是什么?
    线程同步是指线程之间所具有的⼀种制约关系,⼀个线程的执⾏依赖另⼀个线程的消息,当它没有得到另⼀个线程的消息时应等待,直到消
    息到达时才被唤醒。
    线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若⼲个线程都要使⽤某⼀共享资源时,任何时刻最多只允许⼀
    个线程去使⽤,其它要使⽤该资源的线程必须等待,直到占⽤资源者释放该资源。线程互斥可以看成是⼀种特殊的线程同步。
    线程间的同步⽅法⼤体可分为两类:⽤户模式和内核模式。顾名思义,内核模式就是指利⽤系统内核对象的单⼀性来进⾏同步,使⽤时需要
    切换内核态与⽤户态,⽽⽤户模式就是不需要切换到内核态,只在⽤户态完成操作。
    ⽤户模式下的⽅法有:原⼦操作(例如⼀个单⼀的全局变量),临界区。内核模式下的⽅法有:事件,信号量,互斥量。
    51、什么是竞争条件?你怎样发现和解决竞争?
    当多个进程都企图对共享数据进⾏某种处理,⽽最后的结果⼜取决于进程运⾏的顺序时,则我们认为这发⽣了竞争条件(race condition)。
    52、为什么我们调⽤start()⽅法时会执⾏run()⽅法,为什么我们不能直接调⽤run()⽅法?
    当你调⽤start()⽅法时你将创建新的线程,并且执⾏在run()⽅法⾥的代码。
    但是如果你直接调⽤run()⽅法,它不会创建新的线程也不会执⾏调⽤线程的代码,只会把run⽅法当作普通⽅法去执⾏。
    53、Java中你怎样唤醒⼀个阻塞的线程?
    在Java发展史上曾经使⽤suspend()、resume()⽅法对于线程进⾏阻塞唤醒,但随之出现很多问题,⽐较典型的还是死锁问题。
    解决⽅案可以使⽤以对象为⽬标的阻塞,即利⽤Object类的wait()和notify()⽅法实现线程阻塞。
    ⾸先,wait、notify⽅法是针对对象的,调⽤任意对象的wait()⽅法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调⽤任意
    对象的notify()⽅法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执⾏;
    其次,wait、notify⽅法必须在synchronized块或⽅法中被调⽤,并且要保证同步块或⽅法的锁对象与调⽤wait、notify⽅法的对象是同⼀
    个,如此⼀来在调⽤wait之前当前线程就已经成功获取某对象的锁,执⾏wait阻塞后当前线程就将之前获取的对象锁释放。
    54、在Java中CycliBarriar和CountdownLatch有什么区别?
    CyclicBarrier可以重复使⽤,⽽CountdownLatch不能重复使⽤。
    Java的concurrent包⾥⾯的CountDownLatch其实可以把它看作⼀个计数器,只不过这个计数器的操作是原⼦操作,同时只能有⼀个线程去
    操作这个计数器,也就是同时只能有⼀个线程去减这个计数器⾥⾯的值。
    你可以向CountDownLatch对象设置⼀个初始的数字作为计数值,任何调⽤这个对象上的await()⽅法都会阻塞,直到这个计数器的计数值被
    其他的线程减为0为⽌。
    所以在当前计数到达零之前,await ⽅法会⼀直受阻塞。之后,会释放所有等待的线程,await的所有后续调⽤都将⽴即返回。这种现象只出
    现⼀次——计数⽆法被重置。如果需要重置计数,请考虑使⽤ CyclicBarrier。
    CountDownLatch的⼀个⾮常典型的应⽤场景是:有⼀个任务想要往下执⾏,但必须要等到其他的任务执⾏完毕后才可以继续往下执⾏。假
    如我们这个想要继续往下执⾏的任务调⽤⼀个CountDownLatch对象的await()⽅法,其他的任务执⾏完⾃⼰的任务后调⽤同⼀个
    CountDownLatch对象上的countDown()⽅法,这个调⽤await()⽅法的任务将⼀直阻塞等待,直到这个CountDownLatch对象的计数值减到0
    为⽌。
    CyclicBarrier⼀个同步辅助类,它允许⼀组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及⼀组固定⼤⼩的线程的
    程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有⽤。因为该 barrier 在释放等待线程后可以重⽤,所以称它为循环的
    barrier。
    55、什么是不可变对象,它对写并发应⽤有什么帮助?
    不可变对象(Immutable Objects)即对象⼀旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable
    Objects)。
    不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和
    BigDecimal等。
    不可变对象天⽣是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态⽆法修改,这些常量永远不会变。
    不可变对象永远是线程安全的。只有满⾜如下状态,⼀个对象才是不可变的;它的状态不能在创建后再被修改;所有域都是final类型;并
    且,它被正确创建(创建期间没有发⽣this引⽤的逸出)。
    56、什么是多线程中的上下⽂切换?
    在上下⽂切换过程中,CPU会停⽌处理当前运⾏的程序,并保存当前程序运⾏的具体位置以便之后继续运⾏。从这个⾓度来看,上下⽂切换
    有点像我们同时阅读⼏本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
    在程序中,上下⽂切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会
    ⼀直保存到CPU的内存中,直到他们被再次使⽤。
    上下⽂切换是存储和恢复CPU状态的过程,它使得线程执⾏能够从中断点恢复执⾏。上下⽂切换是多任务操作系统和多线程环境的基本特
    征。
    57、Java中⽤到的线程调度算法是什么?
    计算机通常只有⼀个CPU,在任意时刻只能执⾏⼀条机器指令,每个线程只有获得CPU的使⽤权才能执⾏指令.所谓多线程的并发运⾏,其实是
    指从宏观上看,各个线程轮流获得CPU的使⽤权,分别执⾏各⾃的任务。
    在运⾏池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的⼀项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程
    分配CPU的使⽤权.
    有两种调度模型:分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得cpu的使⽤权,并且平均分配每个线程占⽤的
    CPU的时间⽚这个也⽐较好理解。
    java虚拟机采⽤抢占式调度模型,是指优先让可运⾏池中优先级⾼的线程占⽤CPU,如果可运⾏池中的线程优先级相同,那么就随机选择⼀
    个线程,使其占⽤CPU。处于运⾏状态的线程会⼀直运⾏,直⾄它不得不放弃CPU。
    58、什么是线程组,为什么在Java中不推荐使⽤?
    线程组和线程池是两个不同的概念,他们的作⽤完全不同,前者是为了⽅便线程的管理,后者是为了管理线程的⽣命周期,复⽤线程,减少
    创建销毁线程的开销。
    59、为什么使⽤Executor框架⽐使⽤应⽤创建和管理线程好?
    为什么要使⽤Executor线程池框架?
    1、每次执⾏任务创建线程 new Thread()⽐较消耗性能,创建⼀个线程是⽐较耗时、耗资源的。
    2、调⽤ new Thread()创建的线程缺乏管理,被称为野线程,⽽且可以⽆限制的创建,线程之间的相互竞争会导致过多占⽤系统资源⽽导致
    系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
    3、直接使⽤new Thread() 启动的线程不利于扩展,⽐如定时执⾏、定期执⾏、定时定期执⾏、线程中断等都不便实现。
    使⽤Executor线程池框架的优点:
    1、能复⽤已存在并空闲的线程从⽽减少线程对象的创建从⽽减少了消亡线程的开销。
    2、可有效控制最⼤并发线程数,提⾼系统资源使⽤率,同时避免过多资源竞争。
    3、框架中已经有定时、定期、单线程、并发数控制等功能。综上所述使⽤线程池框架Executor能更好的管理线程、提供系统资源使⽤率。
    60、java中有⼏种⽅法可以实现⼀个线程?
  3. 继承 Thread 类
  4. 实现 Runnable 接⼝
  5. Callable接⼝和FutureTask类,需要实现的是 call() ⽅法
  6. 线程池创建线程。
    61、如何停⽌⼀个正在运⾏的线程?
  7. 使⽤共享变量的⽅式
    在这种⽅式中,之所以引⼊共享变量,是因为该变量可以被多个执⾏相同任务的线程⽤来作为是否中断的信号,通知中断线程的执⾏。
  8. 使⽤interrupt⽅法终⽌线程
    如果⼀个线程由于等待某些事件的发⽣⽽被阻塞,⼜该怎样停⽌该线程呢?这种情况经常会发⽣,⽐如当⼀个线程由于需要等候键盘输⼊⽽
    被阻塞,或者调⽤Thread.join()⽅法,或者Thread.sleep()⽅法,在⽹络中调⽤ServerSocket.accept()⽅法,或者调⽤了
    DatagramSocket.receive()⽅法时,都有可能导致线程阻塞,使线程处于处于不可运⾏状态时,即使主程序中将该线程的共享变量设置为
    true,但该线程此时根本⽆法检查循环标志,当然也就⽆法⽴即中断。
    这⾥我们给出的建议是,不要使⽤stop()⽅法,⽽是使⽤Thread提供的interrupt()⽅法,因为该⽅法虽然不会中断⼀个正在运⾏的线程,但是
    它可以使⼀个被阻塞的线程抛出⼀个中断异常,从⽽使线程提前结束阻塞状态,退出堵塞代码。
    62、notify()和notifyAll()有什么区别?
    当⼀个线程进⼊wait之后,就必须等其他线程notify/notifyall,使⽤notifyall,可以唤醒所有处于wait状态的线程,使其重新进⼊锁的争夺队列
    中,⽽notify只能唤醒⼀个。
    如果没把握,建议notifyAll,防⽌notigy因为信号丢失⽽造成程序异常。
    63、什么是Daemon线程?它有什么意义?
    所谓后台(daemon)线程,是指在程序运⾏的时候在后台提供⼀种通⽤服务的线程,并且这个线程并不属于程序中不可或缺的部分。
    因此,当所有的⾮后台线程结束时,程序也就终⽌了,同时会杀死进程中的所有后台线程。反过来说,只要有任何⾮后台线程还在运⾏,程
    序就不会终⽌。
    必须在线程启动之前调⽤setDaemon()⽅法,才能把它设置为后台线程。注意:后台进程在不执⾏finally⼦句的情况下就会终⽌其run()⽅
    法。
    ⽐如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。
    64、java如何实现多线程之间的通讯和协作?
    中断和共享变量
    65、什么是可重⼊锁(ReentrantLock)?
    举例来说明锁的可重⼊性
    public class UnReentrant{
    Lock lock = new Lock();
    public void outer(){
    lock.lock();
    inner();
    lock.unlock();
    }
    public void inner(){
    lock.lock();
    //do something
    lock.unlock();
    }
    }
    outer中调⽤了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调⽤outer的线程已经获取了lock锁,但是不能在inner中重复利
    ⽤已经获取的锁资源,这种锁即称之为不可重⼊可重⼊就意味着:线程可以进⼊任何⼀个它已经拥有的锁所同步着的代码块。
    synchronized、ReentrantLock都是可重⼊的锁,可重⼊锁相对来说简化了并发编程的开发。
    66、当⼀个线程进⼊某个对象的⼀个synchronized的实例⽅法后,其它线程是否可进⼊此对象的其它⽅
    法?
    如果其他⽅法没有synchronized的话,其他线程是可以进⼊的。
    所以要开放⼀个线程安全的对象时,得保证每个⽅法都是线程安全的。
    67、乐观锁和悲观锁的理解及如何实现,有哪些实现⽅式?
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会阻
    塞直到它拿到锁。
    传统的关系型数据库⾥边就⽤到了很多这种锁机制,⽐如⾏锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再⽐如Java⾥⾯的同步
    原语synchronized关键字的实现也是悲观锁。
    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别⼈不会修改,所以不会上锁,但是在更新的时候会判断⼀下在此期间别⼈有
    没有去更新这个数据,可以使⽤版本号等机制。
    乐观锁适⽤于多读的应⽤类型,这样可以提⾼吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
    在Java中java.util.concurrent.atomic包下⾯的原⼦变量类就是使⽤了乐观锁的⼀种实现⽅式CAS实现的。
    乐观锁的实现⽅式:
    1、使⽤版本标识来确定读到的数据与提交时的数据是否⼀致。提交后修改版本标识,不⼀致时可以采取丢弃和再次尝试的策略。
    2、java中的Compare and Swap即CAS ,当多个线程尝试使⽤CAS同时更新同⼀个变量时,只有其中⼀个线程能更新变量的值,⽽其它线
    程都失败,失败的线程并不会被挂起,⽽是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存
    位置(V)、进⾏⽐较的预期原值(A)和拟写⼊的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会⾃动将该位置值更新为
    新值B。否则处理器不做任何操作。
    CAS缺点:
  9. ABA问题:⽐如说⼀个线程one从内存位置V中取出A,这时候另⼀个线程two也从内存中取出A,并且two进⾏了⼀些操作变成了B,然
    后two⼜将V位置的数据变成A,这时候线程one进⾏CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成
    功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包⾥提供了⼀个类AtomicStampedReference来解决ABA问题。
  10. 循环时间长开销⼤:对于资源竞争严重(线程冲突严重)的情况,CAS⾃旋的概率会⽐较⼤,从⽽浪费更多的CPU资源,效率低于
    synchronized。
  11. 只能保证⼀个共享变量的原⼦操作:当对⼀个共享变量执⾏操作时,我们可以使⽤循环CAS的⽅式来保证原⼦操作,但是对多个共享
    变量操作时,循环CAS就⽆法保证操作的原⼦性,这个时候就可以⽤锁。
    68、SynchronizedMap和ConcurrentHashMap有什么区别?
    SynchronizedMap⼀次锁住整张表来保证线程安全,所以每次只能有⼀个线程来访为map。ConcurrentHashMap使⽤分段锁来保证在多线程
    下的性能。
    ConcurrentHashMap中则是⼀次锁住⼀个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常⽤操作只锁当前需
    要⽤到的桶。这样,原来只能⼀个线程进⼊,现在却能同时有16个写线程执⾏,并发性能的提升是显⽽易见的。
    另外ConcurrentHashMap使⽤了⼀种不同的迭代⽅式。在这种迭代⽅式中,当iterator被创建后集合再发⽣改变就不再是抛出
    ConcurrentModificationException,取⽽代之的是在改变时new新的数据从⽽不影响原有的数据,iterator完成后再将头指针替换为新的数据
    ,这样iterator线程可以使⽤原来⽼的数据,⽽写线程也可以并发的完成改变。
    69、CopyOnWriteArrayList可以⽤于什么应⽤场景?
    CopyOnWriteArrayList(免锁容器)的好处之⼀是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在
    CopyOnWriteArrayList中,写⼊将导致创建整个底层数组的副本,⽽源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全
    地执⾏。
    1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容⽐较多的情况下,可能导致young gc或者full gc;
    2、不能⽤于实时读的场景,像拷贝数组、新增元素都需要时间,所以调⽤⼀个set操作后,读取到数据可能还是旧的,虽然
    CopyOnWriteArrayList 能做到最终⼀致性,但是还是没法满⾜实时性要求;
    CopyOnWriteArrayList透露的思想
    1、读写分离,读和写分开
    2、最终⼀致性
    3、使⽤另外开辟空间的思路,来解决并发冲突
    70、什么叫线程安全?servlet是线程安全吗?
    线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调⽤时,能够正确地处理多个线程之间的共享变量,使程序功能正确完
    成。
    Servlet不是线程安全的,servlet是单实例多线程的,当多个线程同时访问同⼀个⽅法,是不能保证共享变量的线程安全性的。
    Struts2的action是多实例多线程的,是线程安全的,每个请求过来都会new⼀个新的action分配给这个请求,请求完成后销毁。
    SpringMVC的Controller是线程安全的吗?不是的,和Servlet类似的处理流程。
    Struts2好处是不⽤考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不⽤处理太多的gc,可以使⽤
    ThreadLocal来处理多线程的问题。
    71、volatile有什么⽤?能否⽤⼀句话说明下volatile的应⽤场景?
    volatile保证内存可见性和禁⽌指令重排。
    volatile⽤于多线程环境下的单次操作(单次读或者单次写)。
    72、为什么代码会重排序?
    在执⾏程序时,为了提供性能,处理器和编译器常常会对指令进⾏重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满
    ⾜以下两个条件:
    在单线程环境下不能改变程序运⾏的结果;存在数据依赖关系的不允许重排序需要注意的是:重排序不会影响单线程环境的执⾏结果,但是
    会破坏多线程的执⾏语义。
    73、在java中wait和sleep⽅法的不同?
    最⼤的不同是在等待时wait会释放锁,⽽sleep⼀直持有锁。Wait通常被⽤于线程间交互,sleep通常被⽤于暂停执⾏。
    直接了解的深⼊⼀点吧,在Java中线程的状态⼀共被分成6种:
    (1)初始态:NEW
    创建⼀个Thread对象,但还未调⽤start()启动线程时,线程处于初始态。
    (2)运⾏态:RUNNABLE
    在Java中,运⾏态包括就绪态和运⾏态。就绪态该状态下的线程已经获得执⾏所需的所有资源,只要CPU分配执⾏权就能运⾏。所有就绪
    态的线程存放在就绪队列中。
    运⾏态获得CPU执⾏权,正在执⾏的线程。由于⼀个CPU同⼀时刻只能执⾏⼀条线程,因此每个CPU每个时刻只有⼀条运⾏态的线程。
    (3)阻塞态
    当⼀条正在执⾏的线程请求某⼀资源失败时,就会进⼊阻塞态。⽽在Java中,阻塞态专指请求锁失败时进⼊的状态。由⼀个阻塞队列存放所
    有阻塞态的线程。处于阻塞态的线程会不断请求资源,⼀旦请求成功,就会进⼊就绪队列,等待执⾏。PS:锁、IO、Socket等都资源。
    (4)等待态
    当前线程中调⽤wait、join、park函数时,当前线程就会进⼊等待态。也有⼀个等待队列存放所有等待态的线程。线程处于等待态表⽰它需
    要等待其他线程的指⽰才能继续运⾏。进⼊等待态的线程会释放CPU执⾏权,并释放资源(如:锁)
    (5)超时等待态
    当运⾏中的线程调⽤sleep(time)、wait、join、parkNanos、parkUntil时,就会进⼊该状态;它和等待态⼀样,并不是因为请求不到资源,⽽
    是主动进⼊,并且进⼊后需要其他线程唤醒;进⼊该状态后释放CPU执⾏权和占有的资源。与等待态的区别:到了超时时间后⾃动进⼊阻
    塞队列,开始竞争锁。
    (6)终⽌态
    线程执⾏结束后的状态。
    注意:
    wait()⽅法会释放CPU执⾏权和占有的锁。
    sleep(long)⽅法仅释放CPU使⽤权,锁仍然占⽤;线程被放⼊超时等待队列,与yield相⽐,它会使线程较长时间得不到运⾏。
    yield()⽅法仅释放CPU执⾏权,锁仍然占⽤,线程会被放⼊就绪队列,会在短时间内再次执⾏。
    wait和notify必须配套使⽤,即必须使⽤同⼀把锁调⽤;
    wait和notify必须放在⼀个同步块中调⽤wait和notify的对象必须是他们所处同步块的锁对象。
    74、为什么wait和notify⽅法要在同步块中调⽤?
    Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有⼀个原因是为了避免wait和notify之间
    产⽣竞态条件。
    75、为什么你应该在循环中检查等待条件?
    处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满⾜结束条件的情况下退出。
    76、Java中的同步集合与并发集合有什么区别?
    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更⾼。在Java1.5之前程序员们只有同步集
    合来⽤且在多线程并发的时候会导致争⽤,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还⽤锁
    分离和内部分区等现代技术提⾼了可扩展性。
    77、什么是线程池?为什么要使⽤它?
    创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,⽽且⼀个进程能创建的线程数有限。
    为了避免这些问题,在程序启动的时候就创建若⼲线程来响应处理,它们被称为线程池,⾥⾯的线程叫⼯作线程。从JDK1.5开始,Java
    API提供了Executor框架让你可以创建不同的线程池。
    78、怎么检测⼀个线程是否拥有锁?
    在java.lang.Thread中有⼀个⽅法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
    79、你如何在Java中获取线程堆栈?
    kill -3 [java pid]不会在当前终端输出,它会输出到代码执⾏的或指定的地⽅去。⽐如,kill -3 tomcat pid, 输出堆栈到log⽬录下。Jstack [java
    pid]这个⽐较简单,在当前终端显⽰,也可以重定向到指定⽂件中。-JvisualVM:Thread Dump不做说明,打开JvisualVM后,都是界⾯操
    作,过程还是很简单的。
    80、JVM中哪个参数是⽤来控制线程的栈堆栈⼩的?
    -Xss 每个线程的栈⼤⼩
    81、Thread类中的yield⽅法有什么作⽤?
    使当前线程从执⾏状态(运⾏状态)变为可执⾏态(就绪状态)。
    当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执⾏状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。
    82、Java中ConcurrentHashMap的并发度是什么?
    ConcurrentHashMap把实际map划分成若⼲部分来实现它的可扩展性和线程安全。这种划分是使⽤并发度获得的,它是
    ConcurrentHashMap类构造函数的⼀个可选参数,默认值为16,这样在多线程情况下就能避免争⽤。
    在JDK8后,它摒弃了Segment(锁段)的概念,⽽是启⽤了⼀种全新的⽅式实现,利⽤CAS算法。同时加⼊了更多的辅助变量来提⾼并发
    度,具体内容还是查看源码吧。
    83、Java中Semaphore是什么?
    Java中的Semaphore是⼀种新的同步类,它是⼀个计数信号。从概念上讲,从概念上讲,信号量维护了⼀个许可集合。如有必要,在许可
    可⽤前会阻塞每⼀个 acquire(),然后再获取该许可。
    每个 release()添加⼀个许可,从⽽可能释放⼀个正在阻塞的获取者。但是,不使⽤实际的许可对象,Semaphore只对可⽤许可的号码进⾏
    计数,并采取相应的⾏动。信号量常常⽤于多线程的代码中,⽐如数据库连接池。
    84、Java线程池中submit() 和 execute()⽅法有什么区别?
    两个⽅法都可以向线程池提交任务,execute()⽅法的返回类型是void,它定义在Executor接⼝中。
    ⽽submit()⽅法可以返回持有计算结果的Future对象,它定义在ExecutorService接⼝中,它扩展了Executor接⼝,其它线程池类像
    ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些⽅法。
    85、什么是阻塞式⽅法?
    阻塞式⽅法是指程序会⼀直等待该⽅法完成期间不做其他事情,ServerSocket的accept()⽅法就是⼀直等待客户端连接。这⾥的阻塞是指调
    ⽤结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和⾮阻塞式⽅法在任务完成前就返回。
    86、Java中的ReadWriteLock是什么?
    读写锁是⽤来提升并发程序性能的锁分离技术的成果。
    87、volatile 变量和 atomic 变量有什么不同?
    Volatile变量可以确保先⾏关系,即写操作会发⽣在后续的读操作之前, 但它并不能保证原⼦性。例如⽤volatile修饰count变量那么 count++
    操作就不是原⼦性的。
    ⽽AtomicInteger类提供的atomic⽅法可以让这种操作具有原⼦性如getAndIncrement()⽅法会原⼦性的进⾏增量操作把当前值加⼀,其它数据
    类型和引⽤变量也可以进⾏相似操作。
    88、可以直接调⽤Thread类的run ()⽅法么?
    当然可以。但是如果我们调⽤了Thread的run()⽅法,它的⾏为就会和普通的⽅法⼀样,会在当前线程中执⾏。为了在新的线程中执⾏我们的
    代码,必须使⽤Thread.start()⽅法。
    89、如何让正在运⾏的线程暂停⼀段时间?
    我们可以使⽤Thread类的Sleep()⽅法让线程暂停⼀段时间。需要注意的是,这并不会让线程终⽌,⼀旦从休眠中唤醒线程,线程的状态将
    会被改变为Runnable,并且根据线程调度,它将得到执⾏。
    90、你对线程优先级的理解是什么?
    每⼀个线程都是有优先级的,⼀般来说,⾼优先级的线程在运⾏时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关
    的(OS dependent)。
    我们可以定义线程的优先级,但是这并不能保证⾼优先级的线程会在低优先级的线程前执⾏。线程优先级是⼀个int变量(从1-10),1代表最低
    优先级,10代表最⾼优先级。
    java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如⾮特别需要,⼀般⽆需设置线程优先级。
    91、什么是线程调度器(Thread Scheduler)和时间分⽚(Time Slicing )?
    线程调度器是⼀个操作系统服务,它负责为Runnable状态的线程分配CPU时间。⼀旦我们创建⼀个线程并启动它,它的执⾏便依赖于线程
    调度器的实现。同上⼀个问题,线程调度并不受到Java虚拟机控制,所以由应⽤程序来控制它是更好的选择(也就是说不要让你的程序依赖
    于线程的优先级)。
    时间分⽚是指将可⽤的CPU时间分配给可⽤的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
    92、你如何确保main()⽅法所在的线程是Java 程序最后结束的线程?
    我们可以使⽤Thread类的join()⽅法来确保所有程序创建的线程在main()⽅法退出前结束。
    93、线程之间是如何通信的?
    当线程间是可以共享资源时,线程间通信是协调它们的重要的⼿段。Object类中wait()\notify()\notifyAll()⽅法可以⽤于线程间通信关于资源的
    锁的状态。
    94、为什么线程通信的⽅法wait(), notify()和notifyAll()被定义在Object 类⾥?
    Java的每个对象中都有⼀个锁(monitor,也可以成为监视器) 并且wait(),notify()等⽅法⽤于等待对象的锁或者通知其他线程对象的监视器可
    ⽤。
    在Java的线程中并没有可供任何对象使⽤的锁和同步器。这就是为什么这些⽅法是Object类的⼀部分,这样Java的每⼀个类都有⽤于线程间
    通信的基本⽅法。
    95、为什么wait(), notify()和notifyAll ()必须在同步⽅法或者同步块中被调⽤?
    当⼀个线程需要调⽤对象的wait()⽅法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进⼊等待状态直到其他线程调
    ⽤这个对象上的notify()⽅法。
    同样的,当⼀个线程需要调⽤对象的notify()⽅法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这
    些⽅法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步⽅法或者同步块中被调⽤。
    96、为什么Thread类的sleep()和yield ()⽅法是静态的?
    Thread类的sleep()和yield()⽅法将在当前正在执⾏的线程上运⾏。所以在其他处于等待状态的线程上调⽤这些⽅法是没有意义的。这就是为
    什么这些⽅法是静态的。它们可以在当前正在执⾏的线程中⼯作,并避免程序员错误的认为可以在其他⾮运⾏线程调⽤这些⽅法。
    97、如何确保线程安全?
    在Java中可以有很多⽅法来保证线程安全——同步,使⽤原⼦类(atomic concurrent classes),实现并发锁,使⽤volatile关键字,使⽤不变
    类和线程安全类。
    98、同步⽅法和同步块,哪个是更好的选择?
    同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步⽅法会锁住整个对象,哪怕这个类中有多个不相
    关联的同步块,这通常会导致他们停⽌执⾏并需要等待获得这个对象上的锁。
    同步块更要符合开放调⽤的原则,只在需要锁住的代码块锁住相应的对象,这样从侧⾯来说也可以避免死锁。
    99、如何创建守护线程?
    使⽤Thread类的setDaemon(true)⽅法可以将线程设置为守护线程,需要注意的是,需要在调⽤start()⽅法前调⽤这个⽅法,否则会抛出
    IllegalThreadStateException异常。
    100、什么是Java Timer 类?如何创建⼀个有特定时间间隔的任务?
    java.util.Timer是⼀个⼯具类,可以⽤于安排⼀个线程在未来的某个特定时间执⾏。Timer类可以⽤安排⼀次性任务或者周期任务。
    java.util.TimerTask是⼀个实现了Runnable接⼝的抽象类,我们需要去继承这个类来创建我们⾃⼰的定时任务并使⽤Timer去安排它的执
    ⾏。⽬前有开源的Qurtz可以⽤来创建定时任务。

作者:小可爱的猫206
链接:https://wenku.baidu.com/tfview/76125f8cd9ef5ef7ba0d4a7302768e9950e76e51.html
来源:百度文库
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(java)