JAVA多线程(三) 线程池和锁的深度化

 github演示代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-service/src/main/java/com/kawa/thread
java

1.线程池git

 1.1 线程池是什么程序员

Java中的线程池是运用场景最多的并发框架,几乎全部须要异步或并发执行任务的程序均可以使用线程池。在开发过程当中,合理地使用线程池可以带来3个好处。
第一:下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
第二:提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。
第三:提升线程的可管理性。线程是稀缺资源,若是无限制地建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一分配、调优和监控。

1.2 线程池做用github

线程池是为忽然大量爆发的线程设计的,经过有限的几个固定线程为大量的操做服务,减小了建立和销毁线程所需的时间,从而提升效率。
若是一个线程的时间很是长,就不必用线程池了(不是不能做长时间操做,而是不宜),何况咱们还不能控制线程池中线程的开始、挂起、和停止。

1.3 线程池的分类算法

JDK1.5以后加入了java.util.concurrent包,java.util.concurrent包的加入给予开发人员开发并发程序以及解决并发问题很大的帮助。这篇文章主要介绍下并发包下的Executor接口,Executor接口虽然做为一个很是旧的接口(JDK1.5 2004年发布),可是不少程序员对于其中的一些原理仍是不熟悉,所以写这篇文章来介绍下Executor接口,同时巩固下本身的知识。spring

Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不一样而已。经过传入不一样的参数,就能够构造出适用于不一样应用场景下的线程池,那么它的底层原理是怎样实现的呢,这篇就来介绍下ThreadPoolExecutor线程池的运行过程。数据库

corePoolSize: 核心池的大小。 当有任务来以后,就会建立一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能建立多少个线程;
keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。
unit: 参数keepAliveTime的时间单位,有7种取值缓存

Java经过Executors(jdk1.5并发包)提供四种线程池,分别为:
newCachedThreadPool建立一个可缓存线程池,若是线程池长度超过处理须要,可灵活回收空闲线程,若无可回收,则新建线程。
案例演示:

newFixedThreadPool 建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 建立一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行

 

演示代码: https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-service/src/main/java/com/kawa/thread/threadpool
并发

1.4 线程池的原理框架

提交一个任务到线程池中,线程池的处理流程以下:
1、判断线程池里的核心线程是否都在执行任务,若是不是(核心线程空闲或者还有核心线程没有被建立)则建立一个新的工做线程来执行任务。
若是核心线程都在执行任务,则进入下个流程。
2、线程池判断工做队列是否已满,若是工做队列没有满,则将新提交的任务存储在这个工做队列里。若是工做队列满了,则进入下个流程。 三、判断线程池里的线程是否都处于工做状态,若是没有,则建立一个新的工做线程来执行任务。若是已经满了,则交给饱和策略来处理这个任务。

 

 

1.5 线程池的合理配置

要想合理的配置线程池,就必须首先分析任务特性,能够从如下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
任务的优先级:高,中和低。
任务的执行时间:长,中和短。
任务的依赖性:是否依赖其余系统资源,如数据库链接。
任务性质不一样的任务能够用不一样规模的线程池分开处理。CPU密集型任务配置尽量少的线程数量,如配置Ncpu
+1个线程的线程池。
IO密集型任务则因为须要等待IO操做,线程并非一直在执行任务,则配置尽量多的线程,如2*Ncpu。
混合型的任务,若是能够拆分,则将其拆分红一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,
那么分解后执行的吞吐率要高于串行执行的吞吐率,若是这两个任务执行时间相差太大,则不必进行分解。
咱们能够经过Runtime.getRuntime().availableProcessors()方法得到当前设备的CPU个数。 优先级不一样的任务可使用优先级队列PriorityBlockingQueue来处理。它可让优先级高的任务先获得执行,须要注意的是若是一直有优先级高的任务提交到队列里,
那么优先级低的任务可能永远不能执行。 执行时间不一样的任务能够交给不一样规模的线程池来处理,或者也可使用优先级队列,让执行时间短的任务先执行。 依赖数据库链接池的任务,由于线程提交SQL后须要等待数据库返回结果,若是等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。 CPU密集型时,任务能够少配置线程数,大概和机器的cpu核数至关,这样可使得每一个线程都在执行任务 IO密集型时,大部分线程都阻塞,故须要多配置线程数,
2*cpu核数 操做系统之名称解释: 某些进程花费了绝大多数时间在计算上,而其余则在等待I/O上花费了大可能是时间, 前者称为计算密集型(CPU密集型)computer-bound,后者称为I/O密集型,I/O-bound。

 

2.锁的深度化

2.1 悲观锁,乐观锁

悲观锁:悲观锁悲观的认为每一次操做都会形成更新丢失问题,在每次查询时加上排他锁。
每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。 Select
* from xxx for update; 乐观锁:乐观锁会乐观的认为每次查询都不会形成更新丢失,利用版本字段控制

2.2 重入锁

锁做为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为咱们开发提供了便利。
重入锁,也叫作递归锁,指的是同一线程 外层函数得到锁以后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

 演示代码:https://github.com/showkawa/springBoot_2017/blob/master/spb-demo/spb-brian-query-service/src/main/java/com/kawa/thread/lock/ReentrantLockThread.java

2.3 读写锁

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操做,且写操做没有读操做那么频繁。
在没有写操做的时候,两个线程同时读一个资源没有任何问题,因此应该容许多个线程能在同时读取共享资源。
可是若是有一个线程想去写这些共享资源,就不该该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。
这就须要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。

 演示代码:https://github.com/showkawa/springBoot_2017/blob/master/spb-demo/spb-brian-query-service/src/main/java/com/kawa/thread/lock/WriteReadLockThread.java

2.4 CAS无锁机制

 

(1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但因为其非阻塞性,它对死锁问题天生免疫,而且,线程间的相互影响也远远比基于锁的方式要小。
更为重要的是,使用无锁的方式彻底没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以,它要比基于锁的方式拥有更优越的性能。 (
2)无锁的好处: 第一,在高并发的状况下,它比有锁的程序拥有更好的性能; 第二,它天生就是死锁免疫的。 就凭借这两个优点,就值得咱们冒险尝试使用无锁的并发。 (3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,若是V值和E值不一样,
则说明已经有其余线程作了更新,则当前线程什么都不作。最后,CAS返回当前V的真实值。 (
4)CAS操做是抱着乐观的态度进行的,它老是认为本身能够成功完成操做。当多个线程同时使用CAS操做一个变量时,只有一个会胜出,并成功更新,其他均会失败。
失败的线程不会被挂起,仅是被告知失败,而且容许再次尝试,固然也容许失败的线程放弃操做。基于这样的原理,CAS操做即便没有锁,也能够发现其余线程对当前线程的干扰,
并进行恰当的处理。

 

2.5 自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其余线程改变时 才能进入临界区。

 

public class Test implements Runnable {
    static int sum;
    private SpinLock lock;

    public Test(SpinLock lock) {
        this.lock = lock;
    }

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        for (int i = 0; i < 100; i++) {
            Test test = new Test(lock);
            Thread t = new Thread(test);
            t.start();
        }

        Thread.currentThread().sleep(1000);
        System.out.println(sum);
    }

    @Override
    public void run() {
        this.lock.lock();

           this.lock.lock();

           sum++;

           this.lock.unlock();

           this.lock.unlock();

     }

}

当一个线程 调用这个不可重入的自旋锁去加锁的时候没问题,当再次调用lock()的时候,由于自旋锁的持有引用已经不为空了,该线程对象会误认为是别人的线程持有了自旋锁

使用了CAS原子操做,lock函数将owner设置为当前线程,而且预测原来的值为空。unlock函数将owner设置为null,而且预测值为当前线程。

当有第二个线程调用lock操做时因为owner值不为空,致使循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

因为自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,因此响应速度更快。但当线程数不停增长时,性能降低明显,由于每一个线程都须要执行,占用CPU时间。若是线程竞争不激烈,而且保持锁的时间段。适合使用自旋锁。

相关文章
相关标签/搜索