JUC学习系列笔记(三):Java内存模型(JMM)和各种锁

92 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情

JUC学习系列笔记(三)

JMM(Java Memory Model)——Java内存模型

参考:zhuanlan.zhihu.com/p/258393139

JMM是一个理论模型,类似于一个约定。面试时问内存模型实际上是想要询问多线程的内容

Java 内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。

不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

volatile关键字

1、保证可见性

代码示例:

public class VolatileVisibilityTest {
    // 添加volatile关键字保证主线程中修改num,A线程中可见。不添加volatile关键字可能会导致A线程对num的修改不可见,陷入死循环
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        // 开启一个线程,num==0时持续循环
        new Thread(() -> {
            while (num == 0) {
                System.out.println("循环中···");
            }
        }, "A").start();

        // 模拟延时,主线程停2秒,让A线程先执行
        TimeUnit.SECONDS.sleep(2);
        // num==1时A线程循环停止
        num = 1;
        System.out.println(num);
    }
}

2、不保证原子性

可以使用原子类来保证原子性

代码示例:

public class VolatileAutoTest {
    // private volatile static int num = 0;
    // 保证原子性
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add() {
        // 不是一个原子性操作
        // num++;
        // +1操作
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    add();
                }
            }, String.valueOf(i)).start();
        }
        // 当线程大于2时(除了main 和 gc线程),还有其他线程,那么main线程让出CPU给其他线程执行
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        // 理论上这里num应该是2000,但是volatile不保证原子性,这里实际上并不是2000
        System.out.println(Thread.currentThread().getName() + " -> " + num);
    }
}

3、禁止指令重排

参考:segmentfault.com/a/119000003…

指令重排:程序v可能并非按照代码编写顺序执行

源代码 -> 编译器优化可能导致重排 -> 指令并行也可能重排 -> 内存系统也可能重排 -> 执行

volatile关键字可以利用内存屏障禁止指令重排

单例模式

饿汉式单例(可能浪费资源) -> 懒汉式单例(线程不安全) -> 双重检验机制饿汉式(线程安全,可能存在指令重排问题) -> volatile改进双重检验机制饿汉式(可能存在反射破解问题) -> 枚举类型单例模式

CAS机制

参考:segmentfault.com/a/119000001…

Compare And Swap 比较并交换,是操作系统的并发原语。比较当前工作内存中的值与主内存中的值是否一致,一直则执行操作,不一致则一直循环。

CAS实际上就是乐观锁 的一种实现方式,乐观锁参考:www.cnblogs.com/kismetv/p/1…

缺点:

  • 使用自旋锁,循环比较耗时
  • 一次性只能保证一个变量的原子性
  • 存在ABA问题

Unsafe类

Java无法直接操作内存,C++可以操作内存,Java可以调用C++,进而来简介操作内存。native表明方法为本地方法,是通过C/C++实现的,供Java来调用。Unsafe类中有大量的native方法,Unsafe 类相当于一个Java操作内存的手段(途径)。

原子类的CAS机制

以整形原子类为例,查看+1操作的源码,分析CAS机制

源码如下:

AtomicInteger类中的操作:

// 原子地递增当前值,具有VarHandle.getAndAdd指定的记忆效应。
// 等效于getAndAdd(1)
public final int getAndIncrement(){
        return U.getAndAddInt(this,VALUE,1);
        }

Unsafe类中的操作:

// 以原子方式将给定值添加到给定对象o中给定offset处的字段或数组元素的当前值。
// 参数:o – 用于更新字段/元素的对象/数组
//     偏移量 - 字段/元素偏移量
//     delta – 要添加的值 
// 返回:之前的值
public final int getAndAddInt(Object o,long offset,int delta){
        int v;
        // 这里是一个自旋锁,一直在循环
        do{
        // 调用本地方法通过对象和偏移量获得内存中的值
        v=getIntVolatile(o,offset);
        // 判断并更新值,如果现在内存地址对应的值和获得v的值相同,则更新内存地址中的值为v+data
        }while(!weakCompareAndSetInt(o,offset,v,v+delta));
        return v;
        }

CAS存在的ABA问题

假设有两个线程A、B,和一个资源p,假设p=2021,现在作一个操作:p=2021时A把p改为2022,B线程判断p=2021,此时B线程修改p=2025,然后修改为2021,A线程进行判断时虽然p还是2021,但这时的p 已经是被修改过的了。这就是ABA问题,p从2021变为2022再变为2021

原子引用

带版本号的原子操作,AtomicStampedReference类,AtomicReference类,可以使用时间戳也可以使用自定义的版本号。这里面的CAS机制比较的是对象的引用

public class AtomicReferenceTest {
    public static void main(String[] args) {
        /**
         * 这里有一个坑,compareAndSet方法底层是使用==比较的,初始化的值Integer类型,如果直接传入一个整数会进行装箱,如果是-128~127
         * 之间,则可以正常使用,因为在内存中是指向的是同一个内存地址。如果不在这个范围内,进行自动装箱之后,再通过原子操作对该数据进行修改,则实际上修改的不是同一个对象
         * 这里可以在外部定义一个Integer类型的变量,作为参数传递进去,例如Integer integer = Integer.valueOf(2020);将integer作为参数initial
         */
        AtomicStampedReference<Integer> integerAtomicReference = new AtomicStampedReference<>(1, 1);

        new Thread(() -> {
            // 获得版本号
            int stamp = integerAtomicReference.getStamp();
            System.out.println("A1->" + stamp);
            try {
                // 模拟延时
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新值,修改为2,里面的CAS机制比较的是对象的引用
            integerAtomicReference.compareAndSet(1, 2, integerAtomicReference.getStamp(),
                    integerAtomicReference.getStamp() + 1);
            // 输出新的版本号
            System.out.println("A2->" + integerAtomicReference.getStamp());
            // 再修改为1,这里虽然expectedReference(期望值)再次被修改回来了,但是版本号已经改变了
            System.out.println("A->" + integerAtomicReference.compareAndSet(2, 1, integerAtomicReference.getStamp(),
                    integerAtomicReference.getStamp() + 1));
            System.out.println("A3->" + integerAtomicReference.getStamp());
        }, "A").start();
        new Thread(() -> {
            // 获得版本号
            int stamp = integerAtomicReference.getStamp();
            System.out.println("B1->" + stamp);
            try {
                // 模拟延时
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新值,修改为5,这里更新失败,因为之前(延时前)获得的stamp和现在再获得的integerAtomicReference.getStamp()
            // 已经是不同了,A中进行了一次修改,虽然expectedReference(期望值)是相同的,但是仍旧不能修改成功
            System.out.println("B->" + integerAtomicReference.compareAndSet(1, 5, integerAtomicReference.getStamp(),
                    integerAtomicReference.getStamp() + 1));
            // 输出新的版本号
            System.out.println("B2->" + integerAtomicReference.getStamp());
        }, "B").start();
    }
}

各种锁的再理解

公平锁、非公平锁

公平锁: 排队获取资源,不可抢占,申请锁时会直接进入等待队列,等待队列的第一个线程才能获得锁

非公平锁: 可以抢占,一般默认是非公平的,申请锁时会直接尝试获得锁,如果获取失败则进入队列

可重入锁

可重入锁(递归锁):获得最外层的锁之后,就可以获得内部所有的锁,这就是可重入锁。所有的锁都是可重入锁

代码示例:

资源类:

public class Phone {
    // sendMail方法上了锁,在里面又调用了加了锁的call方法,如果获得sendMail方法的锁,同时会获得call方法的锁,call方法执行完毕才会释放锁。
    // 这里实际上获得就是Phone对象实例的锁,对应方法执行完毕之后才会释放锁。这里对应call执行完毕之后,sendMail才执行结束。
    public synchronized void sendMail() {
        System.out.println(Thread.currentThread().getName() + "发邮件");
        call();
    }

    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "打电话");
    }
}

测试类:

public class ReentrantLockTest {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(phone::sendMail, "A").start();
        new Thread(phone::sendMail, "B").start();
    }
}

自旋锁

一直进行循环,直到获得想要的资源之后才结束循环。

自定义自旋锁:

public class SpinLockDiy {
    private AtomicReference atomicReference = new AtomicReference<>();

    // 使用CAS机制,加锁
    public void lock() {
        Thread thread = Thread.currentThread();
        // 使用CAS机制实现自旋锁,如果当前值不为空则一直循环,直到当前值为空,没有其他线程获得锁,则当前循环结束,停止自旋
        while (!atomicReference.compareAndSet(null, thread)) {
            System.out.println(thread.getName() + "自旋中");
        }
        System.out.println(thread.getName() + " -> lock");
    }

    // 解锁
    public void unlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " -> unlock");
        // 解锁,当前值设为null
        atomicReference.compareAndSet(thread, null);
    }
}

自定义自旋锁测试:

public class SpinLockDiyTest {
    public static void main(String[] args) throws InterruptedException {
        SpinLockDiy spinLockDiy = new SpinLockDiy();
        new Thread(()->{
            // 添加锁
            spinLockDiy.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                spinLockDiy.unlock();
            }
        },"A").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            // 添加锁
            spinLockDiy.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                spinLockDiy.unlock();
            }
        },"B").start();
    }
}

死锁分析

死锁排查方法参考:www.cnblogs.com/itsoku123/p…

死锁模拟:

参考:blog.csdn.net/feichitianx…

资源类:

public class Resource {
    // 这里s1和s2都是再常量池中获取的,每次实例化resource使用的都是常量池中同一个内存地址的s1和s2;new 方式产生的字符串,每次实例化Resource都会在堆中构建一个新的对象。
    // String s1 = new String("s1");这里会产生两个对象,先在常量池中查找是否存在s1,如果不存在则在常量池中创建一个,存在则不管。因为new关键字需要在堆中创建一个对象。
    private String s1 = "s1";
    private String s2 = "s2";
    // 如果是这种方式创建对象则不会发生死锁,因为每次实例化Resource都会创建新的s1和s2,锁的不是同一个对象,所以不会发生死锁
    // private String s1 = new String("s1");
    // private String s2 = new String("s2");

    // 先获得s1,再获得s2
    public void get1() {
        synchronized (s1) {
            System.out.println(Thread.currentThread().getName() + "获得s1,想要获得s2");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (s2) {
                System.out.println(Thread.currentThread().getName() + "获得s2");
            }
        }
    }

    // 先获得s2,再获得s1
    public void get2() {
        synchronized (s2) {
            System.out.println(Thread.currentThread().getName() + "获得s2,想要获得s1");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (s1) {
                System.out.println(Thread.currentThread().getName() + "获得s1");
            }
        }
    }
}

测试类:

public class DeadLockTest {
    public static void main(String[] args) {
        // 定义一个资源类
        Resource resource = new Resource();
        // 两个线程互相抢夺互斥资源,发生死锁
        new Thread(() -> {
            resource.get1();
        }, "A").start();
        new Thread(() -> {
            resource.get2();
        }, "B").start();
    }
}

死锁排查方法:

  • 查看日志
  • 查看堆栈信息

查看堆栈信息方式:

# 首先查看Java线程状态
PS C:\IdeaProject\WorkPlace\JavaBase> jps -l
16420 org.jetbrains.jps.cmdline.Launcher
17396 com.zhang.Java10JUC.JUC15lock.deadLock.DeadLockTest
13016 
16092 jdk.jcmd/sun.tools.jps.Jps

# 查看指定线程的堆栈信息
PS C:\IdeaProject\WorkPlace\JavaBase> jstack 17396
2021-09-15 09:23:29
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.8+10-LTS mixed mode):

Threads class SMR info:
_java_thread_list=0x00000207d4c23d00, length=13, elements={
0x00000207d48a0800, 0x00000207d48ab000, 0x00000207d4906000, 0x00000207d4908000,
0x00000207d490a000, 0x00000207d490d000, 0x00000207d4098000, 0x00000207d4027800,
0x00000207d4ca0800, 0x00000207d4ca1000, 0x00000207d4be7800, 0x00000207d4be8800,
0x00000207b5367800
}