谈谈多线程
谈谈多线程
多线程真的是一个很宽的话题,可以聊一串东西线程安全、同步机制、锁、线程运行状态、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 称为阻塞状态,线程在阻塞状态下,能不能响应中断?这个问题更宽泛一点就是:如何取消线程所执行的任务?
- java.util.concurrent.ExecutorService 通过submit方法提交Runnable任务或者Callable任务返回一个Future对象,通过Future.cancel方法取消任务
- 线程可响应中断,通过中断的方式取消任务
- 线程不可响应中断,比如执行阻塞IO、阻塞 SocketIO 进入了WAITING状态,无法响应中断,这时可通过关闭底层Socket连接、强行关闭已打开的文件描述符都会强制中断线程,退出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)
原文地址:https://www.cnblogs.com/hapjin/p/12040107.html
- 程序员你为什么这么累【续】:编码习惯-函数编写建议
- 那些年,我们一起碰到过的骗局
- Spring Security (五) 动手实现一个IP_Login
- 史上最全Linux提权后获取敏感信息方法
- Spring Security (四) 核心过滤器源码分析
- Spring Security (三) 核心配置解读
- Spring Cloud配置中心获取不到最新配置信息的问题
- 总是听别人说响应式布局,原来这么简单
- Spring Cloud Zuul重试机制探秘
- Eureka中RetryableClientQuarantineRefreshPercentage参数探秘
- Edgware.RC1中ZuulFallbackProvider的改进
- JPA的多表复杂查询:详细篇
- 尝试使用Memcached遇到的狗血问题
- Enumerable#Zip 实现一下
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- php中file_get_contents()函数用法实例
- PHP通过GD库实现验证码功能示例
- Thinkphp 5.0实现微信企业付款到零钱
- 使用npy转image图像并保存的实例
- php实现有序数组旋转后寻找最小值方法
- 基于python实现音乐播放器代码实例
- PHP实现微信对账单处理
- Win10下配置tensorflow-gpu的详细教程(无VS2015/2017)
- PHP Include文件实例讲解
- ThinkPHP5.1框架页面跳转及修改跳转页面模版示例
- Python Switch Case三种实现方法代码实例
- PHP正则表达式笔记与实例详解
- php实现微信分享朋友链接功能
- python交互模式基础知识点学习
- 浅析Python 抽象工厂模式的优缺点