认识JUC 多线程

JUC 多线程

1. 认识多线程

一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在 于充分利用了 CPU 的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行.数据的交互.资源的分配等问题更加易于解决。

2. 什么是线程?

进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

3. 什么是进程?

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个 应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建.运行到消亡的过程。

4. 进程与线程的区别?

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

注意:

\1. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是不 能完全控制的(可以设置线程优先级,有限度的控制顺序)。而这也就造成的多线程的随机性。

\2. Java 程序的进程里面至少包含两个线程,主线程也就是 main()方法线程,另外一个是垃圾回收机制线程。每 当使用 java 命令执行一 个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系 统中启动了一个 线程,java 本身具备了垃圾的收集机制,所以在 Java运行时至少会启动两个线程。

\3. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建 多线程,而不是创建多进程。

4.1什么是线程安全?

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量. 静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的; 若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

5. 什么时候使用线程同步?

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全问题,Java 中提供了同步机制(synchronized)来解决。

6. 线程同步机制有哪些?

同步代码块.同步方法.lock 锁等。

7. 对死锁的理解?

多线程死锁:同步中嵌套同步,导致锁无法释放。

死锁解决办法:不要在同步中嵌套同步

8. 多线程并发的 3 个特性?

原子性,可见性和有序性。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任

何因素打断,要么就都不执行

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其

他线程能够立即看得到修改的值

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

9. 锁优化

synchronized 是重量级锁,效率不高。但在 JDK 1.6 中对 synchronize 的实现进行了各种优化,使得它显得不是那么重了。JDK1.6 对锁的实现引入了 大量的优化,如自旋锁.适应性自旋锁.锁消除.锁粗化.偏向锁.轻量级锁等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态,他们会随着竞争的激烈而逐渐升级。

注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

10. 自旋锁

线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这 一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁 的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。

所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有 获取到锁,则应该被挂起。自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用-XX:+UseSpinning 开开启,在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数-XX:PreBlockSpin 来调整;如果通过参数-XX:preBlockSpin 来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为 10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是 JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

11. 适应自旋锁

JDK 1.6 引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意 味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥 有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功, 那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋 过程,以免浪费处理器资源。 有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

12. 锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM 检测到不可能存在共享数据竞争,这是 JVM 会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义 的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定, 但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些 JDK 的内置 API 时,如 StringBuffer.Vector.HashTable 等,这个时候会存在隐形的加锁操作。比如StringBuffer 的 append()方法,Vector 的 add()方法:

13. 锁粗化

在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作量尽可能 缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。锁粗话概念比较好理解,就是将多个连续的加锁.解锁操作连接在一起, 扩展成一个范围更大的锁。如上面实例:vector 每次 add 的时候都需要加锁操作,JVM 检测到对同一个对象(vector)连续加锁.解锁操作,会合并一个更大范围的加锁.解锁操作,即加锁解锁操作会移到 for 循环之外。

14. 偏向锁

轻量级锁的加锁解锁操作是需要依赖多次 CAS 原子指令的。而偏向锁只 需要检查是否为偏向锁.锁标识为以及 ThreadID 即可,可以减少不必要的 CAS操作。

15. 轻量级锁

引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重 量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞 争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。轻量级锁主要使用 CAS 进行原子操作。但是对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的 CAS 操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

16. 重量锁

重量级锁通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock(互斥锁)实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

猜你喜欢

转载自blog.csdn.net/Your1221/article/details/118788735