Java基础简历2

一Java中的锁

一、Lock接口提供的Synchronized关键字不具备的主要特性

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功并持有锁
能被中断的获取锁 与Synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

二、同步队列器

同步队列器(AbstractQueueSynchronizer),是用来构建锁或者其他同步组件的基础架构,他用了一个int型成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,主要使用是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

重写同步器指定的方法时,需要使用同步器提供的如下三个方法。

getstate():获取当前同步状态

setState(int newState):设置当前同步状态

compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

1独占式同步状态获取和释放:在获取同步状态时,同步器维护了一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点并且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头结点的后继节点。

2写操作要求对资源的独占式访问,而读操作可以是共享式访问。调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态。在该方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int 类型,当返回值大于等于0时,表示能够获取同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquire(int arg)返回值大于等于0.

三、重入锁ReentrantLock

Synchronized隐式的支持重进入。ReentrantLock支持重进入,即该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平性选择。

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

ReentrantLock提供了一个构造函数,能够控制锁是否公平。

事实上公平锁的效率往往没有非公平的效率高,但是并不是任何场景都是以TPS作为唯一的指标,公平锁能减少饥饿的发生的概率,等待越久越是能够得到优先满足。

1可重入实现:两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。  

2读写锁:之前提到锁(如Mutex和ReentrantLock)基本都是排它锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。读写锁维护了一对锁,一个读锁和写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大提升。

ReentrantReadWriteLock的特性

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

锁降级:把持住写锁(当前拥有的),再获取到读锁,随后释放(先前拥有的)写锁的释放过程。

四、condition接口

任意一个对象,都拥有一组监视器方法(定义在Java.lang.Object上),主要包括wait(),wait(long),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。但是这两者在使用方式以及功能特性上还是有差别的。

Object监视器方法和Condition接口对比

对比项 Object Monitor Methods Condition
前置条件 获取对象锁

调用Lock.lock()获取锁

调用Lock.newCondition()获取Condition对象

调用方法 直接调用:如object.wait() 直接调用 如:condition.await()
等待队列个数 一个 多个
当前线程释放锁并进入等待状态 支持 支持

当前线程释放锁并进入等待状态,在等待

状态中不响应中断。

不支持 支持
当前线程释放锁并进入超时等待 支持 支持
当前线程释放锁并进入等待状态到将来的某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueueSynchronizer(AQS)。AQS使用一个整型的volatile变量(state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

volatile变量的内存意义:

volatile变量自身具有下列特性:

可见性。对一个volatile变量读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性。对任意单个volatile变量的读/写具有原子性,但类似volatile这种复合操作不具有原子性。

volatile写-读的内存语义:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

Java中的悲观锁和乐观锁,活锁,死锁:

悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。在比如Java里面的同步原语synchronized关键字的实现也是悲观锁

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

活锁和死锁:

死锁:两个或多个线程相互等待对方释放锁,则会出现死锁现象。Java虚拟机没有检测,也没有采用措施来处理死锁情况,所以多线程编程是应该采取避免死锁的出现。一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态。

形成死锁的条件

  1. 互斥条件:线程使用的资源必须至少有一个是不能共享的(至少有锁);
  2. 请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源(至少两个线程持有不同锁,又在等待对方持有锁);
  3. 非剥夺条件:分配资源不能从相应的线程中被强制剥夺(不能强行获取被其他线程持有锁);
  4. 循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程(线程A等线程B;线程B等线程C;...;线程N等线程A。如此形成环路)。

死锁预防

  1. 加锁顺序

    当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。这种方式是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,但总有些时候是无法预知的。

  2. 加锁时限
    在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁。
  3. 死锁检测(检测死锁的比如有jstack)
    死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。检测到死锁之后:
    一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。

    一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

打破死锁

这里不做讨论和验证,原则是形成死锁条件的四个必要条件打破任意一个条件,就可以打开死锁。

活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

活锁的几个例子

单一实体的活锁

例如线程从队列中拿出一个任务来执行,如果任务执行失败,那么将任务重新加入队列,继续执行。假设任务总是执行失败,或者某种依赖的条件总是不满足,那么线程一直在繁忙却没有任何结果。

协同导致的活锁

生活中的典型例子: 两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复。一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞,所以是活的状态。

通信中也有类似的例子,多个用户共享信道(最简单的例子是大家都用对讲机),同一时刻只能有一方发送信息。发送信号的用户会进行冲突检测, 如果发生冲突,就选择避让,然后再发送。 假设避让算法不合理,就导致每次发送,都冲突,避让后再发送,还是冲突。

计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁。

活锁的解决方法

解决协同活锁的一种方案是调整重试机制。

比如引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。

另外为了避免可能的死锁,适当加入一定的重试次数也是有效的解决办法。尽管这在业务上会引起一些复杂的逻辑处理。

比如约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞。

一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因?

synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有的线程挂起,让CPU通过操作系统指令,去调度多线程之间,谁执行代码,谁进入阻塞状态。这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,这样就会大量消耗资源,程序运行的效率低下。为了提高效率,JVM的开发人员,引入了偏向锁,和轻量级锁,尽量让多线程访问公共资源的时候,不进行程序运行状态的切换,由用户态进入内核态,借助操作系统进行互斥。

-->jvm规范中可以看到synchronized在jvm里实现原理,jvm基于进入和退出Monitor对象来实现方法同步和代码块同的。在代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和异常结束处)织入monitorexit指令实现。线程执行到monitorenter处,讲会获取锁对象锁对应的monitor的所有权,即尝试获得对象的锁。(任意对象都又一个monitor与之关联,当且一个monitor被持有后,他处于锁定状态)

--->java的多线程安全是基于lock机制实现的,而lock的性能往往不如人意。原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是jvm依赖操作系统互斥(mutex)来实现的。

--->互斥是一种会导致线程挂起,并在较短时间内又需要重新调度回原线程的,较为消耗资源的操作。

--->为了优化java的Lock机制,从java6开始引入轻量级锁的概念。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了cpu原语Compare-And-Swap(cas,汇编指令CMPXCHG),尝试进入互斥前,进行补救。

二:为什么要自旋或者自适应自旋?

--->前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如 果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有 锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 

--->自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的, 所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作, 反而会带来性能的浪费。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次 数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。 

--->在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象 上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自 旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 

三:锁削除

--->锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数 据支持(第11章已经讲解过逃逸分析技术),如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待, 认为它们是线程私有的,同步加锁自然就无须进行。 

--->也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的 情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。比如:(只是说明概念,但实际情况并不一定如例子)在线程安全的环境中使用stringBuffer进行字符串拼加。则会在java文件编译的时候,进行锁销除。

四:锁粗化
--->原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快地拿到锁。
--->大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

--->如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(锁粗化)到整个操作序列的外部。

五:偏向锁,轻量级锁,重量级锁对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

对象头的存储内容(monitor)

长度 内容 说明
32/64bit Mark Word 存储对象的hashcode或锁信息
32/64bit 类对象的地址 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)

Mark Word存储内容(monitor)的状态变化

锁状态 25bit,4bit 1bit(是否是偏向锁) 2bit(锁标示位)
轻量级锁 指向栈中锁记录的指针   00
重量级锁 指向互斥量(重量级锁)的指针   10
 GC   11
偏向锁 线程id,对象hashcode,对象分代年龄 1 01

六:锁的状态

--->锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态

--->锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

七:偏向锁

--->a线程获得锁,会在a线程的的栈帧里创建lock record(锁记录变量),则在锁对象的对象头里和lock record里存储a线程的线程id.以后该线程的进入,就不需要cas操作,只需要判断是否是当前线程。

--->a线程获取锁,不会释放锁。直到b线程也要竞争该锁时,a线程才会释放锁。

--->偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。

--->关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。

八:轻量级锁

--->a线程获得锁,会在a线程的栈帧里创建lock record(锁记录变量),让lock record的指针指向锁对象的对象头中的mark word.再让mark word 指向lock record.这就是获取了锁。

--->轻量级锁,b线程在锁竞争时,发现锁已经被a线程占用,则b线程不进入内核态,让b线程自旋,执行空循环,等待a线程释放锁。如果,完成自旋策略还是发现a线程没有释放锁,或者让c线程占用了。则b线程试图将轻量级锁升级为重量级锁。

十:重量级锁

--->重量级锁,就是让争抢锁的线程从用户态转换成内核态。让cpu借助操作系统进行线程协调。

猜你喜欢

转载自blog.csdn.net/cclliii/article/details/82936871
今日推荐