多线程进阶篇

目录

线程池

创建线程池的目的

线程池的概念

JDK中线程池的使用

线程池的核心父类接口:ExecutorService接口

​编辑 Executors=>线程池的工具类

固定大小的线程池

数量动态变化的缓存池

单线程池

定时线程池

 线程池的接口和类

 ThreadPoolExector子类的核心构造方法参数

 线程池工作流程

 常见锁的策略

1.乐观锁和悲观锁

乐观锁

 悲观锁:

2.读写锁

读写锁的适用条件

读写锁的特性

3.重量级锁和非重量级锁

重量级锁

轻量级锁

4.公平锁和非公平锁

公平锁

非公平锁

synchronized锁的升级策略


线程池

创建线程池的目的

线程池创建的目的在于:如果我们直接采用像之前的Thread构造方法创建线程,其实在每次创建和销毁线程时都有一定的开支,当线程太多时这个开支还是挺明显的。

“池”:目的就是让某些对象被多次重复利用,减少频繁创建和销毁对象带来的开支问题

所以说,线程池最大的好处就是可以减少每次启动和销毁线程的损耗(提高时间和空间利用率)

线程池的概念

线程池内部创建了若干个线程,这些线程都是Runnable的状态,只需要从系统中取出任务(run),就可以立即开始执行。

我们将线程池比作一个餐厅:

餐厅中的固定员工:线程池中的线程

后面招聘的临时员工:当线程池中的线程不够用时,可产生临时员工。

JDK中线程池的使用

线程池的核心父类接口:ExecutorService接口

执行任务方法:excute

提交任务方法:submit

提交一个任务到线程池(线程的run或者call方法),池中会派遣空闲线程执行任务 

 终止线程方法:shutdown

立即终止所有线程:shutdownNow();  无论线程是否在空闲状态

停止在空闲态的线程,运行线程在结束后停止:shutdown();

多线程进阶篇_第1张图片 
Executors=>线程池的工具类

这个类可以创建JDK内置的四大线程池

加s的基本都是工具类 比如Arrays(数组工具类,copyOf,sort等等)

固定大小的线程池

        //固定大小的线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);

采用.submit()方法执行任务

pool.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
                }
            }
        });

工具类源码:

数量动态变化的缓存池

        //数量动态变化的缓存池
        ExecutorService pool1 = Executors.newCachedThreadPool();

工具类源码:

橘色的参数基本是用不到的,因为最大线程数有四十多亿

单线程池

ps:单线程池的意义在于,省去创建线程和销毁线程的时间,来一个任务执行一个任务,还未执行的任务在工作队列排队执行。

        //只包含一个线程的单线程池
        ExecutorService pool2 = Executors.newSingleThreadExecutor();

工具类源码:

多线程进阶篇_第2张图片

定时线程池

ps:使用的是定时器线程接口,线程池核心接口的子接口

        //定时器线程池
        ScheduledExecutorService pool3 = Executors.newScheduledThreadPool(10);

采用.schedule接收任务,可规定延迟多久执行任务

pool3.schedule(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
                }
            }
        },3, TimeUnit.SECONDS);

工具类源码:

多线程进阶篇_第3张图片

 线程池的接口和类

ExecutorService:线程池的核心接口,提供excute方法,submit方法,shutdown方法

ScheduledExecutorService:定时器接口,线程池核心接口的子接口,追加.schedule方法,可以延迟启动任务

ThreadPoolExector类:实现核心接口的子类,也是线程池接口的最常用实现子类

Executors:线程池的工具类,提供了ThreadPoolExector的对象,内置创建好的四大线程池。

多线程进阶篇_第4张图片

 ThreadPoolExector子类的核心构造方法参数

一图流

多线程进阶篇_第5张图片

 线程池工作流程

此工作流程基于submit提交一个新任务时线程池内部的执行流程

首先判断核心池是否已经拉满,没拉满我们可以再创建一个固定线程(正式工)执行此任务

若已经拉满,我们就去判断工作队列是否已满,没有满的话,我们就在队列中排队等待,

否则,进行线程池的最大数量判断,如果没满的话就创建一个空闲线程来执行此任务,

否则,若线程池已经达到了最大数量,说明此时线程池已经达到最大负荷了,我们就需要执行拒绝策略了

开始执行

 若已经达到核心池最大数量

多线程进阶篇_第6张图片

 若队列已经满了

多线程进阶篇_第7张图片

 当线程数量达到上线,执行拒绝策略

多线程进阶篇_第8张图片

 常见锁的策略

1.乐观锁和悲观锁

synchronized最开始就是乐观锁,当竞争激烈就变成悲观锁。

乐观锁

每次读写数据都认为不会发生冲突,不会阻塞,一般来说只有在数据更新时才会检查是否发生冲突,若没有冲突直接更新,有冲突再去解决冲突。

当线程冲突不严重时,可以采用乐观锁策略来避免多次的加锁解锁操作

eg:

乐观锁一般通过版本号机制来实现

多线程进阶篇_第9张图片

 比如这两个线程,如果线程1先结束并将主内存的版本从version1变成version2时,线程2写回主内存检测到主内存的版本和自己的工作内存版本对不上,就会直接报错,不写回。

多线程进阶篇_第10张图片

 当下次再进行线程操作时,就会读取新的版本号,这个时候重新进行操作,尝试写回。

多线程进阶篇_第11张图片

 悲观锁:

每次进行读写数据都会冲突,都需要尝试加锁操作,保证同一时间只有一个线程在读写数据。

当线程冲突严重时,就需要加锁,来避免线程频繁访问共享数据失败带来的CPU空转问题

2.读写锁

读写锁的适用条件

特别适用于线程基本都在读数据,很少有些数据的情况。

多数据在访问数据时,并发读数据不会冲突,只有在写数据时才有可能发生冲突,所有就有了读写锁的概念,JDK内置的读写锁是ReentrantReadWriteLock,用这个可实现读写操作。

读写锁的特性

1.多个线程并发读数据时,都可以访问到读锁,并发执行不互斥。

2.多个线程写数据时,两个线程互斥,只有一个能访问到写锁,其他线程阻塞。

3.一个线程读,另一个线程写,也会互斥,当写的过程结束读线程才能继续执行。

3.重量级锁和非重量级锁

重量级锁

需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(os,用户态切换到内核态,开销非常大)

eg:

比如去银行办理业务

用户态:在窗口外自己处理的业务

内核态:窗口内部,需要工作人员协助

这样来回切换是非常耗时的

轻量级锁

尽量在用户态执行操作,线程不阻塞,不会进行状态切换。

轻量级锁的常用实现是采用自旋锁:

之前的方式是,获取锁失败就进入Blocked状态,线程阻塞等待锁释放,然后由CPU唤醒(这个时间一般都比较长,由用户态切换到内核态)。

自旋锁:

就是一个循环的概念

while(获取lock==false)就进行死循环,但不会让出CPU,线程也不会阻塞,不会切换状态,线程就在CPU上空跑,直到锁被释放,就可以很快速地获取到锁。

4.公平锁和非公平锁

公平锁

获取锁失败的线程进入等待队列,当锁被释放,在队列中等待时间最长的线程首先获取到锁。

非公平锁

阻塞队列的各个线程获取到锁的几率相等,不分先后。

synchronized锁的升级策略

偏向锁 ->轻量级锁 ->重量级锁

你可能感兴趣的:(多线程,java,开发语言)