java多线程(二)

一、 线程池
转载至线程池

1.为什么要用线程池?
答:降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控

2.线程池体系类图:
在这里插入图片描述
3.ThreadPoolExecutor参数
corePoolSize 核心线程数量,线程池维护线程的最少数量;
maximumPoolSize 线程池维护线程的最大数量;
keepAliveTime 线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁;
unit keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS;
workQueue 线程池所使用的任务缓冲队列;
threadFactory 线程工厂,用于创建线程,一般用默认的即可;
handler 线程池对拒绝任务的处理策略。

当线程池任务处理不过来的时候(什么时候认为处理不过来后面描述),可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:
1.ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程);
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
可以通过实现RejectedExecutionHandler接口自定义处理方式。

4.线程池执行流程
在这里插入图片描述
5.线程池的状态

RUNNING, 运行状态,值也是最小的,刚创建的线程池就是此状态。
SHUTDOWN,停工状态,不再接收新任务,已经接收的会继续执行
STOP,停止状态,不再接收新任务,已经接收正在执行的,也会中断
清空状态,所有任务都停止了,工作的线程也全部结束了
TERMINATED,终止状态,线程池已销毁

在这里插入图片描述

6.线程池的关闭方式有几种,各自的区别是什么。
shutdown():
1、调用之后不允许继续往线程池内继续添加线程;
2、线程池的状态变为SHUTDOWN状态;
3、所有在调用shutdown()方法之前提交到ExecutorSrvice的任务都会执行;
4、一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。

shutdownNow():
1、该方法返回尚未执行的 task 的 List;
2、线程池的状态变为STOP状态;
3、阻止所有正在等待启动的任务, 并且停止当前正在执行的任务。

简单点来说,就是:
shutdown()调用后,不可以再 submit 新的 task,已经 submit 的将继续执行。
shutdownNow()调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list。

7.execute/submit
向线程池提交任务有这2种方式,execute是ExecutorService接口定义的,submit有三种方法重载都在AbstractExecutorService中定义,都是将要执行的任务包装为FutureTask来提交,使用者可以通过FutureTask来拿到任务的执行状态和执行最终的结果,最终调用的都是execute方法,其实对于线程池来说,它并不关心你是哪种方式提交的,因为任务的状态是由FutureTask自己维护的,对线程池透明。

8.addWorker
添加线程的核心方法。

execute方法虽然没有加锁,但是在addWorker方法内部,加锁了,这样可以保证不会创建超过我们预期的线程数,大师在设计的时候,做到了在最小的范围内加锁,尽量减少锁竞争。

core参数,只是用来判断当前线程数是否超量的时候跟corePoolSize还是maxPoolSize比较,Worker本身无核心或者非核心的概念。

9.线程池的线程是如何做到复用的。
线程池中的线程在循环中尝试取任务执行,这一步会被阻塞,如果设置了allowCoreThreadTimeOut为true,则线程池中的所有线程都会在keepAliveTime时间超时后还未取到任务而退出。或者线程池已经STOP,那么所有线程都会被中断,然后退出。

10.线程池是如何做到高效并发的。
看整个线程池的工作流程,有以下几个需要特别关注的并发点.
①: 线程池状态和工作线程数量的变更。这个由一个AtomicInteger变量 ctl来解决原子性问题。
②: 向工作Worker容器workers中添加新的Worker的时候。这个线程池本身已经加锁了。
③: 工作线程Worker从等待队列中取任务的时候。这个由工作队列本身来保证线程安全,比如LinkedBlockingQueue等。

11.Executors
JDK已经给我们提供了很方便的线程池工厂类Executors, 方便我们快速创建线程池。

newFixedThreadPool, 可以看到我们需要传入一个线程数量的参数nThreads,这样线程池的核心线程数和最大线程数都会设成nThreads, 而它的等待队列是一个LinkedBlockingQueue,它的容量限制是Integer.MAX_VALUE, 可以认为是没有边界的。核心线程keepAlive时间0,allowCoreThreadTimeOut默认false。所以这个方法创建的线程池适合能估算出需要多少核心线程数量的场景。

newSingleThreadExecutor, 有且只有一个线程在工作,适合任务顺序执行,缺点但是不能充分利用CPU多核性能。

newCachedThreadPool, 核心线程数0,最大线程数Integer.MAX_VALUE, 线程keepAlive时间60s,用的队列是SynchronousQueue,这种队列本身不会存任务,只做转发,所以newCachedThreadPool适合执行大量的,轻量级任务。

newScheduledThreadPool, 执行周期性任务,类似定时器。

二、 锁
转载至:java锁

1.乐观锁/悲观锁

乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用“数据版本机制”或“CAS操作”来实现。

悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。如数据库的行锁、表锁。

2.独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。

共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

对于Synchronized而言,当然是独享锁。

3.互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock。

读写锁在Java中的具体实现就是ReadWriteLock。

4.可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。

对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。

对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

5.公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

6.分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

7.偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

8.自旋锁/适应性自旋
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

9.MySQL中排它锁和共享锁
排它锁(exclusive lock)
排他锁又叫写锁,如果事务T对A加上排它锁,则其他事务都不能对A加任何类型的锁。获准排它锁的事务既能读数据,又能写数据。

共享锁(share lock)
共享锁又叫读锁,如果事务T对A加上共享锁,则其他事务只能对A再加共享锁,不能加其他锁。共享锁的事务只能读数据,不能写数据。

10.饥饿
是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。比如:
1)它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级的线程无法工作。在自然界中,母鸡喂食雏鸟时,很容易出现这种情况,由于雏鸟很多,食物有限,雏鸟之间的食物竞争可能非常厉害,小雏鸟因为经常抢不到食物,有可能会被饿死。线程的饥饿也非常类似这种情况。
2)另外一种可能是,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。
与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如高优先级的线程已经完成任务,不再疯狂的执行)

11.活锁
是一种非常有趣的情况。不知道大家是不是有遇到过这样一种情况,当你要坐电梯下楼,电梯到了,门开了,这时你正准备出去,但不巧的是,门外一个人挡着你的去路,他想进来。于是你很绅士的靠左走,避让对方,但同时对方也很绅士,但他靠右走希望避让你。结果,你们又撞上了。于是乎,你们都意识到了问题,希望尽快避让对方,你立即向右走,他也立即向左走,结果又撞上了!不过介于人类的只能,我相信这个动作重复 2、 3 次后,你应该可以顺利解决这个问题,因为这个时候,大家都会本能的对视,进行交流,保证这种情况不再发生。
但如果这种情况发生在两个线程间可能就不会那么幸运了,如果线程的智力不够,且都秉承着 “谦让” 的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有的资源而正常执行。这种情况就是活锁。

12.死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

12.1产生死锁的原因
1.竞争资源
2.进程推进顺序不当

12.2产生死锁的条件
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

12.3预防死锁
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

12.4避免死锁:
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。

银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

12.5检测死锁
首先为每个进程和每个资源指定一个唯一的号码;
然后建立资源分配表和进程等待表。

12.6解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

12.7死锁的例子

public class DeadLockDemo {

    public static void main(String[] args) {
        // 线程a
        Thread td1 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo.method1();
            }
        });
        // 线程b
        Thread td2 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo.method2();
            }
        });

        td1.start();
        td2.start();
    }

    public static void method1() {
        synchronized (String.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程a尝试获取integer.class");
            synchronized (Integer.class) {

            }
            System.out.println("线程a已经获取integer.class"); //这条语句没有执行

        }
    }

    public static void method2() {
        synchronized (Integer.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程b尝试获取String.class");
            synchronized (String.class) {

            }
            System.out.println("线程b已经获取String.class"); //这条语句没有执行
        }
    }

}
---------------- 
线程b尝试获取String.class 
线程a尝试获取integer.class 
.... 
...
.. 
. 
无限阻塞下去

13.锁优化、锁粗化、锁消除
锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

三、 ThreadLocal

个人理解:每一线程都维护了一个ThreadLocalMap集合,以ThreadLocal对象引用为key,该线程需要存储的值为value(该线程私有的,类似该线程的局部变量)。而ThreadLoacl内部定义一个静态内部类ThreadLocalMap,实现了set、get、remove等方法。使用ThreadLoacl对象get、set、remove时,先获取当前线程,查看线程的map是否存在,在进行修改、查看。需要注意的是ThreadLocalMap中存储key值是Threadlocal的弱应用,并且set、remove是会删除value值为null的元素,防止内存溢出。

个人认为这篇文章写的很好,详见ThreadLocal自问自答

四、 阻塞队列(BlockingQueue)
转载至:https://www.jianshu.com/p/32665a52eba1

JUC包下的接口,阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

在阻塞队列不可用时,这两个附加操作提供了4种处理方式

在这里插入图片描述
抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”) 异常。当队列为空时,从队列里获取元素时会抛出 NoSuchElementException 异常 。

返回特殊值:插入方法会返回是否成功,成功则返回 true。移除方法,则是从队列里拿出一个元素,如果没有则返回 null

一直阻塞:当阻塞队列满时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里 take 元素,队列也会阻塞消费者线程,直到队列可用。

超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。

1.JDK7 提供了 7 个阻塞队列。分别是

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

2.阻塞队列实现
基于ReentrantLock和Condition实现。
在阻塞队列中A调用put()方法的时候,如果队列已满,则A线程挂起,等待恢复,如果B线程调用take()后,消耗一个队列元素后,会通知put()方法中挂起的A线程,因为这个时候B线程已经消耗一个元素可以在向队列中添加元素。相反如果队列一空 B线程调用tack()方法会阻塞,当A线程调用put()调用后通知tack()方法中的B线程,有元素可以获取。这就是阻塞的实现过程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值