【java】多线程详解2

狂神老师的视频地址:https://space.bilibili.com/95256449

1、ForkKJoin和Stream并发

1.1 简介

Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。类似于Java 8中的parallel Stream。
在大数据量下的利用ForkKJoin并发提高效率,数据少就没必要了。

原理:工作窃取

如下图:

当A和B同时执行任务队列,B先执行完毕之后,可以帮A从队尾开始执行任务,节省时间

1.2、实现

计算类

/*
*  求和计算的任务!
*  如何使用ForkJoin
* 1、通过forkjoinpool来执行
* 2、计算任务forkjoinPool.execute(ForkJoinTask task)
* 3、计算类要继承RecursiveTask
* */
public class ForkJoinDemo extends RecursiveTask<Long> {
    private Long start;
    private Long end;
    private Long temp = 10000l;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if ((end - start) > temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            long  middle = (end - start) / 2;
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork();
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork();
            return task1.join() + task2.join();
        }
    }
}

对比普通方法和ForkJoin计算时间和Stream

public class Test {
    public static void main(String[] args) {
        test1();
        test2();
        test3();
    }
    //普通方式
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }
    //ForkJoin方式
    public static void test2() {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(1L, 10_0000_0000L);
        //forkJoinPool.execute(task);//执行任务无返回结果
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);//异步提交
        Long sum = null;
        try {
            sum = submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }
    //Stream
    public static void test3() {
        long start = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(1L,10_0000_0000L).parallel().reduce(0,Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }
}

Console:

sum=500000000500000000 时间:5841
sum=500000000500000000 时间:5020
sum=500000000500000000 时间:160

可以看到ForkJoin效率提高了一部分(可以调节临界值来进一步提高),而Stream效率明显要高很多!

2、异步回调

2.1、概念

Ajax中已经讲过了异步请求是什么,即不需要等待前面的请求执行完毕,随时可以发送请求。

异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。

如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。

CompletableFuture

/**
 * 异步调用:例如Ajax
 */
public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //没有返回值的异步回调
        /*CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "runAsyc=>Void");
        });
        System.out.println("111");
        completableFuture.get();*/

        //有返回值的异步回调
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
            //int i = 10 / 0;                 //制造错误
            return 1024;
        });

        System.out.println(completableFuture.whenComplete((t, u) -> {
            System.out.println("t==>" + t);  //正常的返回结果
            System.out.println("u==>" + u);  //错误信息

        }).exceptionally((e) -> {
            System.out.println(e.getMessage());
            return 233;    //错误时候的返回结果
        }).get());

        //使用ExecutorService运行异步
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        completableFuture = CompletableFuture.runAsync(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "runAsyc=>Void");
        },executorService);

    }
}

没有返回结果:

111
ForkJoinPool.commonPool-worker-9runAsyc=>Void

有返回结果无错误时:

t==>1024
u==>null
1024

有返回结果有错误时(int i = 10 / 0;注释取消):

ForkJoinPool.commonPool-worker-9supplyAsync=>Integer
t==>null
u==>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
233

2.2、常用方法

# 对异步线程的下一步处理
thenApply(): 将调用它的异步线程的返回值作为输入值,自身带有返回值
thenApplyAsync(): 与前者不同的是会自己再起一个线程
thenAccept(): 将调用博客后台 - 博客园的异步线程的返回值作为输入值,自身不带有返回值
thenAcceptAsync(): 与前者不同的是会自己再起一个线程
thenRun(): 接着调用它的异步线程后面执行,没有输入,也没有输出
thenRunAsync(): 与前者不同的是会自己再起一个线程
whenComplete(): 将调用它的异步线程的返回值和异常情况作为输入值,自己的返回值和输入的值一样
whenCompleteAsync():前者不同的是会自己再起一个线程
handle()和handleAsync(): 与whenComplete不同的是有自己的返回值
# 对异步线程的下一步组合处理
thenCombine():将两个线程的参数作为输入值,有自己的返回值
thenAcceptBoth(): 将两个线程的参数作为输入值,没有自己的返回值
runAfterBoth(): 没有输入值,也没有返回值

  • 异步运行

  • 获取线程状态
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture completableFuture = CompletableFuture.runAsync(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        //isDone() 判断是否完成,不管是异常还是正常
        System.out.println(completableFuture.isDone());
        //isCancelled() 判断正常完成之前是否取消了
        System.out.println(completableFuture.isCancelled());
        //isCompletedExceptionally() 判断是否异常结束,包括取消cancel、显示调用completeExceptionally、中断。
        System.out.println(completableFuture.isCompletedExceptionally());
        //取消线程
        completableFuture.cancel(true);
        //告知CompletableFuture任务完成。
        completableFuture.complete(true);
        //做超时处理,如果超时报错,注意超时和被取消不一样
        completableFuture.get(5, TimeUnit.SECONDS);
    }
}

3、Volatile

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

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

而要了解指令重拍就得了解JMM

3.1、JMM

JMM即java内存模型,是一种概念

JMM不保证可见性的原因:B线程修改了flag值,但是线程A不能及时可见

3.2、保证可见性

public class JMMDemo {
    //不加上volatile,无法保证可见性
    private static int num = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            while (num == 0) {

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println(num);
    }
}

会发现,num变成了1,但是我们new的线程一直没有结束,说明它一直没有获取到num的值

如果给num加上volatile,那就可以了

3.3、不保证原子性

public class VDemo2 {
    private volatile static int num = 0;

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

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(num);
    }
}

console:

19884 //每次不一样

会发现,即使添加了volatile,结果也和逾期对不上。

因为num++不是一个原子性操作,其实底层是获取值、+1、写入值三步骤。

所以这一类情况,使用原子类就可以保证原子性:

private  static AtomicInteger num = new AtomicInteger(0);

    public static void add() {
        num.getAndIncrement(); // CAS
    }

那么为什么不用synchronized呢?因为Atomic类底层用了CAS。

3.4、指令重排

代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,当然这是在不影响数据逻辑依赖的前提下的。

但是在多线程情况下,线程A中的没有依赖关系的指令,和线程B中没有依赖关系的指令,之间是可能有逻辑关系的,重排之后就会出问题。

而volatile可以通过插入内存屏障来禁止指令重排

4、深入理解CAS

4.1、什么是CAS(compare and swap)锁

即在修改数据之前验证值是否会修改,如果被修改读取最新的值,如果没被修改保存数据。

4.2、AtomicInteger中的使用

上文原子性讲的AtomicInteger类中的getAndIncrement方法是使用了CAS的:

4.3、CAS锁的弊端:

  • 要是结果一直就一直循环了(自旋锁),CUP开销是个问题
  • 只能保证一个共享变量原子操作的问题
  • 还有ABA问题

4.4、解决ABA问题

给变量添加一个版本号就好了嘛

AtomicStampedReference类就是干这个的:

5、常见的死锁和定位

最常见的死锁就是A调用B,B调用A,循环等待,例如下面这个代码

public class Demo1 {
    public static void main(String[] args) {
        String a = "a";
        String b = "b";
        new Thread(new Mythread(a,b)).start();
        new Thread(new Mythread(b,a)).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);  //当第一个线程拿到A锁的,等待的时候,第二个线程拿到B,开始循环等待
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "lock:" + lockB + "=>get" + lockA);
            }
        }
    }
}

console:

Thread-0lock:a=>getb
Thread-1lock:b=>geta 
//一直等待出现死锁

那么怎么定位死锁呢?除了常见的查看代码逻辑,查看日志之外,还可以查看堆栈信息

使用 jps -l 定位进程

使用 jstack 进程id 查看堆栈信息

C:\Users\Administrator>jstack 10548
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001d5f3628 (object 0x000000076bf99088, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001d5f0d98 (object 0x000000076bf990b8, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.company.lock.Mythread.run(Demo1.java:33)
        - waiting to lock <0x000000076bf99088> (a java.lang.String)
        - locked <0x000000076bf990b8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.company.lock.Mythread.run(Demo1.java:33)
        - waiting to lock <0x000000076bf990b8> (a java.lang.String)
        - locked <0x000000076bf99088> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
posted @ 2022-06-21 16:54  吴承勇  阅读(76)  评论(0编辑  收藏  举报