JUC的简单理解(下)

1、ForkJoin

其本质思想就是大任务拆分成小任务,类似分治法

ForkJoin 特点:工作窃取
原理就是利用双端队列,当某个线程执行完自己的任务后会去窃取其他线程的任务来执行
在这里插入图片描述

使用

步骤:
1、创建任务类继承RecursiveTask抽象类,重写compute()方法
2、在compute()中采用分治递归的思想编写业务代码
3、创建ForkJoin池实例new ForkJoinPool()
4、将你的任务类实例传入实例池的submit()方法pool.submit(new MyForkJoinTask(1,10001));,获得一个ForkJoinTask的实例
5、通过ForkJoinTask实例的get()方法获得结果

测试(1-10001相加):

/**
 * 这是一个简单的Join/Fork计算过程,将1—10001数字相加
 */
public class ForkJoinPoolTest {

    // 数字个数在200个以下时进行计算
    private static final Integer MAX = 200;

    static class MyForkJoinTask extends RecursiveTask<Integer> {
        // 子任务开始计算的值
        private Integer startValue;

        // 子任务结束计算的值
        private Integer endValue;

        public MyForkJoinTask(Integer startValue , Integer endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }

        @Override
        protected Integer compute() {
            // 如果条件成立,说明这个任务所需要计算的数值分为足够小了
            // 可以正式进行累加计算了
            if(endValue - startValue < MAX) {
                System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
                Integer totalValue = 0;
                for(int index = this.startValue ; index <= this.endValue  ; index++) {
                    totalValue += index;
                }
                return totalValue;
            }
            // 否则再进行任务拆分,拆分成两个任务
            else {
                MyForkJoinTask subTask1 = new MyForkJoinTask(startValue, (startValue + endValue) / 2);
                subTask1.fork();
                MyForkJoinTask subTask2 = new MyForkJoinTask((startValue + endValue) / 2 + 1 , endValue);
                subTask2.fork();
                return subTask1.join() + subTask2.join();
            }
        }
    }

    public static void main(String[] args) {
        // 这是Fork/Join框架的线程池
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Integer> taskFuture =  pool.submit(new MyForkJoinTask(1,10001));
        try {
            Integer result = taskFuture.get();
            System.out.println("result = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace(System.out);
        }
    }
}

执行结果:
在这里插入图片描述

2、JMM(Java内存模型)

JMM(Java Memery Moudle):Java内存模型
在这里插入图片描述
如图,当线程要对a进行操作的时候要分成几步
1、将主内存中的a读取到本线程的工作内存
2、对a进行操作修改
3、将更改后的a刷新回主内存

这时如果没有线程同步的话,就很容易出现问题,也就是当线程1还未将更改后的a刷回主内存的时候,线程2要修改a,但是它取的依然是未经更改的主内存中的a,这样就会出现问题

JMM的八种交互操作(原子性)

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

JMM八种指令的规则

1、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

2、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

3、不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

4、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

5、如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

6、对一个变量进行unlock操作之前,必须把此变量同步回主内存

3.Volatile

一个关键字
1、保证了可见性
2、禁止指令重排(有序性)
3、不保证原子性

3.1、可见性

测试(不加volatile):

public class Demo01 {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag){
                System.out.print("flag==>"+flag);
                // System.out.println();  
                // 此处不能用这句打印,因为此语句中包含了Synchronized
            }
        },"A").start();

        // 为了让线程A先进入while循环
        Thread.sleep(3000);

        flag = false;
        System.out.println(flag);

    }
}

执行结果:
在这里插入图片描述
这里线程投资是我强制停止的,否则会无限打印
原理其实很简单,就是当线程A进入while循环读取flag的时候,将flag从主内存读取到线程A的工作内存中进行使用,当主线程更改flag值的时候线程A并不知道,于是一直在使用工作内存中的flag

加了volatile:

public class Demo01 {
    public static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag){
                System.out.print("flag==>"+flag);
                // System.out.println();  // 此语句中包含了Synchronized
            }
        },"A").start();

        // 为了让线程A先进入while循环
        Thread.sleep(3000);

        flag = false;
        System.out.println(flag);

    }
}

执行结果:
在这里插入图片描述
虽然依然输出许多值,但它是自己停止的

加了volatile修饰后的flag,每当它的值更改的时候,都会去提醒其他线程,这个flag已经被更改了

3.2、禁止指令重排(有序性)

指令重排:JVM在执行我们写的代码的时候会进行优化,也就是当两句代码之间没有关系的时候,它们的执行顺序可能会不一致,例如如下代码,a和b之间没有关系,JVM可能会先执行int b = 2的操作

int a = 1;
int b = 2;

内存也会重排:例如Integer a = new Integer(3);,这句指令大致有3个步骤:
1、实例化:在堆中开辟一片空间
2、初始化:赋值
3、指向引用地址
但是JVM可能会改变它们的执行顺序,会先指向引用地址,之后再进行赋值

而volatile就禁止JVM进行指令重排

3.3、原子性

原子性:不可分割
线程A执行任务的时候要么全部成功,要么全部失败

测试:

/**
 * volatile 原子性测试
 */
public class AtomicTest {
    public static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    num++;
                }
            }).start();
        }

        // 此处是为了让线程已经启动
        Thread.sleep(1000);

        while(true) {
            // 此处 == 2 是因为IDEA除了主线程外还有一个监控线程
            if (Thread.activeCount() == 2){
                System.out.println("num = " + num);
                break;
            }
        }
    }
}

执行结果:
在这里插入图片描述
理论上是200000,但是这里只加到了182399,原因就是volatile不能保证原子性
原因:这里num++一共有3个步骤:
1、从主内存中读取值
2、执行自增操作
3、将值刷入主内存
也就是说,当线程A读取num,还没自增,此时线程B读取num,执行自增,两个线程自增完同时写入的话,值相当于只加了一次

此处要想正确执行的话,可以加synchronized、Lock或者AtomicInteger

4、CAS

CAS(Compare And Swap):比较并交换

当我们要进行num++的操作的时候,利用CAS,它在刷入主存的时候会和主存的值进行比较,如果主存中的num没有改变的话,它才会写进主存,若是主存中的值改变了,它会继续获取主存的num值再次执行num++的操作,再次比较,这就是自旋

缺点:

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

ABA问题

也就是当进行CAS比较的时候,num的值可能看似没有变化,但实际上已经来了两个线程对num进行了修改,只是刚好将num改回了原始值,但是在进行CAS比较的时候不知道,这就是ABA问题

解决方法也很简单,加版本号
在num上加上一个版本号,每次对这个num进行更改的时候,都会更新改变版本号,这样CAS的时候就知道有没有其他线程来操作过这个对象了

AtomicReference的简单使用:

/**
 * 测试 AtomicReference
 * 解决ABA问题
 */
public class AtomicReferenceDemo01 {
    public static void main(String[] args) {
        // AtomicStampedReference<Integer>(初始值,初始版本号)
        // 注意,这里初始值不能设置过大,因为它会进行自动装箱(Integer),在-128~127这个范围之外可能导致错误
        AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<Integer>(50,1);

        new Thread(()->{
            System.out.println("A线程版本号1===>"+atomicInteger.getStamp());

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

            // 将2000设置为2022
            System.out.println(atomicInteger.compareAndSet(50, 60,
                    1, atomicInteger.getStamp() + 1));
            System.out.println(atomicInteger.getReference());
        }).start();
    }
}

执行结果:
在这里插入图片描述

5、锁

5.1、公平锁与非公平锁

公平锁:非常公平,线程获取锁不可以插队
非公平锁:非常不公平,线程获取锁可以插队

synchronized是非公平锁
Lock可以设置是公平还是非公平锁,默认是非公平锁

Lock lock = new ReentrantLock();  // 非公平锁
Lock lock = new ReentrantLock(true);  // 公平锁

5.2、可重入锁

可重入锁(递归锁):当获得了锁之后,再次获取相同的锁不会出现死锁
例:

/**
 * 可重入锁
 * Synchronized
 * 拿到外面的锁之后会默认拿到里面的锁
 * 例:在sms方法里拿到了锁之后,默认会拿到call里的锁
 */
public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            try {
                phone.sms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{

    public synchronized void sms() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"==>sms");
        TimeUnit.SECONDS.sleep(3);
        call();
    }

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

执行结果:
在这里插入图片描述
按理说,当A执行sms()方法会获得一把锁,但是当A执行call()方法的时候按理说上一把锁还没有释放,A应该会等待自己去释放锁,也就是造成死锁,但是这里没有造成死锁现象,因为synchronized是可重入锁,A两次获得的是同一把锁,所以无需重复获取

5.3、怎么排除死锁

死锁:两个以上的线程都需要获取对方的锁,都在等待对方释放锁

模拟死锁,死锁的实现
/**
 * 死锁的实现
 */
public class DeadLockTest {
    public static void main(String[] args) {
        // 创建两个资源
        Resource resourceA = new Resource("A");
        Resource resourceB = new Resource("B");

        // 将两个资源分别传给两个任务对象
        MyThread run1 = new MyThread(resourceA, resourceB);
        MyThread run2 = new MyThread(resourceB, resourceA);

        new Thread(run1).start();
        new Thread(run2).start();
    }
}
// 任务对象
class MyThread implements Runnable {

    // 两个资源 A  B
    private Resource lockA;
    private Resource lockB;

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

    @SneakyThrows
    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() +
                    "获取到了资源" + lockA.toString());
            // 这里是为了让另一个线程先占有另一个资源
            Thread.sleep(1000);
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() +
                        "获取到了资源" + lockB.toString());
            }
        }
    }
}
// 资源对象
class Resource {
    private String name;

    public Resource(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

执行结果:
在这里插入图片描述
可以看到,两个线程都阻塞了,出现死锁

排查死锁

(1)可以查看日志

(2)使用指令查看信息
使用的图片来自狂神的视频
步骤:
1、使用 jps -l定位进程号
在这里插入图片描述
2、使用jstack 进程号找到死锁问题
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值