对象的内存布局与锁类型

来源:互联网 发布:2016大数据教程百度云 编辑:程序博客网 时间:2024/06/05 08:54

对象的内存布局

对象在内存中的存储的布局可以分为3块区域:对象头(Header)、示例数据(Instance Data)、和对齐填充(Padding)

1. 对象头

对象头分为两部分,一部分用于存储自身对象的运行时数据,如哈希码,GC粉黛年龄,锁状态,等等,这里举例了32位虚拟机上的mark word。

另一部分是类型指针,即对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据信息不一定要通过对象的本身,另外,如果对象是一个java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。

2. 实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中的定义的,都需要记录起来,我这部分的存储顺序会受到虚拟机分配策略参数和字段在java源码中定义的顺序的影响。根据HotSpot的默认的分配策略,相同宽度的字段总是被分配到一起,在满足这个前提下,父类中定义的变量会出现在子类之前。

3. 对齐填充

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的做哟哦那个,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此当对象实力数据部分没有对齐的时候,就会通过对齐填充来补全

锁的状态

需要注意的是锁的状态,并不是锁的种类,这两个意义不能够混淆!锁的状态总共包含:无锁状态、偏向锁、轻量级锁、重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现的锁的降级)JDK1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

1.偏向锁

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1.1 偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

1.2 偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

2. 轻量锁

轻量级锁的加锁过程

  (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

  (2)拷贝对象头中的Mark Word复制到锁记录中。

  (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

  (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。

  (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

img

图2.1 轻量级锁CAS操作之前堆栈与对象的状态

img

图2.2 轻量级锁CAS操作之后堆栈与对象的状态

3. 重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

4. 转化

重量级锁、轻量级锁和偏向锁之间转换

img

5. 锁的优缺点对比

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

锁的类型

1. 自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区

public Class SpinLock{    private AtomicReference<Thread> sign = new AtomicReference<>();    public void lock()    {        Thread current = Thread.currentThread();        while(!sign.compareAndSet(null, current){        }    }    public void unlock (){        Thread current = Thread.currentThread();        sign.compareAndSet(current, null);    }}

使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。

2. 阻塞锁

被阻塞的线程,不会争夺锁。阻塞锁,与自旋锁不同,改变了线程的运行状态。
在JAVA环境中,线程Thread有如下几个状态:

  1. 新建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 死亡状态

阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c经常使用)
下面是一个JAVA 阻塞锁实例

public class CLHLock1 {    public static class CLHNode {        private volatile Thread isLocked;    }    @SuppressWarnings("unused")    private volatile CLHNode tail;    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>();    private static final AtomicReferenceFieldUpdater<CLHLock1, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock1.class,CLHNode.class, "tail");    public void lock() {        CLHNode node = new CLHNode();        LOCAL.set(node);        CLHNode preNode = UPDATER.getAndSet(this, node);        if (preNode != null) {            preNode.isLocked = Thread.currentThread();            LockSupport.park(this);            preNode = null;            LOCAL.set(node);        }    }    public void unlock() {        CLHNode node = LOCAL.get();        if (!UPDATER.compareAndSet(this, node, null)) {            System.out.println("unlock\t" + node.isLocked.getName());            LockSupport.unpark(node.isLocked);        }        node = null;    }}

在这里我们使用了LockSupport.unpark()的阻塞锁。 该例子是将CLH锁修改而成。
阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。
在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。
理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。

3. 可重入锁

可重入锁 多次进入改锁的域(转载http://ifeve.com/java_lock_see4/#more-15758)
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

可重入锁最大的作用是避免死锁
我们以自旋锁作为例子,

public Class SpinLock{    private AtomicReference<Thread> sign = new AtomicReference<>();    public void lock()    {        Thread current = Thread.currentThread();        while(!sign.compareAndSet(null, current){        }    }    public void unlock (){        Thread current = Thread.currentThread();        sign.compareAndSet(current, null);    }}

对于自旋锁来说,
1、若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁
说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
2、若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。
(采用计数次进行统计)
修改之后,如下:

public class SpinLock1 {    private AtomicReference<Thread> owner =new AtomicReference<>();    private int count =0;    public void lock(){        Thread current = Thread.currentThread();        if(current==owner.get()) {            count++;            return ;        }        while(!owner.compareAndSet(null, current)){        }    }    public void unlock (){        Thread current = Thread.currentThread();        if(current==owner.get()){            if(count!=0){                count--;            }else{                owner.compareAndSet(current, null);            }        }    }}

该自旋锁即为可重入锁。

4. 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。ReadWriteLock是java中的读写锁

读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是,在同一时刻,它只允许有一个写操作在进行。并且,在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。
这样的规则对于针对同一块数据的并发读写来讲是非常贴切的。因为,无论读操作的并发量有多少,这些操作都不会对数据本身造成变更。而写操作不但会对同时进行的其他写操作进行干扰,还有可能造成同时进行的读操作的结果的不正确。例如,在32位的操作系统中,针对int64类型值的读操作和写操作都不可能只由一个CPU指令完成。在一个写操作被进行的过程当中,针对同一个只的读操作可能会读取到未被修改完成的值。该值既不与旧的值相等,也不等于新的值。这种错误往往不易被发现,且很难被修正。因此,在这样的场景下,读写锁可以在大大降低因使用锁而对程序性能造成的损耗的情况下完成对共享资源的访问控制。

5. 互斥锁

互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。Lock接口及其实现类ReentrantLock

6. 悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

7. 乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

/** * 乐观锁 * * 场景:有一个对象value,需要被两个线程调用,由于是共享数据,存在脏数据的问题 * 悲观锁可以利用synchronized实现,这里不提. * 现在用乐观锁来解决这个脏数据问题 * * @author lxz * */public class OptimisticLock {    public static int value = 0; // 多线程同时调用的操作对象    /**     * A线程要执行的方法     */    public static void invoke(int Avalue, String i)            throws InterruptedException {        Thread.sleep(1000L);//延长执行时间        if (Avalue != value) {//判断value版本            System.out.println(Avalue + ":" + value + "A版本不一致,不执行");            value--;        } else {            Avalue++;//对数据操作            value = Avalue;;//对数据操作            System.out.println(i + ":" + value);        }    }    /**     * B线程要执行的方法     */    public static void invoke2(int Bvalue, String i)            throws InterruptedException {        Thread.sleep(1000L);//延长执行时间        if (Bvalue != value) {//判断value版本            System.out.println(Bvalue + ":" + value + "B版本不一致,不执行");        } else {            System.out.println("B:利用value运算,value="+Bvalue);        }    }    /**     * 测试,期待结果:B线程执行的时候value数据总是当前最新的     */    public static void main(String[] args) throws InterruptedException {        new Thread(new Runnable() {//A线程            public void run() {                try {                    for (int i = 0; i < 3; i++) {                        int Avalue = OptimisticLock.value;//A获取的value                        OptimisticLock.invoke(Avalue, "A");                    }                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        new Thread(new Runnable() {//B线程            public void run() {                try {                    for (int i = 0; i < 3; i++) {                        int Bvalue = OptimisticLock.value;//B获取的value                        OptimisticLock.invoke2(Bvalue, "B");                    }                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }}

7.1 CAS方式

Java非公开API类Unsafe实现的CAS(比较-交换),由C++编写的调用硬件操作内存,保证这个操作的原子性,concurrent包下很多乐观锁实现使用到这个类,但这个类不作为公开API使用,随时可能会被更改.我在本地测试了一下,确实不能够直接调用,源码中Unsafe是私有构造函数,只能通过getUnsafe方法获取单例,首先去掉eclipse的检查(非API的调用限制)限制以后,执行发现报 java.lang.SecurityException异常,源码中getUnsafe方法中执行访问检查,看来java不允许应用程序获取Unsafe类. 值得一提的是反射是可以得到这个类对象的.

7.2 加锁方式

利用Java提供的现有API来实现最后数据同步的原子性(用悲观锁).看似乐观锁最后还是用了悲观锁来保证安全,效率没有提高.实际上针对于大多数只执行不同步数据的情况,效率比悲观加锁整个方法要高.特别注意:针对一个对象的数据同步,悲观锁对这个对象加锁和乐观锁效率差不多,如果是多个需要同步数据的对象,乐观锁就比较方便.

8. 公平锁

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

//公平获取锁  java.util.concurrent.locks.ReentrantLock$FairSync.java protected  final  boolean  tryAcquire( int  acquires) {     final  Thread current = Thread.currentThread();     int  c = getState();     //状态为0,说明当前没有线程占有锁     if  (c ==  0 ) {         //如果当前线程是等待队列的第一个或者等待队列为空,则通过cas指令设置state为1,当前线程获得锁         if  (isFirst(current) &&             compareAndSetState( 0 , acquires)) {             setExclusiveOwnerThread(current);             return  true ;         }     }//如果当前线程本身就持有锁,那么叠加状态值,持续获得锁     else  if  (current == getExclusiveOwnerThread()) {         int  nextc = c + acquires;         if  (nextc <  0 )             throw  new  Error( "Maximum lock count exceeded" );         setState(nextc);         return  true ;      }      //以上条件都不满足,那么线程进入等待队列。      return  false ;} 

9. 非公平锁

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

        //非公平获取锁  java.util.concurrent.locks.ReentrantLock$UnFairSync.java final boolean nonfairTryAcquire(int acquires) {            final Thread current = Thread.currentThread();            int c = getState();            if (c == 0) {                //如果当前没有线程占有锁,当前线程直接通过cas指令占有锁,管他等待队列,就算自己排在队尾也是这样                if (compareAndSetState(0, acquires)) {                    setExclusiveOwnerThread(current);                    return true;                }            }            else if (current == getExclusiveOwnerThread()) {                int nextc = c + acquires;                if (nextc < 0) // overflow                    throw new Error("Maximum lock count exceeded");                setState(nextc);                return true;            }            return false;        }
原创粉丝点击