JAVA中锁的概念

  • 自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。

  • 悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。

  • 乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。

  • 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)

  • 共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)

image-20200827093337312
  • 可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
image-20200827093401077
  • 公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。

几种重要的锁实现方式: synchronized、 ReentrantLock、 ReentrantReadWriteLock

同步关键字 synchronized

概念

属于最基本的线程通信机制,基于对象监视器实现的。

Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。

一次只有一个线程可以锁定监视器。

试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。

持性:

  • 可重入
  • 独享
  • 悲观锁

锁的范围:

  • 类锁
  • 对象锁
  • 锁消除
  • 锁粗化

可见性

同步关鍵字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)

代码示例

synchronized代码示例

// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {

    //普通方法+同步关键字,不能同步,因为锁的是对象,main里两个线程创建了两个对象
    public synchronized void test1(){
		try {
			System.out.println(Thread.currentThread() + " 我开始执行");
			Thread.sleep(3000L);
			System.out.println(Thread.currentThread() + " 我执行结束");
		} catch (InterruptedException e) {
		}
    }
    
    //static方法+同步关键字,能同步,因为锁的是类
    public synchronized static void test1(){
		try {
			System.out.println(Thread.currentThread() + " 我开始执行");
			Thread.sleep(3000L);
			System.out.println(Thread.currentThread() + " 我执行结束");
		} catch (InterruptedException e) {
		}
    }
        
    //synchronized (this),不能同步
    static Object temp = new Object();
    public void test1() {
		synchronized (this) {
            try {
                System.out.println(Thread.currentThread() + " 我开始执行");
                Thread.sleep(3000L);
                System.out.println(Thread.currentThread() + " 我执行结束");
            } catch (InterruptedException e) {
            }
        }
    }
    
    //synchronized(static变量),synchronized(ObjectSyncDemo1.class),能同步,两个线程争抢的是同一把锁
    static Object temp = new Object();
    public void test1() {
		//synchronized (ObjectSyncDemo1.class) {
        synchronized (temp) {
            try {
                System.out.println(Thread.currentThread() + " 我开始执行");
                Thread.sleep(3000L);
                System.out.println(Thread.currentThread() + " 我执行结束");
            } catch (InterruptedException e) {
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            new ObjectSyncDemo1().test1();
        }).start();

        Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
        new Thread(() -> {
            new ObjectSyncDemo1().test1();
        }).start();
    }
}

可重入的代码示例

// 可重入
public class ObjectSyncDemo2 {

    public synchronized void test1(Object arg) {
        System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
        if (arg == null) {
            test1(new Object());
        }
        System.out.println(Thread.currentThread() + " 我执行结束" + arg);
    }

    public static void main(String[] args) throws InterruptedException {
        new ObjectSyncDemo2().test1(null);
    }
}

锁粗化的概念

https://www.oracle.com/java/technologies/javase/6performance.html#2.1.2

锁粗化是发生在编译器级别(JIT)的一种锁优化方式。

JVM会对热点代码(不间断、高频地)中的多个锁合并成一个锁,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。

// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看,修改优化级 别为C2
public class ObjectSyncDemo3 {
    int i;

    public void test1(Object arg) {
        synchronized (this) {
            i++;
        }
        synchronized (this) {
            i++;
        }
        //jvm会对热点代码进行优化,变成下面这样
        /*
        synchronized (this) {
            i++;
            i++;
        }*/
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000000; i++) {
            new ObjectSyncDemo3().test1("a");
        }
    }
}

锁消除的概念

锁消除是发生在编译器级别(JIT)的一种锁优化方式。

有时候我们写的代码完全不需要加锁,却执行了加锁操作。

比如StringBuffer类的append操作,方法内部是加锁的,所以在执行一系列的append操作时JVM就会在保证线程安全的情况下消除锁

// 锁消除(jit)
public class ObjectSyncDemo4 {
    public void test3(Object arg) {
        StringBuilder builder = new StringBuilder();
        builder.append("a");
        builder.append(arg);
        builder.append("c");
        System.out.println(arg.toString());
    }

    public void test2(Object arg) {
        String a = "a";
        String c = "c";
        System.out.println(a + arg + c);
    }


    public void test1(Object arg) {
        // jit 优化, 消除了锁
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("a");
        stringBuffer.append(arg);
        stringBuffer.append("c");
        // System.out.println(stringBuffer.toString());
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000000; i++) {
            new ObjectSyncDemo4().test1("123");
        }
    }
}

同步关键字加锁原理

image-20200827140333983

HotSpot中,对象前面会有一个类指针和标题,储标识哈希码的标题字以及用于分代垃圾收集的年龄和标记位

参考来源:

https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf

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

CAS轻量级加锁其实就是替换mark word 的内容

image-20200827155650779
image-20200827155756686

使用CAS修改mark word完毕,加锁成功。则mark word中的tag进入00状态(01=unlocked,00=Light-Weight locked,10=Heavy-weight locked)。

解锁的过程,则是一个逆向恢复 mark word的过程

偏向锁到轻量级锁

默认情况下JVM锁会经历:偏向锁-->轻量级锁-->重量级锁这四个状态

image-20200827153255997

偏向标记第一次有用,出现过争用后就没用了。-XX: -UseBiasedLocking禁用使用偏置锁定,-XX:+UseBiasedLocking设置启用偏向锁。

偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步

JVM为了少千活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作

image-20200827160155278

重量级锁 - 监视器(monitor)

修改mark word如果失败,会自旋CAS一定次数,该次数可以通过参数配置

超过次数,仍未抢到锁,则锁升级为重量级锁进入阻塞。

monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。

image-20200827161120818

在虚拟机的源码中可以看到监视器的内容

简单过一下Lock锁相关

方法 描述
lock 获取锁的方法,若锁被其他线程获取,则等待(阻塞)
lockInterruptibly 在锁的获取过程中可以中断当前线程
tryLock 尝试非阻塞地获取锁,立即返回
unlock 释放锁

根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。

ReentrantLock

独享锁;支持公平锁、非公平锁两种模式;可重入锁;

ReentrantLock在上锁时,会根据实例化时指定的策略去获取锁,默认为非公平锁。如果上锁成功,锁状态值+1(重入,最大次数为 Integer.MAX_VALUE),并将锁持有者设置为当前线程实例。在 Sync(AQS) 内部维护了一个队列,存放了所有上锁失败的线程。公平锁在上锁前,会检查在自己前面是否还有其他线程等待,如果有就放弃竞争,继续等待。而非公平锁会抓住每个机会,不管是否前面是否还有其它线程等待,只顾上锁

ReetrantLock在释放锁时,将状态计数器减一(重入),当状态计数器为0时,锁可用。此时再从等待队列中寻找合适的线程唤醒,默认从队首开始,如果队列正在更新中,且未找到合适的线程,那么从队尾开始寻找。

ReadWriteLock

维护一对关联锁,一个用于只读操作,一个用于写入;读锁可以由多个读线程同时持有,写锁是排他的。

适合读取线程比写入线程多的场景,改进互斥锁的性能,

示例场景:缓存组件、集合的并发线程安全性改造。

写锁是线程独占,读锁是共享,所以写->读是升级。(读->写,是不能实现的)

关于锁降级

锁降级指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

// 缓存示例
public class CacheDataDemo {
    // 创建一个map用于缓存
    private Map<String, Object> map = new HashMap<>();
    private static ReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        // 1 读取缓存里面的数据
        // cache.query()
        // 2 如果缓存没数据,则取数据库里面查询  database.query()
        // 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
    }

    public Object get(String id) {
        Object value = null;
        // 首先开启读锁,从缓存中去取
        rwl.readLock().lock();
        try {
            if (map.get(id) == null) {
                // TODO database.query();  全部查询数据库 ,缓存雪崩
                // 必须释放读锁
                rwl.readLock().unlock();
                // 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
                rwl.writeLock().lock(); // 所有线程在此处等待  1000  1  999 (在同步代码里面再次检查是否缓存)
                try {
                    // 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
                    if (map.get(id) == null) {
                        // TODO value = ...如果缓存没有,就去数据库里面读取
                    }
                    rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性			
                } finally {
                    rwl.writeLock().unlock(); // 释放写锁@
                }
            }
            //TODO cache.query();锁降级就是为了在这里保持数据可见性,避免在前面释放写锁后数据被别的线程修改
            
        } finally {
            rwl.readLock().unlock();
        }
        return value;
    }
}

如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程T获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

如果遵循锁降级的步骤,线程C在释放写锁之前获取读锁,那么线程T在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。

Condition

用于替代wait/notify

Object中的 wait(), notify(), notifyAll()方法是和 synchronized配合使用的,可以唤醒一个或者全部(单个等待集)

Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/ unpark机制);

image-20200828160438102