synchronized原理详解

synchronized关键字

synchronized可以把任意一个非NULL的对象当做锁,HotSpotVM中,这个锁称为 对象监视器(Object Monitor):

  • 作用于静态方法时,锁住的是Class实例,相当于一个全局锁,对所有调用这个静态方法的线程有效。
  • 作用于非静态方法时,锁住的是对象的实例(this)。
  • 作用于一个代码块时,可以通过自定义任意一个对象obj当做锁,锁住的是所有以该对象为锁的代码块。

保证并发三大特性

synchronized能保证可见性、原子性和有序性

synchronized保证可见性原理
执行到synchronized代码块时,JVM会执行lock这个原子操作,将这个线程的工作内存中共享变量的副本值清空,之后这个线程再次需要用这些变量的时候,发现自己工作内存中的值已经失效了,就会重新从主存中获取最新值。当执行完同步代码块时,JVM会执行unlock这个原子操作,工作内存的变量副本会立刻同步到主存。

synchronized保证原子性原理
只有获取到锁的线程才能去执行synchronized中的代码块,即使中途发生线程切换,这个线程持有的锁不会释放,所以这期间其他线程也无法获取到锁去执行这个代码块。但是发生异常的话,Synchronized锁会自动释放。

synchronized保证有序性原理
加上了synchronized关键词的代码块,编译器还是会进行代码重排序的优化,只是synchronized保证了代码块只能同步访问,下一个线程获取锁之后,上一个线程对共享变量做的改变对它是可见的,就是通过lock刷新工作内存的机制,这样,编译器优化的问题就不会出现。

synchronized的两个特性

可重入特性

指的是同一线程的外层函数获得锁之后,内层函数还可以再次获取该锁,也就是说一个线程可以多次获取同一把锁。
好处

  • 避免死锁(对于不可重入锁,如果A方法中调用B方法,A和B方法都要获取同一把锁,A方法先获取了锁,去调用B方法的时候,B方法此时要等A释放锁才能执行,但是A方法要等B方法执行完了才能释放锁,这就造成了死锁)
  • 可以让我们更好的来封装代码,而不是将所有的逻辑都写在一个同步方法中

示例

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


// 自定义一个线程类
class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(getName() + "进入了同步代码块1");
            synchronized (MyThread.class) {
                System.out.println(getName() + "进入了同步代码块2");
            }
              // 这个代码块可以是调用本类或者其他类方法中的同步代码块
//            test0();
        }
    }

//    public void test0() {
//        synchronized (MyThread.class) {
//            System.out.println(getName() + "进入了同步代码块2");
//        }
//    }
}

结果:

Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2

实现原理:
同一个线程能多次获取同一把锁进入synchronized代码块中执行,实现原理是利用了一个变量记录(假设微lockedThread)当前获取到锁的线程,还有一个变量(假设微lockedCount)记录当前锁被获取的次数,当一个线程尝试获取锁时,如果这个锁已经被获取过了,会去判断获取到锁的线程是不是就是当前这个线程,如果是,则对lockedCount变量进行加1,获取锁成功。释放锁的时候,先对lockedCount变量进行减1,只有当lockedCount减为0的时候,才会真正释放锁。下面给出一个简易模拟代码:

public class Lock {
    boolean isLocked = false; // 标识锁是否被线程获得
    Thread lockedBy = null; // 记录获得锁的线程
    int lockedCount = 0; // 记录一个线程中,锁被获取的次数

    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread(); // 当前尝试获取锁的线程

        // 锁已经被线程获得,并且获取锁的线程不是当前线程,则当前线程等待
        while(isLocked && lockedBy != thread) {
            wait();
        }

        isLocked = true;
        lockedBy = thread;
        lockedCount++;
    }

    public synchronized void unlock() {
        lockedCount--;

        // 该线程中,获取锁的程序都执行了释放锁操作,线程才真正释放锁
        if(0 == lockedCount) {
            isLocked = false;
            notify();
        }
    }
}

不可中断特性

一个线程获得锁后,其他尝试获取这个锁的线程只能等待或者阻塞,不能被中断,只能一直等着拥有锁的线程释放锁。

public class Test {
    private static  Object obj = new Object();

    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = () -> {
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "进入同步代码块");

                // 保证不退出同步代码块
                try {
                    Thread.sleep(88888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(name + "执行结束");
                }
            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();

        System.out.println("停止第二个线程之前");
        t2.interrupt();
        System.out.println("停止第二个线程之后");

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}
Thread-0进入同步代码块
停止第二个线程之前
停止第二个线程之后
TIMED_WAITING #第一个线程进入等待状态
BLOCKED       #第二个线程进入阻塞状态
Thread-0执行结束
Thread-1进入同步代码块
Thread-1执行结束
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.exapmle.service.test9.Test.lambda$main$0(Test.java:14)
	at java.lang.Thread.run(Thread.java:748)

Lock锁可以设置为可中断的,也可以设置为不可中断的

public class LockTest {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test02();
    }

    // 不可中断锁 lock()
    public static void test01() throws InterruptedException {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            try{
                lock.lock();
                System.out.println(name+"获取锁");
                // 让获取锁的线程暂时不退出
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name+"释放锁");
            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();

        System.out.println("停止第二个线程之前");
        t2.interrupt();
        System.out.println("停止第二个线程之后");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }

    // 可中断锁 tryLock()
    public static void test02() throws InterruptedException {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try{
                // 3s之内没有获取到锁,就停止阻塞,去执行其他任务
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if(b) {
                    System.out.println(name+"获取锁");
                    // 让获取锁的线程暂时不退出
                    Thread.sleep(88888);
                }else{
                    System.out.println(name+"没有获取到锁");
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(b) {
                    lock.unlock();
                    System.out.println("name"+"释放锁");
                }

            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();
    }
}

test01执行结果:

Thread-0获取锁
停止第二个线程之前
停止第二个线程之后
TIMED_WAITING
WAITING            # 虽然执行了t2.interrupt(); #但是线程2并没有被中断,而是进入等待状态
Thread-0释放锁
Thread-1获取锁      # 等线程1释放锁之后,第二个线程获取锁
Thread-1释放锁
java.lang.InterruptedException: sleep interrupted  # 中断异常,因为不可中断
	at java.lang.Thread.sleep(Native Method)
	at com.exapmle.service.test9.LockTest.lambda$test01$0(LockTest.java:22)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

test02执行结果:

Thread-0获取锁
Thread-1没有获取到锁         #3s之内没有获取到锁,就停止阻塞,去执行其他任务
name释放锁

Java对象

在JVM中,对象在内存中的布局分为三块区域:

  • 对象头:Java对象头在32位虚拟机上占64bit、在64位虚拟机上占96bit
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

在这里插入图片描述

对象头

Hotspot虚拟机的对象头包括两部分,Mark Word(标记字段,64位虚拟机上占64bit)、Klass Pointer(类型指针,Hotspot虚拟机默认开启指针压缩,所以64位虚拟机上占32bit。没有开启指针压缩时一个指针占8字节。开启指针压缩是因为对象太大,会减小缓存命中率,GC开销增大。使用参数-XX:-UseCompressedOops可以关闭指针压缩):

  • Klass Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个Class
  • Mark Word用于存储对象自身的运行时数据,如:HashCode、GC分代年龄、Synchronized锁状态标志
    是实现Synchronized锁的关键,Synchronized加锁实际是对对象头状态的改变

Synchronized锁状态:无锁不可偏向、无锁可偏向(但还没偏向)、偏向锁、轻量级锁、重量级锁

Mark Word:
在这里插入图片描述
Lock Record
Lock Record是线程私有的数据结构,每一个被锁住的对象Mark Word都会和一个Lock Record关联,用于存储锁对象的Mark Word的拷贝。

synchronized原理

public class Test {
    private static Object obj = new Object();

    // synchronized作用在代码块上
    public static void main(String[] args) {
        synchronized (obj) {
            System.out.println("1");
        }
    }

    // synchronized作用在方法上
    public synchronized void fun() {
        System.out.println("2");
    }
}

现在通过反汇编字节码文件看看synchronized汇编源码:

javap -p -v 字节码文件xx.class #-p是显示所有方法 -v是显示所有细节

在这里插入图片描述
synchronized是通过monitor监视器锁来实现,monitor对象中有两个主要属性:owner记录当前拥有锁的线程,recursion记录当前锁被获取的次数。对于synchronized修饰的代码块,在源码编译成字节码的时候,会在同步代码块的入口和出口分别插入monitorenter和monitorexit这两个字节码指令。对于synchronized修饰的方法,会在该方法上添加ACC_SYNCHRONIZED的标识,表示它是一个同步方法。

monitorenter字节码指令
当程序执行到monitorenter指令时会尝试去获取当前锁对象对应的monitor权限:

  • 如果monitor的recursion为0,则该线程进入monitor,然后将recursion设置为1、owner设置为当前线程,该线程成为monitor的所有者;
  • 如果线程已经占用了该monitor(即判断到recursion不为0,然后看owner是否为当前线程),说明这时候是持有锁的线程再次获取这个锁,则可以获取成功,将recursion加1
  • 如果其他线程占用了monitor,则该线程通过自旋操作再次尝试几次去获取锁,如果还没有获取到就被阻塞

monitorexit字节码指令
当执行到monitorexit指令时会将recursion的值减1,当这个值减到0的时候,当前线程就不再拥有这个monitor对象的所有权,就会释放锁,然后其他被这个锁阻塞的线程就可以尝试去获取这个monitor对象。

从上面反汇编结果图中看到还存在一个monitorexit指令,下面有一个Exception table,记录的是有可能出现异常的指令。这就说明,同步代码块中如果出现了异常,那么会执行到monitorexit指令将recursion减1。所以,synchronized出现异常时会释放锁。

在这里插入图片描述
当synchronized作用于方法上时,会给这个方法设置一个叫做ACC_SYNCHRONIZED的标识。当一个方法被调用时,会检测方法是否设置了ACC_SYNCHRONIZED标识,如果设置了,线程会去获取monitor,获取成功之后才能执行同步方法体,执行完后释放monitor所有权。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

monitor监视器锁

monitor监视器锁也就是通常说的synchronized对象锁。任何一个Java对象都有一个Monitor与之关联,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的是Monitor对象的指针),这就是为什么Java中任意对象可以作为锁的原因。
一个Monitor被持有后,它将处于锁定状态。Monitor是由ObjectMonitor实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的。

//ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,
//用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),
//_owner指向持有ObjectMonitor对象的线程

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 重入次数
    _waiters      = 0, // 等待线程数
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 当前持有锁的线程
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表,有资格成为候选资源的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

monitor竞争

当有多个线程竞争锁时,流程如下:
在这里插入图片描述

monitor等待

当一个线程尝试获取锁,如果锁已经被其他线程获取到了,它会再次自旋尝试获取锁,如果还是获取不到,则进行下面的流程:
在这里插入图片描述
第2步要通过CAS把node节点push到_cxq列表中,因为一次push操作可能失败

monitor释放

  1. 执行完同步代码快时会让_recursions减1,当_recursions减为0时,释放该锁
  2. 根据不同的策略唤醒等待该锁的其他线程

synchronized是重量级锁

Synchronized实现线程互斥会导致用户态和内核态的切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。所以说synchronized是重量级锁,JDK6开始对Synchronized进行了锁升级优化

CAS

CAS(Compare And Swap)比较相同再交换,是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。
CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。

在concurrent并发包下提供的AtomicInteger类就使用了CAS保证并发操作下对共享变量自增操作的正确性:

import java.util.concurrent.atomic.AtomicInteger;

public class Test {
    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];

        for(int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 1000; j++) {
                        // 自增操作
                        atomicInteger.incrementAndGet();
                    }
                }
            });
            threads[i].start();
        }

        // join()的意思是等待所有的子线程都执行完了,主线程才继续往后执行
        for(Thread t : threads) {
            t.join();
        }

        System.out.println(atomicInteger.get());//10000
    }
}

CAS原理

CAS操作依赖3个值:内存中的值V,旧的估计值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中
AtomicInteger和Unsafe部分源码:

package java.util.concurrent.atomic;

public class AtomicInteger extends Number implements java.io.Serializable {
	// ...
	private static final long valueOffset; // 根据AtomicInteger对象的内存地址和偏移量valueOffset就能找到value的内存地址
	private volatile int value; // 保存实际的值,用volatile修饰,保证可见性
	
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
    	// 调用了sun.misc.getAndAddInt()
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
	// ...
}
package sun.misc;
public final class Unsafe {
	// ...
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5; // 旧的预估值,就是此时内存中的值
        do {
        	// var1:AtomicInteger对象 
        	// var2:AtomicInteger对象中的偏移量valueOffset
            var5 = this.getIntVolatile(var1, var2);
            //CAS操作,比较相同则交换一个int值
            // var5+var4:要修改的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 

        return var5;
    }
    // ...
}
  • 现在线程1和线程2都在对atomicInteger.value执行自增操作,假设线程2线先执行
  • 假设现在atomicInteger.value是0,线程2执行到getAndAddInt()时,取到旧的预估值就是0
  • 此时CPU切换执行线程1,线程1执行到getAndAddInt()时,取到旧的预估值也是0
  • 线程1执行`compareAndSwapInt(var1, var2, var5, var5 + var4),var1和var2结合找到当前内存中的最新值【现在是0】,var5就是旧的预估值(之前取的内存中的值)【现在是0】,var4就是自增操作加1的这个【数值1】,现在比较内存最新值【0】和预估值【0】相等,则将var5+var4的值【1】赋给内存中的这个值。
  • 线程1赋值成功,compareAndSwapInt()返回true,线程1结束
  • 此时切换回线程2,找到当前内存最新值【1】,线程1旧的预估值还是【0】,比较两者不相等,compareAndSwapInt()返回false,继续执行do while循环,此时重新取内存中的值【1】给var5,然后再执行compareAndSwapInt()。此时找到当前内存最新值【1】,就的预估值【2】,两者相等,则将var5+var4【2】赋给内存中的这个值
  • 线程1赋值成功,compareAndSwapInt()返回true,线程1结束
  • 最终,内存中的值是2
  • 乐观锁:认为读多写少,遇到并发写的可能性很小,因此每次去拿数据的时候都认为别的线程不会修改,就不会上锁,但是在更新数据的时候会判断当前数据是否被修改过了。
    Java中的乐观锁基本都是通过CAS实现的
  • 悲观锁:认为写的情况很多,遇到并发写的可能性高,因此每次去拿数据的时候都会认为别的线程会修改这个数据,因此线程一上来执行就加锁,直到执行完才释放锁
    synchronized和ReentrantLock属于悲观锁

CAS适用场景

  • CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于多核CPU下线程间竞争不激烈的场景
  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,那线程在getAndAddInt()中的do while循环处肯定会发生多次重试,反而会影响效率

synchronized锁升级过程

HotSpot虚拟机中,JDK1.6之前,synchronized是重量级锁,即使是线程交替执行无竞争并发的情况下,一个线程也要执行Synchronized加锁,进行用户态和内核态的切换。

JDK6开始对锁进行了改进和优化,使得线程之间更高效地操作共享数据,以及解决竞争问题,从而提高程序运行效率。在JDK6中,synchronized锁粒度是一个升级的过程:无锁->偏向锁->轻量级锁-> 重量级锁

锁存在四种状态依次是:无锁状态(可偏向和不可偏向)、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

偏向锁

HotSpot研究者发现,大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获得与释放,为了让线程获得锁的代价更低,引入了偏向锁。偏向锁就是这个锁会偏向第一个获得锁的线程,在整个运行过程中只有一个线程的时候是这种锁。

无锁—>偏向锁

  • A线程访问同步代码块,使用CAS操作将Thread ID放到锁对象的Mark Word中
  • 如果CAS操作成功,此时线程A获取到锁
  • 如果CAS操作失败,证明还有别的线程持有锁,则启动偏向锁撤销

偏向锁—>撤销

  • 让A线程在全局安全点阻塞
  • 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态
  • 恢复A线程,将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程

优缺点

  • 在单线程重复执行同步代码块时提升了性能,因为如果只有一个线程执行同步代码块,就没必要调用操作系统内核加锁。
  • 如果有很多线程竞争锁,偏向锁是无效的,还因为撤销偏向锁的动作必须等待全局安全点才行,反而降低了性能。

适用场景
适用於单线程反复进入同步代码块的情况。

JVM开启/关闭偏向锁

  • 开启偏向锁(JDK1.6之前):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 开启偏向锁(JDK1.6及其之后):-XX:BiasedLockingStartupDelay=0
    JDK6之后默认开启,JDK1.8中默认会延迟4s后才开启偏向锁,这是为了提高JVM启动速度,使用参数-XX:BiasedLockingStartupDelay=0可以关闭延迟。听说在JDK11中没有延迟,可以自己验证下哦!
  • 关闭偏向锁:-XX:-UseBiasedLocking

参数-XX:+PrintFlagsInitial打印出的信息中显示了偏向锁默认延迟时间(JDK1.8):
在这里插入图片描述

偏向锁提升单线程反复执行同步代码快性能的原理之一
按照HotSpot的设计,每次加锁/解锁都会涉及一些CAS操作,CAS操作会延迟本地调用。偏向锁的做法是一旦线程获得了这个锁,这个线程之后再次执行执行获取这个锁是不用走加锁/解锁操作的,即只需要判断当前是偏向锁并且锁的拥有者是它自己就行。
CAS为什么会延迟本地调用?
多核cpu、并发情况下,用volatile修饰的共享变量要保证可见性,假如此时core1和core2同时把一个共享变量拷贝到了自己的cpu缓存中,当core1修改了这个共享变量的值,通过总线写回到主存的时候,通过总线嗅探机制,会使core2中对应的这个值失效,也就是将他的缓存清空,当core使用这个变量发现数据失效了,就会重新取主存中的这个变量,这种通过总线监听来回通信称为“Cache 一致性流量”。core1和core2的值再一次相等时,称为“Cache一致性”。
而CAS刚好导致了Cache一致性流量情况加重,偏向锁通过消除不必要的CAS降低了Cache一致性流。

轻量级锁

在多线程交替执行同步代码快的情况下(就是线程A执行完了线程B才来执行,线程B执行完了线程C才来执行…,多个线程之间不会有锁竞争的情况),会使用轻量级锁。如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁。

轻量级锁加锁过程

  • 在A线程栈帧中建立一个锁记录(Lock Record),将Mark Word拷贝到自己栈帧中的Lock Record中,这个位置叫 displayced hdr
    在这里插入图片描述
  • 将Lock Record中的owner指向锁对象
  • 使用CAS操作将Lock Record的地址记录到Mark Word中,如果操作成功则进行下一步,否则进行最后一步 在这里插入图片描述
  • CAS操作成功,那么这个线程就获取到了这个锁,然后将锁标志位设为轻量级锁模式(00)
  • CAS操作失败,JVM首先会检查锁对象Mark Word中是否已经指向了当前栈帧,如果是则说明是锁重入;否则说明多个线程竞争锁,轻量级锁升级为重量级锁,不过在这之前还有自旋操作

这里为什么要使用CAS操作?
假如A、B两个线程都将MarkWord拷贝到自己栈帧中的LockRecord中,A线程先将MarkWord更新为指向自己LockRecord的指针,A线程就算获取锁成功了;B线程在执行CAS操作将MarkWord更新为指向自己LockRecord的指针,发现MarkWord变了,CAS操作就会失败,说明存在锁竞争,则锁开始膨胀。

轻量级锁释放过程

  • 取出Lock Record中保存的Mark Word信息,用CAS操作将取出的数据重新赋值到Mark Word中,操作成功,则释放锁成功
  • 否则,说明其他线程尝试获取锁,需要升级为重量级锁

轻量级锁加锁过程中为什么要把对象头里的Mark Word复制到线程栈的锁记录中?
因为升级为轻量级锁是在多线程的情况下,这些线程可能会竞争锁,那么获取到锁的线程将自己栈帧中的LockRecord地址记录到MarkWord中时要进行CAS操作,如果发现MarkWord中的值发生了变化,那CAS操作失败,说明存在锁竞争。

优点
在多线程交替执行同步代码快的情况下,可以避免重量级锁引起的性能消耗。

自旋

重量级锁的开销很大,要尽量避免轻量级锁转为重量级锁。因此,当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数依然没有拿到,锁就会升级为重量级锁。自旋锁是JDK4中引入的,在JDK6中才默认开启。

JVM开发团队发现在很多应用中,共享数据的锁定状态只会持续很短的一段时间,如果为了这么短的一段时间使线程阻塞和唤醒导致的开销不值得。先进行自旋,这个线程就不会放弃处理器执行时间而挂起。
自旋次数默认是10次,可以使用参数-XX:PreBlockSpin来更改。这个自旋次数不好确定,在JDK6中引入了适应性自旋锁。

适应性自旋锁:
自适应意味着自旋次数不再固定,而是由前一个尝试获取这个锁的线程自旋时间来决定。假如前一个线程自旋10次就获得了锁,JVM会认为这个锁很容易获取,那么当前这个线程也可以自旋10次或者再多几次就能获取到锁。假如前面的线程自旋了很多次还没有获取到锁,JVM会认为这个锁很难获取,那以后要获取这个锁时就不再进行自旋过程,以免浪费资源。

monitor锁的竞争过程就用到了自适应自旋锁。

适用场景
线程持有锁的时间短,否则自旋时间长对CPU也会造成压力。

重量级锁

synchronized锁是通过对象关联的一个叫做监视器锁(monitor)的对象来实现的,监视器锁本质又是依赖于底层的操作系统Mutex Lock来实现,而操作系统实现线程之间的切换就需要用户态和内核态的转换,这个成本很高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

锁消除

锁消除是JDK6对锁的优化。下面这段代码,StringBuffer是线程安全的,append方法上加了synchronized关键字,但是对于new StringBuffer().append(s1).append(s2).append(s3).toString()这行代码,锁对象是this,也就是StringBuffer的一个实例,每个线程执行到这句代码时,都会实例化一个StringBuffer对象,它们的锁和要锁住的资源是不同的,因此也就没必要在append方法上加锁,因此JVM会自动将这个锁消除。

锁消除的依据是逃逸分析的数据支持

public class Test {
    public static void main(String[] args) {
        contactString("aa", "bb", "cc");
    }

    public static String contactString(String s1, String s2, String s3) {
        return new StringBuffer().append(s1).append(s2).append(s3).toString();
    }
}

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的情况下,对锁进行消除。

锁粗化

下面这段代码,for循环中,会进出100次append同步方法,JVM就会将append方法是的锁消除,将锁加到for循环上,这要很多锁就变成了一个锁。

public class Test {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        
        for(int i = 0; i < 100; i++) {
            sb.append("aa");
        }
    }
}

锁粗化是指JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需加锁一次即可。

示例

JDK8
参数:-XX:BiasedLockingStartupDelay=0

public class Test {
     final static Object LOCK = new Object();
     public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        
         Thread t1 = new Thread() {
             @Override
             public void run() {
                 getLock();
             }
         };
         t1.setName("t1");
         t1.start();


        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

         Thread t2 = new Thread() {
             @Override
             public void run() {
                 getLock();
             }
         };
         t2.setName("t2");
         t2.start();

    }
    public static void getLock() {
        synchronized(LOCK) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        }
    }
}

在这里插入图片描述
将线程1、2中间的sleep()注释掉,两线程就会竞争锁,此时是重量级锁,并且可以看到MarkWord中有,除了锁标记(10)外,其余52个bit也是相等的,其中记录的就是锁对象关联的ObjectMonitor的地址:
在这里插入图片描述

public class Test {
     final static Object LOCK = new Object();
     public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        System.out.println(Integer.toHexString(LOCK.hashCode()));
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        getLock();

    }
    public static void getLock() {
        synchronized(LOCK) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        }
    }
}

在这里插入图片描述

synchronized小结

synchronized底层使用了monitorenter和monitorexit指令,每个锁对象都会关联一个monitor监视器,它有两个主要属性:owner记录当前拥有锁的线程,recursion记录当前锁被获取的次数。当执行到monitorexit时,recursion会减1,当它的值减到0时,这个线程就会释放锁。

synchronized和Lock的区别

synchronized和Lock都可以用来解决多线程安全问题,保证线程同步。区别是:

  • synchronized是关键字,Lock是接口,必须通过实例化一个实现了Lock锁的接口才能得到Lock锁对象,比如ReentrantLock
  • synchronized发生异常时,会自动释放锁;Lock锁必须通过调用unLock()去释放,因此可能造成死锁现象
  • synchronized不能让等待锁的线程中断;而Lock可以让等待锁的线程中断,就是通过调用它的tryLock()方法,如果调用的是lock()方法,则是不可中断的
  • synchronized无法知道线程是否成功获取锁,而Lock可以,当设置为可中断锁时,tryLock()方法返回布尔值代表是否获得锁
  • synchronized能锁住方法和代码块,加锁和释放锁是由JVM自动完成的,Lock只能锁住代码块,加锁和释放锁的时机由程序员自己决定
  • Lock可以使用读锁提高多线程的读效率,读锁:Lock的一个实现类ReentrantReadWriteLock,允许多个线程读,但只允许一个线程写
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。公平锁就是唤醒线程的时候,哪个线程先来就先唤醒那个,非公平锁是随机唤醒一个线程。可以给ReentrantLock的构造器传一个布尔值设定是否是公平锁。

平时写代码如何对synchronized优化

  • 减小synchronized的范围,同步代码块中代码执行时间尽量短
  • 降低synchronized锁的粒度
  • 读写分离,读取时不加锁,写入和删除时加锁
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章