Java 线程
概述
- Java内置支持多线程编程
- 多线程程序包含两条或两条以上并发运行部分/指令流
- 每个部分/指令流称为线程
- 每个线程都有独立的执行路径
- 许多多线程其实是模拟出来的,真正的多线程需要多核,即多CPU
- 即使没有创建多线程,后台也存在多个线程,如main线程、gc线程等
- main()线程即主线程,为系统入口,用于执行整个程序
- 多任务处理
- 多线程是其一种特殊的形式
- 有两种截然不同的类型:基于进程和基于线程
- 基于进程
- 进程:本质是一个执行程序
- 特点:允许计算机同时运行两个或更多的程序
- 调度程序所分派的最小单位:程序
- 用于程序处理“大图片”
- 基于线程
- 最小执行单位:线程
- 线程又称轻量级进程
- 是进程的组成部分
- 与进程一样拥有独立的执行控制,由操作系统负责调度
- 可同时执行两个或多个任务的功能
- 用于程序处理细节问题
- 线程会带来额外开销:如CPU调度时间、并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
- 最小执行单位:线程
- 基于进程
- 多进程与多线程的区分
- 多线程比多进程程序需要的管理费用少
- 进程是重量级任务,需分配独立地址空间;线程是轻量级,之间共享地址空间且共享同个进程
- 进程间转换需要花费;线程通信简单且便宜,转换成本低
- 多进程不受Java控制,多线程则受Java控制
- 多进程即能同一时刻运行多个程序,如一边编辑word,一边播放音乐;多线程指同个进程中可执行多个任务,如音乐软件可在播放音乐的同时,进行浏览其他歌曲信息、互动等其他操作
- 多线程优点
- 帮助写出CPU利用率最大的程序,使空闲时间保持最低,对交互式网络互连环境至关重要
- 多线程与单线程最大区别:各线程控制流彼此独立,但使得代码乱序执行,带来线程调度和同步的问题
线程在Java中的实现
- Java提供类java.lang.Thread进行多线程编程
- 该类提供大量方法方便控制各个线程
run()
方法- Thread类最重要方法
- 为Thread类下的start()方法所调用
- 使用
- 方法一:继承Thread类,覆盖run()方法
- 优点:简单,符合习惯
- 缺点:若一个类已从另一类继承,则其无法继承Thread类
- 不建议使用,避免OOP单继承局限性
- 核心方法
方法 说明 CurrentThread() 返回当前运行的Thread对象 start() 启动线程 run() 由线程调度调用 stop() 使调用它的线程立即停止执行 sleep(int n) 使线程睡眠n毫秒,之后可再次运行,其他线程不受影响;若休眠过程被其他线程中断,则抛出InterruptedException异常,是检查性异常 suspend() 使线程暂时挂起,暂停运行Not Runnable resume() 恢复挂起的线程,使其处于可运行状态Runnable yield() 暂停当前正在执行的线程对象,将CPU控制权主动移交到下个可运行线程 setPriority(int newPriority) 更改线程优先级 void join() 等待该线程终止 void interrupt() 中断线程,不建议使用这种方式 boolean isActive() 测试线程是否处于活动状态 - 休眠:sleep
- sleep存在InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可模拟网络延时、倒计时等
- sleep不对释放对象锁
- 礼让:yield
- 当前线程暂停但不阻塞,从运行态转为就绪态
- CPU试情况进行重新调度当前暂停进程或其他进程
- 强制执行:join
- Join合并线程,待此线程执行完成再执行其他线程,其他线程阻塞
- 类似,插队
- 休眠:sleep
- 方法二:实现Runnable接口
- java.lang.Runnable
- 其下只有一个方法:run()
- 优点:灵活方便
- 避免单继承局限性
- 方便同一个对象被多个线程使用
- 缺点:其并不对任何线程支持,需要创建Thread类实例:public Thread(Runnable target);
- 方法一:继承Thread类,覆盖run()方法
线程状态
- 五大状态
- 新建:线程被创建后所处的状态
- 可运行(就绪态):线程有资格运行,但其调度程序尚未执行
- 可运行线程池:所有可运行线程组成的集合
- 运行:线程调度从可运行线程池中选定一个线程并运行,该线程则进入运行状态
- 运行状态的线程可回到可运行状态或进入阻塞状态
- 阻塞:线程由于某些限制暂停,进入阻塞状态
- 处于阻塞的线程并不终结,等待的特定事件发生后,重新回到可运行态
- 终结:线程运行完毕,不能再回到可运行状态
- 不推荐使用stop()、destory()方法
- 推荐让线程自行停止
- 使用标志位进行终止:flag = false
- 状态观测:Thread.State
- NEW:尚未启动的线程
- RUNNABLE:在Java虚拟机执行线程
- BLOCKED:被阻塞等待监视器锁定线程
- WAITING:等待另一线程执行特定操作的线程
- TIMED_WAITING:等待另一线程执行指定操作到指定时间的线程
- TERMINATED:已退出的线程
线程调度
- 从可运行线程池中,依据一定原则,选定一个线程运行
- 一般由操作系统中的线程调度程序负责
- Java程序则由Java虚拟机负责
- 调度器与操作熊铁男紧密相关,调度顺序不可人为干预
- Java采用的调度策略:
抢占式
- 在以下情况下,线程放弃占用CPU
- 当前时间片用完
- 线程执行调用了yield()或sleep()方法
- 进行I/O访问,等待用户输入,或等候一个条件变量,线程使用wait()方法,线程进入阻塞态
- 高优先级的线程参与进调度
- 在以下情况下,线程放弃占用CPU
- 线程优先级:用数字表示,范围从1~10,主线程默认为5
- 改变:getPriority().setPriority(int xxx)
- 线程优先级设定建议在start()调度前
线程资源的同步处理
- 概述
- 同步:被多个线程共享的数据,在同一时刻内只允许一个线程处于操作中,保证数据完整性
- 实质:等待机制
- 多个访问同个对象线程进入该对象等待池形成队列,待前方线程完成再轮到下个线程
- 队列+锁机制
- 实质:等待机制
- 异步:与同步相反
- 并发:同个对象被多个线程同时操作
- 同步:被多个线程共享的数据,在同一时刻内只允许一个线程处于操作中,保证数据完整性
- 临界资源问题
- 同个进程多个线程共享空间所带来的冲突
- 需要加入并发控制
- Java解决该问题所提供的机制:Synchronized,排它锁
- 每个对象都有一个锁标志,当一个线程别访问时,将被Synchronized修饰上锁,将独占资源,阻止其他线程访问
- 每个synchronized方法都必须获得调用该方法对象的锁才能执行,否则线程阻塞
- 方法一旦执行,独占该锁至方法返回才释放,后面被阻塞线程才能获得这个锁且继续执行
- 方法修改了内容时才需要锁,减少锁的使用以避免资源浪费
- 当前线程访问完其数据后,将释放锁标志,使其他线程可访问该资源数据
- synchronized关键字可作为函数修饰符,也可作为函数内语句
- 即同步方法和同步语句块
- 同步方法:public synchronized void method(int args){}
- 同步块:synchronized(Obj){}
- Obj:同步监视器
- 可为任意对象,但推荐使用共享资源
- 同步方法中无需指定监视器,其为对象本身或class
- 同步监视器执行过程
- 第一个线程访问:锁定监视器,执行代码
- 第二个线程:检测同步监视器被锁定,无法访问
- 第一个线程访问完毕:解锁同步监视器
- 第二个线程:检测到同步监视器未锁定,锁定并访问
- Obj:同步监视器
- 无论是其加在方法还是对象上,其取得的锁都是对象,且同步方法也可能被其他线程访问
- 即同步方法和同步语句块
- 存在问题:影响效率
- 一个线程持有锁,其他需要该锁的线程将被挂起
- 多线程竞争时,加锁与释放锁将导致较多上下文切换和调度延时,引起性能问题
- 若高优先级线程等待低优先级线程释放锁,将导致优先级倒置,引起性能问题
- 每个对象都有一个锁标志,当一个线程别访问时,将被Synchronized修饰上锁,将独占资源,阻止其他线程访问
- wait()和notify()方法
- notify():通知等待者执行
- 这两个方法配套使用,可解决许多临界资源访问问题
- 使用要求:
- 必须在synchronized方法或块中调用,只有在同步代码段中才存资源锁定
- 这对方法直属于Object类,而不是Thread类
- 使用要求:
- 同个进程多个线程共享空间所带来的冲突
- 死锁
- 多个线程各自占有的资源才能运行,而导致两个或多个线程在等待对方释放资源,都停止执行的情形
- 某个同步块同时拥有两个以上对象锁就可能发生死锁
- 产生条件:只要破除其中一个条件即可避免死锁
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程间形成一种头尾相接的循环等待资源关系
- Lock锁
- JDK5.0开始,Java提供更强大的线程同步机制
- 通过显示定义同步锁对象实现同步
- 同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口:
- 控制多个线程对共享资源进行访问的工具
- 锁提供了对共享资源的独占访问,每个只能有一个线程对Lock对象加锁
- 线程开始访问共享资源前先获得Lock对象
- ReentrantLock类
- 实现了Lock
- 拥有与synchronized相同的并发性和内存语义
- 实现线程安全中比较常用
- 可显示加锁、释放锁
- 与synchronized的区别
- Lock是显示锁,手动开启和关闭;synchronized是隐式锁,出了作用域自动释放
- Lock只有代码锁,synchronized有代码锁和方法锁
- 使用Lock锁,JVM将花费较少时间调度线程,性能更好,且具更好的扩展性
- 优先使用顺序:
- Lock > 同步代码块(方法体内) > 同步方法(方法体外)
- JDK5.0开始,Java提供更强大的线程同步机制
线程池
- 线程常创建和销毁、使用大量资源,并发情况下线程对性能影响极大
- 通过提前创建多个线程放入线程池,使用时直接,用完放回即可,避免创建销毁开销,实现重复利用
- 优点
- 提高响应速度
- 降低资源消耗
- 便于线程管理
- corePoolSize:核心池大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后终止
- 相关API:ExecutorService和Executors
- ExecutorService:真正的线程池接口
- 常见子类:ThreadPoolExecutor
- 方法:
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- < T > Future< T >submit(Callable< T > task):执行任务,有返回值,一般用于执行Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- ExecutorService:真正的线程池接口
拓展
线程在Java中的实现方法三:实现Callable接口
- 该接口需要返回值类型
- 重写call方法时需要抛出异常
- 实现步骤
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future< Boolean> result1 = ser.submit(t1);
- 获取结果:boolean r1 = result1.get()
- 关闭服务:ser.shutdownNow();
静态代理模式
- 23种设计模式之一
- 组成:真实角色、代理角色、实现接口
Lambda表达式
- λ(Lambda):希腊字母表中排序第十一的字母
- 作用:
- 避免匿名内部类定义过多
- 使代码变简洁
- 去掉没有意义的代码,只留下核心逻辑
- 实质:属于函数式编程的概念
- 使用
- (参数列表) -> 表达式
- (参数列表) -> 语句
- (参数列表) -> {语句块}
守护线程:daemon
- 线程分为:用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕,但不必等待守护线程执行完毕
线程协作/通信
生产者/消费者问题
- 生产者从数据缓冲区存数据,消费者从数据缓冲区取数据
- 若数据缓冲区没有数据,消费者停止取数据并等待生产者存入数据
- 若数据缓冲区有数据,则生产者停止生产并等待消费者取出数据
- 是线程同步问题
- 生产者与消费者共享同个资源,且相互依赖,互为条件
- 仅有synchronized不够
- synchronized可阻止并发更新同一个共享资源,实现同步
- 但synchronized不能实现不同线程间的消息传递
- 解决方法:
方法 作用 wait() 表示线程一直等待,至其他线程通知,与sleep不同,会释放锁 wait(long timeout) 指定毫秒数 notify() 唤醒一个处于对等待状态的线程 notify() 唤醒同个对象上所有调用wait()方法的线程,优先级高的先调度 - 均为Object类方法
- 都只能在同步方法或同步代码块中使用,否则会抛出IllegalMonitorStateException异常
解决方法
管程法
- 利用缓冲区解决
信号灯法
- 利用标志位解决:flag