多线程基础
Posted bee4j
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础相关的知识,希望对你有一定的参考价值。
- 一、CPU时间分片实现
- CPU只管干活,是操作系统实现的纳秒级时间分片
- 二、并发和并行
- 核心区别在于进程是否同时执行,如KTV话筒,
- 并行指的是有多少人可以使用话筒同时唱歌
- 并发指的是同一个话筒被多人轮流使用
- 并发
- 指在某个时间段内,多任务交替处理的能力
- CPU把可执行时间均匀的分成若干份,轮流抢占使用
- 并行
- 指同时处理多任务的能力。
- 目前CPU已经发展为多核,可以同时执行多个互不依赖的指令及执行块。
- 核心区别在于进程是否同时执行,如KTV话筒,
- 三、并发环境下,由于程序的封闭性被打破,出现了以下特点
- 并发程序之间有相互制约的关系
- 直接制约,一个程序等待另一个程序的计算结果
- 间接制约,多个程序竞争共享资源,如处理器、缓冲区等
- 并发程序的执行过程是断断续续的
- 当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率
- 并发程序之间有相互制约的关系
- 四、线程
- CPU调度和分派的基本单位
- 多线程的作用是提高任务的平均执行速度
- 效率最大化,合适的线程数才能让CPU资源被充分利用(太多容易造成通道拥堵)
- 线程可以拥有自己的操作栈、程序计数器、局部变量表等资源
- 线程与同一进程内的其它线程共享该进程的所有资源
- 线程在生命周期内存在多种状态
- NEW:新建状态
- 线程被创建且未启动的状态
- 创建线程的三种方式
- 继承自Thread类
- 实现Runnable接口
- 推荐方式,因为继承自Thread类不符合里氏替换原则,而实现Runnable接口可以编程更加灵活,对外暴漏的细节更少,让使用者专注于线程的run()方法上。
- 通过setDefaultUncaughtExceptionHandler()在主线程中捕获到子线程异常
- 实现Callable接口
- 可以通过call()获得返回值
- call()可以抛出异常
- RUNNABLE:就绪状态
- 调用start()之后运行之前的状态(多次调用start抛出IllegalStateException)
- RUNNING:运行状态
- run()正在执行时线程的状态
- 线程会由于某些因素而退出RUNNING,如时间、异常、锁、调度等
- BLOCKED:阻塞状态,进入此状态,有以下几种情况
- 同步阻塞:锁被其它线程占用
- 主动阻塞:调用Thread的某些方法,主动让出CPU执行权,比如sleep()、join()等
- 等待阻塞:执行了wait()
- DEAD:终止状态
- run()执行结束,或因异常退出后的状态
- 此状态不可逆转
- NEW:新建状态
- 五、保证高并发场景下的安全,可以从以下四个维度考量
- 数据单线程内可见
- 单线程总是安全的
- 通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改
- 最典型的就是线程局部变量,它存储在独立的虚拟机栈帧的局部变量表中
- ThreadLocal采用这种方式来实现线程安全的
- 只读对象
- 只读对象总是安全的
- 允许复制、拒绝写入
- 最典型的只读对象就是String、Integer等
- 一个对象想要拒绝任何写入,必须要满足以下条件
- 使用final关键字修饰类,避免被继承
- 使用private final关键字避免属性被中途修改
- 返回值不能是可变对象的引用
- 线程安全类
- 如StringBuffer
- 同步与锁机制
- 开发工程师在代码中实现安全的同步机制
- 数据单线程内可见
- 六、java.util.concurrent,JUC
- Java并发包中大多数类注释都写有:@author Doug Lea
- Doug Lea在大学当老师时,专攻并发编程和并发数据结构设计,主导设计了JUC并发包,提高了Java并发编程的易用性,大大推进了Java的商用进程
- 并发包主要分成以下几个类族
- 线程同步类
- 这些类使线程间的协调更加容易,支持了丰富的线程协调场景
- 逐步淘汰了使用Object的wait()和notigy()进行同步的方式
- 主要代表
- CountDownLatch
- 倒计时器
- 时间维度,基于执行时间的同步类
- 用完后只能重新创建一个新的再使用
- Semaphore
- 基于信号维度
- permits定义为1时,就是互斥锁
- permits定义为0时,就是共享锁
- CyclicBarrier
- 基于ReentrantLock实现的
- 可以循环使用的屏障式多线程协作方式
- 批量执行
- 设置信号窗口为3,3个线程同时执行,只有3个线程同时都执行完,才会放下一批进入。
- 通过CyclicBarrier的reset释放线程资源
- 其它,如StampedLock、ReentrantReadWriteLock
- CountDownLatch
- 并发集合类
- 集合并发操作的要求是执行速度快、提取数据准
- 最著名的非ConcurrentHashMap莫属,不断优化,由刚开始的锁分段到后来的CAS,不断的提升并发性能
- 其它还有ConcurrentSkipList、CopyOnWriteArrayList、BlockingQueue等
- 线程管理类
- 把Thread发扬光大的是线程池
- 使用Executors静态工厂或者ThreadPoolExecutor等
- 通过ScheduledExecutorService执行定时任务
- 锁相关类
- 以Lock接口为核心,派生出在一些实际场景中进行互斥操作的锁相关类
- 最有名的是ReentrantLock
- 线程同步类
- Java并发包中大多数类注释都写有:@author Doug Lea
- 七、什么是锁?
- 从悲观锁,发展到乐观锁、偏向锁、分段锁
- 特性
- 互斥性
- 不可见性
- 因为锁的存在,某些操作对外界来说是黑箱进行的,只有锁的持有者才知道对变量进行了什么修改
- Java中常用锁实现的方式
- 用并发包中的锁类
- Lock接口,实现逻辑并未用到synchronized,而是利用了volatile的可见性
- ReentrantLock
- 可重入锁
- 对于Lock接口的实现主要依赖了Sync
- Sync继承了AbstractQueuedSynchronizer(AQS),是JUC包实现同步的基础工具
- 在AQS中,定义了一个volatile int state变量作为共享资源,如果线程获取资源失败,则进入同步FIFO队列中等待;如果成功获取资源就执行临界区代码,执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行。
- AQS是抽象类,内置自旋锁实现的同步队列,封装入队和出队的操作,提供独占、共享、中断等特性的方法。
- 公平锁、非公平锁
- 利用同步代码块
- 一般使用synchronized关键字,非公平锁、悲观锁
- 加锁方式
- 在方法签名处加synchronized关键字
- 使用synchronized(对象或类)
- 加锁原则
- 锁的范围尽可能的小,即能锁对象,就不要锁类
- 锁的时间尽可能的短,即能锁代码块,就不要锁方法
- synchronized锁特性由JVM负责实现
- JVM底层是通过监视锁来实现synchronized同步的,监视锁,即monitor,是每个对象与生俱来的一个隐藏字段。monitor非0,其它线程就会进入阻塞状态。
- 通过字节码学习synchronized锁是如何实现的
- JDK6版本后,synchronized提供三种锁实现(也提供自动升级和降级机制)
- 偏向锁
- JVM利用CAS在对象头上设置线程ID,表示这个对象偏向于当前线程,这就是偏向锁。
- 可以降低无竞争开销,不是互斥锁,不存在线程竞争的情况,省区再次判断的步骤,提升了性能。
- 为了在资源没有被多线程竞争的情况下尽量减少锁带来的性能开销。在锁对象的对象头中有一个ThreadId字段,当第一个线程访问锁时,如果该锁没有被其它锁访问过,即ThreadId字段为空,那么JVM让其持有偏向锁,并将ThreadId字段的值设置为该线程的ID。当下一次获取锁时,会判断当前线程的ID是否于锁对象的ThreadId一致。如果一致,那么该线程不会再重复获取锁,从而提高了程序运行效率。
- 轻量级锁
- 如果出现锁竞争情况,那么偏向锁会被撤销并升级为轻量级锁
- 重量级锁
- 如果资源的竞争非常激烈,会升级为重量级锁
- 偏向锁
- 用并发包中的锁类
- 八、线程同步
- 原子性
- 指不可分割的一系列操作指令,在执行完毕前不会被任何其它操作中断,要么全部执行,要么全部不执行。
- 典型的如i++或++i操作,看似原子性,实际不是,需要分三步
- ILOAD->IINC->ISTORE
- 更加复杂的CAS具备原子性
- 不会出现线程的上下文切换
- 线程同步
- 就是线程之间按某种机制协调先后次序执行,当有一个线程对内存操作时,其它线程都不可以对这个内存地址进行操作
- 实现线程同步的方式
- 同步方法
- 锁
- 阻塞队列
- 其它
- 关键字volatile
- 解决的是多线程共享变量的可见性问题,但不具备互斥性。
- 对volatile变量的操作并非都具有原子性
- 只是轻量级的线程操作可见方式,并非同步方式
- 适合一写多读的场景,最典型的应用是CopyOnWriteArrayList
- 所有的操作都需要同步给内存变量,因此volatile一定会使线程的执行速度变慢。
- 线程的切换和执行都是纳秒级的
- 指令优化(指令重排)
- 效率最大化处理
- 计算机并不会根据代码顺序按部就班的执行相关指令
- CPU在处理信息时进行指令优化
- 哪些取数据动作可以合并进行
- 哪些存数据动作可以合并进行
- happen before
- 时钟顺序的先后,并不能保证线程交互的可见性
- 可见性
- 指某线程修改共享变量的指令对其他线程来说都是可见的
- 它反映的是指令执行的实时透明度
- 线程本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。在这个时间差内,该线程对副本的操作,对于其它线程都是不可见的。
- 使用volatile修饰变量
- 意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排的发生。
- 著名的双重检查锁定(Double-checked Locking)问题
- 对象引用在没有同步的情况下进行读操作,导致用户可能会获取未构造完成的对象。
- 使用单例设计模式时,即使用双检锁也不一定会拿到最新的数据。
- 与Java虚拟机的优化有关
- 对Java编译器而言,初始化某个类类型的实例和将对象地址写到成员属性字段并非原子操作,且这两个阶段的执行顺序是未定义的。
- 一种较为简单的解决方案
- 使用volatile关键字修饰目标属性(适用于JDK5及以上版本)
- 限制了编译器对它的相关读写操作,对它的读写操作进行指令重排,确定对象实例化之后才返回引用。
- 锁也可以确保变量的可见性
- 线程得到锁时读入副本,释放时写回内存
- 锁的操作尤其要符合happen before原则
- 解决的是多线程共享变量的可见性问题,但不具备互斥性。
- 信号量同步
- 在不同的线程之间,通过传递同步信号量来协调线程执行的先后顺序。
- Semaphore信号同步类
- Semaphore semaphore = new Semaphore(3);//3个服务窗口
- 只有再调用Semaphore对象的acquire()成功后,才可以往下执行,完成后执行release()释放持有的信号量,下一个线程就可以马上获取这个空闲的信号量进入执行
- Semaphore的窗口信号量等于1,就是典型的互斥锁
- 尽量使用JUC包提供的信号同步类,避免使用对象的wait()和notigy()方式来进行同步
- 原子性
以上是关于多线程基础的主要内容,如果未能解决你的问题,请参考以下文章