并发编程总结归纳

多线程并发编程

进程、线程、线程池、锁

1、线程池都有哪些状态 ?

2、线程同步机制,synchronized底层实现原理是什么?

4、为什么会有线程?

6、何时使用多进程,何时使用多线程?

7、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?

8、怎么防止死锁?

9、请说出与线程同步以及线程调度相关的方法

10、使用多线程可能带来什么问题?

11、说一说自己对于synchronized关键字的了解

12、Lock和synchronized有什么区别?

13、说说线程的生命周期和状态?

14、说说自己是怎么使用synchronized关键字,在项目中用到了吗

15、线程的sleep()方法和yield()方法有什么区别?

16、synchronized和volatile的区别是什么?

17、创建线程池有哪几种方式?

18、线程的run()和start()有什么区别?

19、什么是线程池(thread pool)?

20、举例说明同步和异步

21、什么是线程死锁?如何避免死锁?

22、如何让list变成线程安全

23、wait和sleep的区别(线程)

24、什么是上下文切换?

25、Java中实现多线程有几种实现方式

26、notify()和notifyAll()有什么区别?

27、在java程序中如何保证多线程的运行安全?

28、说说并发与并行的区别?

29、启动一个线程事调用run()还是start()方法?

30、创建线程的方式?

31、守护线程是什么

32、多线程锁的升级原理是什么?

33、Synchronized锁升级过程?

34、为什么锁对象必须被final修饰,同理为什么有些引用必须被final修饰?

35、一个锁对象包含哪些部分,分别是什么含义?

36、volatile你是怎么理解的,哪些地方用到过volatile

37、CAS知道吗? 怎么实现的?

40、CAS有哪些问题,如何解决?

41、AQS知道吗?讲讲

42、讲一讲AtomicInteger的底层实现

41、Java中有哪些引用?分别有什么用?

42、ThreadLocal是什么? 是哪种引用类型?

43、线程数量是不是越多越好

44、线程如何打断?如何优雅的结束一个线程?终止线程4种方式

45、进程间如何通信?

46、线程间如何通信?

47、启动一个线程是调用run()还是start()方法?

48、怎么控制多个线程按序执行

49、进程上下文切换包括哪些步骤

50、进程调度算法有哪些?

51、虚拟内存知道吗?

52、JMM内存模型

53、知道线程池吗?

54、线程池有哪些参数?常用的线程池有哪些?你一般用哪种(自己创建)?

55、一个任务从被提交到被执行,线程池做了哪些工作? 了解

56、有哪些拒绝策略?

57、阻塞队列

58、并发下 ArrayList 不安全的吗

59、SynchronousQueue 同步队列

60、线程池:3大方法、7大参数、4种拒绝策略

61、start 与 run 区别

62、什么是乐观锁

Syschronized关键字

63、什么是synchronized关键字?

64、Java内存的可见性问题

65、synchronized关键字三大特性是什么?

66、synchronized关键字可以实现什么类型的锁?

67、synchronized关键字的使用方式

68、synchronized关键字的底层原理

69、自旋锁

70、了解锁消除吗?

71、了解锁粗化吗

72、如何唤醒一个阻塞的线程?

volatile关键字

73、volatile的作用是什么?

74、volatile的特性有哪些?

75、Java内存的可见性问题

76、为什么代码会重排序?

78、重排序会引发什么问题?

79、as-if-serial规则和happens-before规则的区别?

80、voliatile的实现原理?

81、volatile实现内存可见性原理
82、volatile实现有序性原理

83、Java虚拟机插入内存屏障的策略

84、编译器对内存屏障插入策略的优化

86、volatile、synchronized的区别?

ConcurrentHashMap

87、什么是ConcurrentHashMap?相比于HashMap和HashTable有什么优势?

88、java中ConcurrentHashMap是如何实现的?

89、ConcurrentHashMap结构中变量使用volatile和final修饰有什么作用?

90、ConcurrentHashMap有什么缺点?

91、ConcurrentHashMap默认初始容量是多少?每次扩容为原来的几倍?

92、ConCurrentHashMap 的key,value是否可以为null?为什么?HashMap中的key、value是否可以为null?

93、ConCurrentHashmap在JDK1.8中,什么情况下链表会转化为红黑树?

94、ConcurrentHashMap在JDK1.7和JDK1.8版本中的区别?

95、ConcurrentHashMap迭代器是强一致性还是弱一致性?

ThreadLocal

96、什么是ThreadLocal?有哪些应用场景?

97、ThreadLocal原理和内存泄露?

98、为什么ThreadLocal会发生内存泄漏呢?

99、如何解决ThreadLocal的内存泄漏?

100、为什么要将key设计成ThreadLocal的弱引用?

线程池

101、什么是线程池?为什么使用线程池

102、为什么使用线程池?

103、创建线程池的几种方法

104、ThreadPoolExecutor构造函数的重要参数分析

105、ThreadPoolExecutor的饱和策略(拒绝策略)

106、线程池的执行流程

107、execute()方法和submit()方法的区别

CAS

108、什么是CAS?

109、CAS存在的问题

110、Atomic 原子类

AQS

111、什么是AQS?

112、AQS的原理

113、AQS的资源共享方式有哪些?

114、如何使用AQS自定义同步器?

并发多线程编程 2
进程、线程、线程池、锁

1、线程池都有哪些状态 ?

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated

线程池各个状态切换框架图:

1、RUNNING

(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

2、 SHUTDOWN

(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。·

3、STOP

(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING

(1)状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。

terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。

(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

2、线程同步机制,synchronized底层实现原理是什么?

使用Synchronized关键字进行线程同步,锁的是对象不是代码块,例如this, class,锁定方法和非锁定方法可以同时执行。

Synchronized底层原理是moniter对象锁,方法同步还是代码块同步都是基于进入和退出monitor对象来实现,然而二者在具体实现上又存在很大的区别。

当使用Synchronized修饰代码块时,Synchronized代码块同步在需要同步的代码块开始的位置插入moniterenter指令,在同步结束的位置或者异常出现的位置

插入monitorexit指令;JVM要保证moniterenter和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,

它将处于锁定状态。

3: monitorenter //注意此处,进入同步方法

4: aload_0

5: dup

6: getfield #2 // Field i:I

9: iconst_1

10: iadd

11: putfield #2 // Field i:I

14: aload_1

15: monitorexit //注意此处,退出同步方法

16: goto 24

19: astore_2

20: aload_1

21: monitorexit //注意此处,退出同步方法

将使用Synchronized修饰的代码块进行反编译后发现,同步方法块在进入代码块时插入了moniterenter语句,在退出代码块时插入了monitorexit语句,

为了保证不论是正常执行完毕(第15行)还是异常跳出代码块(第21行)都能执行monitorexit语句,因此会出现两句monitorexit语句。

Synchronized修饰方法:由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志实现的,如果方法表结构(method_info Structure)

中的ACC_SYNCHRONIZED标志被设置(代表使用Synchronized修饰方法),那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,

执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。

public synchronized void syncTask();

descriptor: ()V

//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法

flags: ACC_PUBLIC, ACC_SYNCHRONIZED

Code:

stack=3, locals=1, args_size=1

0: aload_0

1: dup

2: getfield #2 // Field i:I

5: iconst_1

6: iadd

7: putfield #2 // Field i:I

10: return

LineNumberTable:

line 12: 0

line 13: 10

}
3、进程与线程有什么区别?分别都有什么状态?☆☆☆

何为进程?

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

进程:操作系统资源分配的最小单位。

何为线程?

线程:通常在一个进程中可以包含若干个线程,一个进程中至少有一个线程。同类的多个线程共享进程所拥有的资源。

线程是cpu独立运行和独立调度的基本单位,由于线程比进程更轻量级,故操作系统对线程之间的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

线程6种状态包括:

初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。

就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

阻塞(BLOCKED):表示线程阻塞于锁。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

进程的状态:

调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。

与等待队列相关的步骤和图

线程1获取对象A的锁,正在使用对象A。

线程1调用对象A的wait()方法。

线程1释放对象A的锁,并马上进入等待队列。

锁池里面的对象争抢对象A的锁。

线程5获得对象A的锁,进入synchronized块,使用对象A。

线程5调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。若线程5调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒

的那个线程进入同步队列。notifyAll()方法所在synchronized结束,线程5释放对象A的锁。同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。

总结:

其实waiting状态和阻塞状态的关键就在于,waiting状态其实是将该线程加入到锁/某对象的等待队列中去了,因此将他们从工作队列中暂时移除,

而notify()、sleep()时间到、join()的线程结束、unpark的作用都是将该线程从等待队列重新加入到工作队列中,notify和unpark是显示的将线程重新

加入到工作队列,而sleep时间结束、join()线程结束相当于执行了一个回调函数,将线程从等待队列加入到工作队列中,如果加到工作队列中是去抢锁,

如果抢锁失败就会进入到阻塞队列中(阻塞队列和等待队列的区别就在于,阻塞队列中的进程在锁释放后都会去抢锁,而等待队列是啥也不干,必须等重新加入到

工作队列中等到CPU分配时间片了才会干活)

(9条消息) Java线程的6种状态及切换(透彻讲解)_潘建南的博客-CSDN博客_线程状态

4、为什么会有线程?

每个进程都有自己的地址空间,即进程空间,在网络或多用户环境下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程

显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。

6、何时使用多进程,何时使用多线程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

7、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?

sleep()方法是Thread类的静态方法,可随时调用,调用此方法会让当前线程暂停执行指定的时间,并释放CPU资源,但不会释放对象锁;

wait()方法是Object类的方法,只能在同步方法或同步代码块中使用,调用此方法会让线程进入休眠状态,并释放CPU资源与对象锁,

需要我们调用notify()/notifyAll()方法唤醒指定或全部的休眠线程,再次竞争CPU资源。

8、怎么防止死锁?

一、什么是死锁

死锁是指多个进程因竞争资源而造成的一种互相等待,若无外力作用,这些进程都将无法向前推进。

例如,在某一个计算机系统中只有一台打印机和一台输入设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2所占用,

而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

二、死锁产生的原因

1.系统资源的竞争

系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。

2.进程运行推进顺序不合适

进程在运行过程中,请求和释放资源的顺序不当,会导致死锁

三、死锁的四个必要条件

互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

不可剥夺条件:进程所获得的资源在使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

循环等待条件: 若干进程间形成首尾相接、循环等待资源的关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

四、 死锁的避免与预防

1.死锁避免

死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,

则不予分配;否则予以分配,这是一种保证系统不进入死锁状态的动态策略。

2.死锁预防

通过破坏死锁产生的4个必要条件来预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。

1,破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被

其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。

2,破坏”请求与保持条件“:第一种方法:静态分配,即每个进程在开始执行时就申请他所需要的全部资源。第二种是:动态分配,即每个进程在申请所需要的

资源时他本身不占用系统资源。

3,破坏“循环等待”条件:采用资源有序分配。其基本思想是:将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号

的顺序进行,一个进程只有获得较小编号的资源才能申请较大编号的资源。

9、请说出与线程同步以及线程调度相关的方法

wait():只能作用在同步方法、同步代码块中,使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

sleep():可以在任意地方使用,使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;

notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;

notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

10、使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,

比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

11、说一说自己对于synchronized关键字的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

12、Lock和synchronized有什么区别?

1、Synchronized 内置的Java关键字,是JVM层面的,Lock 是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁

3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁

4、Synchronized 线程 1(获得锁,如果线程1阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;

5、Synchronized 不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。

6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

原始构成:sync是JVM层面的,底层通过monitorenter和monitorexit来实现的。Lock是JDK API层面的。(sync一个enter会有两个exit,

一个是正常退出,一个是异常退出,因此不需要手动释放锁)

使用方法:sync不需要手动释放锁,而Lock需要手动释放。

是否可中断:sync不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。

是否为公平锁:sync只能是非公平锁,而Lock既能是公平锁,又能是非公平锁。

绑定多个条件:sync不能,只能随机唤醒。而Lock可以通过Condition来绑定多个条件,精确唤醒。

13、说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

由上图可以看出:线程创建之后它将处于 NEW(新建)状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行)状态。可运行状态的线程获得了

CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态

(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

当线程执行 wait()方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而

TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)

方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,

在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞)状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止)状态。

14、说说自己是怎么使用synchronized关键字,在项目中用到了吗

synchronized关键字最主要的三种使用方式:

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,

不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到静态

方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。

面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”

双重校验锁实现对象单例(线程安全)

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

为 uniqueInstance 分配内存空间

初始化 uniqueInstance

将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有

初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,

但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

15、线程的sleep()方法和yield()方法有什么区别?

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

java中的sleep、wait、yield、join之间的区别

16、synchronized和volatile的区别是什么?

volatile是变量修饰符,而synchronized则作用于一段代码或者方法;

volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器,同步所有变量的值。

显然synchronized要比volatile消耗更多资源;
17、创建线程池有哪几种方式?

线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):

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

newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;

newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;

newScheduledThreadPool:创建一个可以执行延迟任务的线程池;

newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;

newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。

ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。

18、线程的run()和start()有什么区别?

调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的。

一个线程对象的 start() 方法只能调用一次,多次调用会抛出 lang.IllegalThreadStateException 异常;run() 方法没有限制。

19、什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,

以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,

这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,

使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

20、举例说明同步和异步

如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程

写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不

希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作

21、什么是线程死锁?如何避免死锁?

死锁是指多个进程因竞争资源而造成的一种互相等待的情况,若无外力作用,这些进程都将无法向前推进。

死锁产生的原因

系统资源的竞争

系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。

进程运行推进顺序不合适

进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

死锁的四个必要条件:

互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,只能是主动释放。

循环等待条件:若干进程间形成首尾相接循环等待资源的关系

死锁避免:

死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配。

死锁预防

通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性无法改变。

破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的

进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。

破坏“请求与保持”条件:第一种方法静态分配,即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配,每个进程在申请所需要的资源时他本身

不占用系统资源。

破坏“循环等待”条件:采用资源有序分配,其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,

一个进程只有获得较小编号的资源才能申请较大编号的资源。

22、如何让list变成线程安全

1、使用synchronized关键字;

2、使用Collections.synchronizedList();使用方法如下:

假如你创建的代码如下:

List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();

那么为了解决这个线程安全问题你可以这么使用Collections.synchronizedList(),如:

List<Map<String, Object>> data = Collections.synchronizedList(new ArrayList<Map<String, Object>())

其他的都没变,使用的方法也几乎与ArrayList一样,大家可以参考下api文档;

23、wait和sleep的区别(线程)

1,sleep()来自 Thread 类,wait()来自 Object 类;

2,调用 sleep()方法,线程不会释放对象锁。而调用 wait 方法线程会释放对象锁;

3,sleep()睡眠后不出让系统资源,wait() 让其他线程可以占用 CPU;

4,sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。而 wait()需要配合 notify()或者 notifyAll()使用。

24、什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略

是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态,并把CPU核心让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。

任务从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,

每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

25、Java中实现多线程有几种实现方式

继承Thread类,重写run方法

实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target

通过Callable和FutureTask创建线程

通过线程池创建线程

继承 Thread 类:

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方法就是通过 Thread 类的 start()方法。

start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

public class MyThread extends Thread { public void run() {

System.out.println(“MyThread.run()”);

}

}

MyThread myThread1 = new MyThread();

myThread1.start();

实现 Runnable 接口:

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口

public class MyThread extends OtherClass implements Runnable {

public void run() {

System.out.println(“MyThread.run()”);

}

}

//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例: MyThread myThread = new MyThread();

Thread thread = new Thread(myThread);

thread.start();

//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用

target.run()

public void run() {

if (target != null) {

target.run();

}

}

ExecutorService、 Callable、 Future 有返回值线程

如果需要返回值使用callable,如果不需要返回最好使用runnable,因为继承只能单继承,所以不推荐使用Thread。

26、notify()和notifyAll()有什么区别?

在java中,每个对象都有两个池,锁(monitor)池和等待池

wait() ,notifyAll(),notify() 三个方法都是Object类中的方法.

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),

由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法

之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的

等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的

等待池中的线程(随机)会进入该对象的锁池.

并发多线程编程 3
notify和notifyAll的区别:

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的

锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到

锁池中,等待锁竞争。优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,

它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,

竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

参考:(9条消息) java中的notify和notifyAll有什么区别?_djzhao的专栏-CSDN博客_notify

27、在java程序中如何保证多线程的运行安全?

1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);

2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile); 3.有序性:一个线程观察其他线程中的指令执行顺序,

由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

下面是对3个要素的详细解释:

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 在Java中,基本数据类型的变量的读取和赋值操作是

原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即

看得到修改的值。 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。

普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,

因此无法保证可见性。 更新主存的步骤:当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值跟新到主存,

更新成功后将其他线程的缓存行更新为新的主存地址其他线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,

然后去对应的主存读取最新的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,

却会影响到多线程并发执行的正确性。 可以通过volatile关键字来保证一定的“有序性”。 当在处理并发编程的时候,只要程序满足了原子性,可见性和有序性,

那么程序就不会发生脏数据的问题。

28、说说并发与并行的区别?

并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);

并行: 单位时间内,多个任务同时执行。

29、启动一个线程事调用run()还是start()方法?

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。

run()方法是线程启动后要进行回调(callback)的方法。

30、创建线程的方式?

继承Thread类

实现Runnable接口

通过Callable和FutureTask创建线程

使用Executor框架创建线程池

31、守护线程是什么

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。

所以java里线程分2种: 1、守护线程,比如垃圾回收线程,就是最典型的守护线程。

2、用户线程,就是应用程序里的自定义线程。

32、多线程锁的升级原理是什么?

多线程优化锁升级 monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的(系统调用)。

互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。 JDK1.6对线程进行了优化,目的就是减少多线程编程下

对锁竞争产生的性能开销。

1.无锁状态:没有加任何锁。 2.偏向锁:如果程序中只有一个线程在访问对象,那么这个对象的锁就会偏向于这一个线程,之后一系列的原子操作都不会产生

同步开销,直到其他线程在竞争锁的时候,偏向锁才会解除掉。 3.轻量级锁(Lightweight Locking):多个线程会去竞争锁,但是尽可能地减少多线程

进入互斥的几率。它并不是要替代互斥,因为随着竞争越来越激烈,它最后也会升级成重量级锁。

乐观锁:多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起

(适应性自旋:不要旋转太久,否则会消耗过多的cpu),而是被告知这次竞争中失败,并可以再次尝试。乐观锁就是轻量级锁,悲观锁就是重量级锁。

轻量级锁是如何实现的呢?利用了CPU原语CompareAndSwap(汇编指令为CMPXCHG),尝试在进入互斥前,进行补救。 轻量级锁加锁

(竞争的线程不会阻塞,提高了程序的响应速度) 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。 重量级锁:重量锁又叫对象监视器(Monitor),它实际上是利用操作系统中的Mutex,除了具备Mutex互斥的功能,它还负责实现Semaphore的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

33、Synchronized锁升级过程?34、为什么锁对象必须被final修饰,同理为什么有些引用必须被final修饰?

final修饰的对象必须被初始化(在初始化阶段),不能被修改。非final的对象可以被重新赋值,如果锁对象没有使用final修饰,引用就能够指向新的对象,

就不受管控了。当一个锁被其他对象占有时,当前线程可以对锁对象重新赋值(相当于从新创建了一个锁对象),从而也拿到了运行的权利。

35、一个锁对象包含哪些部分,分别是什么含义?

通常Java对象都是分配在堆内存上的,主要由三部分组成,对象头,实例变量和对齐填充

对象头:

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据

会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

数组长度(只有数组对象才有)

https://img-blog.csdn.net/20180322153316377

关注点:

偏向锁:锁偏向的线程Id;

轻量级锁:栈中锁记录的指针;

重量级锁:指向重量级锁的指针(就是指向的锁对象);

重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),

相关的数据结构如下:

ObjectMonitor() {

_count = 0; //用来记录该对象被线程获取锁的次数

_waiters = 0;

_recursions = 0; //锁的重入次数

_owner = NULL; //指向持有ObjectMonitor对象的线程

_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet

_WaitSetLock = 0 ;

_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表

}

实例变量:对象的字段属性和值

对齐填充:JVM要求对象大小必须是8字节的整数倍

36、volatile你是怎么理解的,哪些地方用到过volatile

Volatile是java提供的一种轻量级的同步机制

3个特点:保证可见性,不保证原子性,禁止指令重排

本质上有两个功能,在生成的汇编语句中加入LOCK关键字和内存屏障,作用就是保证每一次线程load和write两个操作,都会直接从主内存中进行读取和覆盖,

而非普通变量从线程内的工作空间

volatile保证有序性

volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。

那么禁止指令重排序通过加内存屏障实现。JMM为volatile加内存屏障有以下4种情况:

在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。

在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。

在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。

在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

volatile保证可见性

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在工作内存的数据写回到系统内存。确保了如果有线程对

声明了volatile变量进行修改,则立即更新主内存中数据。但这时候其他线程的缓存还是旧的,为了保证各个线程缓存一致,每个线程通过嗅探在总线上传播

的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据

进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

volatile用在单例模式下,禁止指令重排

public static Singleton {

private volatile static Singleton instance = null;

private Singoleton() {}

public Singleton getInstance () {

if (instance == null) {

synchronized (Singleton.class) {

if (instance == null) {

instance = new Singleton();

}

}

return instance;

}

}

}

37、CAS知道吗? 怎么实现的?

CAS可以看做是乐观锁的一种实现方式, 全称Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

CAS涉及到三个属性:

需要读写的内存位置V

需要进行比较的预期值A

需要写入的新值U

CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中,否则使用自旋不断获取值进行判断,保证一致性

和并发性,但是比较消耗CPU资源。使用CAS就可以不用加锁来实现线程安全。

原子性保证:CAS算法依赖于rt.jar包下的sun.misc.Unsafe类,该类中的所有方法都是native修饰的,直接调用操作系统底层资源执行相应的任务,

在调用这个类中的CAS方法中,JVM就会若干条系统指令,完整这些指令的过程中是不允许被中断的,所以CAS是一条CUP的原子指令,所以它不会造成数据不一致问题。

40、CAS有哪些问题,如何解决?

如果期望的数值和从内存中读取的数值不一样,会一直自旋,开销比较大。

引出了ABA问题。

ABA问题的解决:

所谓ABA问题,就是比较并交换的循环,存在一个时间差,在这个时间差内,比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,

但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。有的需求,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,

中间不能发生任何修改,可以使用AtomicStampedReference,使用stamp添加版本号来解决这个问题。

41、AQS知道吗?讲讲

AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…。

AQS,它维护了一个volatile int state(代表共享资源)状态变量和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,

获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;

释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。

AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、

setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个

节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

42、讲一讲AtomicInteger的底层实现

AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。

从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作;以 volatile 的 value 字段,Unsafe

会利用 value 字段的内存地址偏移,直接完成操作。CAS 是 Java 并发中所谓 lock-free 机制的基础。

41、Java中有哪些引用?分别有什么用?

强引用:new出来的,内存不够也不回收

软引用:内存不够就回收,用在缓存中

弱引用:只要gc就会被回收,一般用在容器中,weakhashmap

threadlocal中弱引用的应用,非弱引用可能导致内存泄漏,使用threadlocal务必remove

虚引用:当对象被回收时,通过引用队列queue可以检测到,然后此时可以清理堆外内存,使用unsafe native方法可以free memory

42、ThreadLocal是什么? 是哪种引用类型?

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要

进行额外的同步措施才能保证线程安全性。

ThreadLocal是除了加锁这种同步方式之外的一种保证规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候

访问的都是线程自己的变量这样就不会存在线程不安全问题。

43、线程数量是不是越多越好

线程数量不是越多越好,因为线程之间的切换也是需要消耗系统资源的,那么工作线程数(线程池中的线程)设置多少合适呢?

对于IO任务,Nthread = Ncpu * Ucpu * (1+Tio/Tcalcu), Ucpu是期望的CPU的利用率,如果期望的IO时间和CPU计算的时间的1:1,那么设置最佳的

线程数是CPU核心数*2

我们怎么怎么直到一个CPU多长时间在计算多长时间在等待呢? 可以使用profiler进行分析

44、线程如何打断?如何优雅的结束一个线程?终止线程4种方式

1、正常运行结束

程序运行结束,线程自动结束。

2、使用退出标志退出线程

一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。

使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,

代码示例 :

public class ThreadSafe extends Thread {

public volatile boolean exit = false;

public void run() {

while (!exit){

//do something

}

}

}

定义了一个退出标志 exit,当 exit 为 true 时, while 循环退出, exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,

这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
3、Interrupt 方法结束线程

使用 interrupt()方法来中断线程有两种情况:

1.线程处于阻塞状态: 如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的

interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,

从而让我们有机会结束这个线程的执行。 通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException

异常之后通过 break 来跳出循环,才能正常结束 run 方法。

2.线程未处于阻塞状态: 使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,

和使用自定义的标志来控制循环是一样的道理。

public class ThreadSafe extends Thread {

public void run() {

while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出

try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出

}catch(InterruptedException e){

e.printStackTrace();

break;//捕获到异常之后,执行 break 跳出循环

}

}

}

}

4、stop 方法终止线程(线程不安全)

程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,

可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,

。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),

那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程 序错误。因此,并不推荐使用 stop 方法来终止线程。

45、进程间如何通信?

(1)共享内存

(2)消息队列

(3)信号量

(4)套接字(socket)

信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,

其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。

共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式

字节流以及缓冲区大小受限等缺点。

46、线程间如何通信?

线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。

线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流。每种方式有不同的方法来实现

共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信,可以使用volatile关键字实现

消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。

wait()当前线程释放锁并进入等待(阻塞)状态,notify()唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁,

notifyAll()唤醒所有正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁。

管道流:PipedInputStream 用于向管道中写入数据。PipedOutputStream 用于从管道中读取写入的数据。

47、启动一个线程是调用run()还是start()方法?

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。

run()方法是线程启动后要进行回调的方法。

48、怎么控制多个线程按序执行

在主线程中通过join()方法指定顺序

通过倒数计时器CountDownLatch实现

通过创建单一化线程池newSingleThreadExecutor()实现

使用线程的Condition(条件变量)方法

使用CyclicBarrier(回环栅栏)实现线程按顺序运行

使用Sephmore(信号量)实现线程按顺序运行

49、进程上下文切换包括哪些步骤

(1)选择适当的时机

(2)保存当前执行进程的上下文,包括CPU寄存器中的值、进程的状态以及堆栈中的内容

(3)使用进程调度算法,选择一处于就绪状态的进程。

(4)装配所选进程的上下文,将CPU控制权交到所选进程手中。

50、进程调度算法有哪些?

51、虚拟内存知道吗?

52、JMM内存模型

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。 每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被

该线程使用的变量的主内存副本, 线程对变量的所有操作(读取、 赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据。 不同的线程之间

也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成。

53、知道线程池吗?

线程池主要是控制运行线程的数量,将待处理任务放到等待队列,然后创建线程执行这些任务。如果超过了最大线程数,则等待。

优点:

线程复用:不用一直new新线程,重复利用已经创建的线程来降低线程的创建和销毁开销,节省系统资源。

提高响应速度:当任务达到时,不用创建新的线程,直接利用线程池的线程。

管理线程:可以控制最大并发数,控制线程的创建等。

54、线程池有哪些参数?常用的线程池有哪些?你一般用哪种(自己创建)?

55、一个任务从被提交到被执行,线程池做了哪些工作? 了解

1.向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。

2.如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。

3.如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就创建新线程来执行任务。

4.如果已经达到了最大线程数,则执行指定的拒绝策略。

56、有哪些拒绝策略?

当等待队列满时,且达到最大线程数,再有新任务到来,就需要启动拒绝策略。

AbortPolicy:默认的策略,队列满了丢任务抛出异常

CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务

DiscardOldestPolicy:将最早进入队列的任务删,之后再尝试加入队列

DiscardPolicy:直接丢弃任务,不做任何处理。

57、阻塞队列

当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。阻塞队列不用手动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。

体系:Collection→Queue→BlockingQueue→七个阻塞队列实现类。

阻塞队列

需要注意的是LinkedBlockingQueue虽然是有界的,但有个巨坑,其默认大小是Integer.MAX_VALUE,高达21亿,一般情况下内存早爆了

(在线程池的ThreadPoolExecutor有体现)。

58、并发下 ArrayList 不安全的吗

List、ArrayList 等在并发多线程条件下,不能实现数据共享,多个线程同时调用一个list对象时候就会出现并发修改异常ConcurrentModificationException

解决方案:

Vector

synchronizedList

CopyOnWriteArrayList

方案1、List list = new Vector<>();

方案2、List list = Collections.synchronizedList(new ArrayList<>());

方案3、List list = new CopyOnWriteArrayList<>();

Set也一样

Set、Hash 等在并发多线程条件下,不能实现数据共享,多个线程同时调用一个set对象时候就会出现并发修改异常ConcurrentModificationException

59、SynchronousQueue 同步队列

没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!

同步队列: 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素,put了一个元素,必须从里面先take取出来,否则不能在put进去值!

60、线程池:3大方法、7大参数、4种拒绝策略

// Executors 工具类、3大方法

// Executors.newSingleThreadExecutor();// 创建单个线程的线程池

// Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池

// Executors.newCachedThreadPool();// 创建一个可伸缩的线程池

因为实际开发中工具类Executors 不安全,所以需要手动创建线程池,自定义7个参数。

61、start 与 run 区别

start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。

通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态,并没有运行。

方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束,此线程终止。

然后 CPU 再调度其它线程。

62、什么是乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下

在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。

Syschronized关键字

63、什么是synchronized关键字?

在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而synchronized关键字则是用来保证线程同步的。

64、Java内存的可见性问题

在了解synchronized关键字的底层原理前,需要先简单了解下Java的内存模型,看看synchronized关键字是如何起作用的。

这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,它包含了控制器、运算器、缓存等。同时Java内存模型规定,线程对共享变量的操作

必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?,

线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,

这时主内存及本地内存中的X的值都为1。

线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,

此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。

线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。

该问题Java内存模型是通过synchronized关键字和volatile关键字就可以解决,那么synchronized关键字是如何解决的呢,其实进入synchronized块就是

把在synchronized块内使用到的变量从线程的本地内存中擦除,这样在synchronized块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获取,

解决了内存不可见问题。

65、synchronized关键字三大特性是什么?

面试时经常拿synchronized关键字和volatile关键字的特性进行对比,synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,

而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized。

原子性:一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。

可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,会对应执行 lock、unlock原子操作,保证可见性。

有序性:程序的执行顺序会按照代码的先后顺序执行。

66、synchronized关键字可以实现什么类型的锁?

悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。

非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。

可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。

独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

67、synchronized关键字的使用方式

synchronized主要有三种使用方式:修饰普通同步方法、修饰静态同步方法、修饰同步方法块。

68、synchronized关键字的底层原理

这个问题也是面试比较高频的一个问题,也是比较难理解的,理解synchronized需要一定的Java虚拟机的知识。

在jdk1.6之前,synchronized被称为重量级锁,在jdk1.6中,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁。下面先介绍jdk1.6

之前的synchronized原理。

对象头

在HotSpot虚拟机中,Java对象在内存中的布局大致可以分为三部分:对象头、实例数据和填充对齐。因为synchronized用的锁是存在对象头里的,

这里我们需要重点了解对象头。如果对象头是数组类型,则对象头由Mark Word、Class MetadataAddress和Array length组成,如果对象头非数组类型,

对象头则由Mark Word和Class MetadataAddress组成。在32位虚拟机中,数组类型的Java对象头的组成如下表:

内容

说明

长度

Mark Word

存储对象的hashCode、分代年龄和锁标记位

32bit

Class MetadataAddress

存储到对象类型数据的指针

32bit

Array length 数组的长度

32bit

这里我们需要重点掌握的是Mark Word。

Mark Word

在运行期间,Mark Word中存储的数据会随着锁标志位的变化而变化,在32位虚拟机中,不同状态下的组成如下:

其中线程ID表示持有偏向锁线程的ID,Epoch表示偏向锁的时间戳,偏向锁和轻量级锁是在jdk1.6中引入的。

69、自旋锁

Java锁的几种状态并不包括自旋锁,当轻量级锁的竞争就是采用的自旋锁机制。

什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环等待,当线程A释放锁后,线程B可以马上获得锁。

引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。

自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,

如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,

一般默认为10。

自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。

如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。

如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。

偏向锁、轻量级锁、重量级锁的对比

优点

缺点

实用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在竞争,会额外带来锁撤销的消耗

适用于只有一个线程访问同步块场景

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到锁竞争的线程,使用自旋会消耗CPU

追求响应时间,同步块执行速度非常快

重量级锁

线程竞争不使用自旋,不会消耗CPU

线程阻塞,响应时间缓慢

追求吞吐量,同步块执行速度较慢

70、了解锁消除吗?

锁消除是指Java虚拟机在即时编译时,通过对运行上下的扫描,消除那些不可能存在共享资源竞争的锁。锁消除可以节约无意义的请求锁时间。

71、了解锁粗化吗

一般情况下,为了提高性能,总是将同步块的作用范围限制到最小,这样可以使得需要同步的操作尽可能地少。但如果一系列连续的操作一直对某个对象反复

加锁和解锁,频繁地进行互斥同步操作也会引起不必要的性能消耗。

如果虚拟机检测到有一系列操作都是对某个对象反复加锁和解锁,会将加锁同步的范围粗化到整个操作序列的外部。可以看下面这个经典案例。

并发多线程编程 4
72、如何唤醒一个阻塞的线程?

如果线程是由于wait()、sleep()、join()、yield()等方法进入阻塞状态的,是可以进行唤醒的。如果线程是IO阻塞是无法进行唤醒的,因为IO是操作

系统层面的,Java代码无法直接接触操作系统。

wait():可用notify()或notifyAll()方法唤醒。

sleep():调用该方法使得线程在指定时间内进入阻塞状态,等到指定时间过去,线程再次获取到CPU时间片进而被唤醒。

join():当前线程A调用另一个线程B的join()方法,当前线程A转入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。

yield():使得当前线程放弃CPU时间片,但随时可能再次得到CPU时间片进而激活。

volatile关键字

73、volatile的作用是什么?

volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性。相比于synchronized关键字,volatile

关键字的执行成本更低,效率更高。

74、volatile的特性有哪些?

并发编程的三大特性为可见性、有序性和原子性。通常来讲volatile可以保证可见性和有序性。

可见性:volatile可以保证不同线程对共享变量进行操作时的可见性。即当一个线程修改了共享变量时,另一个线程可以读取到共享变量被修改后的值。

有序性:volatile会通过禁止指令重排序进而保证有序性。

原子性:对于单个的volatile修饰的变量的读写是可以保证原子性的,但对于i++这种复合操作并不能保证原子性。这句话的意思基本上就是说volatile

不具备原子性了。

75、Java内存的可见性问题

Java的内存模型如下图所示。

这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,它包含了控制器、运算器、缓存等。同时Java内存模型规定,线程对共享变量的操作

必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?,

线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,

这时主内存及本地内存A中的X的值都为1。

线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,

并刷新到主内存中,此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。

线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。

该问题Java内存模型是通过synchronized关键字和volatile关键字就可以解决。

76、为什么代码会重排序?

计算机在执行程序的过程中,编译器和处理器通常会对指令进行重排序,这样做的目的是为了提高性能。具体可以看下面这个例子。

int a = 1; int b = 2; int a1 = a; int b1 = b; int a2 = a + a; int b2 = b + b; …

像这段代码,不断地交替读取a和b,会导致寄存器频繁交替存储a和b,使得代码性能下降,可对其进入如下重排序。

int a = 1; int b = 2; int a1 = a; int a2 = a + a; int b1 = b; int b2 = b + b; …

按照这样地顺序执行代码便可以避免交替读取a和b,这就是重排序地意义。

指令重排序一般分为编译器优化重排、指令并行重拍和内存系统重排三种。

编译器优化重排:编译器在不改变单线程程序语义的情况下,可以对语句的执行顺序进行重新排序。

指令并行重排:现代处理器多采用指令级并行技术来将多条指令重叠执行。对于不存在数据依赖的程序,处理器可以对机器指令的执行顺序进行重新排列。

内存系统重排:因为处理器使用缓存和读/写缓冲区,使得加载(load)和存储(store)看上去像是在乱序执行。

注:简单解释下数据依赖性:如果两个操作访问了同一个变量,并且这两个操作有一个是写操作,这两个操作之间就会存在数据依赖性,例如:

a = 1; b = a;

如果对这两个操作的执行顺序进行重排序的话,那么结果就会出现问题。

其实,这三种指令重排说明了一个问题,就是指令重排在单线程下可以提高代码的性能,但在多线程下可以会出现一些问题。

78、重排序会引发什么问题?

前面已经说过了,在单线程程序中,重排序并不会影响程序的运行结果,而在多线程场景下就不一定了。可以看下面这个经典的例子,该示例出自《Java并发编程的艺术》。

class ReorderExample{

int a = 0;

boolean flag = false;

public void writer(){

a = 1; // 操作1

flag = true; // 操作2

}

public void reader(){

if(flag){ // 操作3

int i = a + a; // 操作4

}

}

}

假设线程1先执行writer()方法,随后线程2执行reader()方法,最后程序一定会得到正确的结果吗?

答案是不一定的,如果代码按照下图的执行顺序执行代码则会出现问题。

操作1和操作2进行了重排序,线程1先执行flag=true,然后线程2执行操作3和操作4,线程2执行操作4时不能正确读取到a的值,导致最终程序运行结果出问题。

这也说明了在多线程代码中,重排序会破坏多线程程序的语义。
79、as-if-serial规则和happens-before规则的区别?

区别:

as-if-serial定义:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。

happens-before定义:一个操作happens-before另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。

但这并不意味着Java虚拟机必须按照这个顺序来执行程序。如果重排序的后的执行结果与按happens-before关系执行的结果一致,Java虚拟机也会允许重排序的发生。

happens-before关系保证了同步的多线程程序的执行结果不被改变,as-if-serial保证了单线程内程序的执行结果不被改变。

相同点:happens-before和as-if-serial的作用都是在不改变程序执行结果的前提下,提高程序执行的并行度。

80、voliatile的实现原理?

前面已经讲述volatile具备可见性和有序性两大特性,所以volatile的实现原理也是围绕如何实现可见性和有序性展开的。

81、volatile实现内存可见性原理

导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,

但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?

volatile可以保证内存可见性的关键是volatile的读/写实现了缓存一致性,缓存一致性的主要内容为:

每个处理器会通过嗅探总线上的数据来查看自己的数据是否过期,一旦处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存设为无效状态。此时,

如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。

当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。

那缓存一致性是如何实现的呢?可以发现通过volatile修饰的变量,生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile

关键字可以保证内存可见性的关键,它主要有两个作用:

将当前处理器缓存的数据刷新到主内存。

刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效。

82、volatile实现有序性原理

前面提到重排序可以提高代码的执行效率,但在多线程程序中可以导致程序的运行结果不正确,那volatile是如何解决这一问题的呢?

为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。

内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。

83、Java虚拟机插入内存屏障的策略

Java内存模型把内存屏障分为4类,如下表所示:

屏障类型

指令示例

说明

LoadLoad Barriers

Load1;LoadLoad;Load2

保证Load1数据的读取先于Load2及后续所有读取指令的执行

StoreStore Barriers

Store1;StoreStore;Store2

保证Store1数据刷新到主内存先于Store2及后续所有存储指令

LoadStore Barriers

Load1;LoadStore;Store2

保证Load1数据的读取先于Store2及后续的所有存储指令刷新到主内存

StoreLoad Barriers

Store1;StoreLoad;Load2

保证Store1数据刷新到主内存先于Load2及后续所有读取指令的执行

注:StoreLoad Barriers同时具备其他三个屏障的作用,它会使得该屏障之前的所有内存访问指令完成之后,才会执行该屏障之后的内存访问命令。

Java内存模型对编译器指定的volatile重排序规则为:

当第一个操作是volatile读时,无论第二个操作是什么都不能进行重排序。

当第二个操作是volatile写时,无论第一个操作是什么都不能进行重排序。

当第一个操作是volatile写,第二个操作为volatile读时,不能进行重排序。

根据volatile重排序规则,Java内存模型采取的是保守的屏障插入策略,volatile写是在前面和后面分别插入内存屏障,volatile读是在后面插入两个内存屏障,

具体如下:

volatile读:在每个volatile读后面分别插入LoadLoad屏障及LoadStore屏障(根据volatile重排序规则第一条),如下图所示

LoadLoad屏障的作用:禁止上面的所有普通读操作和上面的volatile读操作进行重排序。

LoadStore屏障的作用:禁止下面的普通写和上面的volatile读进行重排序。

volatile写:在每个volatile写前面插入一个StoreStore屏障(为满足volatile重排序规则第二条),在每个volatile写后面插入一个StoreLoad屏障

(为满足volatile重排序规则第三条),如下图所示

StoreStore屏障的作用:禁止上面的普通写和下面的volatile写重排序

StoreLoad屏障的作用:防止上面的volatile写与下面可能出现的volatile读/写重排序。

84、编译器对内存屏障插入策略的优化

因为Java内存模型所采用的屏障插入策略比较保守,所以在实际的执行过程中,只要不改变volatile读/写的内存语义,编译器通常会省略一些不必要的内存屏障。

代码如下:

public class volatileBarrierDemo{

int a;

volatile int b = 1;

volatile int c = 2;

public void test(){

int i = b; //volatile

int j = c; //volatile

a = i + j; //普通写

}

}

指令序列示意图如下:

从上图可以看出,通过指令优化一共省略了两个内存屏障(虚线表示),省略第一个内存屏障LoadStore的原因是最后的普通写不可能越过第二个volatile读,

85、volatile能使一个非原子操作变成一个原子操作吗?

volatile只能保证可见性和有序性,但可以保证64位的long型和double型变量的原子性。

对于32位的虚拟机来说,每次原子读写都是32位的,会将long和double型变量拆分成两个32位的操作来执行,这样long和double型变量的读写就不能保证原子性了,

而通过volatile修饰的long和double型变量则可以保证其原子性。

86、volatile、synchronized的区别?

volatile主要是保证内存的可见性,即变量在寄存器中的内存是不确定的,需要从主存中读取。synchronized主要是解决多个线程访问资源的同步性。

volatile作用于变量,synchronized作用于代码块或者方法。

volatile仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。

volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。

ConcurrentHashMap

87、什么是ConcurrentHashMap?相比于HashMap和HashTable有什么优势?

CocurrentHashMap可以看作线程安全且高效的HashMap,相比于HashMap具有线程安全的优势,相比于HashTable具有效率高的优势。

88、java中ConcurrentHashMap是如何实现的?

这里经常会将jdk1.7中的ConcurrentHashMap和jdk1.8中的ConcurrentHashMap的实现方式进行对比。

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry数组组成,Segment存储的是链表数组的形式,如图所示。

从上图可以看出,ConcurrentHashMap定位一个元素的过程需要两次Hash的过程,第一次Hash的目的是定位到Segment,第二次Hash的目的是定位到链表的头部。

两次Hash所使用的时间比一次Hash的时间要长,但这样做可以在写操作时,只对元素所在的segment加锁,不会影响到其他segment,这样可以大大提高并发能力。

JDK1.8不在采用segment的结构,而是使用Node数组+链表/红黑树的数据结构来实现的(和HashMap一样,链表节点个数大于8,链表会转换为红黑树)

如下图所示

从上图可以看出,对于ConcurrentHashMap的实现,JDK1.8的实现方式可以降低锁的粒度,因为JDLK1.7所实现的ConcurrentHashMap的锁的粒度是

基于Segment,而一个Segment包含多个HashEntry。

89、ConcurrentHashMap结构中变量使用volatile和final修饰有什么作用?

​final修饰变量可以保证变量不需要同步就可以被访问和共享,volatile可以保证内存的可见性,配合CAS操作可以在不加锁的前提支持并发。

90、ConcurrentHashMap有什么缺点?

因为ConcurrentHashMap在更新数据时只会锁住部分数据,并不会将整个表锁住,读取的时候也并不能保证读取到最近的更新,只能保证读取到已经顺利插入的数据。

91、ConcurrentHashMap默认初始容量是多少?每次扩容为原来的几倍?

默认的初始容量为16,每次扩容为之前的两倍。

92、ConCurrentHashMap 的key,value是否可以为null?为什么?HashMap中的key、value是否可以为null?

ConCurrentHashMap中的key和value为null会出现空指针异常,而HashMap中的key和value值是可以为null的。

原因如下:ConCurrentHashMap是在多线程场景下使用的,如果ConcurrentHashMap.get(key)的值为null,那么无法判断到底是key对应的value的值

为null还是不存在对应的key值。而在单线程场景下的HashMap中,可以使用containsKey(key)来判断到底是不存在这个key还是key对应的value的值为null。

在多线程的情况下使用containsKey(key)来做这个判断是存在问题的,因为在containsKey(key)和ConcurrentHashMap.get(key)两次调用的过程中,

key的值已经发生了改变。

93、ConCurrentHashmap在JDK1.8中,什么情况下链表会转化为红黑树?

当链表长度大于8,Node数组数大于64时。

94、ConcurrentHashMap在JDK1.7和JDK1.8版本中的区别?

实现结构上的不同,7是基于Segment实现的,JDK1.8是基于Node数组+链表/红黑树实现的。

保证线程安全方面:7采用了分段锁的机制,当一个线程占用锁时,会锁住一个Segment对象,不会影响其他Segment对象。JDK1.8则是采用了CAS和

synchronize的方式来保证线程安全。

在存取数据方面:

JDK1.7中的put()方法:

先计算出key的hash值,利用hash值对segment数组取余找到对应的segment对象。

尝试获取锁,失败则自旋直至成功,获取到锁,通过计算的hash值对hashentry数组进行取余,找到对应的entry对象。

遍历链表,查找对应的key值,如果找到则将旧的value直接覆盖,如果没有找到,则添加到链表中。(7是插入到链表头部,JDK1.8是插入到链表尾部,

这里可以思考一下为什么这样)

JDK1.8中的put()方法:

计算key值的hash值,找到对应的Node,如果当前位置为空则可以直接写入数据。

利用CAS尝试写入,如果失败则自旋直至成功,如果都不满足,则利用synchronized锁写入数据。

95、ConcurrentHashMap迭代器是强一致性还是弱一致性?

与HashMap不同的是,ConcurrentHashMap迭代器是弱一致性。

这里解释一下弱一致性是什么意思,当ConcurrentHashMap的迭代器创建后,会遍历哈希表中的元素,在遍历的过程中,哈希表中的元素可能发生变化,

如果这部分变化发生在已经遍历过的地方,迭代器则不会反映出来,如果这部分变化发生在未遍历过的地方,迭代器则会反映出来。换种说法就是put()方法将一个

元素加入到底层数据结构后,get()可能在某段时间内还看不到这个元素。这样的设计主要是为ConcurrenthashMap的性能考虑,如果想做到强一致性,

就要到处加锁,性能会下降很多。所以ConcurrentHashMap是支持在迭代过程中,向map中添加元素的,而HashMap这样操作则会抛出异常。
ThreadLocal

96、什么是ThreadLocal?有哪些应用场景?

ThreadLocal是 JDK java.lang 包下的一个类,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,

并且不会和其他线程的局部变量冲突,实现了线程间的数据隔离。

ThreadLocal的应用场景主要有以下几个方面:

保存线程上下文信息,在需要的地方可以获取

线程间数据隔离

数据库连接

97、ThreadLocal原理和内存泄露?

要搞懂ThreadLocal的底层原理需要看下他的源码,太长了,有兴趣的同学可以自己看看相关资料,这里只是简单介绍下结构,因为Threadlocal内存泄露是个

高频知识点,并且需要简单了解ThreadLocal结构。

ThreadLocal的原理可以概括为下图:

从上图可以看出每个线程都有一个ThreadLocalMap,ThreadLocalMap中保存着所有的ThreadLocal,而ThreadLocal本身只是一个引用本身并不保存值,

值都是保存在ThreadLocalMap中的,其中ThreadLocal为ThreadLocalMap中的key。其中图中的虚线表示弱引用。

这里简单说下Java中的引用类型,Java的引用类型主要分为强引用、软引用、弱引用和虚引用。

强引用:发生 gc 的时候不会被回收。

软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。

弱引用:有用但不是必须的对象,在下一次GC时会被回收。

虚引用:无法通过虚引用获得对象,虚引用的用途是在 gc 时返回一个通知。

98、为什么ThreadLocal会发生内存泄漏呢?

因为ThreadLocal中的key是弱引用,而value是强引用。当ThreadLocal没有被强引用时,在进行垃圾回收时,key会被清理掉,而value不会被清理掉,

这时如果不做任何处理,value将永远不会被回收,产生内存泄漏。

99、如何解决ThreadLocal的内存泄漏?

其实在ThreadLocal在设计的时候已经考虑到了这种情况,在调用set()、get()、remove()等方法时就会清理掉key为null的记录,所以在使用完

ThreadLocal后最好手动调用remove()方法。

100、为什么要将key设计成ThreadLocal的弱引用?

如果ThreadLocal的key是强引用,同样会发生内存泄漏的。如果ThreadLocal的key是强引用,引用的ThreadLocal 的对象被回收了,但是ThreadLocalMap

还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,发生内存泄漏。

如果是弱引用的话,引用的ThreadLocal的对象被回收了,即使没有手动删除,ThreadLocal也会被回收。value也会在ThreadLocalMap调用 set()、

get()、remove() 的时候会被清除。

所以两种方案比较下来,还是ThreadLoacl的key为弱引用好一些。

线程池

101、什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交给线程池来管理。

102、为什么使用线程池?

降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度,当任务到达时,任务可以不需要等到线程创建就立即执行。

提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一分配。

103、创建线程池的几种方法

线程池的常用创建方式主要有两种,通过Executors工厂方法创建和通过new ThreadPoolExecutor方法创建。

Executors工厂方法创建,在工具类 Executors 提供了一些静态的工厂方法

newSingleThreadExecutor:创建一个单线程的线程池。

newFixedThreadPool:创建固定大小的线程池。

newCachedThreadPool:创建一个可缓存的线程池。

newScheduledThreadPool:创建一个大小无限的线程池。

new ThreadPoolExecutor方法创建: 通过newThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,

TimeUnit unit, BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) 自定义创建
104、ThreadPoolExecutor构造函数的重要参数分析

三个比较重要的参数:

corePoolSize:核心线程数,定义了最小可以同时运行的线程数量。

maximumPoolSize:线程中允许存在的最大工作线程数量

workQueue:存放任务的阻塞队列。新来的任务会先判断当前运行的线程数是否到达核心线程数,如果到达的话,任务就会先放到阻塞队列。

其他参数:

keepAliveTime:当线程池中的数量大于核心线程数时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到时间超过keepAliveTime时才会被销毁。

unit:keepAliveTime参数的时间单位。

threadFactory:为线程池提供创建新线程的线程工厂。

handler:线程池任务队列超过maxinumPoolSize之后的拒绝策略

105、ThreadPoolExecutor的饱和策略(拒绝策略)

当同时运行的线程数量达到最大线程数量并且阻塞队列也已经放满了任务时,ThreadPoolExecutor会指定一些饱和策略。主要有以下四种类型:

AbortPolicy策略:该策略会直接抛出异常拒绝新任务

CallerRunsPolicy策略:当线程池无法处理当前任务时,会将该任务交由提交任务的线程来执行。

DiscardPolicy策略:直接丢弃新任务。

DiscardOleddestPolicy策略:丢弃最早的未处理的任务请求。

106、线程池的执行流程

创建线程池创建后提交任务的流程如下图所示:

107、execute()方法和submit()方法的区别

这个地方首先要知道Runnable接口和Callable接口的区别,之前有写到过

execute()和submit()的区别主要有两点:

execute()方法只能执行Runnable类型的任务。submit()方法可以执行Runnable和 Callable类型的任务。

submit()方法可以返回持有计算结果的Future对象,同时还可以抛出异常,而execute()方法不可以。

换句话说就是,execute()方法用于提交不需要返回值的任务,submit()方法用于需要提交返回值的任务。

CAS

108、什么是CAS?

CAS即CompareAndSwap,翻译成中文即比较并替换。Java中可以通过CAS操作来保证原子性,原子性就是不可被中断的一些列操作或者一个操作,简单来说就是

一系列操作,要么全部完成,要么失败,不能被中断。

CAS主要包含三个参数(V,A,E), V 表示要更新的变量(内存值)、E 表示预期值(旧值)、N 表示新值。算法流程是首先比较A和E的值,如果相等,

将N值赋值给A,如果不相等说明有其他线程对该变量做了更新。这个参数有的地方也会用(V,A,B)表示,其中A表示预期值,B表示新值。

当多个线程同时操作一个共享变量时,只有一个线程可以对变量进行成功更新,其他线程均会失败,但是失败并不会被挂起,进行再次尝试,也就是自旋。

Java中的自旋锁就是利用CAS来实现的。

109、CAS存在的问题

其中ABA问题是面试中比较常见的问题

ABA问题

在CAS的算法流程中,首先要先比较V的值和E的值,如果相等则进行更新。ABA问题是指,E表示的这个旧值本来是A,然后变成了B,后来又变成了A,

但这时有线程来更新,发现E表示的值是A,则直接进行更新了,这样肯定是不对的,但又该怎么解决呢?

ABA的问题的解决方式:ABA的解决方法也很简单,就是利用版本号。给变量加上一个版本号,每次变量更新的时候就把版本号加1,这样即使E的值从A—>B—>A,

版本号也发生了变化,这样就解决了CAS出现的ABA问题。基于CAS的乐观锁也是这个实现原理。

循环时间过长导致开销太大

CAS自旋时间过长会给CPU带来非常大的开销

只能保证一个共享变量的原子操作

在操作一个共享变量时,可以通过CAS的方式保证操作的原子性,但如果对多个共享变量进行操作时,CAS则无法保证操作的原子性,这时候就需要用锁了。

在看《Java并发编程的艺术》时,里面提到了一个办法可以参考一下,就是将多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,

合并成ij=2a,然后用CAS来操作ij

CAS的优点:在并发量不是很大时提高效率。

110、Atomic 原子类

原子操作类是CAS在Java中的应用,从JDK1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、

线程安全地更新一个变量的方式。Atomic包里的类基本都是使用Unsafe实现的包装类。

JUC包中的4种原子类

基本类型:使用原子的方式更新基本类型

AtomicInteger:整形原子类

AtomicLong:长整型原子类

AtomicBoolean:布尔型原子类

数组类型:使用原子的方式更新数组里的某个元素

AtomicIntegerArray:整形数组原子类

AtomicLongArray:长整形数组原子类

AtomicReferenceArray:引用类型数组原子类

引用类型:

AtomicReference:引用类型原子类,存在ABA问题

AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS

进行原子更新时可能出现的ABA问题。

AtomicMarkableReference:原子更新带有标记位的引用类型

原子更新字段类

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。

AtomicLongFieldUpdater:原子更新长整型字段的更新器。

AtomicReferenceFieldUpdater:引用类型更新器原子类

AQS

AQS这部分相对复杂一些,想要深入理解需要去阅读源码,篇幅有限,这里就不展开介绍了。

111、什么是AQS?

AQS的全称是AbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架,像ReentrantLock,Semaphore,FutureTask都是基于AQS实现的。

112、AQS的原理

简单来说,AQS就是维护了一个共享资源,然后使用队列来保证线程排队获取资源的一个过程。

AQS的原理图如下:

AQS的工作流程:当被请求的共享资源空闲,则将请求资源的线程设为有效的工作线程,同时锁定共享资源。如果被请求的资源已经被占用了,AQS就用过队列

实现了一套线程阻塞等待以及唤醒时锁分配的机制

这个队列是通过CLH队列实现的,从上图可以看出,该队列是一个双向队列,有Node结点组成,每个Node结点维护一个prev引用和next引用,这两个引用分别

指向自己结点的前驱结点和后继结点,同时AQS还维护两个指针Head和Tail,分别指向队列的头部和尾部。

从上图可以看出,AQS是维护了一个共享资源和一个FIFO的线程等待队列。

private volatile int state;

通过volatile来保证state的线程可见性,state的访问方式主要有三种,如下。

protected final int getState() {

//获取state的值 return state;

}

protected final void setState(int newState) {

//设置state的值 state = newState;

}

protected final boolean compareAndSetState(int expect, int update) {

//通过CAS操作更新state的值

return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

}

113、AQS的资源共享方式有哪些?

Exclusive:独占,只有一个线程可以执行,例如ReentrantLock

Share:共享,多个线程可同时执行,如Semaphore/CountDownLatch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值