JavaBasic-8-多线程
概述
基本介绍
- 线程由进程创建的,是进程的⼀个实体
- ⼀个进程可以拥有多个线程
- 单线程:同⼀个时刻,只允许执行⼀个线程
- 多线程:同⼀个时刻,可以执行多个线程
- 并发:同⼀个时刻,多个任务交替执行
- 并⾏:同⼀个时刻,多个任务同时执行
进程和线程区别
- 进程:正在运行的软件
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以和其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,称为单线程程序
- 多线程:一个进程如果有多条执行路径,称为多线程程序
线程的生命周期
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行run() ,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
多线程实现方式
继承 Thread 类
- 自定义类继承 Thread 类
- 在自定义类中重写 run() 方法
- 创建自定义类的对象
- 启动线程
1 | public class Test { |
- run()方法用来封装被线程执行的代码,如果直接调用该方法不能实现多线程,而是单纯的方法调用
- start()方法用来启动一个线程,然后JVM调用该线程的run()方法
实现 Runable 接口
自定义类实现 Runnable 接口
在自定义类中重写 run() 方法
创建自定义类对象
创建 Thread 类对象,把自定义类对象作为构造方法的参数
启动线程
1 | public class Test { |
Callable 接口和 FutureTask 类
- 自定义类实现 Callable 接口
- 在自定义类中重写 call() 方法
- 创建自定义类的对象
- 创建 FutureTask 类的对象,把自定义类的对象作为构造方法的参数
- 创建 Thread 类的对象,把 FutureTask 对象作为构造方法的参数
- FutrueTask 中的 get() 方法可以获取线程执行完后的返回值,如果在线程开启前调用该方法,那么该程序会一直停留在该代码处
1 | public class Test { |
以上三种方式对比
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程简单,run()方法中能直接使用Thread类中的方法 | 扩展性差,不能继承其他类 |
实现Runnable接口或实现Callable接口 | 扩展性强,实现接口的同时还能继承其它类 | 编程复杂,run()方法或者call()方法中不能直接使用Thread类中的方法 |
Thread 类常用方法
方法名 | 说明 |
---|---|
String getName() | 获取线程名称 |
void setName(String name) | 设置线程名称 |
public static Thread currentThread() | 返回当前线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定时间,单位为毫秒 |
public final void setPriority(int priority) | 设置线程优先级(优先级范围是1~10,默认是5) |
public final int getPriority() | 获取线程优先级 |
public final void setDaemon(boolean on) | 设置线程为守护线程 |
线程调度和守护线程
- 线程调度
- 多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行代码,各个线程轮流获得CPU的使用权,分别执行各自的任务
- 常见两种调度模型:
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程优先级相同,则随机执行,优先级高的线程获得的时间片的概率较高
- java使用第二种调度模型
- 守护线程:守护线程是为了守护普通线程而存在的,普通线程执行完毕后,守护线程也会停止运行
线程安全问题
- 多线程操作共享数据时,会出现多线程的数据安全问题
- 解决方式:把操作共享数据的多条代码锁起来,让任意时刻只能有一个线程去执行,Java中提供同步代码块的方式来解决
- 同步代码块
- 格式:
synchronized(obj){共享数据的代码}
- obj表示任意一个对象,每一个对象都有一个锁
- 一个线程要想执行代码块中的代码必须获得这个锁,任何时刻只能有一个线程可以获得这个代码块的锁,多个线程想要实现同步则必须共用一个锁
- 当代码块执行完毕时或者代码块中抛出异常都会释放锁
- 优点是解决了多线程数据安全问题,弊端是降低程序执行效率
- 格式:
- 同步方法
- 格式:
修饰符 synchronized 返回值类型 方法名(方法参数){}
- 同步方法的锁对象是this
- 如果同步方法是静态方法,则锁对象是类名.class
- 格式:
- 同步方法和同步代码块区别
- 同步代码块是锁住指定代码,同步方法是锁住方法中全部代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
- Lock锁
- 为了更加清晰表达如何加锁和释放锁,JDK5以后提供了一个锁对象Lock
- 由于Lock是接口,所以实际使用它的实现类ReentrantLock类来实例化
- Lock提供了两个方法
void lock();
和void unlock();
来分别加锁和释放锁,需要同步的代码则处于这两个方法之间 - 为了避免在同步代码中出现异常导致程序中断而没能执行unlock()方法,所以unlock()方法一般放在finally块中处理
线程池
每一个线程的启动和结束都是比较消耗时间和资源的,如果在系统中用到很多线程,大量的线程启动和结束操作会导致性能变卡、响应变慢,为了解决这个问题,引入线程池的设计思想:创建若干个线程放入池子,有任务需要处理时将任务提交到线程池中的任务队列,任务处理完后线程并不会销毁,而是继续在线程池中等待下一个任务
静态方法创建线程池
- 使用 Executors 类中的静态方法
static ExecutorService newCachedThreadPool()
创建线程池,默认线程池是空的,根据需要创建线程,超过60秒未被使用的线程则 销毁,最多能创建 int 最大值个线程 - 使用 Executors 类中的静态方法
static ExecutorService newFixedThreadPool(int nThreads)
创建线程池,默认线程池是空的,根据需要创建线程,参数表示线程池最多能够创建的线程,创建的线程将 一直存在直到显式调用 shutdown() 方法 - 这两个方法返回值类型是 ExecutorService 接口,这个接口里边定义了操作线程池的方法,常用的两个方法是
submit(task)
,task 是需要执行的任务,可以是实现 Runnable 接口或 Callable 接口的类对象,也可以是 Lambda 表达式shutdown()
,用于任务执行后关闭线程池
使用 ThreadPoolexecutor 类创建线程池
上述使用静态方法创建的线程池实际上是使用了该类来创建并返回的线程池
常用构造方法
1
2
3
4
5
6
7
8
9public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
){}构造方法参数解析
corePoolSize
:核心线程数量maximumPoolSize
:最大线程数量keepAliveTime
:空闲线程存活时间的值unit
:存活时间的单位workQueue
:任务队列threadFactory
:线程工厂,指定创建线程的方式handler
:任务拒绝策略,当任务队列已满,新任务不能提交到线程池时触发对新任务的处理策略
任务拒绝策略
ThreadPoolExecutor.AbortPolicy
,丢弃任务并抛出RejectExecutionException异常,默认的任务拒绝策略ThreadPoolExecutor.DiscardPolicy
,丢弃任务但不抛出异常,不推荐使用ThreadPoolExecutor.DiscardOldestPolicy
,抛弃队列中等待最久的任务然后将当前任务加入任务队列ThreadPoolExecutor.CallerRunsPolicy
,调用任务的run()方法绕过线程池直接执行
面试问题
问题
堆内存是唯一的,每一个线程都有自己的线程栈
线程在使用堆里面的变量时,会先拷贝一本到变量的副本中
线程每次使用变量都是从变量副本中获取的
- 所以,当一个线程修改了共享变量中的值时,其他线程不一定能够及时使用最新的值
解决方式1:使用volatile关键字修饰共享变量,作用是线程在每次使用该变量的时候,都会查看共享变量的值
- 解决方式2:使用synchronized同步代码块,作用是:
- 线程获得锁
- 清空变量副本
- 拷贝共享变量最新的值到变量副本中
- 执行代码
- 将修改后变量副本的值赋值给共享数据
- 释放锁
并发工具类
- HashTable类
- HashMap是线程不安全的,为了保证数据安全性可以使用线程安全的HashTable代替
- HashTable效率比较低下
- HashTable采用sychronized悲观锁,当有线程访问时会将整个集合加锁
- ConcurrentHashMap类
- ConcurrentHashMap是线程安全的,效率较高
- JDK8原理
- 使用无参构造创建对象时并不会创建底层数组,而是在第一次添加数据时初始化长度为16,加载因子为0.75的数组
- 添加元素时计算应存入的索引,如果索引为null,则利用CAS算法将元素添加到此处;如果不为null,则利用volatile关键字获取当前位置最新的节点地址,将当前元素挂在它下面变成链表,当链表长度大于8时转为红黑树
- 保证数据安全的方式是对链表或者红黑树头节点加锁,配合悲观锁保证多线程操作集合时的安全性
- CountDownLatch类
- Semaphore类