并发编程面试专栏
[TOC]
# 并发编程面试专栏
# 854.线程池的参数配置,为什么java官⽅提供⼯⼚⽅法给线程池?-Java面试题
- 线程池简介:
- 核⼼参数:
- ⼯⼚⽅法作⽤:ThreadPoolExecutor类就是Executor的实现类,但ThreadPoolExecutor在使⽤上并不是那么⽅便,在实例化时需要传⼊很多歌参数,还要考虑线程的并发数等与线程池运⾏效率有关的参数,所以官⽅建议使⽤Executors⼯程类来创建线程池对象。
# 853.⾼并发系统如何做性能优化?如何防⽌库存超卖?-Java面试题
- ⾼并发系统性能优化:优化程序,优化服务配置,优化系统配置
- 尽量使⽤缓存,包括⽤户缓存,信息缓存等,多花点内存来做缓存,可以⼤量减少与数据库的交互,提⾼性能。
- ⽤jprofiler等⼯具找出性能瓶颈,减少额外的开销。
- 优化数据库查询语句,减少直接使⽤hibernate等⼯具的直接⽣成语句(仅耗时较⻓的查询做优化)。
- 优化数据库结构,多做索引,提⾼查询效率。
- 统计的功能尽量做缓存,或按每天⼀统计或定时统计相关报表,避免需要时进⾏统计的功能。
- 能使⽤静态⻚⾯的地⽅尽量使⽤,减少容器的解析(尽量将动态内容⽣成静态html来显示)。
- 解决以上问题后,使⽤服务器集群来解决单台的瓶颈问题。
- 防⽌库存超卖:
- 悲观锁:在更新库存期间加锁,不允许其它线程修改;
- 数据库锁:select xxx for update;
- 分布式锁;
- 乐观锁:使⽤带版本号的更新。每个线程都可以并发修改,但在并发时,只有⼀个线程会修改成功,其它会返回失败。
- redis watch:监视键值对,作⽤时如果事务提交exec时发现监视的监视对发⽣变化,事务将被取消。
- 悲观锁:在更新库存期间加锁,不允许其它线程修改;
- 消息队列:通过 FIFO 队列,使修改库存的操作串⾏化。
- 总结:总的来说,不能把压⼒放在数据库上,所以使⽤ "select xxx for update" 的⽅式在⾼并发的场景下是不可⾏的。FIFO 同步队列的⽅式,可以结合库存限制队列⻓,但是在库存较多的场景下,⼜不太适⽤。所以相对来说,我会倾向于选择:乐观锁 / 缓存锁 / 分布式锁的⽅式。
# 852.线程a,b,c,d运⾏任务,怎么保证当a,b,c线程执⾏完再执⾏d线程?-Java面试题
- CountDownLatch类
⼀个同步辅助类,常⽤于某个条件发⽣后才能执⾏后续进程。给定计数初始化CountDownLatch,调⽤countDown()⽅法,在计数到达零之前,await⽅法⼀直受阻塞。
重要⽅法为countdown()与await(); join⽅法
将线程B加⼊到线程A的尾部,当A执⾏完后B才执⾏。1 public static void main(String[] args) throws Exception { 2 Th t = new Th("t1"); 3 Th t2 = new Th("t2"); 4 t.start(); 5 t.join(); 6 t2.start(); 7 }
- notify、wait⽅法,Java中的唤醒与等待⽅法,关键为synchronized代码块,参数线程间应相同,也常⽤Object作为参数。
# 851.concurrent包下有哪些类?-Java面试题
ConcurrentHashMap、Future、FutureTask、AtomicInteger…
# 850.、jdk8中对ConcurrentHashmap的改进-Java面试题
- Java 7为实现并⾏访问,引⼊了Segment这⼀结构,实现了分段锁,理论上最⼤并发度与Segment个数相等。
- Java 8为进⼀步提⾼并发性,摒弃了分段锁的⽅案,⽽是直接使⽤⼀个⼤的数组。同时为了提⾼哈希碰撞下的寻址性能,Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红⿊树(寻址时间复杂度为O(long(N)))。
其数据结构如下图所示
- 源码:
1 public V put(K key, V value) { 2 return putVal(key, value, false); 3 } 4 5 /** Implementation for put and putIfAbsent */ 6 final V putVal(K key, V value, boolean onlyIfAbsent) { 7 //ConcurrentHashMap 不允许插⼊null键,HashMap允许插⼊⼀个null键 8 if (key == null || value == null) throw new NullPointerException(); 9 //计算key的hash值 10 int hash = spread(key.hashCode()); 11 int binCount = 0; 12 //for循环的作⽤:因为更新元素是使⽤CAS机制更新,需要不断的失败重试,直到成功为⽌。 13 for (Node<K,V>[] tab = table;;) { 14 // f:链表或红⿊⼆叉树头结点,向链表中添加元素时,需要synchronized获取f的锁。 15 Node<K,V> f; int n, i, fh; 16 //判断Node[]数组是否初始化,没有则进⾏初始化操作 17 if (tab == null || (n = tab.length) == 0) 18 tab = initTable(); 19 //通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使⽤CAS进⾏添加(链表的头结点),添加失败则进⼊下次循环。 20 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 21 if (casTabAt(tab, i, null, 22 new Node<K,V>(hash, key, value, null))) 23 break; // no lock when adding to empty bin 24 } 25 //检查到内部正在移动元素(Node[] 数组扩容) 26 else if ((fh = f.hash) == MOVED) 27 //帮助它扩容 28 tab = helpTransfer(tab, f); 29 else { 30 V oldVal = null; 31 //锁住链表或红⿊⼆叉树的头结点 32 synchronized (f) { 33 //判断f是否是链表的头结点 34 if (tabAt(tab, i) == f) { 35 //如果fh>=0 是链表节点 36 if (fh >= 0) { 37 binCount = 1; 38 //遍历链表所有节点 39 for (Node<K,V> e = f;; ++binCount) { 40 K ek; 41 //如果节点存在,则更新value 42 if (e.hash == hash && 43 ((ek = e.key) == key || 44 (ek != null && key.equals(ek)))) { 45 oldVal = e.val; 46 if (!onlyIfAbsent) 47 e.val = value; 48 break; 49 } 50 //不存在则在链表尾部添加新节点。 51 Node<K,V> pred = e; 52 if ((e = e.next) == null) { 53 pred.next = new Node<K,V>(hash, key, 54 value, null); 55 break; 56 } 57 } 58 } 59 //TreeBin是红⿊⼆叉树节点 60 else if (f instanceof TreeBin) { 61 Node<K,V> p; 62 binCount = 2; 63 //添加树节点 64 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, 65 value)) != null) { 66 oldVal = p.val; 67 if (!onlyIfAbsent) 68 p.val = value; 69 } 70 } 71 } 72 } 73 74 if (binCount != 0) { 75 //如果链表⻓度已经达到临界值8 就需要把链表转换为树结构 76 if (binCount >= TREEIFY_THRESHOLD) 77 treeifyBin(tab, i); 78 if (oldVal != null) 79 return oldVal; 80 break; 81 } 82 } 83 } 84 //将当前ConcurrentHashMap的size数量+1 85 addCount(1L, binCount); 86 return null; 87 }
# 849.ThreadLocal为什么会发⽣内存泄漏?-Java面试题
- threadlocal原理图:
- OOM实现:
- ThreadLocal的实现是这样的:每个Thread 维护⼀个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
- 也就是说 ThreadLocal 本身并不存储值,它只是作为⼀个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使⽤ ThreadLocal 的弱引⽤作为 Key 的,弱引⽤的对象在 GC 时会被回收。
- ThreadLocalMap使⽤ThreadLocal的弱引⽤作为key,如果⼀个ThreadLocal没有外部强引⽤来引⽤它,那么系统 GC的时候,这个ThreadLocal势必会被回收,这样⼀来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会⼀直存在⼀条强引⽤链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远⽆法回收,造成内存泄漏。
- 预防办法:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap⾥所有key为null的value。但是这些被动的预防措施并不能保证不会内存泄漏:
- 使⽤static的ThreadLocal,延⻓了ThreadLocal的⽣命周期,可能导致内存泄漏。
- 分配使⽤了ThreadLocal⼜不再调⽤get(),set(),remove()⽅法,那么就会导致内存泄漏,因为这块内存⼀直存在。
# 848.如何保证数据不丢失:-Java面试题
- 使⽤消息队列,消息持久化;
- 添加标志位:未处理 0,处理中 1,已处理 2。定时处理。
# 847. java中加锁的⽅式有哪些,如何实现怎么个写法.-Java面试题
- java中有两种锁:⼀种是⽅法锁或者对象锁(在⾮静态⽅法或者代码块上加锁),第⼆种是类锁(在静态⽅法或者class上加锁);
- 注意:其他线程可以访问未加锁的⽅法和代码;synchronized同时修饰静态⽅法和实例⽅法,但是运⾏结果是交替进⾏的,这证明了类锁和对象锁是两个不⼀样的锁,控制着不同的区域,它们是互不⼲扰的。
- 示例代码:
- ⽅法锁和同步代码块:
1 public class TestSynchronized
2 {
3 public void test1()
4 {
5 synchronized(this)
6 {
7 int i = 5;
8 while( i-- > 0)
9 {
10 System.out.println(Thread.currentThread().getName() + " : " + i);
11 try
12 {
13 Thread.sleep(500);
14 }
15 catch (InterruptedException ie)
16 {
17 }
18 }
19 }
20 }
21
22 public synchronized void test2()
23 {
24 int i = 5;
25 while( i-- > 0)
26 {
27 System.out.println(Thread.currentThread().getName() + " : " + i);
28 try
29 {
30 Thread.sleep(500);
31 }
32 catch (InterruptedException ie)
33 {
34 }
35 }
36 }
37
38 public static void main(String[] args)
39 {
40 final TestSynchronized myt2 = new TestSynchronized();
41 Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } },
42 Thread test2 = new Thread( new Runnable() { public void run() { myt2.test2(); } },
43 test1.start();;
44 test2.start();
45 // TestRunnable tr=new TestRunnable();
46 // Thread test3=new Thread(tr);
47 // test3.start();
48 }
49 }
2. 类锁:
1 public class TestSynchronized
2 {
3 public void test1()
4 {
5 synchronized(TestSynchronized.class)
6 {
7 int i = 5;
8 while( i-- > 0)
9 {
10 System.out.println(Thread.currentThread().getName() + " : " + i);
11 try
12 {
13 Thread.sleep(500);
14 }
15 catch (InterruptedException ie)
16 {
17 }
18 }
19 }
20 }
21
22 public static synchronized void test2()
23 {
24 int i = 5;
25 while( i-- > 0)
26 {
27 System.out.println(Thread.currentThread().getName() + " : " + i);
28 try
29 {
30 Thread.sleep(500);
31 }
32 catch (InterruptedException ie)
33 {
34 }
35 }
36 }
37
38 public static void main(String[] args)
39 {
40 final TestSynchronized myt2 = new TestSynchronized();
41 Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } },
42 Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2
43 test1.start();
44 test2.start();
45 // TestRunnable tr=new TestRunnable();
46 // Thread test3=new Thread(tr);
47 // test3.start();
48 }
49
50 }
# 846.java线程如何启动:-Java面试题
- 继承Thread类;
- 实现Runnable接⼝;
- 直接在函数体内:
- ⽐较:
- 实现Runnable接⼝优势:
- 适合多个相同的程序代码的线程去处理同⼀个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独⽴。
- 继承Thread类优势:
- 可以将线程类抽象出来,当需要使⽤抽象⼯⼚模式设计时。
- 多线程同步
- 在函数体使⽤优势
- ⽆需继承thread或者实现Runnable,缩⼩作⽤域。
- 实现Runnable接⼝优势:
# 845.java锁多个对象:-Java面试题
例如: 在银⾏系统转账时,需要锁定两个账户,这个时候,顺序使⽤两个synchronized可能存在死锁的情况,在⽹上搜索到下⾯的例⼦:
1 public class Bank {
2 final static Object obj_lock = new Object();
3
4 // Deadlock crisis 死锁
5 public void transferMoney(Account from, Account to, int number) {
6 synchronized (from) {
7 synchronized (to) {
8 from.debit();
9 to.credit();
10 }
11 }
12 }
13
14 // Thread safe
15 public void transferMoney2(final Account from, final Account to, int number) {
16 class Help {
17 void transferMoney2() {
18 from.debit();
19 to.credit();
20 }
21 }
22
23 //通过hashCode⼤⼩调整加锁顺序
24 int fromHash = from.hashCode();
25 int toHash = to.hashCode();
26
27 if (fromHash < toHash) {
28 synchronized (from) {
29 synchronized (to) {
30 new Help().transferMoney2();
31 }
32 }
33 } else if (toHash < fromHash) {
34 synchronized (to) {
35 synchronized (from) {
36 new Help().transferMoney2();
37 }
38 }
39 } else {
40 synchronized (obj_lock) {
41 synchronized (to) {
42 synchronized (from) {
43 new Help().transferMoney2();
44 }
45 }
46 }
47 }
48 }
49 }
若操作账户A,B:
- A的hashCode⼩于B, 先锁A再锁B
- B的hashCode⼩于A, 先锁B再锁A
- 产⽣的hashCode相等,先锁住⼀个全局静态变量,在锁A,B 这样就避免了两个线程分别操作账户A,B和B,A⽽产⽣死锁的情况。需要为Account对象写⼀个好的hashCode算法,使得不同账户间产⽣的hashCode尽量不同。
# 844.线程池原理:-Java面试题
- 使⽤场景:假设⼀个服务器完成⼀项任务所需时间为:T1-创建线程时间,T2-在线程中执⾏任务的时间,T3-销毁线程时间。如果T1+T3远⼤于T2,则可以使⽤线程池,以提⾼服务器性能;
- 组成:
- 线程池管理器(ThreadPool):⽤于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
- ⼯作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执⾏任务;
- 任务接⼝(Task):每个任务必须实现的接⼝,以供⼯作线程调度任务的执⾏,它主要规定了任务的⼊⼝,任务执⾏完后的收尾⼯作,任务的执⾏状态等;
- 任务队列(taskQueue):⽤于存放没有处理的任务。提供⼀种缓冲机制。
- 原理:线程池技术正是关注如何缩短或调整T1,T3时间的技术,从⽽提⾼服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者⼀些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
- ⼯作流程:
- 线程池刚创建时,⾥⾯没有⼀个线程(也可以设置参数prestartAllCoreThreads启动预期数量主线程)。任务队列是作为参数传进来的。不过,就算队列⾥⾯有任务,线程池也不会⻢上执⾏它们。
- 当调⽤ execute() ⽅法添加⼀个任务时,线程池会做如下判断:
- 如果正在运⾏的线程数量⼩于 corePoolSize,那么⻢上创建线程运⾏这个任务;
- 如果正在运⾏的线程数量⼤于或等于 corePoolSize,那么将这个任务放⼊队列;
- 如果这时候队列满了,⽽且正在运⾏的线程数量⼩于 maximumPoolSize,那么还是要创建⾮核⼼线程⽴刻运⾏这个任务;
- 如果队列满了,⽽且正在运⾏的线程数量⼤于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
- 当⼀个线程完成任务时,它会从队列中取下⼀个任务来执⾏。
- 当⼀个线程⽆事可做,超过⼀定的时间(keepAliveTime)时,线程池会判断,如果当前运⾏的线程数⼤于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的⼤⼩。
# 843.spring单例为什么没有安全问题(ThreadLocal)-Java面试题
- ThreadLocal:spring使⽤ThreadLocal解决线程安全问题;ThreadLocal会为每⼀个线程提供⼀个独⽴的变量副本,从⽽隔离了多个线程对数据的访问冲突。因为每⼀个线程都拥有⾃⼰的变量副本,从⽽也就没有必要对该变量进⾏同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。概括起来说,对于多线程资源共享的问题,同步机制采⽤了“以时间换空间”的⽅式,⽽ThreadLocal采⽤了“以空间换时间”的⽅式。前者仅提供⼀份变量,让不同的线程排队访问,⽽后者为每⼀个线程都提供了⼀份变量,因此可以同时访问⽽互不影响。在很多情况下,ThreadLocal⽐直接使⽤synchronized同步机制解决线程安全问题更简单,更⽅便,且结果程序拥有更⾼的并发性。
- 单例:⽆状态的Bean(⽆状态就是⼀次操作,不能保存数据。⽆状态对象(Stateless Bean),就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。)适合⽤不变模式,技术就是单例模式,这样可以共享实例,提⾼性能。
# 842.数据库死锁机制和解决⽅案-Java面试题
- 死锁:死锁是指两个或者两个以上的事务在执⾏过程中,因争夺锁资源⽽造成的⼀种互相等待的现象。
- 处理机制:解决死锁最有⽤最简单的⽅法是不要有等待,将任何等待都转化为回滚,并且事务重新开始。但是有可能影响并发性能。
- 超时回滚,innodb_lock_wait_time设置超时时间;
- wait-for-graph⽅法:跟超时回滚⽐起来,这是⼀种更加主动的死锁检测⽅式。InnoDB引擎也采⽤这种⽅式。
# 841.分步式锁,程序数据库中死锁机制及解决⽅案-Java面试题
基本原理:⽤⼀个状态值表示锁,对锁的占⽤和释放通过状态值来标识。
- 三种分布式锁:
- Zookeeper:基于zookeeper瞬时有序节点实现的分布式锁,其主要逻辑如下。⼤致思想即为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的⽬录下,⽣成⼀个唯⼀的瞬时有序节点。判断是否获取锁的⽅式很简单,只需要判断有序节点中序号最⼩的⼀个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁⽆法释放,⽽产⽣的死锁问题。
- 优点
锁安全性⾼,zk可持久化,且能实时监听获取锁的客户端状态。⼀旦客户端宕机,则瞬时节点随之消失,zk因⽽能第⼀时间释放锁。这也省去了⽤分布式缓存实现锁的过程中需要加⼊超时时间判断的这⼀逻辑。 - 缺点
性能开销⽐较⾼。因为其需要动态产⽣、销毁瞬时节点来实现锁功能。所以不太适合直接提供给⾼并发的场景使⽤。 - 实现
可以直接采⽤zookeeper第三⽅库curator即可⽅便地实现分布式锁。 - 适⽤场景
对可靠性要求⾮常⾼,且并发程度不⾼的场景下使⽤。如核⼼数据的定时全量/增量同步等。
- memcached:memcached带有add函数,利⽤add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。⽽add的话则相反,add会添加第⼀个到达的值,并返回true,后续的添加则都会返回false。利⽤该点即可很轻松地实现分布式锁。
- 优点
并发⾼效 - 缺点
memcached采⽤列⼊LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。
memcached⽆法持久化,⼀旦重启,将导致信息丢失。 - 使⽤场景
⾼并发场景。需要 1)加上超时时间避免死锁; 2)提供⾜够⽀撑锁服务的内存空间; 3)稳定的集群化管理。
- 优点
redis:redis分布式锁即可以结合zk分布式锁锁⾼度安全和memcached并发场景下效率很好的优点,其实现⽅式和memcached类似,采⽤setnx即可实现。需要注意的是,这⾥的redis也需要设置超时时间,以避免死锁。可以利⽤jedis客户端实现。
1 ICacheKey cacheKey = new ConcurrentCacheKey(key, type); 2 return RedisDao.setnx(cacheKey, "1");
# 840.关于synchronized:-Java面试题
- 在某个对象的所有synchronized⽅法中,在某个时刻只能有⼀个唯⼀的⼀个线程去访问这些synchronized⽅法
- 如果⼀个⽅法是synchronized⽅法,那么该synchronized关键字表示给当前对象上锁(即this)相当于synchronized(this){}
- 如果⼀个synchronized⽅法是static的,那么该synchronized表示给当前对象所对应的class对象上锁(每个类不管⽣成多少对象,其对应的class对象只有⼀个)
# 839.可以运⾏时kill掉⼀个线程吗?-Java面试题
- 不可以,线程有5种状态,新建(new)、可运⾏(runnable)、运⾏中(running)、阻塞(block)、死亡(dead)。
- 只有当线程run⽅法或者主线程main⽅法结束,⼜或者抛出异常时,线程才会结束⽣命周期。
# 838. 如何实现线程串⾏执⾏?-Java面试题
- 为了控制线程执⾏的顺序,如ThreadA->ThreadB->ThreadC->ThreadA循环执⾏三个线程,我们需要确定唤醒、等待的顺序。这时我们可以同时使⽤ Obj.wait()、Obj.notify()与synchronized(Obj)来实现这个⽬标。线程中持有上⼀个线程类的对象锁以及⾃⼰的锁,由于这种依赖关系,该线程执⾏需要等待上个对象释放锁,从⽽保证类线程执⾏的顺序。
- 通常情况下,wait是线程在获取对象锁后,主动释放对象锁,同时本线程休眠,直到有其它线程调⽤对象的notify()唤醒该线程,才能继续获取对象锁,并继续执⾏。⽽notify()则是对等待对象锁的线程的唤醒操作。但值得注意的是notify()调⽤后,并不是⻢上就释放对象锁,⽽是在相应的synchronized(){}语句块执⾏结束。释放对象锁后,JVM会在执⾏wait()等待对象锁的线程中随机选取⼀线程,赋予其对象锁,唤醒线程,继续执⾏。
1 public class ThreadSerialize {
2
3 public static void main(String[] args){
4 ThreadA threadA = new ThreadA();
5 ThreadB threadB = new ThreadB();
6 ThreadC threadC = new ThreadC();
7
8 threadA.setThreadC(threadC);
9 threadB.setThreadA(threadA);
10 threadC.setThreadB(threadB);
11
12 threadA.start();
13 threadB.start();
14 threadC.start();
15
16 while (true){
17 try {
18 Thread.currentThread().sleep(1000);
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 }
23 }
24 }
25
26 class ThreadA extends Thread{
27 private ThreadC threadC;
28 @Override
29 public void run() {
30 while (true){
31 synchronized (threadC){
32 synchronized (this){
33 System.out.println("I am ThreadA。。。");
34 this.notify();
35 }
36 try {
37 threadC.wait();
38 } catch (InterruptedException e) {
39 e.printStackTrace();
40 }
41 }
42 }
43
44 }
45
46 public void setThreadC(ThreadC threadC) {
47 this.threadC = threadC;
48 }
49 }
50 class ThreadB extends Thread{
51 private ThreadA threadA;
52 @Override
53 public void run() {
54 while (true){
55 synchronized (threadA){
56 synchronized (this){
57 System.out.println("I am ThreadB。。。");
58 this.notify();
59 }
60 try {
61 threadA.wait();
62 } catch (InterruptedException e) {
63 e.printStackTrace();
64 }
65 }
66 }
67
68 }
69
70 public void setThreadA(ThreadA threadA) {
71 this.threadA = threadA;
72 }
73 }
74 class ThreadC extends Thread{
75 private ThreadB threadB;
76 @Override
77 public void run() {
78 while (true){
79 synchronized (threadB){
80 synchronized (this){
81 System.out.println("I am ThreadC。。。");
82 this.notify();
83 }
84 try {
85 threadB.wait();
86 } catch (InterruptedException e) {
87 e.printStackTrace();
88 }
89 }
90 }
91
92 }
93
94 public void setThreadB(ThreadB threadB) {
95 this.threadB = threadB;
96 }
97 }
98
99
# 837.什么是守护线程?有什么⽤?-Java面试题
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作⽤——⽽其他的线程只有⼀种,那就是⽤户线程。所以java⾥线程分2种,
- 守护线程,⽐如垃圾回收线程,就是最典型的守护线程。
- ⽤户线程,就是应⽤程序⾥的⾃定义线程。
# 836. HashMap在多线程环境下使⽤需要注意什么?-Java面试题
要注意死循环的问题,HashMap的put操作引发扩容,这个动作在多线程并发下会发⽣线程死循环的问题。
- HashMap不是线程安全的;Hashtable线程安全,但效率低,因为是Hashtable是使⽤synchronized的,所有线程竞争同⼀把锁;⽽ConcurrentHashMap不仅线程安全⽽且效率⾼,因为它包含⼀个segment数组,将数据分段存储,给每⼀段数据配⼀把锁,也就是所谓的锁分段技术。
- HashMap为何线程不安全:
- put时key相同导致其中⼀个线程的value被覆盖;
- 多个线程同时扩容,造成数据丢失;
- 多线程扩容时导致Node链表形成环形结构造成.next()死循环,导致CPU利⽤率接近100%;
- ConcurrentHashMap最⾼效;
# 835.如何检测死锁?怎么预防死锁?-Java面试题
- 概念:
是指两个或两个以上的进程在执⾏过程中,因争夺资源⽽造成的⼀种互相等待的现象,若⽆外⼒作⽤,它们都将⽆法推进下去。此时称系统处于死锁; - 死锁的四个必要条件:
- 互斥条件:进程对所分配到的资源不允许其他进程进⾏访问,若其他进程访问该资源,只能等待,直⾄占有该资源的进程使⽤完成后释放该资源
- 请求和保持条件:进程获得⼀定的资源之后,⼜对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但⼜对⾃⼰获得的资源保持不放
- 不可剥夺条件:是指进程已获得的资源,在未完成使⽤之前,不可被剥夺,只能在使⽤完后⾃⼰释放
- 环路等待条件:是指进程发⽣死锁后,若⼲进程之间形成⼀种头尾相接的循环等待资源关系
- 死锁产⽣的原因:
1.因竞争资源发⽣死锁 现象:系统中供多个进程共享的资源的数⽬不⾜以满⾜全部进程的需要时,就会引起对诸资源的竞争⽽发⽣死锁现象
2.进程推进顺序不当发⽣死锁 - 检查死锁
- 有两个容器,⼀个⽤于保存线程正在请求的锁,⼀个⽤于保存线程已经持有的锁。每次加锁之前都会做如下检测:
- 检测当前正在请求的锁是否已经被其它线程持有,如果有,则把那些线程找出来
- 遍历第⼀步中返回的线程,检查⾃⼰持有的锁是否正被其中任何⼀个线程请求,如果第⼆步返回真,表示出现了死锁
- 死锁的解除与预防:控制不要让四个必要条件成⽴。
# 834.同步⽅法和同步块,哪个是更好的选择?-Java面试题
- 同步块是更好的选择,因为它不会锁住整个对象(当然也可以让它锁住整个对象)。同步⽅法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停⽌执⾏并需要等待获得这个对象上的锁。synchronized(this)以及⾮static的synchronized⽅法(⾄于static synchronized⽅法请往下看),只能防⽌多个线程同时执⾏同⼀个对象的同步代码段。
如果要锁住多个对象⽅法,可以锁住⼀个固定的对象,或者锁住这个类的Class对象。
synchronized锁住的是括号⾥的对象,⽽不是代码。对于⾮static的synchronized⽅法,锁的就是对象本身也就是this。 - 例如:
1 public class SynObj{
2
3 public synchronized void showA(){
4 System.out.println("showA..");
5 try {
6 Thread.sleep(3000);
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 }
11
12 public void showB(){
13 synchronized (this) {
14 System.out.println("showB..");
15 }
16 }
17 }
# 833.线程类的构造⽅法、静态块是被哪个线程调⽤的?-Java面试题
线程类的构造⽅法、静态块是被new这个线程类所在的线程所调⽤的,⽽run⽅法⾥⾯的代码才是被线程⾃身所调⽤的。
# 832. 单例模式的线程安全性?-Java面试题
⽼⽣常谈的问题了,⾸先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建⼀次出来。单例模式有很多种的写法,我总结⼀下:
- 饿汉式单例模式的写法:线程安全
- 懒汉式单例模式的写法:⾮线程安全
- 双检锁单例模式的写法:线程安全
# 831.Java中⽤到的线程调度算法是什么-Java面试题
- 抢占式。⼀个线程⽤完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出⼀个总的优先级并分配下⼀个时间⽚给某个线程执⾏。
# 830.怎么唤醒⼀个阻塞的线程?-Java面试题
- 如果线程是因为调⽤了wait()、sleep()或者join()⽅法⽽导致的阻塞;
- suspend与resume
Java废弃 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都⽆法访问被它占⽤的锁。直到对应的线程执⾏ resume() ⽅法后,被挂起的线程才能继续,从⽽其它被阻塞在这个锁的线程才可以继续执⾏。
但是,如果 resume() 操作出现在 suspend() 之前执⾏,那么线程将⼀直处于挂起状态,同时⼀直占⽤锁,这就产⽣了死锁。⽽且,对于被挂起的线程,它的线程状态居然还是 Runnable。 - wait与notify
wait与notify必须配合synchronized使⽤,因为调⽤之前必须持有锁,wait会⽴即释放锁,notify则是同步块执⾏完了才释放 - await与singal
Condition类提供,⽽Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使⽤Lock锁后⽆法使⽤wait⽅法 - park与unpark
LockSupport是⼀个⾮常⽅便实⽤的线程阻塞⼯具,它可以在线程任意位置让线程阻塞。和Thread.suspenf()相⽐,它弥补了由于resume()在前发⽣,导致线程⽆法继续执⾏的情况。和Object.wait()相⽐,它不需要先获得某个对象的锁,也不会抛出IException异常。可以唤醒指定线程。
- suspend与resume
- 如果线程遇到了IO阻塞,⽆能为⼒,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
# 829. ⽣产者消费者模型的作⽤是什么?-Java面试题
- 通过平衡⽣产者的⽣产能⼒和消费者的消费能⼒来提升整个系统的运⾏效率,这是⽣产者消费者模型最重要的作⽤。
- 解耦,这是⽣产者消费者模型附带的作⽤,解耦意味着⽣产者和消费者之间的联系少,联系越少越可以独⾃发展⽽不需要受到相互的制约。
# 828. 如何在两个线程之间共享数据?-Java面试题
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进⾏唤起和等待,⽐⽅说阻塞队列BlockingQueue就是为线程之间共享数据⽽设计的。
- 卖票系统:
1 package 多线程共享数据;
2 public class Ticket implements Runnable{
3 private int ticket = 10;
4 public void run() {
5 while(ticket>0){
6 ticket--;
7 System.out.println("当前票数为:"+ticket);
8 }
9 }
10 }
11
12 package 多线程共享数据;
13 public class SellTicket {
14 public static void main(String[] args) {
15 Ticket t = new Ticket();
16 new Thread(t).start();
17 new Thread(t).start();
18 }
19 }
2. 银⾏存取款:
1 public class MyData {
2 private int j=0;
3 public synchronized void add(){
4 j++;
5 System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);
6 }
7 public synchronized void dec(){
8 j--;
9 System.out.println("线程"+Thread.currentThread().getName()+"j为:"+j);
10 }
11 public int getData(){
12 return j;
13 }
14 }
15
16 public class AddRunnable implements Runnable{
17 MyData data;
18 public AddRunnable(MyData data){
19 this.data= data;
20 }
21 public void run() {
22 data.add();
23 }
24 }
25
26 public class DecRunnable implements Runnable {
27 MyData data;
28 public DecRunnable(MyData data){
29 this.data = data;
30 }
31 public void run() {
32 data.dec();
33 }
34 }
35
36 public class TestOne {
37 public static void main(String[] args) {
38 MyData data = new MyData();
39 Runnable add = new AddRunnable(data);
40 Runnable dec = new DecRunnable(data);
41 for(int i=0;i<2;i++){
42 new Thread(add).start();
43 new Thread(dec).start();
44 }
45 }
# 827.⼀个线程如果出现了运⾏时异常会怎么样?-Java面试题
- 如果这个异常没有被捕获的话,这个线程就停⽌执⾏了。
- 另外重要的⼀点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被⽴即释放.
# 826.如何保证多线程下 i++ 结果正确?-Java面试题
- volatile只能保证你数据的可⻅性,获取到的是最新的数据,不能保证原⼦性;
- ⽤AtomicInteger保证原⼦性。
- synchronized既能保证共享变量可⻅性,也可以保证锁内操作的原⼦性。
# 825.并发编程(concurrency)并⾏编程(parallellism)有什么区别?-Java面试题
- 解释⼀:并⾏是指两个或者多个事件在同⼀时刻发⽣;⽽并发是指两个或多个事件在同⼀时间间隔发⽣。
- 解释⼆:并⾏是在不同实体上的多个事件,并发是在同⼀实体上的多个事件。
- 解释三:在⼀台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
所以并发编程的⽬标是充分的利⽤处理器的每⼀个核,以达到最⾼的处理性能。
# 824.如果同步块内的线程抛出异常会发⽣什么?-Java面试题
synchronized⽅法正常返回或者抛异常⽽终⽌,JVM会⾃动释放对象锁
# 823.锁的等级:⽅法锁、对象锁、类锁?-Java面试题
- ⽅法锁(synchronized修饰⽅法时)
- 通过在⽅法声明中加⼊ synchronized关键字来声明 synchronized ⽅法。
- synchronized ⽅法控制对类成员变量的访问:
- 每个类实例对应⼀把锁,每个 synchronized ⽅法都必须获得调⽤该⽅法的类实例的锁⽅能执⾏,否则所属线程阻塞,⽅法⼀旦执⾏,就独占该锁,直到从该⽅法返回时才将锁释放,此后被阻塞的线程⽅能获得该锁,重新进⼊可执⾏状态。这种机制确保了同⼀时刻对于每⼀个类实例,其所有声明为 synchronized 的成员函数中⾄多只有⼀个处于可执⾏状态,从⽽有效避免了类成员变量的访问冲突。
- 对象锁(synchronized修饰⽅法或代码块)
- 当⼀个对象中有synchronized method或synchronized block的时候调⽤此对象的同步⽅法或进⼊其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调⽤者占⽤,则需要等待此锁被释放。(⽅法锁也是对象锁)
- java的所有对象都含有1个互斥锁,这个锁由JVM⾃动获取和释放。线程进⼊synchronized⽅法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized⽅法正常返回或者抛异常⽽终⽌,JVM会⾃动释放对象锁。这⾥也体现了⽤synchronized来加锁的1个好处,⽅法抛异常的时候,锁仍然可以由JVM来⾃动释放。
- 类锁(synchronized 修饰静态的⽅法或代码块)
- 由于⼀个class不论被实例化多少次,其中的静态⽅法和静态变量在内存中都只有⼀份。所以,⼀旦⼀个静态的⽅法被申明为synchronized。此类所有的实例化对象在调⽤此⽅法,共⽤同⼀把锁,我们称之为类锁。
- 对象锁是⽤来控制实例⽅法之间的同步,类锁是⽤来控制静态⽅法(或静态变量互斥体)之间的同步
# 822.如果你提交任务时,线程池队列已满,这时会发⽣什么?-Java面试题
- 如果你使⽤的LinkedBlockingQueue,也就是⽆界队列的话,没关系,继续添加任务到阻塞队列中等待执⾏,因为LinkedBlockingQueue可以近乎认为是⼀个⽆穷⼤的队列,可以⽆限存放任务;
- 如果你使⽤的是有界队列⽐⽅说ArrayBlockingQueue的话,任务⾸先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使⽤拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
# 821. ⾼并发、任务执⾏时间短的业务怎样使⽤线程池?并发不⾼、任务执⾏时间⻓的业务怎样使⽤线程池?并发⾼、 业务执⾏时间⻓的业务怎样使⽤线程池?-Java面试题
- ⾼并发、任务执⾏时间短的业务:线程池线程数可以设置为CPU核数+1,减少线程上下⽂的切换。
- 并发不⾼、任务执⾏时间⻓的业务要区分开看:
- 假如是业务时间⻓集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占⽤CPU,所以不要让所有的CPU闲下来,可以加⼤线程池中的线程数⽬,让CPU处理更多的业务
- 假如是业务时间⻓集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)⼀样吧,线程池中的线程数设置得少⼀些,减少线程上下⽂的切换
- 并发⾼、业务执⾏时间⻓,解决这种类型任务的关键不在于线程池⽽在于整体架构的设计,看看这些业务⾥⾯某些数据是否能做缓存是第⼀步,增加服务器是第⼆步,⾄于线程池的设置,设置参考(2)。最后,业务执⾏时间⻓的问题,也可能需要分析⼀下,看看能不能使⽤中间件对任务进⾏拆分和解耦。
# 820. 线程实现的⽅式有⼏种(四种)?-Java面试题
- 继承Thread类,重写run⽅法
- 实现Runnable接⼝,重写run⽅法,实现Runnable接⼝的实现类的实例对象作为Thread构造函数的target
- 实现Callable接⼝通过FutureTask包装器来创建Thread线程
- 通过线程池创建线程
public class ThreadDemo03 { 2 public static void main(String[] args) { 3 Callable<Object> oneCallable = new Tickets<Object>(); 4 FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable); 5 Thread t = new Thread(oneTask); 6 System.out.println(Thread.currentThread().getName()); 7 t.start(); 8 } 9 } 10 11 class Tickets<Object> implements Callable<Object>{ 12 //重写call⽅法 13 @Override 14 public Object call() throws Exception { 15 // TODO Auto-generated method stub 16 System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接⼝通过FutureTask包装器来实现的线程" 17 return null; 18 } 19 }
# 819.线程和进程有什么区别?-Java面试题
- 进程是系统进⾏资源分配的基本单位,有独⽴的内存地址空间
- 线程是CPU独⽴运⾏和独⽴调度的基本单位,没有单独地址空间,有独⽴的栈,局部变量,寄存器, 程序计数器等。
- 创建进程的开销⼤,包括创建虚拟地址空间等需要⼤量系统资源
- 创建线程开销⼩,基本上只有⼀个内核对象和⼀个堆栈。
- ⼀个进程⽆法直接访问另⼀个进程的资源;同⼀进程内的多个线程共享进程的资源。
- 进程切换开销⼤,线程切换开销⼩;进程间通信开销⼤,线程间通信开销⼩。
- 线程属于进程,不能独⽴执⾏。每个进程⾄少要有⼀个线程,成为主线程
# 818.Java中如何获取到线程dump⽂件?-Java面试题
死循环、死锁、阻塞、⻚⾯打开慢等问题,查看线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
- 获取到线程的pid,可以通过使⽤jps命令,在Linux环境下还可以使⽤ps -ef | grep java
- 打印线程堆栈,可以通过使⽤jstack pid命令,在Linux环境下还可以使⽤kill -3 pid
- 另外提⼀点,Thread类提供了⼀个getStackTrace()⽅法也可以⽤于获取线程堆栈。这是⼀个实例⽅法,因此此⽅法是和具体线程实例绑定的,每次获取到的是具体某个线程当前运⾏的堆栈。
# 817.volatile关键字的作⽤?-Java面试题
- 多线程主要围绕可⻅性和原⼦性两个特性⽽展开,使⽤volatile关键字修饰的变量,保证了其在多线程之间的可⻅性,即每次读取到volatile变量,⼀定是最新的数据。
- 代码底层执⾏不像我们看到的⾼级语⾔—-Java程序这么简单,它的执⾏是Java代码–>字节码–>根据字节码执⾏对应的C/C++代码–>C/C++代码被编译成汇编语⾔–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进⾏重排序,多线程下可能会出现⼀些意想不到的问题。使⽤volatile则会对禁⽌语义重排序,当然这也⼀定程度上降低了代码执⾏效率。
# 816.Runnable接⼝和Callable接⼝的区别?-Java面试题
- Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;
- Callable接⼝中的call()⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
# 815.start()⽅法和run()⽅法的区别?-Java面试题
- start()⽅法来启动⼀个线程,真正实现了多线程运⾏。
- 如果直接调⽤run(),其实就相当于是调⽤了⼀个普通函数⽽已,直接调⽤run()⽅法必须等待run()⽅法执⾏完毕才能执⾏下⾯的代码,所以执⾏路径还是只有⼀条,根本就没有线程的特征,所以在多线程执⾏时要使⽤start()⽅法⽽不是run()⽅法。
# 814.线程的五个状态(五种状态,创建、就绪、运⾏、阻塞和死亡)?-Java面试题
线程通常都有五种状态,创建、就绪、运⾏、阻塞和死亡。
- 第⼀是创建状态。在⽣成线程对象,并没有调⽤该对象的start⽅法,这是线程处于创建状态。
- 第⼆是就绪状态。当调⽤了线程对象的start⽅法之后,该线程就进⼊了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运⾏之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 第三是运⾏状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进⼊了运⾏状态,开始运⾏run函数当中的代码。
- 第四是阻塞状态。线程正在运⾏的时候,被暂停,通常是为了等待某个时间的发⽣(⽐如说某项资源就绪)之后再继续运⾏。sleep,suspend,wait等⽅法都可以导致线程阻塞。
- 第五是死亡状态。如果⼀个线程的run⽅法执⾏结束或者调⽤stop⽅法后,该线程就会死亡。对于已经死亡的线程,⽆法再使⽤start⽅法令其进⼊就绪。
# 813.wait()和sleep()的区别?-Java面试题
- sleep()
⽅法是线程类(Thread)的静态⽅法,让调⽤线程进⼊睡眠状态,让出执⾏机会给其他线程,等到休眠时间结束后,线程进⼊就绪状态和其他线程⼀起竞争cpu的执⾏时间。
因为sleep() 是static静态的⽅法,他不能改变对象的机锁,当⼀个synchronized块中调⽤了sleep() ⽅法,线程虽然进⼊休眠,但是对象的机锁没有被释放,其他线程依然⽆法访问这个对象。 - wait()
wait()是Object类的⽅法,当⼀个线程执⾏到wait⽅法时,它就进⼊到⼀个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll⽅法来唤醒等待的线程
# 812. Fork/Join框架的理解?-Java面试题
- Fork就是把⼀个⼤任务切分为若⼲⼦任务并⾏的执⾏。
- Join就是合并这些⼦任务的执⾏结果,最后得到这个⼤任务的结果。
# 811.Condition接⼝及其实现原理?-Java面试题
- 在java.util.concurrent包中,有两个很特殊的⼯具类,Condition和ReentrantLock,使⽤过的⼈都知道,ReentrantLock(重⼊锁)是jdk的concurrent包提供的⼀种独占锁的实现
- 我们知道在线程的同步时可以使⼀个线程阻塞⽽等待⼀个信号,同时放弃锁使其他线程可以能竞争到锁
- 在synchronized中我们可以使⽤Object的wait()和notify⽅法实现这种等待和唤醒
- 但是在Lock中怎么实现这种wait和notify呢?答案是Condition,学习Condition主要是为了⽅便以后学习blockqueue和concurrenthashmap的源码,同时也进⼀步理解ReentrantLock。
# 810.CyclicBarrier和CountDownLatch的⽤法及区别?-Java面试题
CyclicBarrier和CountDownLatch 都位于java.util.concurrent 这个包下
CountDownLatch | CyclicBarrier |
---|---|
减计数⽅式 | 加计数⽅式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时,⽆法重置 | 计数达到指定值时,计数置为0重新开始 |
调⽤countDown()⽅法计数减⼀,调⽤await()⽅法只进⾏阻塞,对计数没任何影响 | 调⽤await()⽅法计数加1,若加1后的值不等于构造⽅法的值,则线程阻塞 |
不可重复利⽤ | 可重复利⽤ |
# 809.ReentrantReadWriteLock读写锁的使⽤?-Java面试题
- 读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm⾃⼰控制的,你只要上好相应的锁即可。
- 如果你的代码只读数据,可以很多⼈同时读,但不能同时写,那就上读锁;
- 如果你的代码修改数据,只能有⼀个⼈在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
# 808.ConcurrentHashMap的并发度是什么?-Java面试题
- ⼯作机制(分⽚思想):它引⼊了⼀个“分段锁”的概念,具体可以理解为把⼀个⼤的Map拆分成N个⼩的segment,根据key.hashCode()来决定把key放到哪个HashTable中。可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
- 应⽤:当读>写时使⽤,适合做缓存,在程序启动时初始化,之后可以被多个线程访问;
- hash冲突:
- 简介:HashMap中调⽤hashCode()⽅法来计算hashCode。由于在Java中两个不同的对象可能有⼀样的hashCode,所以不同的键可能有⼀样hashCode,从⽽导致冲突的产⽣。
- hash冲突解决:使⽤平衡树来代替链表,当同⼀hash中的元素数量超过特定的值便会由链表切换到平衡树
- ⽆锁读:ConcurrentHashMap之所以有较好的并发性是因为ConcurrentHashMap是⽆锁读和加锁写,并且利⽤了分段锁(不是在所有的entry上加锁,⽽是在⼀部分entry上加锁);
# 807. Hashtable的size()⽅法中明明只有⼀条语句”return count”,为什么还要做同步?-Java面试题
- 同⼀时间只能有⼀条线程执⾏固定类的同步⽅法,但是对于类的⾮同步⽅法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执⾏Hashtable的put⽅法添加数据,线程B则可以正常调⽤size()⽅法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size⼀定是不准确的。
- ⽽给size()⽅法加了同步之后,意味着线程B调⽤size()⽅法只有在线程A调⽤put⽅法完毕之后才可以调⽤,这样就保证了线程安全性。
# 806.Java Concurrency API中的Lock接⼝(Lock interface)是什么?对⽐同步它有什么优势?-Java面试题
- Lock接⼝⽐同步⽅法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以⽀持多个相关类的条件对象。
- 它的优势有:
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁,并在⽆法获取锁的时候⽴即返回或者等待⼀段时间
- 可以在不同的范围,以不同的顺序获取和释放锁
# 805.synchronized和ReentrantLock的区别?-Java面试题
基础知识
可重⼊锁。可重⼊锁是指同⼀个线程可以多次获取同⼀把锁。ReentrantLock和synchronized都是可重⼊锁。
可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,⽽ReentrantLock则提供了中断功能。
公平锁与⾮公平锁。公平锁是指多个线程同时尝试获取同⼀把锁时,获取锁的顺序按照线程达到的顺序,⽽⾮公平锁则允许线程“插队”。synchronized是⾮公平锁,⽽ReentrantLock的默认实现是⾮公平锁,但是也可以设置为公平锁。
CAS操作(CompareAndSwap)。CAS操作简单的说就是⽐较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会⾃动将该位置值更新为新值。否则,处理器不做任何操作。⽆论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”Synchronized
- synchronized是java内置的关键字,它提供了⼀种独占的加锁⽅式。synchronized的获取和释放锁由JVM实现,⽤户不需要显示的释放锁,⾮常⽅便。然⽽synchronized也有⼀定的局限性:
- 当线程尝试获取锁的时候,如果获取不到锁会⼀直阻塞。
- 如果获取锁的线程进⼊休眠或者阻塞,除⾮当前线程异常,否则其他线程尝试获取锁必须⼀直等待。
- synchronized是java内置的关键字,它提供了⼀种独占的加锁⽅式。synchronized的获取和释放锁由JVM实现,⽤户不需要显示的释放锁,⾮常⽅便。然⽽synchronized也有⼀定的局限性:
ReentrantLock
- ReentrantLock它是JDK 1.5之后提供的API层⾯的互斥锁,需要lock()和unlock()⽅法配合try/finally语句块来完成。
- 等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
- 公平锁与⾮公平锁多个线程等待同⼀个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁⾮公平锁,
ReentrantLock默认的构造函数是创建的⾮公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
# 804. Concurrent包⾥的其他东⻄:ArrayBlockingQueue、CountDownLatch等等。-Java面试题
- ArrayBlockingQueue 数组结构组成的有界阻塞队列:
- CountDownLatch 允许⼀个或多个线程等待其他线程完成操作;
join⽤于让当前执⾏线程等待join线程执⾏结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远wait
# 803.ThreadPool(线程池)⽤法与优势?-Java面试题
- ThreadPool 优点
- 减少了创建和销毁线程的次数,每个⼯作线程都可以被重复利⽤,可执⾏多个任务
- 可以根据系统的承受能⼒,调整线程池中⼯作线线程的数⽬,防⽌因为因为消耗过多的内存,⽽把服务器累趴下(每个线程需要⼤约1MB内存,线程开的越多,消耗的内存也就越⼤,最后死机)
- 减少在创建和销毁线程上所花的时间以及系统资源的开销
- 如不使⽤线程池,有可能造成系统创建⼤量线程⽽导致消耗完系统内存
- ⽐较重要的⼏个类:
类 | 描述 |
---|---|
ExecutorService | 真正的线程池接⼝。 |
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执⾏的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接⼝实现,周期性任务调度的类实现。 |
Java⾥⾯线程池的顶级接⼜是Executor,但是严格意义上讲Executor并不是⼀个线程池,⽽只是⼀个执⾏线程的⼯具。真正的线程池接⼜是ExecutorService。
- 任务执⾏顺序:
- 当线程数⼩于corePoolSize时,创建线程执⾏任务。
- 当线程数⼤于等于corePoolSize并且workQueue没有满时,放⼊workQueue中
- 线程数⼤于等于corePoolSize并且当workQueue满时,新任务新建线程运⾏,线程总数要⼩于maximumPoolSize
- 当线程总数等于maximumPoolSize并且workQueue满了的时候执⾏handler的rejectedExecution。也就是拒绝策略。
# 802.ThreadLocal的设计理念与作⽤?-Java面试题
Java中的ThreadLocal类允许我们创建只能被同⼀个线程读写的变量。因此,如果⼀段代码含有⼀个ThreadLocal变量的引⽤,即使两个线程同时执⾏这段代码,它们也⽆法访问到对⽅的ThreadLocal变量。
- 概念:线程局部变量。在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同⼀个变量,显然是不⾏的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有⼀种情况之下,我们需要满⾜这样⼀个条件:变量是同⼀个,但是每个线程都使⽤同⼀个初始值,也就是使⽤同⼀个变量的⼀个新的副本。这种情况之下ThreadLocal就⾮常适⽤,⽐如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是⼀个线程安全的变量。⽽我们每个线程都需要使⽤他,并且各⾃使⽤各⾃的。这种情况,ThreadLocal就⽐较好的解决了这个问题。
- 原理:从本质来讲,就是每个线程都维护了⼀个map,⽽这个map的key就是threadLocal,⽽值就是我们set的那个值,每次线程在get的时候,都从⾃⼰的变量中取值,既然从⾃⼰的变量中取值,那肯定就不存在线程安全问题,总体来讲,ThreadLocal这个变量的状态根本没有发⽣变化,他仅仅是充当⼀个key的⻆⾊,另外提供给每⼀个线程⼀个初始值。
- 实现机制:每个Thread对象内部都维护了⼀个ThreadLocalMap这样⼀个ThreadLocal的Map,可以存放若⼲个ThreadLocal。
1 /* ThreadLocal values pertaining to this thread. This map is maintained 2 * by the ThreadLocal class. */ 3 ThreadLocal.ThreadLocalMap threadLocals = null;
- 应⽤场景:当很多线程需要多次使⽤同⼀个对象,并且需要该对象具有相同初始化值的时候最适合使⽤ThreadLocal。
# 801.什么是多线程的上下⽂切换?-Java面试题
- 多线程:是指从软件或者硬件上实现多个线程的并发技术。
- 多线程的好处:
- 使⽤多线程可以把程序中占据时间⻓的任务放到后台去处理,如图⽚、视屏的下载
- 发挥多核处理器的优势,并发执⾏让系统运⾏的更快、更流畅,⽤户体验更好
- 多线程的缺点:
- ⼤量的线程降低代码的可读性;
- 更多的线程需要更多的内存空间
- 当多个线程对同⼀个资源出现争夺时候要注意线程安全的问题。
- 多线程的上下⽂切换:
CPU通过时间⽚分配算法来循环执⾏任务,当前任务执⾏⼀个时间⽚后会切换到下⼀个任务。但是,在切换前会保存上⼀个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态
# 800.什么是同步容器和并发容器的实现?-Java面试题
- 同步容器
- 主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。
- 锁的粒度为当前对象整体。
- 迭代器是及时失败的,即在迭代的过程中发现被修改,就会抛出ConcurrentModificationException。
- 并发容器
- 主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
- 锁的粒度是分散的、细粒度的,即读和写是使⽤不同的锁。
- 迭代器具有弱⼀致性,即可以容忍并发修改,不会抛出ConcurrentModificationException。
ConcurrentHashMap
采⽤分段锁技术,同步容器中,是⼀个容器⼀个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若⼲段,每段维护⼀个锁,以达到⾼效的并发访问;
# 799.什么是FutureTask?-Java面试题
- FutureTask可⽤于异步获取执⾏结果或取消执⾏任务的场景。通过传⼊Runnable或者Callable的任务给FutureTask,直接调⽤其run⽅法或者放⼊线程池执⾏,之后可以在外部通过FutureTask的get⽅法异步获取执⾏结果,因此,FutureTask⾮常适合⽤于耗时的计算,主线程可以在完成⾃⼰的任务后,再去获取结果。另外,FutureTask还可以确保即使调⽤了多次run⽅法,它都只会执⾏⼀次Runnable或者Callable任务,或者通过cancel取消FutureTask的执⾏等。
- futuretask可⽤于执⾏多任务、以及避免⾼并发情况下多次创建数据机锁的出现。
# 798.什么是Callable和Future?-Java面试题
- Callable 和 Future 是⽐较有趣的⼀对组合。当我们需要获取线程的执⾏结果时,就需要⽤到它们。Callable⽤于产⽣结果,Future⽤于获取结果。
- Callable接⼝使⽤泛型去定义它的返回类型。Executors类提供了⼀些有⽤的⽅法去在线程池中执⾏Callable内的任务。由于Callable任务是并⾏的,必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题。
- 在线程池提交Callable任务后返回了⼀个Future对象,使⽤它可以知道Callable任务的状态和得到Callable返回的执⾏结果。Future提供了get()⽅法,等待Callable结束并获取它的执⾏结果。
# 797. 什么是阻塞队列?如何使⽤阻塞队列来实现⽣产者-消费者模型?-Java面试题
- JDK7提供了7个阻塞队列。(也属于并发容器)
- ArrayBlockingQueue :⼀个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :⼀个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :⼀个⽀持优先级排序的⽆界阻塞队列。
- DelayQueue:⼀个使⽤优先级队列实现的⽆界阻塞队列。
- SynchronousQueue:⼀个不存储元素的阻塞队列。
- LinkedTransferQueue:⼀个由链表结构组成的⽆界阻塞队列。
- LinkedBlockingDeque:⼀个由链表结构组成的双向阻塞队列。
- 概念:阻塞队列是⼀个在队列基础上⼜⽀持了两个附加操作的队列。
- 2个附加操作:
⽀持阻塞的插⼊⽅法:队列满时,队列会阻塞插⼊元素的线程,直到队列不满。
⽀持阻塞的移除⽅法:队列空时,获取元素的线程会等待队列变为⾮空。
# 796.什么是Executors框架?-Java面试题
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPool创建⼀个可缓存线程池,如果线程池⻓度超过处理需要,可灵活回收空闲线程,若⽆可回收,则新建线程。
- newFixedThreadPool 创建⼀个定⻓线程池,可控制线程最⼤并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建⼀个定⻓线程池,⽀持定时及周期性任务执⾏。
- newSingleThreadExecutor 创建⼀个单线程化的线程池,它只会⽤唯⼀的⼯作线程来执⾏任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执⾏。
# 795. 什么是原⼦操作?在Java Concurrency API中有哪些原⼦类(atomic classes)?-Java面试题
- 原⼦操作是指⼀个不受其他操作影响的操作任务单元。原⼦操作是在多线程环境下避免数据不⼀致必须的⼿段。
- int++并不是⼀个原⼦操作,所以当⼀个线程读取它的值并加1时,另外⼀个线程有可能会读到之前的值,这就会引发错误。
- 为了解决这个问题,必须保证增加操作是原⼦的,在JDK1.5之前我们可以使⽤同步技术来做到这⼀点。
到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以⾃动的保证对于他们的操作是原⼦的并且不需要使⽤同步。
# 794.什么是AQS?-Java面试题
- AbstractQueuedSynchronizer简称AQS,是⼀个⽤于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的⼤量细节问题。
- AQS使⽤⼀个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。
其他的节点与等待线程关联,每个节点维护⼀个等待状态waitStatus。
# 793. 什么是乐观锁和悲观锁?-Java面试题
- 悲观锁
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使⽤⼀致的锁定协议来协调对共享状态的访问,可以确保⽆论哪个线程持有共享变量的锁,都采⽤独占的⽅式来访问这些变量。独占锁其实就是⼀种悲观锁,所以可以说synchronized是悲观锁。 - 乐观锁
乐观锁( Optimistic Locking)其实是⼀种思想。相对悲观锁⽽⾔,乐观锁假设认为数据⼀般情况下不会造成冲突,所以在数据进⾏提交更新的时候,才会正式对数据的冲突与否进⾏检测,如果发现冲突了,则让返回⽤户错误的信息,让⽤户决定如何去做。
memcached使⽤了cas乐观锁技术保证数据⼀致性。
# 792.什么是CAS?-Java面试题
- CAS(compare and swap)的缩写,中⽂翻译成⽐较并交换。
- CAS 不通过JVM,直接利⽤java本地⽅ JNI(Java Native Interface为JAVA本地调⽤),直接调⽤CPU 的cmpxchg(是汇编指令)指令。
- 利⽤CPU的CAS指令,同时借助JNI来完成Java的⾮阻塞算法,实现原⼦操作。其它原⼦操作都是利⽤类似的特性完成的。
- 整个java.util.concurrent都是建⽴在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很⼤的提升。
- CAS是项乐观锁技术,当多个线程尝试使⽤CAS同时更新同⼀个变量时,只有其中⼀个线程能更新变量的值,⽽其它线程都失败,失败的线程并不会被挂起,⽽是被告知这次竞争中失败,并可以再次尝试。
1、使⽤CAS在线程冲突严重时,会⼤幅降低程序性能;CAS只适合于线程冲突较少的情况使⽤。
2、synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是⾃旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了⾼吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;⽽线程冲突严重的情况下,性能远⾼于CAS。
# 791.什么是⾃旋锁?-Java面试题
⾃旋锁是SMP架构中的⼀种low-level的同步机制。
- 当线程A想要获取⼀把⾃旋锁⽽该锁⼜被其它线程锁持有时,线程A会在⼀个循环中⾃旋以检测锁是不是已经可⽤了。
- ⾃选锁需要注意:
由于⾃旋时不释放CPU,因⽽持有⾃旋锁的线程应该尽快释放⾃旋锁,否则等待该⾃旋锁的线程会⼀直在那⾥⾃旋,这就会浪费CPU时间。
持有⾃旋锁的线程在sleep之前应该释放⾃旋锁以便其它线程可以获得⾃旋锁。 - ⽬前的JVM实现⾃旋会消耗CPU,如果⻓时间不调⽤doNotify⽅法,doWait⽅法会⼀直⾃旋,CPU会消耗太⼤
- ⾃旋锁⽐较适⽤于锁使⽤者保持锁时间⽐较短的情况,这种情况⾃旋锁的效率⽐较⾼。
- ⾃旋锁是⼀种对多处理器相当有效的机制,⽽在单处理器⾮抢占式的系统中基本上没有作⽤。
# 790. 什么是线程安全和线程不安全?-Java面试题
- 线程安全
线程安全: 就是多线程访问时,采⽤了加锁机制,当⼀个线程访问该类的某个数据时,进⾏保护,其他线程能进⾏访问,直到该线程读取完,其他线程才可使⽤。不会出现数据不⼀致或者数据污染。
Vector 是⽤同步⽅法来实现线程安全的, ⽽和它相似的ArrayList不是线程安全的。
- 线程不安全
线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,⽽⽆写操作,⼀般来说,这个全局变量是线程安全的;若多个线程同时执⾏写操作,⼀般都需要考虑线程同步,否则的话就可能影响线程安全。
# 789.什么是线程?-Java面试题
线程是操作系统能够进⾏运算调度的最⼩单位,它被包含在进程之中,是进程中的实际运作单位,可以使⽤线程对进⾏运算提速。
⽐如,如果⼀个线程完成⼀个任务要100毫秒,那么⽤⼗个线程完成改任务只需10毫秒
# 788.提交任务时线程池队列已满会时发会生什么?-Java面试题
当线程数小于最大线程池数 maximumPoolSize 时就会创建新线程来处理,而线程数大于等于最大线程池数 maximumPoolSize 时就会执行拒绝策略
# 787.什么是阻塞式方法?-Java面试题
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
# 786.如何创建线程安全的单例模式?-Java面试题
单例模式即一个 JVM 内存中只存在一个类的对象实例分类
- 懒汉式
类加载的时候就创建实例 - 饿汉式
使用的时候才创建实例
# 785.你如何在 Java 中获取线程堆栈?-Java面试题
对于不同的操作系统,有多种方法来获得 Java 进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在 Windows 你可以使用 Ctrl + Break 组合键来获取线程堆栈,Linux 下用 kill -3 命令。你也可以用 jstack 这个工具来获取,它对线程 id 进行操作,你可以用 jps 这个工具找到 id。
# 784.Java 中堆和栈有什么不同-Java面试题
每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
# 783.为什么你应该在循环中检查等待条件?-Java面试题
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因,你可以在 Eclipse 中创建模板调用 wait和 notify 试一试。
# 782.为什么 wait 和 notify 方法要在同步块中调用?-Java面试题
主 要 是 因 为 Java API 强 制 要 求 这 样 做 , 如 果 你 不 这 么 做 , 你 的 代 码会 抛 出IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify之间产生竞态条件。
# 781.为什么 wait/notify/notifyAll 这些方法不在 thread 类里面?-Java面试题
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object类里是有意义的,还有不把它放在 Thread 类里的原因。一个很明显的原因是JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果 wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
# 780.Java 中 notify 和 notifyAll 有什么区别?-Java面试题
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。
而 notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
# 779.Runnable 和 Thread 用哪个好?-Java面试题
Java 不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Runnable 会更好。
# 778.什么是自旋锁?-Java面试题
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。
# 777.同步方法和同步块,哪种更好?-Java面试题
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
请知道一条原则:同步的范围越小越好。
# 776.Hashtable 的 size()方法为什么要做同步-Java面试题
同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程 A 在执行 Hashtable 的 put方法添加数据,线程 B 则可以正常调用 size()方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对size++,线程 B 就已经读取 size了,那么对于线程 B 来说读取到的 size 一定是不准确的。而给 size()方法加了同步之后,意味着线程 B 调用 size()方法只有在线程 A调用 put 方法完毕之后才可以调用,这样就保证了线程安全性CPU 执行代码,执行的不是 Java 代码,这点很关键,一定得记住。Java 代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到 Java 代码只有一行,甚至你看到 Java 代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。
# 775.什么是乐观锁和悲观锁?-Java面试题
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
# 774.Java 内存模型是什么,哪些区域是线程共享的,哪些是不共享 的?-Java面试题
我们知道的 JVM 内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享。线程不共享区域每个线程的数据区域包括程序计数器、虚拟机栈和本地方法栈,它们都是在新线程创建时才创建的。
程序计数器(Program Counter Rerister)
程序计数器区域一块内存较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。
虚拟机栈(VM Stack)
虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stack)
本地方法栈用于支持本地方法(native 标识的方法,即非 Java 语言实现的方法)。虚拟机栈和本地方法栈,当线程请求分配的栈容量超过 JVM 允许的最大容量时抛出StackOverflowError 异常。
线程共享区域
线程共享区域包含:堆和方法区。
堆(Heap)
堆是最常处理的区域,它存储在 JVM 启动时创建的数组和对象,JVM 垃圾收集也主要是在堆上面工作。如 果 实 际 所 需 的 堆 超 过 了 自 动 内 存 管 理 系 统 能 提 供 的 最 大 容 量 时 抛 出OutOfMemoryError 异常。
方法区(Method Area)
方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内存空间后就会抛出 OutOfMemoryError
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,每一个运行时常量池都分配在 JVM 的方法区中,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建。运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。如果方法区的内存空间不能满足内存分配请求,那 Java 虚 拟 机 将 抛 出 一 个OutOfMemoryError 异常。栈包含 Frames,当调用方法时,Frame 被推送到堆栈。一个 Frame 包含局部变量数组、操作数栈、常量池引用。
# 773.Thread.sleep(0)的作用是什么?-Java面试题
由于 Java 采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到 CPU控制权的情况,为了让某些优先级比较低的线程也能获取到 CPU 控制权,可以使用 Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡 CPU 控制权的一种操作。
# 772.Java 中用到了什么线程调度算法?-Java面试题
抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
# 771.多线程上下文切换是什么意思?-Java面试题
多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
# 770.不可变对象对多线程有什么帮助?-Java面试题
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
# 769.怎么唤醒一个阻塞的线程?-Java面试题
如果线程是因为调用了 wait()、sleep()或者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO是操作系统实现的,Java 代码并没有办法直接接触到操作系统。
# 768.FutureTask 是什么?-Java面试题
FutureTask 表示一个异步运算的任务,FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
# 767.ReadWriteLock 有什么用?-Java面试题
ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
# 766.ThreadLocal 是什么?有什么应用场景?-Java面试题
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。用来解决数据库连接、Session 管理等。
# 765.说几个常用的 Lock 接口实现锁-Java面试题
ReentrantLock、ReadWriteLock
# 764.保证”可见性”有哪几种方式?-Java面试题
synchronized 和 viotatile
# 763.线程之间如何传递数据?-Java面试题
通 过 在 线 程 之 间 共 享 对 象 就 可 以 了 , 然 后 通 过 wait/notify/notifyAll 、await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue 就是为线程之间共享数据而设计的
# 762.Fork/Join 框架使用有哪些要注意的地方?-Java面试题
如果任务拆解的很深,系统内的线程数量堆积,导致系统性能性能严重下降;
如果函数的调用栈很深,会导致栈内存溢出;
# 761.线程 wait()方法使用有什么前提-Java面试题
要在同步块中使用。
# 760.线程同步需要注意什么?-Java面试题
- 尽量缩小同步的范围,增加系统吞吐量。
- 分布式同步锁无意义,要使用分布式锁。
- 防止死锁,注意加锁顺序。
# 755.Jdk 中排查多线程问题用什么命令?-Java面试题
jstack
# 759.怎么检测一个线程是否拥有锁?-Java面试题
java.lang.Thread#holdsLock 方法
# 758.什么是 CAS 算法?在多线程中有哪些应用-Java面试题
CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当然 CAS 一定要 volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,永远都不可能成功。
java.util.concurrent.atomic 包下面的 Atom****类都有 CAS 算法的应用
# 757.说说线程安全的和不安全的集合。-Java面试题
Java 中平时用的最多的 Map 集合就是 HashMap 了,它是线程不安全的。看下面两个场景:
1、当用在方法内的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以这时也不存在线程安全不安全的问题了。
2、当用在单例对象成员变量的时候呢?这时候多个线程过来访问的就是同一个HashMap 了,对同个 HashMap 操作这时候就存在线程安全的问题了。
# 756.线程数过多会造成什么异常?-Java面试题
线程过多会造成栈溢出,也有可能会造成堆异常。
# 754.Fork/Join 框架是干什么的?-Java面试题
大任务自动分散小任务,并发执行,合并小任务结果。
# 753.Synchronized 有哪几种用法?-Java面试题
锁类、锁方法、锁代码块。
# 752.什么是重入锁?-Java面试题
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
# 751.线程 yield()方法有什么用?-Java面试题
Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。
它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
# 750.一个线程运行时发生异常会怎样?-Java面试题
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时 候 JVM 会 使 用Thread.getUncaughtExceptionHandler() 来 查 询 线程 的UncaughtExceptionHandler 并 将 线 程 和 异 常 作 为 参 数 传 递 给 handler 的uncaughtException()方法进行处理。
# 749.什么是守护线程?有什么用?-Java面试题
什么是守护线程?与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程才会跟着结束。也就是守护线程必须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会退出。
# 748.什么是原子性、可见性、有序性?-Java面试题
原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的。
原子性
原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量 n++100 次,如果 n 初始值为 0,n 最后的值应该是 100,所以说它们是互不干扰的,这就是传说的中的原子性。但 n++并不是原子性的操作,要使用 AtomicInteger 保证原子性。
可见性
可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主
内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像CPU 的缓存优化、硬件优化、指令重排及对 JVM 编译器的优化,都会出现可见性的问题。
有序性
我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高 CPU 的处理性能,JVM 和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所
以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。
# 747.什么是活锁、饥饿、无锁、死锁?-Java面试题
死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过 JDK 的CAS 原理及应用即是无锁的实现。
可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合下是非常高效的。
# 746.CyclicBarrier 和 CountDownLatch 的区别?-Java面试题
两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行
- CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
- CyclicBarrier 可 重 用 , CountDownLatch 不 可 重 用 , 计 数 值 为 0 该CountDownLatch就不可再用了
# 745.线程池启动线程 submit()和 execute()方法有什么不同-Java面试题
execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多。
submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常
# 744.常用的几种线程池并讲讲其中的工作原理。-Java面试题
什么是线程池?
很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。
线程池的好处
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。
线程池核心类
在 java.util.concurrent 包中我们能找到线程池的定义,其中 ThreadPoolExecutor 是我们线程池核心类,首先看看线程池类的主要参数有哪些。
如何提交线程
如 可 以 先 随 便 定 义 一 个 固 定 大 小 的 线 程 池
ExecutorService es = Executors.newFixedThreadPool(3);
提交一个线程
es.submit(xxRunnble);
es.execute(xxRunnble);
submit 和 execute 分别有什么区别呢?
execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很多。
submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。
如何关闭线程池
es.shutdown();
不再接受新的任务,之前提交的任务等执行结束再关闭线程池。
es.shutdownNow();
不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list 列表。
# 743.为什么要使用线程池-Java面试题
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。
# 742.怎么控制同一时间只有 3 个线程运行?-Java面试题
用 Semaphore。
# 741.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?-Java面试题
用 join 方法。
# 740.violatile 关键字的作用-Java面试题
volatile 关键字的作用主要有两个:
- 多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据
- 代码底层执行不像我们看到的高级语言—-Java 程序这么简单,它的执行是 Java代码–>字节码–>根据字节码执行对应的 C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率从实践角度而言,volatile 的一个重要 作 用 就 是 和 CAS 结 合 , 保 证 了 原 子 性 , 详 细 的 可 以 参 见java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
# 739.线程怎样拿到返回结果?-Java面试题
实现Callable 接口。
# 738.多线程之间如何进行通信?-Java面试题
wait/notify
# 737.什么是死锁?如何避免死锁?-Java面试题
死锁就是两个线程相互等待对方释放对象锁。
# 736.多线程同步有哪几种方法?-Java面试题
Synchronized 关键字,Lock 锁实现,分布式锁等
# 735.线程中的 wait()和 sleep()方法有什么区别-Java面试题
这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
# 734.一个线程的生命周期有哪几种状态?它们之间如何流转的?-Java面试题
NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。
RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。
BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。
WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行。
TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒后线程重新进行 RUNNABLE 状态继续运行。
TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到RUNNABLE 状
# 733.怎么终止一个线程?如何优雅地终止线程?-Java面试题
stop 终止,不推荐
# 732.启动线程方法 start()和 run()有什么区别-Java面试题
只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。
# 731.Java 实现线程有哪几种方式?-Java面试题
- 继承 Thread 类实现多线程
- 实现 Runnable 接口方式实现多线程
- 使用 ExecutorService、Callable、Future 实现有返回结果的多线程
# 730.线程和进程的区别是什么?-Java面试题
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
# 729.多线程有什么用?-Java面试题
发挥多核CPU 的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
# 728.volatile 类型变量提供什么保证?能使得一个非原子操作变成原子操作吗?-Java面试题
volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。在 Java 中除了 long 和 double 之外的所有基本类型的读和赋值,都是原子性操作。
而 64 位的 long 和 double 变量由于会被 JVM 当作两个分离的 32 位来进行操作,所以不具有原子性,会产生字撕裂问题。但是当你定义 long 或 double 变量时,如果使用 volatile 关键字,就会获到(简单的赋值与返回操作的)原子性
# 727.volatile 变量是什么?volatile 变量和 atomic 变量有什么不同?-Java面试题
volatile 则是保证了所修饰的变量的可见。因为 volatile 只是在保证了同一个变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量,即 Boolean 类型的变量。
volatile 多用于修饰类似开关类型的变量、Atomic 多用于类似计数器相关的变量、其它多线程并发操作用 synchronized 关键字修饰。
volatile 有两个功用:
- 这个变量不会在多个线程中存在复本,直接从内存读取。
- 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
# 726.多线程中的忙循环是什么?-Java面试题
忙循环就是程序员用循环让一个线程等待,不像传统方法 wait(), sleep() 或 yield() 它们都放弃了 CPU 控制,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做的目的是为了保留 CPU 缓存。
在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
# 725.什么是线程饿死,什么是活锁?-Java面试题
当所有线程阻塞,或者由于需要的资源无效而不能处理,不存在非阻塞线程使资源可用。JavaAPI 中线程活锁可能发生在以下情形:
- 当所有线程在序中执行 Object.wait(0),参数为 0 的 wait 方法。程序将发生活锁直到在相应的对象上有线程调用 Object.notify() 或者 Object.notifyAll()。
- 当所有线程卡在无限循环中。
# 724.线程调度和线程控制。-Java面试题
线程调度(优先级):
与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取 CPU 资源的概率较大,优先级低的并非没机会执行。线程的优先级用 1-10 之间的整数表示,数值越大优先级越高,默认的优先级为 5。 在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。
线程控制
sleep( ) // 线程休眠 join( ) // 线程加入 yield( ) // 线程礼让
setDaemon( ) // 线程守护
中断线程
stop( ) interrupt( ) ==(首先选用)==
# 723.请说出你所知道的线程同步的方法。-Java面试题
wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
# 722.run() 和 start() 区别。-Java面试题
run( ):只是调用普通 run 方法
start( ):启动了线程, 由 Jvm 调用 run 方法
启动一个线程是调用 start() 方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行。这并不意味着线程就会立即运行。run() 方法可以产生必须退出的标志来停止一个线程。
# 721.什么是 ThreadLocal?-Java面试题
ThreadLocal 用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。
每个线程都会拥有他们自己的 Thread 变量,它们可以使用 get()\set() 方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal 实例通常是希望它们同线程状态关联起来是 private static 属性。
# 720.概括的解释下线程的几种可用状态。-Java面试题
- 新建 new。
- 就绪 放在可运行线程池中,等待被线程调度选中,获取 cpu。
- 运行 获得了 cpu。
- 阻塞
- 等待阻塞 执行 wait() 。
- 同步阻塞 获取对象的同步琐时,同步锁被别的线程占用。
- 其他阻塞 执行了 sleep() 或 join() 方法)。
- 死亡。
# 719.简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?-Java面试题
主要相同点:Lock 能完成 synchronized 所实现的所有功能。
主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。
synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。Lock 还有更强大的功能,例如,它的 tryLock 方法可以非阻塞方式去拿锁。
举例说明(对下面的题用 lock 进行了改写)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
/**
* @param args
*/
private int j;
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
// TODO Auto-generated method stub
ThreadTest tt = new ThreadTest();
for(int i=0;i<2;i++)
{
new Thread(tt.new Adder()).start();
new Thread(tt.new Subtractor()).start();
}
}
private class Subtractor implements Runnable
{
@Override
public void run() {
// TODO Auto-generated method stub
while(true)
{
/*synchronized (ThreadTest.this) {
System.out.println("j--=" + j--);
//这里抛异常了,锁能释放吗?
}*/
lock.lock();
try
{
System.out.println("j--=" + j--);
}finally
{
lock.unlock();
}
}
}
}
private class Adder implements Runnable
{
@Override
public void run() {
// TODO Auto-generated method stub
while(true)
{
/*synchronized (ThreadTest.this) {
System.out.println("j++=" + j++);
}*/
lock.lock();
try
{
System.out.println("j++=" + j++);
}finally
{
lock.unlock();
}
}
}
}
}
# 718.当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其 它方法?-Java面试题
- 其他方法前是否加了 synchronized 关键字,如果没加,则能。
- 如果这个方法内部调用了 wait,则可以进入其他 synchronized 方法。
- 如果其他个方法都加了 synchronized 关键字,并且内部没有调用 wait,则不能。
- 如果其他方法是 static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是 this。
# 717.同步和异步有何异同,在什么情况下分别使用他们?-Java面试题
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率
# 716.sleep() 和 wait() 有什么区别-Java面试题
sleep 就是正在执行的线程主动让出 cpu,cpu 去执行其他线程,在 sleep 指定的时间过后,cpu 才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep 方法并不会释放锁,即使当前线程使用 sleep 方法让出了 cpu,但其他被同步锁挡住了的线程也无法得到执行。
wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了 notify 方法(notify 并不释放锁,只是告诉调用过 wait 方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果 notify 方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在 notfiy 方法后增加一个等待和一些代码,看看效果),调用 wait 方法的线程就会解除 wait 状态和程序可以再次得到锁后继续向下运行。
# 715. stop() 和 suspend() 方法为何不推荐使用?-Java面试题
反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。
suspend() 方法容易发生死锁。调用 suspend() 的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被 "挂起" 的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用 suspend(),而应在自己的 Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait() 命其进入等待状态。若标志指出线程应当恢复,则用一个 notify() 重新启动线程。
# 714.很 多 人 都 说 要 慎 用 ThreadLocal, 谈 谈 你 的 理 解 , 使 用 ThreadLocal 需 要 注 意 些 什 么 ?-Java面试题
使 用 ThreadLocal 要 注 意 remove!
ThreadLocal 的 实 现 是 基 于 一 个 所 谓 的 ThreadLocalMap, 在 ThreadLocalMap 中 , 它 的 key 是 一 个 弱 引 用 。
通 常 弱 引 用 都 会 和 引 用 队 列 配 合 清 理 机 制 使 用 , 但 是 ThreadLocal 是 个 例 外 , 它 并 没 有 这 么 做 。
这 意 味 着 , 废 弃 项 目 的 回 收 依 赖 于 显 式 地 触 发 , 否 则 就 要 等 待 线 程 结 束 , 进 而 回 收 相 应 ThreadLocalMap! 这 就 是 很 多 OOM 的 来 源 , 所 以 通 常 都 会 建 议 , 应 用 一 定 要 自 己 负 责 remove, 并 且 不 要 和 线 程 池 配 合 , 因 为 worker 线 程 往 往 是 不 会 退 出 的
# 713.请 谈 谈 ThreadLocal 是 怎 么 解 决 并 发 安 全 的 ?-Java面试题
ThreadLocal 这 是 Java 提 供 的 一 种 保 存 线 程 私 有 信 息 的 机 制 , 因 为 其 在 整 个 线 程 生 命 周 期 内 有 效 , 所 以 可 以 方 便 地 在 一 个 线 程 关 联 的 不 同 业 务 模 块 之 间 传 递 信 息 , 比 如 事 务 ID、 Cookie 等 上 下 文 相 关 信 息 。
ThreadLocal 为 每 一 个 线 程 维 护 变 量 的 副 本 , 把 共 享 数 据 的 可 见 范 围 限 制 在 同 一 个 线 程 之 内 , 其 实 现 原 理 是 , 在 ThreadLocal 类 中 有 一 个 Map, 用 于 存 储 每 一 个 线 程 的 变 量 的 副 本 。
# 712.请 对 比 下 volatile 对 比 Synchronized 的 异 同 。-Java面试题
Synchronized 既 能 保 证 可 见 性 , 又 能 保 证 原 子 性 , 而 volatile 只 能 保 证 可 见 性 , 无 法 保 证 原 子 性 。
ThreadLocal 和 Synchonized 都 用 于 解 决 多 线 程 并 发 访 问 , 防 止 任 务 在 共 享 资 源 上 产 生 冲 突 。 但 是 ThreadLocal 与 Synchronized 有 本 质 的 区 别 。
Synchronized 用 于 实 现 同 步 机 制 , 是 利 用 锁 的 机 制 使 变 量 或 代 码 块 在 某 一 时 该 只 能 被 一 个 线 程 访 问 , 是 一 种 “ 以 时 间 换 空 间 ” 的 方 式 。
而 ThreadLocal 为 每 一 个 线 程 都 提 供 了 变 量 的 副 本 , 使 得 每 个 线 程 在 某 一 时 间 访 问 到 的 并 不 是 同 一 个 对 象 , 根 除 了 对 变 量 的 共 享 , 是 一 种“ 以 空 间 换 时 间 ” 的 方 式 。
# 711.既 然 volatile 能 够 保 证 线 程 间 的 变 量 可 见 性 , 是 不 是 就 意 味 着 基 于 volatile 变 量 的 运 算 就 是 并 发 安 全 的-Java面试题
显 然 不 是 的 。 基 于 volatile 变 量 的 运 算 在 并 发 下 不 一 定 是 安 全 的 。volatile 变 量 在 各 个 线 程 的 工 作 内 存 , 不 存 在 一 致 性 问 题 ( 各 个 线 程 的工 作 内 存 中 volatile 变 量 , 每 次 使 用 前 都 要 刷 新 到 主 内 存 ) 。
但 是 Java 里 面 的 运 算 并 非 原 子 操 作 , 导 致 volatile 变 量 的 运 算 在 并发 下 一 样 是 不 安 全 的 。
# 710.请 谈 谈 volatile 有 什 么 特 点 , 为 什 么 它 能 保 证 变 量 对 所 有 线 程 的 可 见 性-Java面试题
关 键 字 volatile 是 Java 虚 拟 机 提 供 的 最 轻 量 级 的 同 步 机 制 。 当 一 个 变 量 被 定 义 成 volatile 之 后 , 具 备 两 种 特 性 :
- 保 证 此 变 量 对 所 有 线 程 的 可 见 性 。 当 一 条 线 程 修 改 了 这 个 变 量 的 值 , 新 值 对 于 其 他 线 程 是 可 以 立 即 得 知 的 。 而 普 通 变 量 做 不 到 这 一 点 。
- 禁 止 指 令 重 排 序 优 化 。 普 通 变 量 仅 仅 能 保 证 在 该 方 法 执 行 过 程 中 , 得 到正 确 结 果 , 但 是 不 保 证 程 序 代 码 的 执 行 顺 序 。
Java 的 内 存 模 型 定 义 了 8 种 内 存 间 操 作 :
lock 和 unlock
- 把 一 个 变 量 标 识 为 一 条 线 程 独 占 的 状 态 。
- 把 一 个 处 于 锁 定 状 态 的 变 量 释 放 出 来 , 释 放 之 后 的 变 量 才 能 被 其 他 线 程 锁 定 。
read 和 write
- 把 一 个 变 量 值 从 主 内 存 传 输 到 线 程 的 工 作 内 存 , 以 便 load。
- 把 store 操 作 从 工 作 内 存 得 到 的 变 量 的 值 , 放 入 主 内 存 的 变 量 中 。
load 和 store
- 把 read 操 作 从 主 内 存 得 到 的 变 量 值 放 入 工 作 内 存 的 变 量 副 本 中 。
- 把 工 作 内 存 的 变 量 值 传 送 到 主 内 存 , 以 便 write。
use 和 assgin
- 把 工 作 内 存 变 量 值 传 递 给 执 行 引 擎 。
- 将 执 行 引 擎 值 传 递 给 工 作 内 存 变 量 值 。
volatile 的 实 现 基 于 这 8 种 内 存 间 操 作 , 保 证 了 一 个 线 程 对 某 个 volatile 变 量 的 修 改 , 一 定 会 被 另 一 个 线 程 看 见 , 即 保 证 了 可 见 性 。
# 709.什 么 是 Java 的 内 存 模 型 , Java 中 各 个 线 程 是 怎 么 彼 此 看 到 对 方 的 变 量 的 ?-Java面试题
Java 的 内 存 模 型 定 义 了 程 序 中 各 个 变 量 的 访 问 规 则 , 即 在 虚 拟 机 中 将 变 量 存 储 到 内 存 和 从 内 存 中 取 出 这 样 的 底 层 细 节 。
此 处 的 变 量 包 括 实 例 字 段 、 静 态 字 段 和 构 成 数 组 对 象 的 元 素 , 但 是 不 包括 局 部 变 量 和 方 法 参 数 , 因 为 这 些 是 线 程 私 有 的 , 不 会 被 共 享 , 所 以 不存 在 竞 争 问 题 。
Java 中 各 个 线 程 是 怎 么 彼 此 看 到 对 方 的 变 量 的 呢 ? Java 中 定 义 了 主 内 存 与 工 作 内 存 的 概 念 :
所 有 的 变 量 都 存 储 在 主 内 存 , 每 条 线 程 还 有 自 己 的 工 作 内 存 , 保 存 了 被该 线 程 使 用 到 的 变 量 的 主 内 存 副 本 拷 贝 。
线 程 对 变 量 的 所 有 操 作 ( 读 取 、 赋 值 ) 都 必 须 在 工 作 内 存 中 进 行 , 不 能直 接 读 写 主 内 存 的 变 量 。 不 同 的 线 程 之 间 也 无 法 直 接 访 问 对 方 工 作 内 存的 变 量 , 线 程 间 变 量 值 的 传 递 需 要 通 过 主 内 存 。
# 708.如 何 在 Java 线 程 池 中 提 交 线 程 ?-Java面试题
线 程 池 最 常 用 的 提 交 任 务 的 方 法 有 两 种 :
- execute(): ExecutorService.execute 方 法 接 收 一 个 Runable 实 例 , 它 用 来 执 行 一 个 任 务
- submit(): ExecutorService.submit() 方 法 返 回 的 是 Future 对 象 。 可 以 用 isDone() 来 查 询 Future 是 否 已 经 完 成 , 当 任 务 完 成 时 ,它 具 有 一 个 结 果 , 可 以 调 用 get() 来 获 取 结 果 。 也 可 以 不 用 isDone() 进 行 检 查 就 直 接 调 用 get(), 在 这 种 情 况 下 , get() 将 阻 塞 , 直 至 结 果 准 备 就 绪 。
# 707.既 然 提 到 可 以 通 过 配 置 不 同 参 数 创 建 出 不 同 的 线 程 池 , 那 么 Java 中 默 认 实 现 好 的 线 程 池 又 有 哪 些 呢 ? 请 比 较 它 们 的 异 同 。-Java面试题
1. SingleThreadExecutor 线 程 池
这 个 线 程 池 只 有 一 个 核 心 线 程 在 工 作 , 也 就 是 相 当 于 单 线 程 串 行 执 行 所有 任 务 。 如 果 这 个 唯 一 的 线 程 因 为 异 常 结 束 , 那 么 会 有 一 个 新 的 线 程 来 替 代 它 。 此 线 程 池 保 证 所 有 任 务 的 执 行 顺 序 按 照 任 务 的 提 交 顺 序 执 行 。
- corePoolSize: 1, 只 有 一 个 核 心 线 程 在 工 作 。
- maximumPoolSize: 1。
- keepAliveTime: 0L。
- workQueue: new LinkedBlockingQueue
<Runnable>
(), 其 缓 冲 队 列是 无 界 的 。
2. FixedThreadPool 线 程 池
FixedThreadPool 是 固 定 大 小 的 线 程 池 , 只 有 核 心 线 程 。 每 次 提 交 一 个 任 务 就 创 建 一 个 线 程 , 直 到 线 程 达 到 线 程 池 的 最 大 大 小 。 线 程 池 的 大 小 一 旦 达 到 最 大 值 就 会 保 持 不 变 , 如 果 某 个 线 程 因 为 执 行 异 常 而 结 束 , 那 么 线 程 池 会 补 充 一 个 新 线 程 。
FixedThreadPool 多 数 针 对 一 些 很 稳 定 很 固 定 的 正 规 并 发 线 程 , 多 用 于 服 务 器 。
- corePoolSize: nThreads
- maximumPoolSize: nThreads
- keepAliveTime: 0L
- workQueue: new LinkedBlockingQueue
<Runnable>
(), 其 缓 冲 队 列是 无 界 的 。
3. CachedThreadPool 线 程 池
CachedThreadPool 是 无 界 线 程 池 , 如 果 线 程 池 的 大 小 超 过 了 处 理 任 务 所 需 要 的 线 程 , 那 么 就 会 回 收 部 分 空 闲 ( 60 秒 不 执 行 任 务 ) 线 程 , 当 任 务 数 增 加 时 , 此 线 程 池 又 可 以 智 能 的 添 加 新 线 程 来 处 理 任 务 。
线 程 池 大 小 完 全 依 赖 于 操 作 系 统 ( 或 者 说 JVM) 能 够 创 建 的 最 大 线 程 大 小 。 SynchronousQueue 是 一 个 是 缓 冲 区 为 1 的 阻 塞 队 列 。
缓 存 型 池 子 通 常 用 于 执 行 一 些 生 存 期 很 短 的 异 步 型 任 务 , 因 此 在 一 些 面 向 连 接 的 daemon 型 SERVER 中 用 得 不 多 。 但 对 于 生 存 期 短 的 异 步任 务 , 它 是 Executor 的 首 选 。
- corePoolSize: 0
- maximumPoolSize: Integer.MAX_VALUE
- keepAliveTime: 60L
- workQueue: new SynchronousQueue
<Runnable>
(), 一 个 是 缓 冲 区为 1 的 阻 塞 队 列 。
4. ScheduledThreadPool 线 程 池
ScheduledThreadPool: 核 心 线 程 池 固 定 , 大 小 无 限 的 线 程 池 。 此 线 程 池 支 持 定 时 以 及 周 期 性 执 行 任 务 的 需 求 。 创 建 一 个 周 期 性 执 行 任 务 的 线 程 池 。 如 果 闲 置 , 非 核 心 线 程 池 会 在 DEFAULT_KEEPALIVEMILLIS 时 间 内 回 收 。
- corePoolSize: corePoolSize
- maximumPoolSize: Integer.MAX_VALUE
- keepAliveTime: DEFAULT_KEEPALIVE_MILLIS
- workQueue: new DelayedWorkQueue()
# 706.线 程 池 中 的 线 程 是 怎 么 创 建 的 ? 是 一 开 始 就 随 着 线 程 池 的 启 动 创 建 好 的 吗 ?-Java面试题
显 然 不 是 的 。 线 程 池 默 认 初 始 化 后 不 启 动 Worker, 等 待 有 请 求 时 才 启 动 。
每 当 我 们 调 用 execute() 方 法 添 加 一 个 任 务 时 , 线 程 池 会 做 如 下 判 断 :
- 如 果 正 在 运 行 的 线 程 数 量 小 于 corePoolSize, 那 么 马 上 创 建 线 程 运 行 这 个 任 务 ;
- 如 果 正 在 运 行 的 线 程 数 量 大 于 或 等 于 corePoolSize, 那 么 将 这 个 任 务 放 入 队 列 ;
- 如 果 这 时 候 队 列 满 了 , 而 且 正 在 运 行 的 线 程 数 量 小 于 maximumPoolSize, 那 么 还 是 要 创 建 非 核 心 线 程 立 刻 运 行 这 个 任 务 ;
- 如 果 队 列 满 了 , 而 且 正 在 运 行 的 线 程 数 量 大 于 或 等 于maximumPoolSize, 那 么 线 程 池 会 抛 出 异 常 RejectExecutionException。
当 一 个 线 程 完 成 任 务 时 , 它 会 从 队 列 中 取 下 一 个 任 务 来 执 行 。 当 一 个 线 程 无 事 可 做 , 超 过 一 定 的 时 间 ( keepAliveTime) 时 , 线 程 池 会 判 断 。
如 果 当 前 运 行 的 线 程 数 大 于 corePoolSize, 那 么 这 个 线 程 就 被 停 掉 。所 以 线 程 池 的 所 有 任 务 完 成 后 , 它 最 终 会 收 缩 到 corePoolSize 的 大 小 。
# 705.创 建 线 程 池 的 几 个 核 心 构 造 参 数 ?-Java面试题
Java 中 的 线 程 池 的 创 建 其 实 非 常 灵 活 , 我 们 可 以 通 过 配 置 不 同 的 参 数 , 创 建 出 行 为 不 同 的 线 程 池 , 这 几 个 参 数 包 括 :
- corePoolSize: 线 程 池 的 核 心 线 程 数 。
- maximumPoolSize: 线 程 池 允 许 的 最 大 线 程 数 。
- keepAliveTime: 超 过 核 心 线 程 数 时 闲 置 线 程 的 存 活 时 间 。
- workQueue: 任 务 执 行 前 保 存 任 务 的 队 列 , 保 存 由 execute 方 法 提 交 的 Runnable 任 务 。
# 704.Java 中 的 线 程 池 是 如 何 实 现 的 ?-Java面试题
- 在 Java 中 , 所 谓 的 线 程 池 中 的 “ 线 程 ” , 其 实 是 被 抽 象 为 了 一 个 静 态 内 部 类 Worker, 它 基 于 AQS 实 现 , 存 放 在 线 程 池 的 HashSet
<Worker>
workers 成 员 量 中 ; - 而 需 要 执 行 的 任 务 则 存 放 在 成 员 变 量 workQueue( BlockingQueue
<Runnable>
workQueue) 中 。这 样 , 整 个 线 程 池 实 现 的 基 本 思 想 就 是 : 从 workQueue 中 不 断 取 出需 要 执 行 的 任 务 , 放 在 Workers 中 进 行 处 理 。
# 703. CyclicBarrier 和 CountDownLatch 看 起 来 很 相 似 , 请 对 比 下 呢-Java面试题
它 们 的 行 为 有 一 定 相 似 度 , 区 别 主 要 在 于 :
- CountDownLatch 是 不 可 以 重 置 的 , 所 以 无 法 重 用 , CyclicBarrier 没有 这 种 限 制 , 可 以 重 用 。
- CountDownLatch 的 基 本 操 作 组 合 是 countDown/await, 调 用 await 的 线 程 阻 塞 等 待 countDown 足 够 的 次 数 , 不 管 你 是 在 一 个 线 程 还 是 多 个 线 程 里 countDown, 只 要 次 数 足 够 即 可 。 CyclicBarrier 的 基 本 操 作 组 合 就 是 await, 当 所 有 的 伙 伴 都 调 用 了 await, 才 会 继 续 进 行 任 务 , 并 自 动 进 行 重 置 。CountDownLatch 目 的 是 让 一 个 线 程 等 待 其 他 N 个 线 程 达 到 某 个 条件 后 , 自 己 再 去 做 某 个 事 ( 通 过 CyclicBarrier 的 第 二 个 构 造 方 法public CyclicBarrier(int parties, Runnable barrierAction), 在 新 线 程 里 做 事 可 以 达 到 同 样 的 效 果 ) 。 而 CyclicBarrier 的 目 的 是 让 N 多 线 程 互 相 等 待 直 到 所 有 的 都 达 到 某 个 状 态 , 然 后 这 N 个 线 程 再 继 续 执 行 各 自 后 续 ( 通 过 CountDownLatch 在 某 些 场 合 也 能 完 成 类 似 的 效 果 ) 。
# 702. 如 何 让 Java 的 线 程 彼 此 同 步 ? 你 了 解 过 哪 些 同 步 器 ? 请 分 别 介 绍 下 。-Java面试题
JUC 中 的 同 步 器 三 个 主 要 的 成 员 : CountDownLatch、 CyclicBarrier 和 Semaphore, 通 过 它 们 可 以 方 便 地 实 现 很 多 线 程 之 间 协 作 的 功 能 。
CountDownLatch 叫 倒 计 数 , 允 许 一 个 或 多 个 线 程 等 待 某 些 操 作 完 成 。 看 几 个 场 景 :
- 跑 步 比 赛 , 裁 判 需 要 等 到 所 有 的 运 动 员 ( “ 其 他 线 程 ” ) 都 跑 到 终 点( 达 到 目 标 ) , 才 能 去 算 排 名 和 颁 奖 。
- 模 拟 并 发 , 我 需 要 启 动 100 个 线 程 去 同 时 访 问 某 一 个 地 址 , 我 希 望 它们 能 同 时 并 发 , 而 不 是 一 个 一 个 的 去 执 行 。
用 法 : CountDownLatch 构 造 方 法 指 明 计 数 数 量 , 被 等 待 线 程 调 用 countDown 将 计 数 器 减 1, 等 待 线 程 使 用 await 进 行 线 程 等 待 。 一 个 简 单 的 例 子 :
CyclicBarrier 叫 循 环 栅 栏 , 它 实 现 让 一 组 线 程 等 待 至 某 个 状 态 之 后 再 全 部 同 时 执 行 , 而 且 当 所 有 等 待 线 程 被 释 放 后 , CyclicBarrier 可 以 被 重 复 使 用 。 CyclicBarrier 的 典 型 应 用 场 景 是 用 来 等 待 并 发 线 程 结 束 。
CyclicBarrier 的 主 要 方 法 是 await(), await() 每 被 调 用 一 次 , 计 数 便 会 减 少 1, 并 阻 塞 住 当 前 线 程 。 当 计 数 减 至 0 时 , 阻 塞 解 除 , 所 有 在 此 CyclicBarrier 上 面 阻 塞 的 线 程 开 始 运 行 。
在 这 之 后 , 如 果 再 次 调 用 await(), 计 数 就 又 会 变 成 N-1, 新 一 轮 重 新开 始 , 这 便 是 Cyclic 的 含 义 所 在 。 CyclicBarrier.await() 带 有 返 回 值 , 用 来 表 示 当 前 线 程 是 第 几 个 到 达 这 个 Barrier 的 线 程 。举 例 说 明 如 下 :
Semaphore, Java 版 本 的 信 号 量 实 现 , 用 于 控 制 同 时 访 问 的 线 程 个 数 , 来 达 到 限 制 通 用 资 源 访 问 的 目 的 , 其 原 理 是 通 过 acquire() 获 取 一 个 许 可 , 如 果 没 有 就 等 待 , 而 release() 释 放 一 个 许 可 。
如 果 Semaphore 的 数 值 被 初 始 化 为 1, 那 么 一 个 线 程 就 可 以 通 过 acquire 进 入 互 斥 状 态 , 本 质 上 和 互 斥 锁 是 非 常 相 似 的 。 但 是 区 别 也 非 常 明 显 , 比 如 互 斥 锁 是 有 持 有 者 的 , 而 对 于 Semaphore 这 种 计 数 器 结 构 , 虽 然 有 类 似 功 能 , 但 其 实 不 存 在 真 正 意 义 的 持 有 者 , 除 非 我 们 进 行 扩 展 包 装 。
# 701. 请 谈 谈 ReadWriteLock 和 StampedLock。-Java面试题
虽 然 ReentrantLock 和 Synchronized 简 单 实 用 , 但 是 行 为 上 有 一 定 局 限 性 , 要 么 不 占 , 要 么 独 占 。 实 际 应 用 场 景 中 , 有 时 候 不 需 要 大 量 竞 争 的 写 操 作 , 而 是 以 并 发 读 取 为 主 , 为 了 进 一 步 优 化 并 发 操 作 的 粒 度 , Java 提 供 了 读 写 锁 。
读 写 锁 基 于 的 原 理 是 多 个 读 操 作 不 需 要 互 斥 , 如 果 读 锁 试 图 锁 定 时 , 写锁 是 被 某 个 线 程 持 有 , 读 锁 将 无 法 获 得 , 而 只 好 等 待 对 方 操 作 结 束 , 这样 就 可 以 自 动 保 证 不 会 读 取 到 有 争 议 的 数 据 。
ReadWriteLock 代 表 了 一 对 锁 , 下 面 是 一 个 基 于 读 写 锁 实 现 的 数 据 结 构 , 当 数 据 量 较 大 , 并 发 读 多 、 并 发 写 少 的 时 候 , 能 够 比 纯 同 步 版 本 凸 显 出 优 势 :
读 写 锁 看 起 来 比 Synchronized 的 粒 度 似 乎 细 一 些 , 但 在 实 际 应 用 中 , 其 表 现 也 并 不 尽 如 人 意 , 主 要 还 是 因 为 相 对 比 较 大 的 开 销 。
所 以 , JDK 在 后 期 引 入 了 StampedLock, 在 提 供 类 似 读 写 锁 的 同 时 ,还 支 持 优 化 读 模 式 。 优 化 读 基 于 假 设 , 大 多 数 情 况 下 读 操 作 并 不 会 和 写 操 作 冲 突 , 其 逻 辑 是 先 试 着 修 改 , 然 后 通 过 validate 方 法 确 认 是 否 进 入 了 写 模 式 , 如 果 没 有 进 入 , 就 成 功 避 免 了 开 销 ; 如 果 进 入 , 则 尝 试 获 取 读 锁
# 700.除 了 ReetrantLock, 你 还 接 触 过 JUC 中 的 哪 些 并 发 工 具 ?-Java面试题
通 常 所 说 的 并 发 包 ( JUC) 也 就 是 java.util.concurrent 及 其 子 包 , 集中 了 Java 并 发 的 各 种 基 础 工 具 类 , 具 体 主 要 包 括 几 个 方 面 :
- 提 供 了 CountDownLatch、 CyclicBarrier、 Semaphore 等 , 比 Synchronized 更 加 高 级 , 可 以 实 现 更 加 丰 富 多 线 程 操 作 的 同 步 结 构 。
- 提 供 了 ConcurrentHashMap、 有 序 的 ConcunrrentSkipListMap, 或 者 通 过 类 似 快 照 机 制 实 现 线 程 安 全 的 动 态 数 组 CopyOnWriteArrayList 等 , 各 种 线 程 安 全 的 容 器 。
- 提 供 了 ArrayBlockingQueue、 SynchorousQueue 或 针 对 特 定 场 景 的PriorityBlockingQueue 等 , 各 种 并 发 队 列 实 现 。
- 强 大 的 Executor 框 架 , 可 以 创 建 各 种 不 同 类 型 的 线 程 池 , 调 度 任 务 运行 等 。
# 699.ReentrantLock 是 如 何 实 现 可 重 入 性 的 ?-Java面试题
ReentrantLock 内 部 自 定 义 了 同 步 器 Sync( Sync 既 实 现 了 AQS,又 实 现 了 AOS, 而 AOS 提 供 了 一 种 互 斥 锁 持 有 的 方 式 ) , 其 实 就 是 加 锁 的 时 候 通 过 CAS 算 法 , 将 线 程 对 象 放 到 一 个 双 向 链 表 中 , 每 次 获 取 锁 的 时 候 , 看 下 当 前 维 护 的 那 个 线 程 ID 和 当 前 请 求 的 线 程 ID 是 否 一 样 , 一 样 就 可 重 入 了 。
# 698.请 尽 可 能 详 尽 地 对 比 下 Synchronized 和 ReentrantLock 的 异 同 。-Java面试题
ReentrantLock 是 Lock 的 实 现 类 , 是 一 个 互 斥 的 同 步 锁 。
从 功 能 角 度 , ReentrantLock 比 Synchronized 的 同 步 操 作 更 精 细( 因 为 可 以 像 普 通 对 象 一 样 使 用 ) , 甚 至 实 现 Synchronized 没 有 的高 级 功 能 , 如 :
- 等 待 可 中 断 : 当 持 有 锁 的 线 程 长 期 不 释 放 锁 的 时 候 , 正 在 等 待 的 线 程 可以 选 择 放 弃 等 待 , 对 处 理 执 行 时 间 非 常 长 的 同 步 块 很 有 用 。
- 带 超 时 的 获 取 锁 尝 试 : 在 指 定 的 时 间 范 围 内 获 取 锁 , 如 果 时 间 到 了 仍 然无 法 获 取 则 返 回 。
- 可 以 判 断 是 否 有 线 程 在 排 队 等 待 获 取 锁 。
- 可 以 响 应 中 断 请 求 : 与 Synchronized 不 同 , 当 获 取 到 锁 的 线 程 被 中 断 时 , 能 够 响 应 中 断 , 中 断 异 常 将 会 被 抛 出 , 同 时 锁 会 被 释 放 。
- 可 以 实 现 公 平 锁 。
从 锁 释 放 角 度 , Synchronized 在 JVM 层 面 上 实 现 的 , 不 但 可 以 通 过 一 些 监 控 工 具 监 控 Synchronized 的 锁 定 , 而 且 在 代 码 执 行 出 现 异 常 时 , JVM 会 自 动 释 放 锁 定 ; 但 是 使 用 Lock 则 不 行 , Lock 是 通 过 代 码 实 现 的 , 要 保 证 锁 定 一 定 会 被 释 放 , 就 必 须 将 unLock() 放 到 finally{} 中 。
从 性 能 角 度 , Synchronized 早 期 实 现 比 较 低 效 , 对 比 ReentrantLock, 大 多 数 场 景 性 能 都 相 差 较 大 。
但 是 在 Java 6 中 对 其 进 行 了 非 常 多 的 改 进 , 在 竞 争 不 激 烈 时 ,Synchronized 的 性 能 要 优 于 ReetrantLock; 在 高 竞 争 情 况 下 ,Synchronized 的 性 能 会 下 降 几 十 倍 , 但 是 ReetrantLock 的 性 能 能 维 持 常 态 。
# 697.那 么 请 谈 谈 AQS 框 架 是 怎 么 回 事 儿 ?-Java面试题
AQS( AbstractQueuedSynchronizer 类 ) 是 一 个 用 来 构 建 锁 和 同 步 器 的 框 架 , 各 种 Lock 包 中 的 锁 ( 常 用 的 有 ReentrantLock、ReadWriteLock) , 以 及 其 他 如Semaphore、 CountDownLatch, 甚至 是 早 期 的 FutureTask 等 , 都 是 基 于 AQS 来 构 建 。
- AQS 在 内 部 定 义 了 一 个 volatile int state 变 量 , 表 示 同 步 状 态 : 当 线 程 调 用 lock 方 法 时 , 如 果 state=0, 说 明 没 有 任 何 线 程 占 有 共 享 资 源 的 锁 , 可 以 获 得 锁 并 将 state=1; 如 果 state=1, 则 说 明 有 线 程 目 前 正 在 使 用 共 享 变 量 , 其 他 线 程 必 须 加 入 同 步 队 列 进 行 等 待 。
- AQS 通 过 Node 内 部 类 构 成 的 一 个 双 向 链 表 结 构 的 同 步 队 列 , 来 完 成 线 程 获 取 锁 的 排 队 工 作 , 当 有 线 程 获 取 锁 失 败 后 , 就 被 添 加 到 队 列 末 尾 。
Node 类 是 对 要 访 问 同 步 代 码 的 线 程 的 封 装 , 包 含 了 线 程 本 身 及 其 状 态 叫waitStatus( 有 五 种 不 同 取 值 , 分 别 表 示 是 否 被 阻 塞 , 是 否 等 待 唤 醒 ,是 否 已 经 被 取 消 等 ) , 每 个 Node 结 点 关 联 其 prev 结 点 和 next 结点 , 方 便 线 程 释 放 锁 后 快 速 唤 醒 下 一 个 在 等 待 的 线 程 , 是 一 个 FIFO 的 过程 。
Node 类 有 两 个 常 量 , SHARED 和 EXCLUSIVE, 分 别 代 表 共 享 模 式 和 独占 模 式 。 所 谓 共 享 模 式 是 一 个 锁 允 许 多 条 线 程 同 时 操 作 ( 信 号 量 Semaphore 就 是 基 于 AQS 的 共 享 模 式 实 现 的 ) , 独 占 模 式 是 同 一 个 时 间 段 只 能 有 一 个 线 程 对 共 享 资 源 进 行 操 作 , 多 余 的 请 求 线 程 需 要 排 队 等 待( 如 ReentranLock) 。
- AQS 通 过 内 部 类 ConditionObject 构 建 等 待 队 列 ( 可 有 多 个 ) , 当 Condition 调 用 wait() 方 法 后 , 线 程 将 会 加 入 等 待 队 列 中 , 而 当 Condition 调 用 signal() 方 法 后 , 线 程 将 从 等 待 队 列 转 移 动 同 步 队 列 中 进 行 锁 竞 争 。
- AQS 和 Condition 各 自 维 护 了 不 同 的 队 列 , 在 使 用 Lock 和Condition 的 时 候 , 其 实 就 是 两 个 队 列 的 互 相 移 动 。
# 696.跟 Synchronized 相 比 , 可 重 入 锁 ReentrantLock 其 实 现 原 理 有 什 么 不 同-Java面试题
其 实 , 锁 的 实 现 原 理 基 本 是 为 了 达 到 一 个 目 的 :
让 所 有 的 线 程 都 能 看 到 某 种 标 记 。
Synchronized 通 过 在 对 象 头 中 设 置 标 记 实 现 了 这 一 目 的 , 是 一 种 JVM 原 生 的 锁 实 现 方 式 , 而 ReentrantLock 以 及 所 有 的 基 于 Lock 接 口 的 实 现 类 , 都 是 通 过 用 一 个 volitile 修 饰 的 int 型 变 量 , 并 保 证 每 个 线 程 都 能 拥 有 对 该 int 的 可 见 性 和 原 子 修 改 , 其 本 质 是 基 于 所 谓 的 AQS 框 架 。
# 695. 乐 观 锁 一 定 就 是 好 的 吗 ?-Java面试题
乐 观 锁 避 免 了 悲 观 锁 独 占 对 象 的 现 象 , 同 时 也 提 高 了 并 发 性 能 , 但 它 也有 缺 点 :
- 乐 观 锁 只 能 保 证 一 个 共 享 变 量 的 原 子 操 作 。 如 果 多 一 个 或 几 个 变 量 , 乐观 锁 将 变 得 力 不 从 心 , 但 互 斥 锁 能 轻 易 解 决 , 不 管 对 象 数 量 多 少 及 对 象颗 粒 度 大 小 。
- 长 时 间 自 旋 可 能 导 致 开 销 大 。 假 如 CAS 长 时 间 不 成 功 而 一 直 自 旋 , 会给 CPU 带 来 很 大 的 开 销 。
- ABA 问 题 。 CAS 的 核 心 思 想 是 通 过 比 对 内 存 值 与 预 期 值 是 否 一 样 而 判断 内 存 值 是 否 被 改 过 , 但 这 个 判 断 逻 辑 不 严 谨 , 假 如 内 存 值 原 来 是 A,后 来 被 一 条 线 程 改 为 B, 最 后 又 被 改 成 了 A, 则 CAS 认 为 此 内 存 值 并 没 有 发 生 改 变 , 但 实 际 上 是 有 被 其 他 线 程 改 过 的 , 这 种 情 况 对 依 赖 过 程 值 的 情 景 的 运 算 结 果 影 响 很 大 。 解 决 的 思 路 是 引 入 版 本 号 , 每 次 变 量 更 新 都 把 版 本 号 加 一 。
# 694.为 什 么 说 Synchronized 是 一 个 悲 观 锁 ? 乐 观 锁 的 实 现 原 理 又 是 什 么 ? 什 么 是 CAS, 它 有 什 么 特 性 ?-Java面试题
Synchronized 显 然 是 一 个 悲 观 锁 , 因 为 它 的 并 发 策 略 是 悲 观 的 :
不 管 是 否 会 产 生 竞 争 , 任 何 的 数 据 操 作 都 必 须 要 加 锁 、 用 户 态 核 心 态 转换 、 维 护 锁 计 数 器 和 检 查 是 否 有 被 阻 塞 的 线 程 需 要 被 唤 醒 等 操 作 。随 着 硬 件 指 令 集 的 发 展 , 我 们 可 以 使 用 基 于 冲 突 检 测 的 乐 观 并 发 策 略 。
先 进 行 操 作 , 如 果 没 有 其 他 线 程 征 用 数 据 , 那 操 作 就 成 功 了 ;
如 果 共 享 数 据 有 征 用 , 产 生 了 冲 突 , 那 就 再 进 行 其 他 的 补 偿 措 施 。 这 种乐 观 的 并 发 策 略 的 许 多 实 现 不 需 要 线 程 挂 起 , 所 以 被 称 为 非 阻 塞 同 步 。乐 观 锁 的 核 心 算 法 是 CAS( Compareand Swap, 比 较 并 交 换 ) , 它 涉 及 到 三 个 操 作 数 : 内 存 值 、 预 期 值 、 新 值 。 当 且 仅 当 预 期 值 和 内 存 值 相 等 时 才 将 内 存 值 修 改 为 新 值 。
这 样 处 理 的 逻 辑 是 , 首 先 检 查 某 块 内 存 的 值 是 否 跟 之 前 我 读 取 时 的 一 样 , 如 不 一 样 则 表 示 期 间 此 内 存 值 已 经 被 别 的 线 程 更 改 过 , 舍 弃 本 次 操 作 , 否 则 说 明 期 间 没 有 其 他 线 程 对 此 内 存 值 操 作 , 可 以 把 新 值 设 置 给 此 块 内 存 。
CAS 具 有 原 子 性 , 它 的 原 子 性 由 CPU 硬 件 指 令 实 现 保 证 , 即 使 用 JNI 调 用 Native 方 法 调 用 由 C++ 编 写 的 硬 件 级 别 指 令 , JDK 中 提 供 了 Unsafe 类 执 行 这 些 操 作 。
# 693.什 么 是 锁 消 除 和 锁 粗 化 ?-Java面试题
- 锁 消 除 : 指 虚 拟 机 即 时 编 译 器 在 运 行 时 , 对 一 些 代 码 上 要 求 同 步 , 但 被 检 测 到 不 可 能 存 在 共 享 数 据 竞 争 的 锁 进 行 消 除 。 主 要 根 据 逃 逸 分 析 。程 序 员 怎 么 会 在 明 知 道 不 存 在 数 据 竞 争 的 情 况 下 使 用 同 步 呢 ? 很 多 不 是程 序 员 自 己 加 入 的 。
- 锁 粗 化 : 原 则 上 , 同 步 块 的 作 用 范 围 要 尽 量 小 。 但 是 如 果 一 系 列 的 连 续 操 作 都 对 同 一 个 对 象 反 复 加 锁 和 解 锁 , 甚 至 加 锁 操 作 在 循 环 体 内 , 频 繁 地 进 行 互 斥 同 步 操 作 也 会 导 致 不 必 要 的 性 能 损 耗 。 锁 粗 化 就 是 增 大 锁 的 作 用 域
# 692. 为 什 么 说 Synchronized 是 非 公 平 锁 ?-Java面试题
非 公 平 主 要 表 现 在 获 取 锁 的 行 为 上 , 并 非 是 按 照 申 请 锁 的 时 间 前 后 给 等待 线 程 分 配 锁 的 , 每 当 锁 被 释 放 后 , 任 何 一 个 线 程 都 有 机 会 竞 争 到 锁 ,这 样 做 的 目 的 是 为 了 提 高 执 行 性 能 , 缺 点 是 可 能 会 产 生 线 程 饥 饿 现 象 。
# 691.JVM 对 Java 的 原 生 锁 做 了 哪 些 优 化 ?-Java面试题
在 Java 6 之 前 , Monitor 的 实 现 完 全 依 赖 底 层 操 作 系 统 的 互 斥 锁 来 实 现 , 也 就 是 我 们 刚 才 在 问 题 二 中 所 阐 述 的 获 取 /释 放 锁 的 逻 辑 。
由 于 Java 层 面 的 线 程 与 操 作 系 统 的 原 生 线 程 有 映 射 关 系 , 如 果 要 将 一 个 线 程 进 行 阻 塞 或 唤 起 都 需 要 操 作 系 统 的 协 助 , 这 就 需 要 从 用 户 态 切 换 到 内 核 态 来 执 行 , 这 种 切 换 代 价 十 分 昂 贵 , 很 耗 处 理 器 时 间 , 现 代 JDK 中 做 了 大 量 的 优 化 。
一 种 优 化 是 使 用 自 旋 锁 , 即 在 把 线 程 进 行 阻 塞 操 作 之 前 先 让 线 程 自 旋 等 待 一 段 时 间 , 可 能 在 等 待 期 间 其 他 线 程 已 经 解 锁 , 这 时 就 无 需 再 让 线 程 执 行 阻 塞 操 作 , 避 免 了 用 户 态 到 内 核 态 的 切 换 。现 代 JDK 中 还 提 供 了 三 种 不 同 的 Monitor 实 现 , 也 就 是 三 种 不 同 的 锁 :
- 偏 向 锁 ( Biased Locking)
- 轻 量 级 锁
- 重 量 级 锁
这 三 种 锁 使 得 JDK 得 以 优 化 Synchronized 的 运 行 , 当 JVM 检 测 到 不 同 的 竞 争 状 况 时 , 会 自 动 切 换 到 适 合 的 锁 实 现 , 这 就 是 锁 的 升 级 、降 级 。
当 没 有 竞 争 出 现 时 , 默 认 会 使 用 偏 向 锁 。
JVM 会 利 用 CAS 操 作 , 在 对 象 头 上 的 Mark Word 部 分 设 置 线 程 ID, 以 表 示 这 个 对 象 偏 向 于 当 前 线 程 , 所 以 并 不 涉 及 真 正 的 互 斥 锁 , 因 为 在 很 多 应 用 场 景 中 , 大 部 分 对 象 生 命 周 期 中 最 多 会 被 一 个 线 程 锁 定 ,使 用 偏 斜 锁 可 以 降 低 无 竞 争 开 销 。如 果 有 另 一 线 程 试 图 锁 定 某 个 被 偏 斜 过 的 对 象 , JVM 就 撤 销 偏 斜 锁 ,切 换 到 轻 量 级 锁 实 现 。
轻 量 级 锁 依 赖 CAS 操 作 Mark Word 来 试 图 获 取 锁 , 如 果 重 试 成 功 ,就 使 用 普 通 的 轻 量 级 锁 ; 否 则 , 进 一 步 升 级 为 重 量 级 锁 。
# 690.什 么 是 可 重 入 性 , 为 什 么 说 Synchronized 是 可 重 入 锁 ?-Java面试题
可 重 入 性 是 锁 的 一 个 基 本 要 求 , 是 为 了 解 决 自 己 锁 死 自 己 的 情 况 。比 如 下 面 的 伪 代 码 , 一 个 类 中 的 同 步 方 法 调 用 另 一 个 同 步 方 法 , 假 如 Synchronized 不 支 持 重 入 , 进 入 method2 方 法 时 当 前 线 程 获 得 锁 ,method2 方 法 里 面 执 行 method1 时 当 前 线 程 又 要 去 尝 试 获 取 锁 , 这 时 如 果 不 支 持 重 入 , 它 就 要 等 释 放 , 把 自 己 阻 塞 , 导 致 自 己 锁 死 自 己 。
对 Synchronized 来 说 , 可 重 入 性 是 显 而 易 见 的 , 刚 才 提 到 , 在 执 行 monitorenter 指 令 时 , 如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 ( 而 不 是 已 拥 有 了 锁 则 不 能 继 续 获 取 ) , 就 把 锁 的 计 数 器 +1, 其 实 本 质 上 就 通 过 这 种 方 式 实 现 了 可 重 入 性 。
# 689.你 刚 才 提 到 获 取 对 象 的 锁 , 这 个 “ 锁 ” 到 底 是 什 么 ? 如 何 确 定 对 象 的 锁-Java面试题
“ 锁 ” 的 本 质 其 实 是 monitorenter 和 monitorexit 字 节 码 指 令 的 一 个 Reference 类 型 的 参 数 , 即 要 锁 定 和 解 锁 的 对 象 。 我 们 知 道 , 使 用 Synchronized 可 以 修 饰 不 同 的 对 象 , 因 此 , 对 应 的 对 象 锁 可 以 这 么 确 定 。
- 如 果 Synchronized 明 确 指 定 了 锁 对 象 , 比 如 Synchronized( 变 量 名 ) 、 Synchronized(this) 等 , 说 明 加 解 锁 对 象 为 该 对 象 。
- 如 果 没 有 明 确 指 定 :
若 Synchronized 修 饰 的 方 法 为 非 静 态 方 法 , 表 示 此 方 法 对 应 的 对 象 为 锁 对 象 ;
若 Synchronized 修 饰 的 方 法 为 静 态 方 法 , 则 表 示 此 方 法 对 应 的 类 对 象 为 锁 对 象 。
注 意 , 当 一 个 对 象 被 锁 住 时 , 对 象 里 面 所 有 用 Synchronized 修 饰 的 方 法 都 将 产 生 堵 塞 , 而 对 象 里 非 Synchronized 修 饰 的 方 法 可 正 常 被 调 用 , 不 受 锁 影 响 。
# 688.Synchronized 用 过 吗 , 其 原 理 是 什 么 ?-Java面试题
这 是 一 道 Java 面 试 中 几 乎 百 分 百 会 问 到 的 问 题 , 因 为 没 有 任 何 写 过 并 发 程 序 的 开 发 者 会 没 听 说 或 者 没 接 触 过 Synchronized。
Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 如 果 你 查 看 被 Synchronized 修 饰 过 的 程 序 块 编 译 后 的 字 节 码 , 会 发 现 ,被 Synchronized 修 饰 过 的 程 序 块 , 在 编 译 前 后 被 编 译 器 生 成 了 monitorenter 和 monitorexit 两 个 字 节 码 指 令 。
这 两 个 指 令 是 什 么 意 思 呢 ?
在 虚 拟 机 执 行 到 monitorenter 指 令 时 , 首 先 要 尝 试 获 取 对 象 的 锁 :
如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 , 把 锁 的 计 数 器 +1; 当 执 行 monitorexit 指 令 时 将 锁 计 数 器 -1; 当 计 数 器 为 0 时 , 锁 就 被 释 放 了 。
如 果 获 取 对 象 失 败 了 , 那 当 前 线 程 就 要 阻 塞 等 待 , 直 到 对 象 锁 被 另 外 一 个 线 程 释 放 为 止 。
Java 中 Synchronize 通 过 在 对 象 头 设 置 标 记 , 达 到 了 获 取 锁 和 释 放 锁 的 目 的 。
# 687.你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?-Java面试题
多线程和并发程序中常遇到的有 Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的 Java 线程问题
# 686.什么是不可变对象,它对写并发应用有什么帮助?-Java面试题
另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个 java 面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么 String 是不可变的。
# 685.在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?-Java面试题
这个线程问题主要用来检测你是否熟悉 JDK5 中的并发包。这两个的区别是 CyclicBarrier 可以重复使用已经通过的障碍,而 CountdownLatch 不能重复使用。
# 684.Java 中你怎样唤醒一个阻塞的线程?-Java面试题
这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了 IO 阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用 wait()、sleep()、或者 join()方法而导致的阻塞,你可以中断线程,并且通过抛出 InterruptedException 来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。
# 683.为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?-Java面试题
这是另一个非常经典的 java 多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级 Java 面试的第一轮被问到。这个问题的回答应该是这样的,当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码。阅读我之前写的《start 与 run 方法的区别》这篇文章来获得更多信息。
# 682.你将如何使用 threaddump?你将如何分析 Thread dump?-Java面试题
在 UNIX 中你可以使用 kill -3,然后 thread dump 将会打印日志,在 windows 中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。
# 681.什么是竞争条件?你怎样发现和解决竞争?-Java面试题
这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于 Java 竞争条件的文章。在我看来这是最好的 java 线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验,or writing code which is free ofdata race or anyother race condition。关于这方面最好的书是《Concurrency practices in Java》。
# 680.Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什 么不同?-Java面试题
自从 Java 5 和 Java 内存模型改变以后,基于 volatile 关键字的线程问题越来越流行。应该准备好回答关于 volatile 变量怎样在并发环境中确保可见性。
# 679.什么是原子操作,Java 中的原子操作是什么?-Java面试题
非常简单的 java 线程面试问题,接下来的问题是你需要同步一个原子操作。
# 678.用 Java 编程一个会导致死锁的程序,你将怎么解决-Java面试题
这是我最喜欢的 Java 线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写 deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有 N 个资源和 N 个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n 可以替换为 2,越大的数据会使问题看起来更复杂。通过避免 Java 中的死锁来得到关于死锁的更多信息。
# 677.用 Java 写代码来解决生产者——消费者问题。-Java面试题
与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在 Java 中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。
# 676.用 Java 实现阻塞队列-Java面试题
这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用 Java 线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用 wait()和 notify()方法来实现阻塞队列,你可以要求他用最新的 Java 5 中的并发类来再写一次。
# 675.在 java 中 wait 和 sleep 方法的不同?-Java面试题
通常会在电话面试中经常被问到的 Java 线程面试问题。最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。
# 674.在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允 许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?-Java面试题
lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap 这样的高性能数据结构和有条件的阻塞。Java 线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。
# 673.现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?-Java面试题
这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用 join 方法实现。