JUC (狂神说笔记三)

16、JMM

什么是JMM?

JMM:Java Memory Model Java内存模型,不是一个真实的存在的东西,它是一个概念,约定!

关于JMM的一些同步的约定:

  1. 线程解锁前,必须把共享变量立刻刷回主存
  2. 线程加锁前,必须读取主存中的最新值到工作内存中
  3. 加锁和解锁必须是同一把锁

线程在实际的运行时,是有工作内存和主内存两个概念的。

实际的运行图示:

在这里插入图片描述

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

对于刚才上面的图示,我们不难发现,当我们其中一个线程修改了主存里面Flag的值以后,其他的线程是没有办法及时获取的,以至于出现了信息不对称的情形。

具体的代码实现:

package pers.mobian.jmm;

import java.util.concurrent.TimeUnit;

public class JMMTest {
    private static int num = 1;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (num == 1 ) {
                //循环体
            }
        }).start();

        //确保让开启的其他线程先执行
        TimeUnit.SECONDS.sleep(2);
        
        //修改变量的值,观察线程的变化
        num = 0;
        System.out.println(num);
    }
}

总结:我们的结果会打印0以后,就陷入了一个死循环。即可以理解为每一个线程将主存的变量都转换为了自己的私有变量,以至于内部修改,外部依然没有变化。


17、Volatile

请你谈谈你对Volatile的理解

  • Volatile是Java虚拟机提供轻量级的同步机制
  • Volatile保证可见性
  • Volatile不保证原子性
  • 禁止指令重排

17.1、保证可见性

将我们的代码修改为:

package pers.mobian.jmm;

import java.util.concurrent.TimeUnit;

public class JMMTest {
    //添加volatile关键字,保证了变量的可见性
    private static volatile int num = 1;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (num == 1 ) {
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        num = 0;
        System.out.println(num);
    }
}

17.2、不保证原子性

按照JMM的概念分析,理论上只要我们的线程信息交换足够快,那么就可以完成结果为1000的运算。可是现实是很难做到。当我们添加了volatile关键字以后,依然不起作用,即无法保证我们运算的原子性。

测试代码:

package pers.mobian.jmm;

import java.util.concurrent.TimeUnit;

public class VoTest01 {
    private static volatile int num = 0;
    //加synchronized锁肯定是可以的
    private static  void add() {
        num++;
    }
    public static void main(String[] args) throws InterruptedException {

        //开启10个线程,每一个线程中执行100次方法
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 100; i1++) {
                    add();
                }
            }).start();
        }

        //避免我们开启的线程还没有结束就执行主线程
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(num);
    }
}

我们反编译该类的.class文件javap -c VoTest01.class,可以得到其对应的字节码文件,通过字节码文件分析,一个简单的num++,需要经历三步,所以其不是原子性。

在这里插入图片描述

如果不添加lock锁和synchronized锁,如何保证原子性呢?

我们可以使用原子类

在这里插入图片描述

测试代码:

package pers.mobian.jmm;

import java.util.concurrent.atomic.AtomicInteger;

public class VoTest02 {
    //使用原子类
    private static AtomicInteger num = new AtomicInteger();

    private static void add() {
        //使用原子类的增加方法
        num.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(num);
    }
}

使用源自类的对象进行增加就不再是简单的+1了,这些类的底层直接和OS相关,直接在内存中修改值。其底层中很关键的一个类就是Unsafe类,它是一个很特殊的存在

17.3、指令重排

指令重排:我们写的程序,计算机并不一定是按照我们写的顺序执行的。

例如:

int a = 2; //1
int b = 3; //2
a = a + 1; //3
b = a + a; //4

我们编写代码的顺序是:1234。但是执行的顺序却有可能是1324、2134

数据a b c d的默认值都是0

指令重排前的结果:a=0、b=0

线程A线程B
a=cd=c
c=2c=5

指令重排后的结果:a=5、b=2

线程A线程B
d=2a=5
a=cb=d

指令重排在逻辑上是存在的

volatile如何保证我们的指令重排?

使用内存屏障(可以理解为CPU指令):

  1. 保证特定的操作执行顺序
  2. 保证某些变量的内存可见性(volatile就是利用了这个特性实现了可见性)

在这里插入图片描述

综上所述:volatile可以保证可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象


18、单例模式

五种单例模式详解


19、CAS

编写一个CAS的测试代码:

package pers.mobian.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest01 {
    public static void main(String[] args) {
        
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        
        //期望、更新 
        //public final boolean compareAndSet(int expect, int update) 
        //达到期望值就跟新数据,否则不更新, CAS是CPU的并发原语! 
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

点到getAndIncrement方法的源码中:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

    public final int getAndIncrement() {
        //参数列表:当前对象,当前对象在内存中的值,数字1
        return U.getAndAddInt(this, VALUE, 1);
    }
}

我们都知道Java无法直接操作内存,但是Java有关键字navite,可以调用C++方法,C++可以可以操作内存。但是Java给自己留了一个后门,可以通过Unsafe类操作内存

Unsafe类的对应源码:这是一个自旋锁

@HotSpotIntrinsicCandidate
//参数列表:当前对象,当前对象在内存中的值,数字1
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        //offset:field/element offset
        //获取对象 o 的地址偏移值,再赋值给 v
        v = getIntVolatile(o, offset);
        //如果我的对象 o 加上地址偏移值还是等于 v ,那么我们就直接+1
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

总结:

CAS可以直接比较工作内存中的值和主内存中的值,如果这个值是期望的,那么就执行操作!如果不是就一直循环

缺点:

  • 循环比较耗费时间
  • 一次性只能保证一个共享变量的原子性
  • ABA问题

ABA问题(狸猫换太子)

可以理解为线程B在很快的时间内执行了两次值的交换,且这一切都是在A线程执行之前。虽然数据进行了交换,但是A线程却不知道。

在这里插入图片描述

package pers.mobian.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest02 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // ============== 捣乱的线程 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());


        // ============== 期望的线程 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
    }
}

执行结果:

true
2021
true
2020
true
6666

通过结果,我们可以再次发现,我们修改了值,但是cas依然可以比较交换


20、原子引用

使用这种原子操作,就可以解决CAS中的ABA问题。其思想就是增加一个乐观锁,此处是添加一个版本号

测试代码:

package pers.mobian.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASTest03 {
    //实际的业务开发中,我们引用的可能是一个对象
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

    public static void main(String[] args) {


        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //参数:期望值,更新值,当前的版本号,增加对应的版本号
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);

            //打印我们的最近的一个版本信息
            System.out.println("a2=>" + atomicStampedReference.getStamp());

            //打印更新值是否成功,返回一个指定的布尔值
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            //打印我们的最近的一个版本信息
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();


        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6, 
                    stamp, stamp + 1));

            //打印更新值是否成功,返回一个指定的布尔值
            //由于版本信息被悄悄地修改过,所以这里返回的是false
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }
}

执行结果:

b1=>1
a1=>1
a2=>2
true
a3=>3
false
b2=>3

补充:Integer 使用了对象缓存机制,默认范围是 128 ~ 127推荐使用静态工厂方法 valueOf 获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间;


21、各种锁的理解

21.1、公平锁、非公平锁

公平锁: 非常公平, 不能够插队,必须先来后到!

非公平锁:非常不公平,可以插队 (默认都是非公平)

//默认使用非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
//传入true,使用公平锁
ReentrantLock reentrantLock2 = new ReentrantLock(true);


public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

21.2、可重入锁

也叫递归锁。

可以理解为你拿着你家大门的钥匙,你只要打开了大门的锁,就可以自动打开房间里面的不同卧室的锁。

synchronized版本

package pers.mobian.relock;

public class ReLockTest01 {
    public static void main(String[] args) {

        Phone phone = new Phone();
        new Thread(() -> {
            phone.sms();
        }, "A").start();
        new Thread(() -> {
            phone.sms();
        }, "B").start();
    }
}

class Phone {
    public synchronized void sms() {
        System.out.println(Thread.currentThread().getName() + "=>sms");
        call(); // 这里也有锁
    }

    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "=>call");
    }
}

lock版本

package pers.mobian.relock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReLockTest02 {
    public static void main(String[] args) {

        Phone2 phone = new Phone2();
        new Thread(() -> {
            phone.sms();
        }, "A").start();
        new Thread(() -> {
            phone.sms();
        }, "B").start();
    }
}

class Phone2 {
    Lock lock = new ReentrantLock();
    
    public void sms() {
        //这是第一把锁,用于锁sms方法
        lock.lock();
        
        //这是第二把锁,用于锁call方法
        //当然我们也可以直接在call方法里面添加一把锁
        lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + "=>sms");
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public void call() {
        try {
            System.out.println(Thread.currentThread().getName() + "=>call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }
}

注意:添加的锁一定要配对出现,不然会出现死锁现象

21.3、自旋锁

前面我们也有提到一个自旋锁(SpinLock)

不断地尝试,直到成功为止

@HotSpotIntrinsicCandidate
//参数列表:当前对象,当前对象在内存中的值,数字1
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        //offset:field/element offset
        //获取对象 o 的地址偏移值,再赋值给 v
        v = getIntVolatile(o, offset);
        //如果我的对象 o 加上地址偏移值还是等于 v ,那么我们就直接+1
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

我们自定义一个自旋锁:

package pers.mobian.relock;

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    
    //传递的参数类型是引用数据类型:默认值为null
    //传递的参数类型是基本数据类型int:默认值是0
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    
    //加锁
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.currentThread().getName()+"=>myLock");
        
        //自旋锁关键
        while (!atomicReference.compareAndSet(null, thread)) {
            
        } 
    }
    
    //解锁
    public void myUnLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.currentThread().getName()+"=>myUnLock");
        atomicReference.compareAndSet(thread,null);
    }
}

测试代码:

package pers.mobian.relock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class SpinLockTest01 {
    public static void main(String[] args) throws InterruptedException {
//        我们传统的使用别人的锁的方式
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();

        //使用我们自定义锁
        SpinLockDemo lock = new SpinLockDemo();
        new Thread(() -> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T1").start();
        TimeUnit.SECONDS.sleep(2);
        
        new Thread(() -> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T2").start();
    }
}

执行结果:

T1=>myLock
T2=>myLock
T1=>myUnLock
T2=>myUnLock

总结:我们的T1会先进去拿到锁,然后又修改对应的信息为thread的下一个版本号,此时不需要进入自旋锁。就在此时,T2又拿到了它的锁,但是此时我们的自旋锁里面的thread的版本号已经不再是null,所以会进入自旋锁,不断循环。当T1释放锁时,其对应地解锁会将thread的版本号再次变成null,此时在自旋锁里面的T2就可以离开,最终释放锁。

21.4、死锁

在这里插入图片描述

测试案例:

package pers.mobian.relock;

import java.util.concurrent.TimeUnit;

public class DeadLockTest01 {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new MyThread(lockA, lockB), "T1").start();
        new Thread(new MyThread(lockB, lockA), "T2").start();
    }
}

class MyThread implements Runnable {
    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "lock:" + lockA + "=>get" + lockB);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "lock:" + lockB + "=>get" + lockA);
            }
        }
    }
}

解决(排错)死锁的方法:

1、在IDEA的使用jsp -l查看对应的进程号

在这里插入图片描述

2、使用jstack 进程号找到死锁的问题

在这里插入图片描述

继而达到排错的效果

排错的方式:查看日志、查看堆栈信息

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值