Java Concurrent Programming (Part 2)

volatile keyword

The two functions of the volatile keyword

  • variable visible across threads
    • Different threads are often isolated, and one thread cannot know the data in another thread. So this may cause threads in a multiprocessing computer to run on different processors, and the values ​​​​in registers or memory caches in different processors may be different. So you need to use keywords when declaring instance variablesvolatile to ensure that when one thread modifies a value, another thread can see the latest value of the same variable (in fact, it does not go through the cache but re-gets the value in memory).
  • Disable instruction reordering
    • If an operation is not atomic, that is, it can be split, then its execution order may be modified by the compiler or the lower-level CPU. Although the compiler will not modify the code order of dependencies, this only works for a single thread. Because in a multi-threaded environment, dependencies may be destroyed due to the execution of other threads.

The volatile keyword cannot guarantee atomicity

what is deadlock

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition c = lock.newCondition();
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                lock.lock();
                if(1==1){
                    try {
                        System.out.println(Thread.currentThread().getName()+":死锁");
                        c.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("解除");
                c.signalAll();
                lock.unlock();
            }).start();
        }
    }
}

To sum up, when all threads are blocked, a deadlock will be formed. The program must be carefully designed, and it must be guaranteed that deadlocks will not occur!

thread local variables

It is often confusing when multiple threads are using the same shared variable. But if you use local variables, you can realize that variables between threads do not affect each other

before using local variables
private static Integer count = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            count++;
        }).start();
    }
    Thread.sleep(1000);
    System.out.println("count : "+count);
}

Output: 3

It can be seen that the operations of all threads directly affect the shared variables count.

after using local variable
//为每个线程构造一个实例
private static ThreadLocal<Integer> t = ThreadLocal.withInitial(() -> Integer.valueOf(10));
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            //获取当前线程的实例
            int count = t.get();
            count++;
        }).start();
    }
    Thread.sleep(1000);
    System.out.println("count : "+t.get());
}

Output: 10

t.get()method to get Integerobjects belonging to the current thread. It can be seen that no matter how the variable is manipulated in the thread, it actually operates the instance (local variable) belonging to the current thread. So using local variables can improve thread safety.

Thread Pool

When do you need to use the thread pool? The thread pool should be used when there are a large number of threads with a very short life cycle in the program.

Future

Future是一个带参数类型的接口,它有一个实现类FutureTask,实现了Future和Runnable接口。这个类的构造方法中可以传递一个Callable对象。Callable是一个封装了带有返回值方法的泛型类,返回值类型就是为泛型指定的类型。Future就保存了Callable返回的这个结果。当完成上面的工作之后,就可以将FutureTask对象传到new Thread()构造器,并调用start()运行。

这种方式并不常用,一般不会将Callable直接传给Future,现在只需要记得Future可以保存异步计算结果即可

创建线程池

执行器Executors类有很多静态方法用来获取线程池

方法 描述
newCachedThreadPool 必要的时候会创建新的线程;池中的空闲线程会保留60秒
newFixedThreadPool 包含固定线程数的线程池;线程会一直保留
newSingleThreadExecutor 只包含一个线程,一次只能运行一个任务,排队执行
使用线程池
//1、创建一个固定大小的线程池,因为我的cpu是4核4线程所以我指定16个线程
ExecutorService es = Executors.newFixedThreadPool(16);
//2、将Callable提交给ExecutorService,执行完的结果保存在Future中,这里只返回1024
Future<Integer> f = es.submit(() -> 1024);
//3、取出执行结果
Integer i = f.get();
System.out.println(i);
//4、如果不需要线程池工作了,记得关闭
es.shutdown();

以上就是线程池使用的步骤,总结以下就是

  1. 通过Executors的静态工厂创建一个指定类型的线程池
  2. Callable或者Runnable接口实例提交到ExecutorService
  3. 通过专门保存执行结果的Future实例用它的get()方法获取结果
  4. 如果没有要执行的任务时,就关闭线程池服务

CallableRunnable很类似,但是Callable可以有返回值,并且它是一个参数化的函数式接口。

线程池执行任务组

上面介绍了线程池的基本用法,并且演示了如何将一个Callable任务对象提交给执行器执行。那么能不能传递多个任务让执行器执行呢?实际上ExecutorService执行器接口提供了invokeAny(Collection<Callable<T>> tasks)以及invokeAll(Collection<Callable<T>> tasks)方法;他们的用法基本相同,只不过前者返回一个任务结果,后者返回所有的任务结果。

public static void main(String[] args) throws InterruptedException, ExecutionException {
    List<Callable<Integer>> listCall = new ArrayList<>();
    Callable<Integer> c = () -> 1024;
    listCall.add(c);
    listCall.add(c);
    ExecutorService es = Executors.newFixedThreadPool(16);
    Integer i = es.invokeAny(listCall);
    System.out.println("invokeAny:");
    System.out.println(i);
    List<Future<Integer>> list = es.invokeAll(listCall);
    System.out.println("invokeAll:");
    list.forEach((f) -> {
        try {
            System.out.println(f.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    });
    es.shutdown();
}

运行结果:
invokeAny:
1024
invokeAll:
1024
1024

现在还有一个问题,invokeAll的返回的多个结果并不是按计算结果顺序存储的。如果想要顺序存储可以使用ExecutorCompletionService处理器来完成,这个处理器中包含执行器Executor变量。

示例代码

public static void main(String[] args) throws InterruptedException, ExecutionException {
    List<Callable<Integer>> listCall = new ArrayList<>();
    Callable<Integer> c = () -> 1024;
    listCall.add(c);
    listCall.add(c);
    ExecutorService es = Executors.newFixedThreadPool(16);
    var service = new ExecutorCompletionService<Integer>(es);
    for (Callable<Integer> call : listCall) {
        //将任务提交给这个处理器
        service.submit(call);
    }
    for (int i = 0; i < listCall.size(); i++) {
        //移出一个已完成的任务并返回结果
        service.take().get();
    }
    es.shutdown();
}

想要顺序获取任务的计算结果,只需要构建这个处理器,传入线程池执行器对象,并调用处理器所提供的响应的方法就好了。

fork-join线程池

从字面意思来看,fork-join是分-和的意思,实际上它的功能也确实如此。它的主要用途是将一个大任务拆分成多个小任务并分别计算结果,最后将所有小任务的结果加在一起形成最终结果

public class ceshi {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建一个实现了RecursiveTask<T>接口的类型,该类中只有一个方法用来执行具体的任务
        var fjt = new ForkJoinTest("hello hello");
        //创建fork-join线程池
        var pool = new ForkJoinPool();
        //运行任务
        pool.invoke(fjt);
        //返回指定类型的结果,这里是String。
        System.out.println(fjt.join());
    }
}

class ForkJoinTest extends RecursiveTask<String>{

    private String msg;

    public ForkJoinTest(String msg){
        this.msg = msg;
    }

    @Override
    protected String compute() {
        if(!msg.contains(" ")){
            return msg+="happy";
        }else{
            String[] msg = this.msg.split(" ");
            var first = new ForkJoinTest(msg[0]);
            var second = new ForkJoinTest(msg[1]);
            //这里会阻塞,知道所有任务全部完成
            invokeAll(first,second);
            return first.join()+first.join();
        }
    }
}

运行结果:hellohappyhellohappy

除了向fork-join线程池中传递RecursiveTask<T>实例外,还可以传递RecursiveAction实例,只不过后者没有返回值;他们都是Future接口的扩展。同时还需要注意一点:在compute()方法中,除了用join()方法获取返回值外,还可以使用get()方法,但是不建议这么做,因为有可能抛出检查型异常,但是在compute()方法中不能抛出这种异常。

CompletableFuture

CompletableFuture实现了Future接口,它解决了一个问题,那就是当我们使用Future对象来获取任务结果的时候,如果还有任务没有执行完,那么这个方法就会干等着

CompletableFuture不仅实现了Future接口,还实现了CompletionStage接口。前者是用来获取计算结果,后者则是用来组合异步任务,通过对任务合理的组合就可以完成无需等待的异步任务。那么如何组合任务呢?

先看看它内部提供的部分方法:

  • supplyAsync(Supplier,Executor) 这是一个静态方法,用来开启一个任务。它的第一个参数是用来返回一个指定类型值的函数式接口、第二个参数可以传递一个线程池类型。
  • thenCompose(T) 这是一个实例方法,他可以传任意类型的参数,他用来处理一个数据并返回CompletableFuture类型
  • thenCombine(CompletableFuture,BiFunction) 这是一个实例方法,它用来组合两个任务并组合两个任务的结果。它有两个参数,第一个参数类型是CompletableFuture,这是它要组合的CompletableFuture;第二个参数是一个函数式接口BiFunction,用来返回组合的结果。

示例代码1:

CompletableFuture<String> c1 = CompletableFuture.supplyAsync(()->"秦军正在攻打赵国上党",
        executor).thenCompose(t -> CompletableFuture.supplyAsync(()->t + "\n燕军正在自不量力搞背后偷袭",executor));
System.out.println(c1.join());

运行结果:
秦军正在攻打赵国上党
燕军正在自不量力搞背后偷袭

示例代码1supplyAsync()方法开启了一个任务,之后用thenCompose()方法连接了一个任务,他们是按顺序执行的

示例代码2:

CompletableFuture<String> c2 = CompletableFuture.supplyAsync(()->"秦军正在攻打赵国上党",
        executor).thenCombine(CompletableFuture.supplyAsync(()->"\n燕军正在自不量力搞背后偷袭",executor),
        (a,b)-> a+b+"\n燕军被廉颇按在地上摩擦");
System.out.println(c2.join());

运行结果:
秦军正在攻打赵国上党
燕军正在自不量力搞背后偷袭
燕军被廉颇按在地上摩擦

示例代码2supplyAsync()方法开启了一个任务,之后用thenCombine()方法将这个任务与方法中第一个参数传递的任务合并,并将这两个任务的结果合并。

CompletableFuture类中还有很多其他好用的方法,他们的用法大同小异。具体可以查看JDK8以上版本的文档。

Guess you like

Origin juejin.im/post/7233961132540969021