Java 基础复习---并发编程

1、多线程实现的方法
(1、继承Thread 类
(2、实现Runnable 接口
(3、使用ExecutorService、Callable、Future实现有返回结果的多线程

2、Thread 和 Runable 的区别联系
(1、联系
1、Thread 类 实现了Runable接口
2、都需要重写里面的Run方法
(2、不同
1、实现Runnable 类更具有健壮性,避免了继承的局限
2、Runnable 更容易实现资源共享, 能多个线程同时处理一个资源。

3、synchronized 概念
synchronized 是一个重量级锁,,在javaSE1.6 对 synchronized 进行优化之后,synchronized 并不会显得那么笨重了。synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法或者代码块可以进入到临界区,同时它还可以保证共享变量的内存可见性。

4、synchronized 的实现原理
Java 里面每一个对象都可以作为锁,这是synchronized 实现同步的基础。
1)普通同步方法,锁是当前实例对象
2)静态同步方法,锁是当前类的class对象
3)同步代码块 , 锁是括号里面的对象。

当一个线程访问同步代码块时,它首先需要得到锁才能执行同步代码,当退出或者抛出异常时必须释放锁。
1)同步代码块是使用 monitorenter 和 monitorexit 指令实现的。
2)同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED 实现。

monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
 通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

5、锁优化

在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的Mutex Lock 来实现的,但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而,在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境),如果每次都调用 Mutex Lock 那么将严重的影响程序的性能。

因此,JDK 1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

1)、自旋锁:所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。

线程的阻塞和唤醒,需要 CPU 从用户态转为核心态。频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时,我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间。为了这一段很短的时间,频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。

如果通过参数 -XX:PreBlockSpin 来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为 10 ,但是系统很多线程都是等你刚刚退出的时候,就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是 JDK 1.6 引入自适应的自旋锁,让虚拟机会变得越来越聪明。

2) 适应自旋锁:所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。

反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

3)锁消除:在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。

4)锁粗化 : 锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。。

5)可重入锁:指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

6、锁的升级
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。它们会随着竞争的激烈而逐渐升级。注意,锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
1)偏向锁:引入偏向锁的目的是为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径。
2)轻量级锁:引入其轻量级锁的主要目的是在没有多线程竞争得出前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
3)重量级锁:重量级锁通过对象内部的监视器(Monitor)实现的,其中,Monitor 的本质是依赖于低层操作系统Mutex Lock 实现的。操作系统实现线程之间的切换,需要从用户太转到内核态,切换成本非常高。

偏向锁在jdk1.6 里面默认是开启的。

猜你喜欢

转载自blog.csdn.net/weixin_43352448/article/details/87978406