Java并发编程:概念和原理
Java并发编程在实际的工作中应用广泛,有时候需要通过多线程去异步做一些事情,有时候需要通过多线程提升一个任务执行的效率。最近又在回顾一些Java编程的基本概念和原理,就顺手记录在这里。
关键概念
上下文切换
- 概念:CPU通过时间片算法,给可运行的线程分配运行时间,在不同线程之间的切换时需要将当前线程的状态保存并回复将要执行的线程状态信息,这个过程就是上下文切换。
- 如何减少或避免上下文切换?
- 无锁并发编程
- CAS算法
- 使用最少线程
- 协程
死锁
- 概念:两个或多个线程持有对方正在等待的锁
- 如何避免死锁?
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁
- 对于数据库锁,加锁和解锁必须在一个数据库连接里
Java并发的底层机制
volatile
- 作用:在多处理器开发中保证多线程之间的共享变量的可见性,即一个线程修改该变量的值时,其他的线程可以立即看到该变量最新的值。
- 原理:对被volatile修饰的变量进行写操作时,会做如下两个事情
- 将当前处理器缓存行的数据写到系统内存;
- 使得其他CPU里缓存了该内存地址的数据无效
- 使用要点:
- volatile只能保证可见性,无法保证同步性。举个例子:如果针对某个变量的改变后的值依赖于上次改变的值,使用volatile就无法保证并发安全了
synchronized
-
定义:synchronized是Java多线程之间的一种通信方式。synchronized的具体应用有三种:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步代码块,锁是synchronized括号里配置的对象
-
使用要点:
- 构造方法不能用synchronized修饰
- 推荐尽量减小锁的粒度,例如,使用同步代码块可以满足需求就不需要使用同步方法
- 如果可以确认应用中的所有锁在大多数情况下都由不同的线程竞争,可以通过-XX:+UseBiasedLocking禁用偏向锁,提升性能。
-
原理:介绍两个概念,Monitor Record(Thread类的私有数据结构)和Java对象头,关系是:Java对象头中存储了Monitor Record的地址,Monitor Record中记录了持有它的线程。
-
monitor:monitor不是一个特殊的对象,是一种方法或机制,Java通过monitor来控制对某个对象的访问。Java中的每个对象都和一个monitor相关联。在同一个时刻,只有一个线程(Thread)可以锁定一个monitor。当某个monitor被一个线程锁定时,其他试图锁定这个monitor的线程只能block等待。
-
对象头:synchronized的锁状态描述在Java对象的头部。对象头中包括Mark word和Klass Word。
-
在32位虚拟机中,整个对象头大小是64bits(即8字节),Mark Word和Klass Word分别占用4字节。
Object Header(32位虚拟机) -
锁状态:Java中的锁按照级别从低到高有四种,无锁状态——>偏向锁——>轻量级锁——>重量级锁。偏向锁是依赖Mark Word中的一个指向当前线程的字段来标识该锁的持有者是否是当前线程,如果是则直接进入同步代码块;假设禁用了偏向锁,轻量级锁指的是两个线程获取锁,一个获取到,另一个获取不成功的状态,首先会CAS自旋获取锁,如果CAS自旋获取失败,该轻量级锁就会膨胀为重量级锁,当前获取锁失败的线程进入阻塞状态。
-
锁升级的过程,只会从低到高,不会从高到低,避免不必要的资源浪费。举个例子,如果一个锁的状态已经达到重量级锁,后面再来竞争这个锁的线程都会直接进入阻塞,不会再进行CAS自旋。参考资料7中提供的一张图很精致,我放在这里:
java_synchronized.png
-
-
原子操作
CPU级别的原子操作
在CPU级别实现原子操作需要依靠CPU指令完成,CPU指令通过总线操作内存中的数据,因此在CPU中有两个方式:
- 锁总线:利用LOCK指令向总线发出信号,实现一个
- 锁缓存:在某个一时刻,只需要保证对某个内存地址的操作是原子性的;
JAVA中的原子操作
在Java中可以通过CAS和锁来实现原子操作。
- 使用CAS实现原子操作,从Java1.5开始,java.lang.concurrent包里提供了很多类来支持原子操作,例如AotmicIntenger、AtomicLong,这些类可以以原子的方式将变量的当前值加1或减1;
- 使用锁实现原子操作,锁机制确保只有持有锁的线程才能操作指定的变量;