Java并发编程

概念:

1、上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下个任务,但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务的保存到再加载的过程就是一次上下文切换

2、线程和进程

进程是程序运行资源分配的最小单位

线程时CPU调度的最小单位,必须依赖于进程而存在

线程无处不在,任何一个程序都必须创建线程,特别是java项目,不管任何程序都必须启动一个main函数的主线程

3、CPU核心数和线程数的关系

多核心:也指单芯片多处理器(简称CMP)

多线程:简称SMT。让用一个处理器上的多个线程同步执行并共享处理器的执行资源

4、并行与并发

并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果开启两个线程执行,就是在几乎不可能察觉的速度下不断的切换这两个任务,已达到“同时执行的效果”

并行:指应用能够同时执行不同的任务

两者的区别:一个是交替执行,一个是同时执行

5、高并发编程的意义、好处和注意事项

好处:1)充分利用CPU的资源

2)加快响应用户的时间

3)可以使代码模块化、异步化、简单化

注意事项:

1)线程之间的安全性

2)线程之间的死锁问题

3)线程太多了会将服务器资源耗尽形成死机宕机

6、线程的调度

协同式线程调度

线程的执行时间由线程自己控制,线程执行自己的工作主动通知系统切换到其他线程。

好处:实现简单,没有同步的问题

坏处:如果一个线程出了问题,则程序就一直阻塞

抢占式线程调度

每个线程执行的时间以及是否切换都由系统控制。这种情况下,线程的执行时间不可控,同样不会由于一个线程的问题引起整个进程的阻塞

在Java中,Thread.yield()可以让出CPU的执行时间,但是对于获取,线程本身是没有办法的。对于获取CPU执行时间,线程唯一可以使用的手段是设置线程的优先级。

Java中线程的优先级是通过映射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,操作系统的优先级有时候并不能和Java中的一一对应,所以Java的优先级并不是非常靠谱

7、线程的优先级

在Java中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int) 方法来修改优先级,默认优先级是5,优先级高的线程分配的时间片的数量要多于优先级低的线程

一、线程的五大状态

1、创建状态(new)

当用new操作符创建一个线程时,例如new Thread(),线程还没有开始运行,此时线程处于新建状态。当一个线程处于新生状态时,程序还没有运行线程中的代码

2、就绪状态 (Runnable)

一个新建的线程并不自动开始运行,要执行线程,也就是必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态

注:处于就绪状态的线程并不一定会马上就执行run方法,还需要等待CPU的调度

3、运行状态 (Running)

当线程获得CPU时间后,它才进入运行状态,真正的开始执行run()方法

4、阻塞状态 (Blocked)

线程的执行过程中由于某些原因进入阻塞状态

例如:调用sleep(休眠)方法、尝试去获得一个锁等

所谓的阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态

5、死亡状态

run方法执行完 或者 执行过程中遇到异常导致线程猝死

如何确认当前线程是否存活:需要使用isAlive()方法。如果是可运行或者被阻塞,这个方法返回true;如果线程仍旧是创建状态且是不可运行的,或者线程死亡了,则返回false

二、并发编程的三要素

1、原子性

原子,即一个不可再被分割的颗粒。在java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败

2、有序性

程序执行的顺序是按照代码的先后顺序执行的。(处理器可能会对指令进行重新排序)

3、可见性

当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值

三、悲观锁与乐观锁

1、悲观锁

每次操作都会加锁,会造成线程阻塞;传统的关系型数据库就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

2、乐观锁

每次操作不加锁而是假设没有冲突而去完成某个操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞

实现方式:

版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会+1。当线程A要更新数据值时,在读取数据的同时也会读取到version的值,在提交更新时,若刚才读取到的version的值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

CAS算法:即compare and swap(比较和交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-bloking Synchronization)。CAS算法涉及到三个操作数(需要读写的内存值V,进行比较的值A,拟写入的新值B)

四、线程之间的协作和共享

1、wait/notify/notifyAll

wait(long): 超时等待一段时间,参数是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回

wait(long,int):对于超时时间可以更细粒度的控制,可以达到纳秒

notify:通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态

notifyAll:通知所有等待在该对象上的线程

2、sleep/yield/join

slepp(long millis) 单位毫秒

暂停当前线程,把CPU时间片让出给其他线程,减缓当前线程的执行。

yield()

线程让步,让出自己CPU的时间片,让自己或其他线程运行。注:是让自己和其他线程运行,并不是单纯的让给其他线程。让线程的状态从“运行状态"转换为”就绪状态“

join方法

是指把指定线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。例如在线程B中调用了线程A的join()方法,知道线程A执行完毕后,才会进行执行B线程

3、调用yield()、sleep()、wait()、notify()等方法对锁有什么影响

yield()、sleep()被调用后,都不会释放当前线程所持有的锁

调用wait()方法后,会释放当前线程持有的锁,而且当前线程被唤醒后,会重新去竞争锁,锁竞争到之后才会执行wait后面的代码

调用nitify()系列方法后,对锁无影响,线程只有在synchronized同步代码执行完后才会自然释放锁,所以notify()系列方法一般都是在synchronized同步代码的最后一行

4、synchronized 内置锁/同步锁

5、volatile关键字

6、ThreadLoca线程副本

  • set() 设置当前线程的线程局部变量的值

  • get() 获取当前线程锁对应的线程的局部变量

  • remove() 删除当前线程局部变量的值,目的是减少内存占用。注:当线程结束后,对应线程的局部变量将自动被垃圾回收, 所以显示调用该方法清除线程的局部变量并不是必须操作,但他可以加速内存回收

  • protected Object initialValue() 返回初始化的值

五、线程的并发工具类

1、Fork-Join

原理:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可拆为止),再将一个个小任务运算的结果进行join汇总。分而治之的原理

工作密取:即当前线程的Task已经被全部执行完毕,则自动到其他线程的Task池中取出Task继续执行

ForkJoinPool中维护着多个线程(一般为CPU核数)在不断地执行Task,每个线程除了执行自己职务内的Task之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高CPU利用率。

Fork-Join 使用的标准范式

我们要使用ForkJoin框架,必须先创建一个ForkJoin任务。它提供在任务中执行fork和join的操作机制,通常我们不直接继承ForkJoinTask类,只需要直接继承其子类

RecursiveAction,用于没有返回结果的任务

RecursiveTask,用于有返回值的任务

task要通过ForkJoinPool来执行,使用submit和invoke提交,两者的区别是:invoke是同步执行,调用后需要等待任务执行完成,才能执行后面的代码;submit是异步执行。

join()和get()方法,当任务完成时返回计算结果。

2、CountDownLatch

  • CountDownLatch也叫闭锁,在jdk1.5被引入,允许一个或多个线程等待其他线程完成后再执行

  • CountDownLatch内部会维护一个初始值为线程数量的计数器,主线程执行await()方法,如果计数器大于0,则阻塞等待。

  • 当一个线程完成任务后,计数器值减1(CountDownLatch.countDown()方法),当计数器为0时,表示所有的线程已经完成任务,等待的主线程被唤醒继续执行

  • CountDownLatch底层是基于AQS{@link java.util.concurrent.locks.AbstractQueuedSynchronizer}实现的

  • 应用场景: 测试一个单例类的最大并行数

主要方法: void countDown(); //调用此方法计数器减1

void await(); //调用此方法会一直阻塞当前线程,直到计数器值为0,除非线程中断

boolean await(long timeout, TimeUnit unit);//调用此方法会一直阻塞线程,直到计数器的值为0,除非线程中断或者计数器超时,返回false代表计数器超时

long getCount(); //得到当前计数器值

3、CyclicBarrier(循环屏障)

  • CyclicBarrier(循环屏障)是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的是该barrier在释放等待线程后可以重用

  • CyclicBarrier支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后(在释放所有线程之前),该命令只在每个屏障点运行一次

主要方法:

// parties表示屏障拦截的线程数量,当屏障撤销时,先执行barrierAction,然后再释放所有线程; barrierAction默认为null

public CyclicBarrier(int parties, Runnable barrierAction)

//int await() 返回当前线程的索引,0表示当前线程是最后一个到达的线程

//调用该方法,当前线程如果不是最后一个线程,将会被阻塞。解除阻塞的情况:

//1)最后一个线程调用await();(达到屏障拦截的线程数)

//2)当前线程呗被断

//3)其他正在该CyclicBarrier上等待的线程被中断/超时

//4)其他某个线程调用该CyclicBarrier的reset()方法

int await();

//将屏障重置为初始状态。如果所有参与者都在屏障处等待,则他们将返回,同时抛出一个BrokenBarrierException

void reset();

CountDownLatch 与 CyclicBarrier的区别

1)CountDownLatch:一个(或多个)线程,等待另外N个线程完成某事之后才能执行;CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有线程都必须等待

2)CountDownLatch:一次性的;CyclicBarrier:可以重复使用

3)CountDownLatch:基于AQS;CyclicBarrier:基于锁和Condition。本质上都是基于volatile和CAS实现的

4、Semaphore 信号量

  • 信号量是用来控制同时访问共享资源的线程数

  • 使用场景:可以用于做流量控制,特别是公共资源有限的场景,比如数据库连接

  • 注意事项:Semaphore中的许可证数量是可以被突破的,当实例化的Semaphore,不去操作acquire(),一直调用release()的话,许可证的数量是回突破实例化的数量的

private static Semaphore semaphore = new Semaphore(3);
public static void test1(){
    System.out.println(semaphore.availablePermits());//可用许可证数量为3
    semaphore.release();
    int queueLength = semaphore.availablePermits();
    System.out.println(queueLength);//可用许可证数量为4
}
  • 常用方法:

getQueueLength();返回当前正在等待获取许可证的线程数
availablePermits();返回信号量中当前可以用的许可证数
acquire();方法是获取一个许可证
release();归还许可证
boolean hasQueuedThreads();是否有线程正在等待获取许可证

5、Exchange(交换器)

/**
 * @Author Feng
 * Exchanger(交换器) 线程间协作的工具类。用于线程间的数据交换。
 * 它提供一个同步点,在这个同步点两个线程可以交换数据,如果第一个线程执行exchange()方法,它会一直等待到第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据。
 **/
public class ExchangerDemo {
    private static Exchanger<List<String>> exchanger = new Exchanger();
    public static void main(String[] args) {
        new Thread(() ->{
            List<String> setA = new ArrayList<>();
            setA.add("A");
            try {
                List<String> exchange = exchanger.exchange(setA);
                System.out.println("A线程" + exchange.get(0));//打印结果"A线程B"
            } catch (InterruptedException e) {
            }
        }).start();
        new Thread(() ->{
            List<String> setB = new ArrayList<>();
            setB.add("B");
            try {
                List<String> exchange = exchanger.exchange(setB);
                System.out.println("B线程" + exchange.get(0));//打印结果"B线程A"
            } catch (InterruptedException e) {
            }
        }).start();
    }
}

6、Callable、Future、FutureTask

/**
 * Callable: 该接口只声明了一个call()方法,返回值是传入进来的泛型
 * Runnable: 该接口只声明了一个run()方法,无返回值
 * Future: 该接口是对具体的Runnable和Callable任务的执行结果进行取消、查询是否完成、获取结果。
 *      常用方法: cancel(boolean mayInterruptIfRunning) 用来取消任务,如果取消任务成功返回true,失败则返回false。
 *              isCancelled() 表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true
 *              idDone() 表示任务是否已经完成,若任务完成,则返回true
 *              get() 用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
 *              get(long timeout, TimeUnit unit) 用来获取执行结果,如果在指定时间内,还没获取到结果,就会直接返回null
 * FutureTask: 实现了RunnableFuture接口,RunnableFuture继承了Runnable和Future接口。是Future的唯一实现类
 */
public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask<Integer> futureTask = new FutureTask<>(() ->{
        int sum = 0;
        System.out.println("Callable子线程开始计算!");
        for(int i=0 ;i<5000;i++){
            if(Thread.currentThread().isInterrupted()) {
                System.out.println("Callable子线程计算任务中断!");
                return null;
            }
            sum=sum+1;
            System.out.println("sum="+sum);
        }
        System.out.println("Callable子线程计算结束!结果为: "+sum);
        return sum;
    });
    new Thread(futureTask).start();
    Integer sum = futureTask.get();
    System.out.println(sum);
}

六、原子操作CAS

1、什么是CAS

即compare and swap(比较和交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-bloking Synchronization)。CAS算法涉及到三个操作数(需要读写的内存值V,进行比较的值A,拟写入的新值B)

2、为什么会有CAS

3、实现原理

4、存在的问题

  • ABA问题:一个值原来是A,变成了B,又变成了A,那么使用CAS检查的时候会发现他的值没有发生变化,但实际上是发生了变化。解决思路:版本号机制。在变量前加上版本号

  • 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

  • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以通过使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有就是可以把多个共享变量合并成一个共享变量来操作。从Java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

5、jdk中相关原子操作类的使用

参考java.util.concurrent.atomic包

七、显示锁和AbstractQueuedSynchronizer(AQS)

1、显示锁

有了synchronized为什么还要Loak?:Java程序是靠synchronized关键字实现锁功能的,使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放

lock特性:

  • 尝试非阻塞的获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁

  • 能被中断地获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁汇被释放

  • 超时获取锁: 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

lock的标准用法:在finally块中释放锁,目的是保证在获取到锁之后,最终能够释放锁。不要将获取锁的过程写在try块中,因为如果在获取锁 (自定义锁的实现)时发生异常,异常抛出的同时,也会导致锁无故释放

lock.lock();
try {
    count++;
} finally {
    lock.unlock();
}

lock中常用的API

  • void lock();获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回

  • void lockInterruptibly();可中断的获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程

  • boolean tryLock();尝试非阻塞的获取锁,调用该方法后立刻返回,如果能获取到锁则返回true,否则返回false

  • boolean tryLock(long time, TimeUnit unit);超时获取锁,当前线程在以下3中情况下会返回:1)当前线程在超时时间内获得了锁;2)当前线程在超时时间内被中断;3)超时时间结束,返回false

  • void unlock();释放锁

ReentrantLock 实现了Lock接口 (排他锁)

  • 锁的可重入性:是指任意线程在获取到锁之后能够再次获取该锁而不会被阻塞,该特性需要解决两个问题:1)线程再次获取锁2)锁的最终释放:线程重复获取n次,随后需要在第n次释放后,其他线程才能获取到该锁

  • 公平锁和非公平锁:如果在时间上,先对锁进行获取请求的一定先被满足,那么这个锁是公平的,反之是不公平的。公平锁的获取,就是等待的时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平锁的机制往往没有非公平的效率高

ReentrantReadWriteLock (读写锁) 实现ReadWriteLock接口

  • ReentrantReadWriteLock是Lock的另一种实现方式,ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据的访问都是读操作远多于写操作,ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量

  • ReentrantReadWriteLock支持的功能

1)支持公平和非公平的获取锁的方式

2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁后 既可以再次获取写锁又可以获取读锁

3)还允许写入降级锁为读取锁,实现方式是:先获取写锁,然后获取读锁,最后释放写锁。但是从读锁升级到写入锁是不允许的

4)读锁和写锁都支持获取锁期间的中断

5)Condition支持。仅写锁提供了一个Condition实现;读锁不支持Condition,readLock().newCondition()会抛出异常UnsupportedOperationException

Condition接口

public class ConditionDemo {	
    private static Lock lock = new ReentrantLock();        
    private static Condition condition =             lock.newCondition();                        
    public static void await() throws                 InterruptedException {                                      
        lock.lock();                        
        System.out.println("await开始等待。。。。");        
        try {                            
            //当前线程进入等待状态,直到被通知,该方法对中断不敏感    
            //condition.awaitUninterruptibly();            
            //当前线程进入等待状态,直到被通知、中断或者到某个时        间。如果没有到指定时间就被通知,方法返回true,否则表示到了指定时间,        
方法返回false                            
            //condition.awaitUntil(new Date());            
            //当前线程进入等待状态,直到被通知、中断或者超时。返        
回值表示剩余时间,如果在nanosTimeout纳秒之前被唤醒,那么返回值就是    nanosTimeout-实际耗时时间。如果返回值是0或者负数,表示超时        
            //long nanos = condition.awaitNanos(1000);        
            //当前线程进入等待状态,直到被通知(signal)或中断,当    
前线程将进入运行状态且从await()方法返回                
            condition.await();                    
            System.out.println("await 通知来了");        
        } finally {                        
            lock.unlock();                    
        }                            
    }                                
    public static void signal() throws                 InterruptedException {                        
        lock.lock();                        
        System.out.println("signal 开始通知");            
        try {                            
            TimeUnit.SECONDS.sleep(2);                
            //唤醒所有等待在Condition上的线程,能够从等待方法返回    的线程必须获得与Condition相关联的锁                    
//            condition.signalAll();                
            //唤醒一个等待在Condition上的线程,该线程从等待方法返    回前必须获得与Condition相关联的锁                    
            condition.signal();                    
            System.out.println("signal 通知发送");        
        } finally {                        
            lock.unlock();                    
        }                            
    }                                
    public static void main(String[] args) {            
        new Thread(() -> {                    
            try {                        
                ConditionDemo.await();                
            } catch (InterruptedException e) {            
            }                            
        }).start();                        
        new Thread(() -> {                    
            try {                        
                ConditionDemo.signal();                
            } catch (InterruptedException e) {            
                e.printStackTrace();                
            }                            
        }).start();                        
    }                                
}                                				

八、并发容器

1、ConcurrentHashMap:并发版HashMap

2、CopyOnWriteArrayList:并发版ArrayList

3、CopyOnWriteArraySet:并发Set

4、ConcurrentLinkedQueue:并发队列(基于链表)

5、ConcurrentLinkedDeque:并发队列(基于双向链表)

6、ConcurrentSkipListMap:基于跳表的并发Map

7、ConcurrentSkipListSet:基于跳表的并发Set

8、ArrayBlockingQueue:阻塞队列(基于数组)

9、LinkedBlockingQueue:阻塞队列(基于链表)

10、LinkedBlockingDeque:阻塞队列(基于双向链表)

11、PriorityBlockingQueue:线程安全的优先队列

12、SynchronousQueue:读写成对的队列

13、LinkedTransferQueue:基于链表的数据交换队列

14、DelayQueue:延时队列

九、线程池

1、为什么使用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

2、ThreadPoolExecutor 的类关系

Executor是一个接口,他是Executor框架的基础,它将任务的提交与任务的执行分离开来。

ExecutorService接口继承了Executor,在其基础上做了一些shutdown()、submit()的扩展,可是说是真正的线程池接口

AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;

ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。

ScheduledExecutorService接口继承了ExecutorService接口,提供了带“周期执行”功能。

ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。

3、线程池创建各个参数的含义

corePoolSize:核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。

keepAliveTime:线程空闲时的存活时间,即当前线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用;

TimeUnit:keepAliveTime的时间单位

workQueue:必须是BlockingQueue阻塞队列。当前线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进阻塞等待。用于保存等待执行的任务的阻塞队列,一般来说,我们尽量使用有界队列,使用无界队列带来的影响:

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize

2)由于1,使用无界队列时maximumPoolSize将是一个无效参数

3)由于1和2,keepAliveTime将是一个无效参数

4)使用无界队列可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要控制队列的大小在一个合适的范围内。

threadFactory:创建线程工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置线程为守护线程;Executors静态工厂中默认的threadFactory,线程的命名规则是"pool-数字-thread-数字"。

RejectedExecutionHandle:线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采用一种策略处理该任务,线程池提供了四种策略:

1)AbortPolicy:直接抛出异常,默认策略

2)CallerRunsPolicy:用调用者所在地线程来执行任务

3)DiscardOldestPolicy:丢弃阻塞队列中靠前的任务,并执行当前任务

4)DiscardPolicy:直接丢弃任务

5)可以根据应用场景实现RejectedExecutionHandle接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务

4、扩展线程池

每个任务执行前后都会调用beforeExecute和afterExecute方法。相当于执行了一个切面。而在调用shutdown方法后会调用terminated方法。

5、线程池的工作机制

  1. 如果当前运行的线程数少于核心线程数(corePoolSize),则会创建新的线程来执行任务(注意:执行这一步骤需要获取全局锁)

  1. 如果运行的线程数等于或多于核心线程数(corePoolSize),则将任务加入阻塞队列(BlockingQueue)

  1. 如果阻塞队列(BlockingQueue)已满,则创建新的线程来处理任务。

  1. 如果当前运行线程超出最大线程数(maximumPoolSize),任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

6、提交任务的两种方式

  • execute():用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

  • submit():用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

7、关闭线程池

shutdown()或shutdownNow();它们两个的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow()首先将线程的状态设置成stop,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

当调用两个其中一个方法,isShutdown()方法就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这时调用isTerminaed()方法会返回ture.。通常调用shutdown()方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow()方法。

十、并发安全

1、什么是线程安全

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

2、线程封闭

实现好的并发


注:有些内容整理不全!!其中的图不记得借自哪里了!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值