Administrator
Published on 2021-06-30 / 178 Visits
0

【线程与并发】synchronized

synchronized

概念

  • 意思为同步,同步的等意思;主要用于处理多线程并发。
  • synchronized的底层实现虚拟机里没有规范要求,由具体的JVM实现(如hotspot)。
  • 保证原子性,保证可见性,保证有序性。
  • 可重入属性。
  • 修饰的方法/代码块中执行如果出现异常(不try catch的话),那么会默认释放锁。
  • 最好不要锁基本数据类型的包装类和String,详情见:

https://blog.csdn.net/FYJ_jie/article/details/80404950

  • 使用对象的锁时,new出来的对象尽量加上final,不然后面可能把引用指向其他new的对象,这时候就会出问题。
final Object o = new Object();

o = new Object();	//不要这样做,所以用加上final避免这种操作。

用法

锁对象

public class Test {
    private int num = 10;
    private Object o = new Object();

    public void test1(){
        //正确说法:其他线程如果要执行{}中的代码,必须拿到对象o的锁
        synchronized (o){
            num--;
        }
    }

    public void test2(){
        //正确说法:其他线程如果要执行{}中的代码,必须拿到本对象的锁
        synchronized (this){
            num--;
        }
    }
}

锁方法

public class Test {
    private int num = 10;

    //等同上面的例子对执行代码synchronized(this)
    public synchronized void test(){
        num--;
    }
}

锁class

public class Test {
    private static int num = 10;

    public static void test1(){
        synchronized(Test.class){
            num--;
        }
    }

    public void test2(){
        synchronized(Test.class){
            num--;
        }
    }
}

锁静态方法

public class Test {
    private static int num = 10;
    
    //等同上面的例子对执行代码synchronized(Test.class)
    public synchronized static void test(){
        num--;
    }
}

静态与非静态的区别

对于非静态synchronized(this)的方法,由于其使用需要对象new出来,所以锁只针对于这个对象而已,假如再new出来一个,那么他们的锁机制是互相不干扰的。静态方法/非静态方法中使用synchronized(xxx.class)则对所有对象起作用的。

/**
 * 本例子演示在非静态方法锁class。
 * 可以看到,虽然我们new出了两个Test,但由于synchronized(Test.class),所以锁还是起作用的,就是锁class对所有对象都起作用。
 * 假如把synchronized(Test.class)换成synchronized(this),那么由于锁的是对象,所以锁只能各自的对象起作用。
 * 
 * 注意的是,这跟test2Method方法加不加static,num加不加static是没关系的,
 * 只是如果test2Method方法加了static,那么不能synchronized(this),因为static方法属于类方法,非static方法属于实例方法,没有this概念。
 */
public class Test {
    private static int num = 5;

    public static void test2Method(){
        synchronized(Test.class){ //假如把Test.class换成this会咋样?
            while (num != 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                num--;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
            num = 5;
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            Test test1 = new Test();
            test1.test2Method();
        },"thread1").start();

        new Thread(() -> {
            Test test1 = new Test();
            test1.test2Method();
        },"thread2").start();
    }

    /*输出结果:
    thread1:4
    thread1:3
    thread1:2
    thread1:1
    thread1:0
    thread2:4
    thread2:3
    thread2:2
    thread2:1
    thread2:0
     */

    /*synchronized(Test.class)换成synchronized(this)的输出结果:
    thread1:3
    thread2:3
    thread2:2
    thread1:2
    thread2:1
    thread1:0
    thread1:4
    thread1:3
    thread1:2
    thread1:1
    thread1:0
     */
}


可重入属性

synchronized可重入属性,也叫可重入锁,即同一线程下的synchronized可重复可递归调用。

public class Test {

    synchronized void m1(){
        System.out.println("调用m1");
        m2();   //如果不支持可重入属性,那么这里就会造成死锁(因为m1已经获取本对象的锁了,再执行m2由于锁还在m1那里)
    }

    synchronized void m2(){
        System.out.println("调用m2");
    }

    public static void main(String[] args) {
        new Test().m1();
    }

    /* 输出结果:
    调用m1
    调用m2
     */

}
public class Test {

    synchronized void m1(){
        System.out.println("调用m1");
    }

    class Child extends Test{
        @Override
        synchronized void m1() {
			//如果不支持可重入,那么也会死锁	
			//补充,调用父类的方法时,锁的也是自己this
            super.m1(); 
        }
    }


    public static void main(String[] args) {
        new Test().new Child().m1();
    }

    /* 输出结果:
    调用m1
     */

}

锁细化,锁粗化,锁消除

//锁细化
public class Test {
    int count = 0;

    //不应该加在方法上
    synchronized void m1() throws InterruptedException {
        Thread.sleep(1000); //模拟业务
        count++;
        Thread.sleep(1000);//模拟业务
    }

    //应该使用加在代码上
    void m2() throws InterruptedException {
        Thread.sleep(1000); //模拟业务
        synchronized (this){
            count++;
        }
        Thread.sleep(1000);//模拟业务
    }

    //这种优化就叫做锁的细化


}
//很多子操作需要加锁,那么直接把锁的范围扩大,这叫做锁粗化。

锁粗化

image.png

锁消除

image.png

synchronized与ReentrantLock的区别

synchronized系统自动加锁自动解锁,ReentrantLock需要手动加锁手动解锁。
底层上synchronized是四种锁升级的实现,ReentrantLock是CAS。
具体见:
https://zhuanlan.zhihu.com/p/126085068

对象的内存布局

用jol(java object layout)工具打印new Object()

import org.openjdk.jol.info.ClassLayout;

public class Test {
    /*<dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>*/

    public static void main(String[] args) {
        //加锁前
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        //加锁后
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }

    /* 打印结果:
    加锁前:
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    加锁后:
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           28 f2 d9 02 (00101000 11110010 11011001 00000010) (47837736)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
     */

}

}

图解

image.png

  • markword(8字节) + class point(4字节) 合为对象头(12字节)。
  • 锁信息记录在markword。
  • 对象属于哪个类,由class point指向那个类。
  • instance data 成员变量的内存占用(如一个类里面有个int属性,那么这里就是4个字节,如果是new Object,那占用就是0)。
  • padding,当上面的几个加起来不能被8整除时,需要此字段来对齐。之所以要对齐,这种方式cpu读起来更快。
  • 一个new Object()的内存占用就是markword(8字节) + class point(4字节)+ instance data(0字节)+ padding (4字节,其他加起来只有12,得补4才能被8整除) = 16字节 ,这跟jol打印的结果一致。

扩展

微信图片_20210628231059.jpg
-XX后面的一些指令为我们在使用java命令的默认带的一些参数。
里面的参数的作用可以百度。
主要有个-XX:+UseCompressedClasssPoointers,这个是开启压缩指针的功能,jdk 64位的,class point应该为8个字节,开启此参数,就是压缩成4字节。
-XX:+UseCompressedOops,假如有个对象里面的成员变量String指针指向一个String对象,这个指针也是应该为8,开启此参数,就是压缩成4字节。

底层实现

在JDK早期synchronized是重量级的,通过OS/内核申请实现。
后来有锁升级机制,执行时间短/竞争不激烈(线程少),为自旋锁,如果
后面执行时间长/竞争激烈(线程多,消耗CPU),那么就从自旋锁(处于用户态)升级为重量级锁(处于内核态),其中的升级条件在jdk1.6后由jvm自适应自旋;之前是有线程自旋达到10次或超过CPU核数的一半时,会升级,也可以通过-XX:PreBlockSpin设置。
由上面的对象的布局的例子可以看出,加锁后对象头发生了变化,表明锁的信息记录在markword中

实现过程:
1.代码层面:关键字synchronized
2.字节码class层面:monitorenter monitorexit
3.锁升级
4.lock comxchg

锁升级

  • 升级过程:new(无锁) -> 偏向锁(只有单个线程进行synchronized时) -> 自旋锁(轻量级锁) -> 重量级锁(操作系统申请)
  • 在不同的阶段,markword的8个字节的结构如图:

image.png

  • 分代年龄,对象被gc回收一次,年龄会+1,当到达一定次数后,会从年轻代到老年代。
  • hashCode只能调用了对应方法后才会存在
  • 锁降级在某些特定情况下会发生,在gc时(vmthread),但是在gc时降级也没意义了。