synchronize的使用和原理

synchronize的使用

我们知道,当出现race condition的时候,应用就不会同步。

举个例子:

package sync;

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class OceanSyncMethods {
    private int sum = 0;

    public void setSum(int sum) {
        this.sum = sum;
    }

    public int getSum() {
        return sum;
    }
    public void calculate(){
        setSum(getSum()+1);
    }

    @Test
    public void givenMultiThread_whenNonSyncMethod() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        OceanSyncMethods summation = new OceanSyncMethods();

        IntStream.range(0,1000)
                .forEach(count -> service.submit(summation::calculate));
        service.awaitTermination(1000, TimeUnit.MILLISECONDS);

        assertEquals(1000,summation.getSum());
    }


}

我们有一个线程池,里面固定3条线程。这些线程执行calculate方法1000次。

按理说,summation.getSum()会是1000,但是,如果有线程安全的问题,那么有些值就会损失掉。

测试的结果往往不是1000:

java.lang.AssertionError: 
Expected :1000
Actual   :997

我们当然可以用synchronize关键字来完成同步。

  • synchronize用于实例方法
   public synchronized void calculate(){
        setSum(getSum()+1);
    }

既然是给实例方法加锁,我们必须知道是锁在谁身上。最终是实例(summation)来调用calculate方法,所以锁的是实例(对象)。它的含义是,类的每个实例只有一条线程能够执行该方法。

  • synchronize用于静态方法
package sync;

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class OceanSyncMethods {
    private static int staticSum = 0;

    public static synchronized void calculate(){
        staticSum = staticSum + 1;
    }

    @Test
    public void givenMultiThread_whenNonSyncMethod() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);


        IntStream.range(0,1000)
                .forEach(count -> service.submit(OceanSyncMethods::calculate));
        service.awaitTermination(5000, TimeUnit.MILLISECONDS);

        assertEquals(1000,OceanSyncMethods.staticSum);
    }


}

锁在静态方法上也是一种锁,那么,此时锁的是谁呢?

锁的是类对象。因为JVM的一个类有且只有一个类对象,因此每个类只有一个线程能够在synchronize静态方法中执行代码。

  • 方法内的synchronize块
package sync;

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class OceanSyncMethods {
   private int count;

    public void setCount(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public void performSynchronizedTask(){
        synchronized (this){
            setCount(getCount()+1);
        }
    }

    @Test
    public void givenMultiThread_whenNonSyncMethod() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        OceanSyncMethods oceanSyncMethods = new OceanSyncMethods();

        IntStream.range(0,1000)
                .forEach(count -> service.submit(oceanSyncMethods::performSynchronizedTask));
        service.awaitTermination(5000, TimeUnit.MILLISECONDS);

        assertEquals(1000,oceanSyncMethods.getCount());
    }


}

有时候锁一整个方法效率不高,我们实际只需对有线程安全顾虑的代码加锁即可,因此有了synchronize代码块。

上面我们锁的是this,这个this是个monitor object。每个monitor object只有一个线程可以执行synchronize代码块中的内容。

那么什么是一个monitor呢?monitor就像一个building,synchronize代码块就像一个special room,monitor可以确保只有一个线程进入这个special room,以此来保护data。

注意,如果方法是静态的,那就锁class对象。这时候,这个class会变成monitor。

package sync;

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class OceanSyncMethods {
    private static int count = 0;

    public static void performSynchronizedTask() {
        synchronized (OceanSyncMethods.class) {
            count = count+1;
        }
    }

    @Test
    public void givenMultiThread_whenNonSyncMethod() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);


        IntStream.range(0, 1000)
                .forEach(count -> service.submit(OceanSyncMethods::performSynchronizedTask));
        service.awaitTermination(5000, TimeUnit.MILLISECONDS);

        assertEquals(1000,OceanSyncMethods.count );
    }


}

synchronize的原理

比方说,锁对象的话,是如何上锁的?

上不上锁我们肯定是有标识的,像flag一样的东西。比如一个对象上锁了,它有什么改变呢?

maven项目里加入依赖:

<dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>

为了简便,我们的类都会极其简单。首先建一个自己的锁对象(因为我们要观察对象的状态)。

package sync;

public class MyLock {
    boolean flag = true;
}

测试:

package sync;

import org.openjdk.jol.info.ClassLayout;

public class TestSync {
    static MyLock myLock = new MyLock();

    static void somePrint(){
        synchronized (myLock){
            System.out.println("Nothing important in the sync block");
        }
    }

    public static void main(String[] args) {

        System.out.println(Integer.toHexString(myLock.hashCode()));
        System.out.println(ClassLayout.parseInstance(myLock).toPrintable());

        somePrint();


    }
}

我们用了synchronize代码块的方式加锁,并且锁的是MyLock对象。

我们在main方法中打印了myLock的hashcode,并且打印了对象布局:

45ee12a7
sync.MyLock object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 a7 12 ee (00000001 10100111 00010010 11101110) (-300767487)
      4     4           (object header)                           45 00 00 00 (01000101 00000000 00000000 00000000) (69)
      8     4           (object header)                           18 0a 89 14 (00011000 00001010 10001001 00010100) (344525336)
     12     1   boolean MyLock.flag                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

Nothing important in the sync block

Process finished with exit code 0

instance size是16bytes,它必须是8的倍数(64为虚拟机中)。

这个对象由三部分组成:对象头,变量,对齐数据。

变量就是那个布尔值,占1byte。对其数据占3bytes。object header占12bytes,也就是96bits。

什么是对象头?

object header
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

对象头,就是每个由gc管理的堆对象的开头的一般结构。

oop是个指针,它指向对象头。

对象头包括堆对象的布局,类型,gc状态,同步状态,以及identityHashcode。

它由2个word组成。

哪两个word呢?

mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

klass pointer
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”

一个是mark word,一个是klass pointer。

我们要找的关于锁的状态的变量,在mark word中。


在64位JVM中,mark word占64bits,klass pointer占64bits。

但是我们刚才的打印中,klass pointer只占了32bits,因为开启了指针压缩。

我们关注普通状态下的mark word。

有32bits是hashcode,和我们打印的hashcode是一样的。

但是,它是从后往前倒着数的。

有4bits是年龄。堆是有分段年龄的,它不是一直在两个survivor区移来移去吗?到十五岁的时候就去老年代了。那么记录年龄的东西在哪里呢?

对,就是在mark word那4bits中,4bits最大表示的就是15岁。

还有1bit标识偏向锁。

剩余的2bits就是我们要找的锁的状态了。


以上的图很好地阐释了mark word的布局,以及不同对象状态的表示。

对象有5种状态:

普通、偏向锁、轻量级锁、重量级锁、为gc标记

图片的右半部份展示了标准的上锁过程。

只要一个对象没有上锁,最后两位的值就是01。当一个方法锁住了一个对象,object header中的相应信息就会存在当前栈帧的lock record中。

然后虚拟机就会试图加载mark word中指向lock record的指针,用的是cas操作。如果成功了,当前线程立马就会得到锁。由于lock records总是在word的边缘对齐,mark word的最后两位就会变成00,标志对象正被上锁。

如果该对象以前上过锁,从而导致cas操作失败,虚拟机会检查mark word是否指向当前线程的方法栈。如果是这样的情况,就说明这个线程已经占有这个对象并且可以继续安全地执行。对于这样的迭代上锁的对象,lock record最初是0而非mark word的值。只有当两个线程当下都要锁同一个对象,轻量级锁就要膨胀成重量级锁,以此来管理等待的线程。

轻量级锁相比重量级锁要廉价,但是它们的性能会受到影响——每一个cas操作都必须原子性地执行在多核处理器上,即使每个对象上锁和解锁都只因为一个特别的线程。

在java6中,这种缺点被叫成所谓 的“免费存储的偏向锁技术”。只有第一次获取锁的时候才执行原子性的cas操作,来把锁住线程的ID加载到mark word中。这个对象因此被说成是对于线程有偏向(歧视)的。未来的对于对象的同一条线程的上锁和解锁是不再需要原子操作的,或者是mark word的更新。甚至在栈上的lock record不会被初始化,因为它永远不会为一个偏向对象做检测。


参考:https://wiki.openjdk.java.net/display/HotSpot/Synchronization

发布了25 篇原创文章 · 获赞 26 · 访问量 1130

猜你喜欢

转载自blog.csdn.net/weixin_43810802/article/details/104107860