stone

一个简单的并发程序(2)volatile
回顾上次讲到volatile这个关键字,主要作用是保证变量的可见性,通过添加内存屏障,来确保进程对变量的修改可以马...
扫描右侧二维码阅读全文
26
2018/09

一个简单的并发程序(2)volatile

回顾

上次讲到volatile这个关键字,主要作用是保证变量的可见性,通过添加内存屏障,来确保进程对变量的修改可以马上被其他进程看到。但是保证可见性并不能保证操作的原子性。先看下面的程序:

package com.stone;

public class Sync {

    private static volatile int i =0;

    public static void main(String[] args) {
        Runnable task1 = new Task();
        Runnable task2 = new Task();

        new Thread(task1).start();
        new Thread(task2).start();

        System.out.println(i);
    }


    static class Task implements Runnable{

        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                increase();
            }
        }

        private void increase() {
            i++;
        }
    }
}

重新说明一点,存在并发问题的程序有两个条件:

  1. 存在共享资源(临界资源)
  2. 多个进程同时访问共享资源

上面的程序是一个存在并发问题的程序,结果不是20000,volatile只能保证每次读取的i都是最新的,但是不能保证修改的是互斥的,所以存在多个线程同时修改同一个时刻的变量,所以结果不正确。

互斥

互斥是指某一段程序某一时刻只能有一个进程执行,不同进程之间是互相排斥的。
sychronized就是一个互斥锁。互斥锁规定一段程序在某一个时刻只能有一个程序在运行,而其他的程序只能等待,所以效率非常低。sychronized是一个重量锁。

java中的锁分为:偏向锁,自旋锁,轻量锁和重量锁,锁的状态可以从低级锁升级为高级锁,这种升级是单向

sychronized可以用来修饰下面三种东西:

  1. 普通方法: 相当于实例锁,访问同一个实例则会触发锁操作
  2. 静态方法: 相当于类锁,访问同一个类的不同对象的该方法会触发锁操作
  3. 代码块:访问该代码块则会触发锁操作

代码块的锁定:

private void increase() {
    synchronized (this){
        i++;
    }
}

问题: synchronize代码块锁定的对象还是代码?

总的来说,synchronized容易理解,使用简单,但是性能较低

synchronized的实现原理

synchronized的实现分为利用MonitorObject(monitorenter,monitorexit)的显式同步和隐式同步。

jvm对象头

参考资料

synchronized的实现是锁定一个对象的,所以怎么锁定一个对象呢?关键在于java对象头。java的对象头分为三部分:

header(Mark Word,Klass Pointer,length),metadata,填充对象

填充对象的存在是因为jvm要求对象的起始位置必须为8的倍数

image

  • 对象头为2个机器码或者3个机器码
    image
  • Mark Word:该部分设计为可变部分,根据锁的状态可以变为两个字节,另外一个字节用来的储存锁的相关信息。
    image

69190-c2bcy2dskph.png

思考:

  1. new Object()占用多少空间?8个字节
  2. new Integer()占用多少空间?16个字节
  3. 下面的对象占用的空间大小?24个字节
Class A {
   int i;
   byte b;
   String str;
}

(byte需要填充)

Object Monitor

image

objectMonitor有这样几个东西:_WaitSet,_EntryList,_owner,_counter

当代码块使用synchronized修饰后,在字节码上回自动添加monitorenter和monitorexit两个指令。当一个对象需要获取锁的时候,会先进入_WaitSet中,当对象_owner为空的时候,可以获取该对象锁,则会进入owner区,_owner设置为需要加锁的对象,并且_count++,当执行到wait的时候,会从释放锁对象,此时会从owner区出去,进入_WaitSet。当退出代码块的时候,则会释放_owner对象,_counter会置为0。

monitorenter必须和monitorexit成对出现,所以会隐式添加try catch机制,确保异常接触的时候也能正确执行monitorexit

方法级的synchronized

方法级的synchronized不需要额外的字节码进行实现,直接使用method的结构信息中的标志位ACC_SYNCHRONIZED来表示,如果设置了,则会去获取monitor,就和上面的原理差不多了。

synchronized的可重入性

objectmonitor对象有一个_counter计数变量,如果同一个线程多次获取对象是允许的,这叫做重入。

others

同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。

Last modification:November 27th, 2018 at 09:57 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment