JUC并发编程知识点,最后一篇,做完笔记就可以睡觉啦(三)

1. JMM

JMM :java内存模型,是一个概念,约定。

JMM的同步约定

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

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

在这里插入图片描述

2. volatile (轻量级的同步机制)

  1. 保证可见性
  2. 不能保证原子性(原子性是在执行任务的过程中,不能被打扰,也不能被分割)
  3. 不允许指令重排
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileDemo {
    //验证可见性
    private volatile static int i = 0;
    
    public static void main(String[] args) {

        new Thread(()->{
            //在没有加上volatile的时候,会出现死循环
            //即使下方对i进行了修改,也不能结束循环
            //加上volatile便可解决
            while(i == 0){

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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


        new Test().test01();
    }

}
//验证它的不能保证原子性原子性
class Test{
    //private volatile int j = 0;

    //解决方法 1 给add方法加上synchronized
    //解决方法 2 用lock锁
    //解决方法 3 使用 JUC包下的Automic
    AtomicInteger j = new AtomicInteger();


    private synchronized void add(){
        //j++;
        j.getAndIncrement();
    }



    public void test01(){

        for (int i = 0; i < 33; i++) {
            new Thread(()->{
                //本应该输出的是33000,但是就是输出不了啊!
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }


        //必须要加上这个,保证其他线程均执行完
        //为什么大于2,因为main和gc
        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(j);
    }
}

2.1 指令重排问题

什么是指令重排? 计算机并不是按照你写的代码顺序执行,会进行重排优化,但是保证结果正确。

volatile可以避免指令重排(利用内存屏障来实现)

  1. 保证特定操作的执行顺序
  2. 保证变量的内存可见性

3. 深入理解CAS

//CAS是啥 compareAndSet 比较并交换
public class CAS {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(3);

        //两个参数,一个期望值,一个要修改成的值,,符合期望值,则对其进行修改。修改成功,返回true,否则为false
        //CAS是CPU的并发原语
        System.out.println(atomicInteger.compareAndSet(3, 5));
        System.out.println(atomicInteger);
    }
}

在这里插入图片描述

CAS:比较当前工作内存中的值和主内存中的值,如果这个值符合期望,那么执行操作,否则,就一直循环。(自旋锁)
弊端:循环会耗时,一次性只能保证一个共享变量的原子性,还有著名的ABA问题。

3.1 ABA问题的解决(带版本号(时间戳)的方法)

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

//解决ABA问题
public class ABA {
    public static void main(String[] args) {

        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(3,1);

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();

            System.out.println("a1 = " + stamp);

            //这个休眠保证开始的时,它们获取的时间戳都是一样的
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //前两个期望值,和新值不再赘述
            //后两个参数,期望的时间戳,如果时间戳符合期望值,那么就进行值的修改,修改完成后将时间戳+1
            atomicStampedReference.compareAndSet(3,5,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);

            System.out.println("a修改后的值 " + atomicStampedReference.getReference());

            atomicStampedReference.compareAndSet(5,3,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);

            System.out.println("a又改回原值 " + atomicStampedReference.getReference());


        },"a").start();




        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();

            System.out.println("b1 = " + stamp);

            //这里的延时保证a进行完成ABA问题,这里再让b进行修改
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(3,9,stamp,stamp + 1);
            System.out.println("b看看修改成9吗?应该是没有哇,还是 " + atomicStampedReference.getReference());
        }).start();
    }
}

4. 锁的理解

4.1 公平锁和非公平锁

公平锁:很公平,不能插队,先来后到执行
非公平锁:非常不公平,能插队(默认都是非公平的)

4.2 可重入锁

在这里插入图片描述

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

//可重入锁的测试
public class Reentrant {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        new Thread(()->{
            phone.msg();
        }).start();


        new Thread(()->{
            phone.msg();
        }).start();
    }
}

//Synchronized版
class Phone{

    public synchronized void msg(){
        System.out.println("发短信");
        call();
    }

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

//Lock版
class Phone2{
    private Lock lock = new ReentrantLock();

    public void msg(){
        lock.lock();
        try{
            System.out.println("发短信");
            call();
        }finally {
            lock.unlock();
        }

    }

    public void call(){
        lock.lock();
        try {
            System.out.println("打电话");
        }finally {
            lock.unlock();
        }

    }
}

4.3 自旋锁

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

//自旋锁
public class SpinLock {
    private AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //获得锁 null 变为 thread

    //释放锁 thread 变为 null
    public void lock(){
        //获取当前线程
        Thread thread = Thread.currentThread();

        System.out.println(Thread.currentThread().getName() + "获取锁");


        //在这里自旋
        //怎么个自旋法呢?
        //A先获取到了锁,那么这个while中的条件为false不执行,
        //此时B来了,要获取,这个条件为true,就一直在这里转,直到A释放锁,把它改成空
        //B才能得到锁
        //这个while条件是针对后来想要获取锁的线程的
        while(!atomicReference.compareAndSet(null,thread)){

        }
    }

    public void unlock(){
        Thread thread = Thread.currentThread();

        System.out.println(Thread.currentThread().getName() + "释放锁");
        atomicReference.compareAndSet(thread,null);
    }
}
class test{

    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();

        new Thread(()->{
            spinLock.lock();

            try {
                //延时,表示A释放了锁后B才能拿到锁
                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.unlock();
            }
        },"a").start();

        //让A先获取锁
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            spinLock.lock();

            try {

            }finally {
                spinLock.unlock();
            }
        },"b").start();
    }
}

4.4 死锁的问题解决

在这里插入图片描述
在这里插入图片描述
查看堆栈

原创文章 34 获赞 8 访问量 1153

猜你喜欢

转载自blog.csdn.net/qq_46225886/article/details/105801795