JUC学习笔记(三)

16.JMM

请你谈谈你对Volatile的理解

只要是谈到Volatile,说出下方的这几个特点:

Volatile是 Java 虚拟机提供的轻量级的同步机制。

1.保证可见性

2.不保证原子性

3.禁止指令重排

说到可见性,那一定要谈JMM
什么是JMM?

JMM:Java内存模型,不存在的东西,只是一个概念!约定!

关于JMM的一些同步约定:

1.线程解锁前,必须把共享变量立刻刷回主存。

2.线程加锁前,必须读取主存中的最新值到工作内存中!

3.加锁和解锁是同一把锁!

线程工作内存主内存之间的工作关系:8种操作!如下图所示

在这里插入图片描述

同样,线程B如果使用Flag数据,也会经历上述8步操作!

内存交互操作有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操作之前,必须把此变量同步回主内存

17.Volatile

当没有使用Volatile关键字时,看下述代码:

//下述代码有一个main线程+一个Thread线程,Thread线程会根据num = 0这个值执行while循环,下述的main线程之后会修改num的值为1,但是测试时Thread线程仍然始终一直执行,没有停止,这就是因为main线程修改num值之后并同步到主存中,但是Thread线程的工作内存没有同步num的最新值,即Thread线程的工作内存中的num仍然为0,所以循环仍然一直执行
public class JmmDemo {
    private static int num = 0;

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

        new Thread(()->{ //该线程对主内存的变化是不可知的,解决该问题的方法就是给num字段用volatile修饰
            while (num==0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(2);

        num = 1;
        System.out.println(num);

    }
}

下图就是产生上述原因的关系图:

在这里插入图片描述

所以我们需要Thread线程知道主内存中的值所发生的变化!

1.保证可见性

上述代码Volatile可以解决可见性问题!

也就是给num字段加上volatile字段修饰。

private volatile static int num = 0;
2.不保证原子性

原子性:不可分割!

加了volatile关键字也不保证原子性!那怎么样可以保证原子性呢?答:使用synchornized或Lock

package com.codeyu.volatile_;

import java.util.concurrent.TimeUnit;

/**
 * volatile关键字不保证原子性!
 */
public class DmDemo01 {

    private volatile static int num = 0;

    public static void add(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        //volatile关键字不保证原子性!所以结果仍然不为20000
        //理论上num结果应该为20000,因为20个线程,每个线程执行++操作1000次
        for(int i=1; i<=20; i++){
            new Thread(()->{
                for(int j=0; j<1000; j++){
                    add();
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);

        System.out.println(Thread.currentThread().getName()+"="+num);

    }
}

上方add方法为何不安全,因为num++操作不是原子性操作,num++实际上是三个操作,1.将数据从内存拿出;2.+1操作;3.将+1后的值再写回原来的内存位置!

如果不准使用synchornized或Lock,还怎样保证原子性操作呢?

答:使用原子类解决原子性问题!

package com.codeyu.volatile_;

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

/**
 * volatile关键字不保证原子性!
 */
public class DmDemo01 {

    //volatile不保证原子性
    //所以使用原子类:AtomicInteger
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
        //num++;
        num.getAndIncrement(); //AtomicInteger的+1方法
    }

    public static void main(String[] args) throws InterruptedException {
        //volatile关键字不保证原子性!所以结果仍然不为20000
        //理论上num结果应该为20000,因为20个线程,每个线程执行++操作1000次
        for(int i=1; i<=20; i++){
            new Thread(()->{
                for(int j=0; j<1000; j++){
                    add();
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);

        System.out.println(Thread.currentThread().getName()+"="+num);

    }
}

//命令行打印输出:20000

原子类的底层都是直接和操作系统挂钩!在内存中改值,所以可能现在理解不了!

3.禁止指令重排
什么是指令重排?

答:你写的程序,计算机并不是按照你写的那样去执行的!

程序员所写的源代码-(会经历)->编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>最终执行

处理器在进行指令重排的时候,会考虑:数据之间的依赖性!

int x = 1; //1
int y = 2; //2
x = x + 5; //3
y = x * x; //4

我们所期望的程序执行步骤是:1234,但是可能执行的时候就会变成:21431324 (这写也都是可以的,不对最终结果造成影响)
但不能是:4123。因为4123会造成错误的结果!
指令重排可能会造成的影响:a b x y 初始值都是0;
线程A线程B
x=ay=b
b=1a=2

正常的结果:x=0,y=0;但是可能由于指令重排

线程A线程B
x=ay=b
b=1a=2

指令重排导致的诡异结果:x=2;y=1;

volatile关键字可以避免指令重排!

volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

18.彻底玩转单例模式

1.懒汉模式
2.饿汉模式

19.深入理解CAS

什么是CAS?

大厂你必须要深入研究底层!有所突破!

原子类的底层就是用的CAS。

package com.codeyu.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    //CAS:compareAndSet 比较并交换(CAS是cpu的并发原语!)
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //public final boolean compareAndSet(int expectedValue, int newValue)
        //如果期望的值(expectedValue)是2020,那么就将值更新为2021
        atomicInteger.compareAndSet(2020,2021);
        System.out.println(atomicInteger.get());

        //因为上方已经修改到了2021,所以下方这句代码会失败,返回false
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
    }
}

Unsafe类

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

CAS:比较当前工作内存中的值和主存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,因为底层是自旋锁!

缺点:

1.由于底层是自旋锁,循环会耗时

2.一次性只能保证一个共享变量的原子性

3.存在ABA问题

CAS:ABA问题(狸猫换太子)

21.各种锁的理解

1.公平锁、非公平锁

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

非公平锁:非常不公平,可以插队!

ReentrantLock reentrantLock = new ReentrantLock();
//ReentrantLock()默认是非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

//但也可以改为公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
//加入true参数
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
2.可重入锁

可重入锁:拿到外面的锁之后,就可以拿到里面的锁,自动获取!

package com.codeyu.lock;

public class Demo01 {
    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");
    }
}

//输出打印:
A=>sms
A=>call
B=>sms
B=>call

上述代码即使首先发短信sms()方法获得锁,但是因为sms()方法调用了call()方法,所以它同样会获得call()方法的锁!即拿到外面的锁之后,就可以拿到里面的锁,自动获取!

Lock版也可实现上述功能!但是Lock看起来是两把锁,一把外面的锁,一把里面的锁:
package com.codeyu.lock;

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

public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();

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

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

class Phone {
    Lock lock = new ReentrantLock();

    public void sms() {
        lock.lock();
        //lock锁必须配对,否则就会死在里面。即lock.lock();和lock.unlock();配对
        try {
            System.out.println(Thread.currentThread().getName()+"=>sms");
            call(); //这里也有锁
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

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

3.自旋锁

spinlock:自旋锁

下方为Unsafe类红的方法:

在这里插入图片描述

自定义自旋锁:
package com.codeyu.lock;

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

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
        //使用java实现的锁
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();

        //使用自己写的锁
        SpinlockDemo lock = new SpinlockDemo();

        new Thread(()->{
            lock.myLock();
            try {
                //业务代码
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(3);

        new Thread(()->{
            lock.myLock();
            try {
                //业务代码
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"B").start();
    }
}


A==>myLock
B==>myLock
A==>myLock
B==>myLock
4.死锁

在这里插入图片描述

死锁测试:
package com.codeyu.lock;

import java.util.concurrent.TimeUnit;

public class DeadLock {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        /**
         * 线程T1和线程T2构造方法的参数反过来
         * 所以实际上T1线程最开始会拿到MyThread类中的lockA对象的锁,然后睡眠2s
         * 在此时,T2线程最开始会拿到MyThread类中的lockB对象的锁(因为两个线程构造参数返过来了)
         * 之后T2线程睡眠2s,之后两线程醒后就会争夺对方的资源,从而死锁!
         */
        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);
            }
        }
    }
}

//输出打印:
T1lock:lockA=>getlockB
T2lock:lockB=>getlockA
之后,程序就会暂停!
排查解决死锁问题:

1.使用jps -l定位进程号(直接在idea的命令行使用即可)

在这里插入图片描述

2.使用jstack + 进程号查看进程信息

在这里插入图片描述

面试工作中排查问题:

1.日志

2.堆栈信息

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值