java 并发编程

目录

 

1 并发编程模型

1.1 jdk并发模型

1.1.1 基本概念

1.1.2 execute与submit

1.1.3 线程池原理

1.1.4 线程池的关闭

1.2 Thread详解

1.2.1 线程状态

1.2.2 线程之间的协调

2 Java并发与JMM模型

2.1 JMM模型

2.2 并发问题的三个特性

2.3 解决办法

2.3.1 解决方法

2.3.2 volatile和synchronized的比较

2.3.3 synchronized与lock的比较

2.3.4 wait与sleep

2.3.5 并发与锁

4 并发编程的应用

4.1 Guava并发编程

4.2 Presto中的并发编程

9 参考资料


1 并发编程模型

1.1 jdk并发模型

 

1.1.1 基本概念

RunableFuture体系,代表执行逻辑及执行结果的获取。

Executor体系代表这对任务生命周期的管理。

AbstractExecutorService 抽象类对接口的部分实现。

ThreadPoolExecutor表示线程池(线程全生命管理者)。

AbstractExecutorService实现了submit方法,这里用的是模板方法,submit方法调用了execute方法,execute方法交由各种子类实现。

ThreadPoolExecutor的execute方法就是将任务添加到队列,然后视情况执行。

1.1.2 execute与submit

首先明确一下:

Runnable:run 表示一个任务的具体内容。无返回内容

Callable: call Object 在runnable的基础上,有返回内容

 

另外AbstractExecutorService的submit也有三种方法,

A: submit(Runable runable);

B: submit(Runable runable, T t)

C: submit(Callable<T> t)

因为runable方法本身没有返回值,所以A 返回的是Future<Void> 是没有返回值的。

如果想要submit runable有返回值,则需要给一个T,在new Future的时候给futrue让异步任务完成时需要的值赋值给t,然后返回。从而有了B

对于C,因为submit callable本身有T,这个t就作为Future的泛型,返回的是带T的Future。

1.1.3 线程池原理

线程池的原理可以从以下几个方面来理解:

 

注意慎用无界队列

【TODO】

1.1.4 线程池的关闭

A pool that is no longer referenced in a program and has no remaining threads will be shutdown automatically.
如果程序中不再持有线程池的引用,并且线程池中没有线程时,线程池将会自动关闭。

两者是且的关系。

线程池不关闭主进程也不会退出。

https://www.jianshu.com/p/bdf06e2c1541

https://blog.csdn.net/moonpure/article/details/80412284

 

shutdown与shutdownNow方法的比较

shutdown关闭线程池

方法定义:public void shutdown()

(1)线程池的状态变成SHUTDOWN状态,此时不能再往线程池中添加新的任务,否则会抛出RejectedExecutionException异常。

(2)线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。 

注意这个函数不会等待提交的任务执行完成,要想等待全部任务完成,可以调用:

public boolean awaitTermination(longtimeout, TimeUnit unit)

shutdownNow关闭线程池并中断任务

方法定义:public List<Runnable> shutdownNow()

(1)线程池的状态立刻变成STOP状态,此时不能再往线程池中添加新的任务。

(2)终止等待执行的线程,并返回它们的列表;

(3)试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

1.2 Thread详解

1.2.1 线程状态

new: start之后的状态

runable: 就绪状态,因为有调度所以有个runable的状态。线程只能由runable状态进入running状态。

running:就绪状态

blocked:线程因为某种原因主动或者被动让出被调度权,处于blocked状态。

dead:完成状态

1.2.2 线程之间的协调

线程的这么些sleep,join和start,yield,notify,nofityAll这么些方法。的理解主要是理解状态之间的转换以及转换的条件。

Java的Object类包含了三个final方法,允许线程就资源的锁定状态进行通信。这三个方法分别是:wait(),notify(),notifyAll(),今天来了解一下这三个方法。在任何对象上调用这些方法的当前线程应具有对象监视器(锁住了一个对象,就是获得对象相关联的监视器),否则会抛出。

值得注意的是:如上图所示,在A用notify唤醒B之后,如果A在唤醒之后释放了锁,那么B则处于runable状态;但是如果A没有释放锁,那么B还是要处于Block状态。

 

1.2.2.1 interrupt

如何让一个线程终止。

public class ThreadInter {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                while (true) {
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("已经是中断状态,主动退出程序");
                        return;
                    }
                    System.out.println("我还没有中断");
                }
            }
        });

        thread.start();
        System.out.println(thread.isInterrupted());
        System.out.println("即将中断");
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }

Result:

interrupt方法不会去真正意义上的打断一个正在运行的线程,而是修改这个线程的中断状态码(interrupt status)。同时,对于处在sleep()、wait()和join()方法阻塞下的线程,该方法会使线程抛出一个异常。对于可中断通道(NIO中的Channel)的阻塞状态,该方法会关闭通道,抛出异常。对于NIO中选择器的阻塞(Selector.selector),该方法等同于调用选择器的wakeup()方法。

之所以这样是因为Java并不希望你暴力的去杀死一个线程(因为Java线程是基于操作系统的,而操作系统是可以直接杀死一个进程的),而是希望你通过优良的设计实现一种优雅的中断方式。(这个我们以后再说)

小结一下:如何让一个线程终止,以下两种情况下的终止方法。

1 对于处在sleep()、wait()和join()方法阻塞下的线程,该方法会使线程抛出一个异常。然后我们可以捕获这个异常,然后处理,让线程结束。

2 对于正在运行中的进程,可以开发者用代码逻辑轮询检测该进程的中断状态码。根据状态吗自己选择是否结束线程(再次说明中断状态码本身不会中断线程)。

https://www.jianshu.com/p/bdf06e2c1541

Future.cancel也是同样的道理,没有执行的停止执行,已经执行的发出中断,设置中断状态标志(是否响应取决于程序)。

2 Java并发与JMM模型

2.1 JMM模型

Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。简单来说,由于CPU执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在CPU里加了好几层高速缓存。 在Java内存模型里,对上述的优化又进行了一波抽象。JMM规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成CPU上的寄存器或者高速缓存。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。

2.2 并发问题的三个特性

很明显在上述机制之下,对于两个线程分别执行i初始值0, i++的时候就有问题了,一个线程进行了+1操作但是没有回写到主存,这个时候另外一个线程读到了0。这个时候就会出现数据的不一致。

出现了这个问题的原因是Java线程对于变量的修改不具有可见性。

JMM主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的,通过解决这三个问题,可以解除缓存不一致的问题。

可见性:略,上面已经解释

有序性:看下面的例子

int a = 0; 
bool flag = false; 
 
public void write() { 
    a = 2;              //1 
    flag = true;        //2 
} 
 
public void multiply() { 
    if (flag) {         //3 
        int ret = a * a;//4 
    } 
} 假如有两个线程执行上述代码段,线程1先执行write,随后线程2再执行multiply,最后ret的值一定是4吗?结果不一定:


如图所示,write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。

假如有两个线程执行上述代码段,线程1先执行write,随后线程2再执行multiply,最后ret的值一定是4吗?结果不一定:

如图所示,write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。

原子性:简单赋值(i=1)和变量读取是原子的。像复杂的赋值比如 b=x这种都不是原子的。

2.3 解决办法

2.3.1 解决方法

用volatile可以保证两个特性:

  1. 保证了不同线程对该变量操作的内存可见性;

  2. 禁止指令重排序

Java并发包下面有atom相关类可以保证原子性

volatile能实现这两点的原理:

A:修改volatile变量时会强制将修改后的值刷新的主内存中。

B:修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

http://www.cnblogs.com/paddix/p/5428507.html

2.3.2 volatile和synchronized的比较

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

2.3.3 synchronized与lock的比较

https://juejin.im/post/5a43ad786fb9a0450909cb5f#heading-17

synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
 
说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
 
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

2.3.4 wait与sleep

、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;

2.3.5 并发与锁

https://tech.meituan.com/2018/11/15/java-lock.html

死锁的相关内容

 

[volitile关键之内容]https://cloud.tencent.com/info/3a3ebb423db98bd11400f1a2d152a708.html

4 并发编程的应用

4.1 Guava并发编程

Future接口为异步计算取回结果提供了一个存根(stub),然而这样每次调用Future接口的get方法取回计算结果往往是需要面临阻塞的可能性。这样在最坏的情况下,异步计算和同步计算的消耗是一致的。Guava库中因此提供一个非常强大的装饰后的Future接口,使用观察者模式为在异步计算完成之后马上执行addListener指定一个Runnable对象,从实现“完成立即通知”。这里提供一个有效的Tutorial :http://ifeve.com/google-guava-listenablefuture/

 

注意

Guava对原生并发编程模型最大的补充就是提供了回调,因此

1 推荐尽量在代码中使用ListernableFuture来代替JDK的future

 

 

4.2 Presto中的并发编程

Presto任务的的执行都是直接execute的并没有任何返回(Future)。

presto因为数据是逐步产出的,并不是一次性完成的因此不适合回调这种用法。猜想因此presto把任务的执行和查询分开,也因此presto核心代码也没有使用Guava的并发编程。

presto中asyncQueryResults中使用guava的ListenFuture

 

 

 

9 参考资料

[Executor框架]: https://www.jianshu.com/p/84350f530465

[Executor] http://josh-persistence.iteye.com/blog/2145120

[java原生线程池及Guava的补充] http://www.cnblogs.com/guguli/p/5198894.html

[状态机和线程池] https://blog.csdn.net/schumyxp/article/details/2175843

[使用FutureTask的正确姿势] http://www.importnew.com/27305.html

[unkown] https://blog.csdn.net/ns_code/article/details/17465497

[volatile关键字和原子性的探讨] https://blog.csdn.net/mianshui1105/article/details/52263228

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值