目录
并发编程
为什么要使用并发编程?
- 充分利用多核CPU的计算能力
- 方便进行业务的拆分,提高系统并发能力和性能
并发编程的缺点
并发编程的目的就是为了提高程序的运行效率,提高程序运行的速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。
并发线程三要素
- 原子性:在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
如何保证多线程的运行安全?
出现安全问题的原因
- 线程是抢占执行的
- 有的操作不是原子的。当 cpu 执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束;
- 缓存导致的可见性问题:多个线程尝试修改同一个变量。
- 指令重排序:Java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,来提高程序的运行效率。
解决的方法
- JDK Atomic 开头的原子类、synchronized、Lock,可以解决原子性问题
- synchronized、volatile、Lock,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题,JVM对程序运行设定的一些规则。
Synchronized锁
为什么要使用synchronized关键字?
Synchronized——线程安全用的同步关键字
作用:对象头进行加锁,对同一个对象加锁的线程同步互斥
原理:多个线程间同步互斥
Synchronized关键字的使用
- 同步代码块:sychronized(某个对象){...}
- 同步方法:
- 实例同步方法:对this对象加锁
- 静态同步方法:对当前类对象加锁
Synchronized锁本质上是对对象进行加锁,而不是对代码块进行加锁。
加锁改变的是对象的对象头
Java的对象的布局:
Java的对象组成:
- Java对象的实例数据——不固定
- Java对象头——固定的
- 数据对齐
64位的JVM下,规定的对象的大小必须是8 byte 的整数倍
Java对象头由什么组成?
JVM规范,规定了对象头的组成:包含堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息
由两部分组成:
- Mark Word:占64bit——不固定
- Klass pointer /Class Metadata Address:占32bit
synchronized底层的工作原理
monitor(对象锁)机制
使用Synchronized进行同步,关键就是必须对象的监视器monitor进行获取,线程获取monitor后才能继续往下执行,否则就只能等待。获取的过程是互斥的,同一个时刻只有一个线程能获取到monitor。
所以说:获取锁的本质就是要获取monitior
-
要对对象的监视器monitior进行获取,线程获取monitor后才能继续往下执行,否则就只能等待
-
编译成字节码指令:一个monitorenter,两个monitorexit
Java虚拟机需要确保所获得的锁在正常执行路径以及异常执行路径上都能够被解锁
-
monitor保存计数器:进入 +1,(退出)释放锁 -1
采用计数器的方式,为了允许同一个线程重复获取同一把锁,实现synchronized的可重入性。
synchronized优化
基于对象头的锁状态来实现优化:
从低到高依次为无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态【锁只能升级不能降级】
-
无锁状态
-
偏向锁状态——同一个对象多次加锁(重入)
-
轻量级锁状态——基于CAS实现,同一个时间点,经常只有一个线程竞争
-
重量级锁状态——基于系统mutex 锁,同一个时间点,经常有多个线程竞争
特点:mutex是系统级别加锁,线程会由用户态切换到内核态,切换成本高。
一个线程总是竞争失败,就会不停的在用户态和内核态之间切换,比较耗费资源
如果有多个竞争失败的线程,性能的影响会很大
其他优化
-
锁粗化:多个synchronized连续执行加锁、释放锁可以合并为一个
示例:StringBuffer静态变量在一个线程多次append
静态变量属于方法区,jdk1.8之后在堆里面,是线程共享的,存在线程安全问题
-
锁消除:删除不必要的加锁操作。不会逃逸到其他线程的变量,执行加锁操作,可以删除加锁。
示例:StringBuffer局部变量,在一个线程多次append
局部变量属于虚拟机栈,是线程私有的