深入了解java同步、锁紧机构

该薄膜还具有从本文试图一个高度来认识我们共同的同步(synchronized)和锁(lock)机制。

我们假定读者想了解更多的并发知识推荐一本书《java并发编程实战》,这是一个经典的书,英语水平良好的学生也可以读《Concurrent programming in Java - design principles and patterns》由Doug Lea亲自操刀。Doug Lea是并发方面的大神,jdk的并发包就是由他完毕的。

我们都知道在java中被synchronized修饰的代码被称为同步代码块。同步代码块意味着同一时刻仅仅有一个线程运行。其它线程都被排斥在该同步块之外,而且訪问也是依照某种顺序运行的。实际上synchronized是基于监视器实现的,每个实例和类都拥有一个监视器,通常我们说的“锁”的动作就是获取该监视器。

因此通常我们讲synchronized是基于JVM层面的,使用的是对象内置的锁。静态方法锁住的是该class的监视器。实例方法锁住的是相应实例的监视器。

同步是使用monitorenter和monitorexit指令实现的。monitorenter尝试获取对象的锁,假设该对象没被锁定或者当前线程已经获取了锁。则把锁的计数器+1,相同monitorexit把锁的计数器-1。

因此synchronized对于同一个线程是可重入的。

监视器支持两种线程:相互排斥(sync)和协作。java通过对象的锁实现对临界区的相互排斥訪问。使用Object的wait(),notify(),notifyAll()方法来实现。

乐观锁和悲观锁

这两个名字非常多地方都出现过,所谓的乐观锁就是当去做某个改动或其它操作的时候它觉得不会有其它线程来做相同的操作(竞争)。这是一种乐观的态度。一般是基于CAS原子指令来实现的。关于CAS能够參见这篇文章java并发包的CAS操作。CAS通常不会将线程挂起,因此有时性能会好一些。(线程的切换是挺耗性能的一个操作)。

悲观锁,依据乐观锁的定义非常easy理解悲观锁是觉得肯定有其它线程来争夺资源,因此无论究竟会不会发生争夺。悲观锁总是会先去锁住资源。

曾经的synchronized都是会堵塞线程的,就是说会发生上下文切换。从用户态切换到内核态。由于这样的方式有时候太耗费资源,因此后来又出现了自旋锁。所谓自旋事实上就是假设锁已经被其它线程占有,当前线程并不会挂起,而是做空操作,自旋事实上从某种程度来说是乐观锁,由于它总是觉得下次会得到锁的。因此自旋锁适合在竞争不激烈的情况下使用,据了解眼下的jvm针对synchronized已经有了这方面的优化。

自旋的使用也是分场景的。有可能线程自旋非常久也没获取到锁。那么CPU就白白被浪费了,还不如挂起线程,因此有出现了自适应的自旋锁,它会更具历史的自旋是否获取到锁的记录来推断自旋的时间或者是否须要自旋。

轻量级锁

轻量级锁的概念是相对须要相互排斥操作的重量级锁而言,轻量级锁的目的是降低多线程的相互排斥几率。并非要取代相互排斥。

要想了解轻量级锁和后面讲到的偏向锁必须先了解下对象头的内存布局。以下这张图就是Object Header的内存布局:

初始都是01表示无锁。00表示轻量级锁,10表示重量级锁等等。在代码进入同步块的时候,假设此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象眼下的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀。即Displaced Mark Word)。然后虚拟机尝试利用CAS操作将对象的轻量级指针指向栈的lock record,假设更新成功当前线程获取到锁,而且标记为00轻量级锁。

假设这个更新操作失败了。虚拟机首先会检查对象的Mark
Word是否指向当前线程的栈帧,假设是就说明当前线程已经拥有了这个对象的锁。那就能够直接进入同步块继续运行,否则说明这个锁对象已经被其它线程抢占了。假设有两条以上的线程争用同一个锁。那轻量级锁就不再有效。要膨胀为重量级锁。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(相互排斥量)的指针,后面等待锁的线程也要进入堵塞状态。

偏向锁

偏向锁就是偏心的意思,当锁被某个线程第一次获取到得时候。会在对象头记录获取到该锁的线程id,以后每次该线程进入同步块的时候都不须要加锁,假设一旦有其它线程获取到该锁,则偏向锁模式宣告失败,锁撤销回未锁定或轻量级锁状态。偏向锁的作用就是全然消除锁。连CAS操作都不做。

以下来看一下线程在进入同步块和出同步块的状态转换。

当多个线程同一时候请求某个对象监视器时。对象监视器会设置几种状态用来区分请求的线程:

  • Contention List:全部请求锁的线程将被首先放置到该竞争队列
  • Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
  • Wait Set:那些调用wait方法被堵塞的线程被放置到Wait Set
  • OnDeck:不论什么时刻最多仅仅能有一个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程

以下是一位网友画得图非常形象:

新请求的线程会被放置到ContentionList中。当某个Owner释放锁的时候。假设EntryList是空则Owner会从ContentionList中移动线程到EntryList。

显然,ContentionList结构事实上是个Lock-Free的队列,由于仅仅有Owner才会从ContentionList取节点。

EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发訪问,为了减少对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并非把锁传递给OnDeck线程,仅仅是把竞争锁的权利交给OnDeck,OnDeck线程须要又一次竞争锁。

这样做尽管牺牲了一定的公平性,但极大的提高了总体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。

可重入锁

可重入锁的最大优点是能够避免思索。由于对于已经获取到锁的线程。不须要再一次去获取锁了,仅仅须要将计数器+1就可以。实际上synchronized也是可重入锁的一种。可是本节我们要讲的是并发包中的ReentrantLock及事实上现。synchronized是JVM层面提供的锁。而在java的语言层面jdk也为我们提供了很优秀的锁,这些锁都在java.util.concurren包中。

先来看一下JVM提供的锁和并发包中的锁有哪些差别:

1.synchronized的加锁和释放都是由JVM提供,不须要我们关注,而lock的加锁和释放所有由我们去控制,通常释放锁的动作要在finally中实现。

2.synchronized仅仅有一个状态条件。也就是每一个对象仅仅有一个监视器,假设须要多个Condition的组合那么synchronized是无法满足的。而lock则提供了多条件的相互排斥。很灵活。

3.ReentrantLock 拥有Synchronized同样的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候。

在解说ReentrantLock之前。先来看下不AtomicInteger源码大体了解下它的实现原理。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
     //该方法相似同步版本号的i++。先将当前值+1,然后返回,
     //能够看到是一个for循环,仅仅有当compareAndSet成功才会返回
     //那么什么时候成功呢?
    public final int incrementAndGet() {
        for (;;) {
            int current = get();//volatile类型的变量。因此每次获取都是最新值
            int next = current + 1;//加1操作
            if (compareAndSet(current, next))//关键的是if中的方法
	    //假设compareAndSet成功,则整个加操作成功,假设失败,则说明有其它线程已经改动了value
	    //那么会进行下一轮的加1操作,直到成功
                return next;
        }
    }
/**
     * Gets the current value.
     *
     * @return the current value
     */
     //get方法很easy,返回value,这个value是类的成员变量。而且是volatile的
    public final int get() {
        return value;
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        //继续跟踪unsafe的方法,发现并没提供,实际上该方法是个基于本地类库的原子方法,使用一个指令就可以完毕操作。
	//假设内存中的值和预期的值同样,也就是没有其它线程改动过该值,则更新该值为预期的值,返回成功,否则返回失败
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

能够预见的是假设竞争很激烈,则失败的概率会大大添加。性能也会受到影响。实际上并发包中的锁大多是基于CAS操作完毕的。本节打算解说可重入锁,但很多事情还是需要知道,刚刚好再次写入介绍ReentrantLock该。

版权声明:本文博客原创文章,博客,未经同意,不得转载。

时间: 2024-10-10 16:15:25

深入了解java同步、锁紧机构的相关文章

java 同步锁(synchronized)

java 同步锁(synchronized) 在java中,Synchronized就是一把锁,他可以锁定一个方法,也可以锁定一个方法,我擦,其实这两个东西就是一样的.块不就是一个没有名字的方法么,方法就是一个有名字的块.本文就用块来测试.所谓锁,就是原子操作,把这个锁定的块作为一个整体,就像你上厕所,拉了就要擦屁屁,当然你也可以不擦,如果你不在意出现的问题的话.信号量Semaphore和这个Synchronized 其实实现的功能差不多,不过效率不同,使用的方式也不同.Synchronized

Java同步锁

Java同步锁 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏.例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. package cn.thread; public class Foo { private int x = 100; public int getX() { return x; } public int fix(int y) { x = x - y; return x; } } package cn.thread;

java 同步锁方法

方法一:动态同步锁 class Demo_thread implements Runnable{ public static int sum = 0; public synchronized void add(){//同步锁,为动态方法 for(int i=0;i<5000;i++){ sum = sum + 1; } } public void run(){ add(); } } 动态同步锁适用于Runnable类中不适用与Thread类,因为其锁的对象为当前实例对象,一个Thread类只能跑

java同步锁的正确使用

同步锁分类 对象锁(this) 类锁(类的字节码文件对象即类名.class) 字符串锁(比较特别) 应用场景 在多线程下对共享资源的安全操作. 需求:启动5个线程对共享资源total进行安全操作. 同步锁在多线程单例模式下的使用 以上三类同步锁都可以. package cn.myThread; public class MyThread implements Runnable { private static int total = 10; @Override public void run()

《深入浅出 Java Concurrency》—锁紧机构(一)Lock与ReentrantLock

转会:http://www.blogjava.net/xylz/archive/2010/07/05/325274.html 前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来总体说明. 从这一章開始花少量的篇幅谈谈锁机制. 上一个章节 中谈到了锁机制,而且针对于原子操作谈了一些相关的概念和设计思想.接下来的文章中.尽可能的深入研究锁机制,而且理解里面的原理和实际应用场合. 虽然synchronized在语法上已经足够简单了.在JDK 5之前仅仅能借助此实现,

Java同步锁——lock与synchronized 的区别【转】

一.Lock和synchronized有以下几点不同: 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中: 2)synchronized在发生异常时,会

Java同步锁何时释放?

任何线程进入同步代码块.同步方法之前,必须获得同步监视器的锁定,那么何时会释放这个锁定呢?在程序中,是无法显式释放对同步监视器的锁的,而会在如下几个情况下释放锁. 1.当前线程的同步方法.代码块执行结束的时候释放 2.当前线程在同步方法.同步代码块中遇到break . return 终于该代码块或者方法的时候释放. 3.....出现未处理的error或者exception导致异常结束的时候释放 4.....程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁 ==============

java中多线程模拟(多生产,多消费,Lock实现同步锁,替代synchronized同步代码块)

import java.util.concurrent.locks.*; class DuckMsg{ int size;//烤鸭的大小 String id;//烤鸭的厂家和标号 DuckMsg(){ } DuckMsg(int size, String id){ this.size=size; this.id=id; } public String toString(){ return id + " 大小为:" + size; } } class Duck{ private int

【Java】多线程冲突解决——同步锁

转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/5827547.html 解决并行冲突最有效的方法就是加同步锁,主要有以下几种方法: 1:动态方法同步锁:锁当前对象.即调用该方法的类对象优先执行完毕才到下一个任务. public synchronized void 方法() { } 实例代码: import java.lang.Thread.State; import org.omg.CORBA.PUBLIC_MEMBER; public class Thr