标签:另一个 问题 bar 活性 代码块 uri cas 条件队列 start
多线程真的是一个很宽的话题,可以聊一串东西线程安全、同步机制、锁、线程运行状态、CAS原子操作、线程池、甚至是JMM、内存可见性等。
而在日常coding中更多地关注是创建线程池提交多个任务执行,分析哪些数据结构被多个线程共享访问,在哪个方法上加锁?如果程序运行一段时间出问题,可能jstack查看线程堆栈执行信息、或者看dump出来的文件、或者用专业一点的工具检查hot区域代码等。日常讨论经常是陷入某个细节中,而这篇文章想从一个总体的角度记录一下我对多线程的理解。
现在的服务器都是多核的cat /proc/cpuinfo
,如果我们写的代码只由一个线程执行的话效率是不高的,比如一个程序里面既要访问redis、又要发送http请求、另一个模块又会去查询MySQL……这些操作有些是可以并行的。用多个线程来执行,发挥cpu多核优势,程序执行效率就高了。
引入多线程后,不可避免地存在:
多个线程访问共享变量的情况
new 了一个HashMap对象在JVM堆中,线程1读取Mysql中一些数据,保存到HashMap;线程2操作HashMap删除某些条件的数据,因此这个HashMap就是共享变量,为了保证数据一致性(线程安全),需要同步机制(也可以采用其他办法保证线程安全)。
多个线程之间竞争cpu
cpu的核数是固定的,操作系统服务需要使用cpu,JVM虚拟机也要使用cpu(比如垃圾回收线程)、然后才是我们写的多线程应用程序也要使用cpu。操作系统一般采用抢占式调度,给每个线程分配时间片(最小执行时间),线程的时间片执行完了,将这个线程调度出去,换另一个线程使用cpu。
总的说:多线程带来的三个问题:
安全性
为了保证线程安全,有多种实现方式,这些实现方式是一个解决问题的方向,而不是针对某个具体问题的解法。
互斥同步,采用加锁方式,比如synchronized或者juc包中的Lock实现类ReentrantLock。
既然用锁来进行互斥同步,锁的实现方式也有多种多样,从不同的角度进行分类:
乐观锁 vs 悲观锁
轻量级锁(基于硬件原子指令) vs 重量级锁(一般伴随上下文切换)
非阻塞同步,CAS操作,比如原子类AtomicLong
ThreadLocal,将共享变量转化成线程私有的变量。
不可变类 将共享变量设计成不可变变量。
活跃性
在redis 分布式锁官方文档提到,锁的2个基本保证:安全性和活性。活跃性可进一步理解成:
正是程序运行不当,存在活跃性,导致了程序的性能问题。
性能问题
其实谈到了锁,又不免地想把JAVA里面的锁与数据库锁对比一下。说起JAVA里面的锁,想到的是synchronized、ReentrantLock、AtomicInteger CAS操作,而数据库里面的锁,则是与事务相关。事务有ACID4个特性,其中隔离性(各个事务操作对象相互分离,在事务提交前对其他事务不可见)就是由锁来保证实现的。这个时候,就站在了一个更细的角度来讨论锁了,比如:锁的粒度对并发的影响 vs 锁的粒度(快照、记录锁、区间锁)与事务隔离级别之间的关系、锁的类型(读锁、写锁、共享锁、排他锁)、死锁检测机制(可中断 vs 不可中断)
再回到多线程,引入多线程带来的开销:
上下文切换
线程占用cpu执行,会加载数据到cpu缓存、会访问寄存器,切换到另一个线程占用cpu时,这些上下文信息都得保存起来,涉及到应用态到内核态的切换,这些算是:是上下文切换开销。所以为什么调度器会给线程分配一个最小执行时间,确保线程至少会执行一段时间,而不是刚占用cpu就立即被调度出去了。
内存同步
同步操作可以保证内存可见性(volatile、synchronized),它们会使用一些特殊的指令:Memeory Barrier(内存栅栏)刷新缓存(JMM 内存模型:主内存 vs 线程的工作内存)、阻止编译器优化,这些都会影响性能。
阻塞的代价
在锁上发生竞争时,竞争失败的线程会阻塞。有没有想过这个阻塞到底是啥子?引用《Java并发编程实战》一句话:
JVM 在实现阻塞行为时,可以采用自旋等待 或者 通过操作系统挂起被阻塞的线程
那么阻塞的代价就里:自旋占用cpu时钟周期、挂起线程导致上下文切换。
当线程无法获取锁,或者在某个条件上等待或者等待IO操作完成时,需要挂起。这个过程涉及到上下文切换、操作系统操作以及必要的缓存操作,被阻塞的线程在其时间片尚未用完前就被调度出去了。
当其他线程释放了锁,锁重新变得可用了,或者IO操作等待的数据已经完成加载到用户缓冲区了,或者其他线程等待条件已经满足了,这个线程又被切换回来
既然引入子多线程,采用了锁来保证线程安全,线程获取锁失败就会进入阻塞状态,但是获取锁的流程还在继续,等到持有锁的线程释放锁后,就会唤醒线程,因此线程的状态就会发生变化,java.lang.Thread.State`定义了6种状态:
NEW
创建了一个线程,还没有执行Thread#start()方法
RUNNABLE
向线程池中提交任务执行,任务会"排队"等待cpu调度执行,此时线程就是RUNNABLE,正在占用cpu执行的线程一般叫做 RUNNING
BLOCKED
当线程争抢监视器(synchronized)失败时,进入BLOKCED状态。对于 synchronized而言,竞争锁失败的线程进入BLOCKED状态,并且不可响应中断,而 ReentrantLock 有一个 lockInterruptibly方法,在竞争锁过程中能够响应中断,从而退出WAITING状态。
WAITING
线程等待另一个线程执行某个特定操作时,进入WAITING状态。比如LinkedBlockingQueue,如果队列中没有数据,那么消费者线程执行take()方法就会进入到WAITING状态;再比如多个线程执行同一个ReentrantLock对象的lock()方法,只会有一个线程获得锁,其他线程进入WAITING状态;
TIMED_WAITING
线程执行 Thread.sleep(mills),sleep一段时间,进入TIMED_WAITING状态。又比如,ReentrantLock有一个tryLock(timeout),竞争锁失败的线程进入TIMED_WAITING状态,阻塞一段时间,然后又恢复到RUNNABLE。
TERMINATED
其中,不严谨地 把BLOCKED、WAITING、TIMED_WAITING 称为阻塞状态,线程在阻塞状态下,能不能响应中断?这个问题更宽泛一点就是:如何取消线程所执行的任务?
另外引出的另一个问题就是:synchronized 监视器锁与juc包中的Lock实现类ReentrantLock的对比:
实现方式 monitor 对象 vs AQS
谈到monitor对象,又联想到对象的内存结构:对象头、实例数据、对齐填充。对象头里面又存储着:对象的hash码、GC分代年龄、类型指针(class对象)、markword……那么这里又涉及到:JVM垃圾回收和内存布局、Class对象及类加载机制、各种锁优化措施(偏向锁、自旋锁、轻量级锁、重量级锁)
可中断 vs 不可中断
线程阻塞状态BLOCKED vs WAITING
公平 vs 非公平
手工加锁(调用lock方法,finally代码块中调用unlock方法) vs JVM 自动加锁释放锁(monitorenter、monitorexit指令)
一个队列 vs 多个条件队列
灵活性。tryLock、tryLock(timeout)
有时候,经常把多线程、锁、同步 这些概念搞混淆起来,要解释一个概念挺难的,尤其是这种熟悉的概念。因此这篇文章不是聚焦于一个点去深入解释某个概念,而是侧重于各个概念之间的联系,里面有很多值得深入的地方,比如讨论数据库事务的ACID的实现方式,锁与隔离级别之间的关系、再比如synchronized底层实现原理与AQS之间的不同、再比如线程池提交任务的顺序、线程池参数、饱和策略(拒绝策略)……本文就当做一个大纲吧。
原文链接:https://www.cnblogs.com/hapjin/p/12040107.html
标签:另一个 问题 bar 活性 代码块 uri cas 条件队列 start
原文地址:https://www.cnblogs.com/hapjin/p/12040107.html