Java 并发锁
1 为什么要加锁
所先JVM将内存划分成2个区域
- 主内存:所有线程共享的内存区域,存储所有的共享变量
- 工作内存:每个线程独有的内存区域,存储该线程使用到的共享变量的副本
线程对变量的操作(读取、赋值)必须在工作内存完成,从主内存读取变量到工作内存,在对工作内存的变量进行操作,返回主内存。因此,当不同线程同时操作一个变量时,就会出现操作的数据实际值和主内存不一致。
2 需要解决那些问题
2.1 可见性问题
当一个共享变量被其他线程改变值后,其他线程无法立即感知到这个变量的值。
2.2 原子性
无法确保对同一个共享变量操作是原子性的。
2.3 有序性
操作系统可能对一些操作指令进行重新排序,从而提高运行速度。例如一个对象赋值操作。 A a = new A()。 单例模式时,会判断对象是否是空对象,所有要两次判断非空
原本顺序:
1. 分配内存空间
2. 初始化对象
3. 将引用指向内存
重排序后顺序:
1. 分配内存空间
3. 初始化对象
3 锁升级过程
虚拟机对象头分为两部分信息:
- 第一部分:用于存储对象自身运行时数据,32BIts(32位系统)或者64Bits(64位系统),也称为Mark Work, 其中有2Bits用来存储锁标识。
- 第二部分:用于存储指向方法区对象类型数据的指针
锁优化过程:
1. 锁消除
如果不会出现同步问题,java编译之后,就会忽略同步
2. 偏向锁(只记录该线程ID)
偏向锁就是在无竞争(只有一个线程)的情况下把整个同步都消除。当锁对象第一次被线程获取的时候,虚拟机将对象头中的标志位设位01,偏向模式。同时使用CAS把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,以后该线程在进入时,都不需要在进行相关的锁操作。
3. 轻量级锁(一个一个来, 很慢, 不会有竞争, 能更换成功)
在无竞争的情况下使用CAS消除数据在同步使用的互斥量。CAS将对象的MarkWord更新为指向Lock Record(线程堆栈,用于存储MarkWord的拷贝)的指针,如果更新成功,将对象头Mark Word里面的锁标志位更新成00。
4. 锁粗化
如果一个代码快有特别多的同步,虚拟机就会执行锁粗化,因为其实对多个对象加锁其实比一个对象加锁开销大的很多,虽然可能每个线程等待的时间会少点。
5. 自旋锁和自适应自旋
当获取不到锁时,不在挂起,而是让他进行自旋等待。自旋等待次数通过-XX:PreBlockSpin来改变。jdk1.6引入自适应自旋锁。自适应意味着自旋的时间不在固定,根据任务执行的长短来决定自旋等待时常。
5. 锁的公平性
- 公平锁:先请求的线程先获得锁,后请求的线程进入等待队列排队,直到前面的线程释放锁后再依次获取
- 非公平锁:新请求锁的线程先尝试拿锁,如果拿到直接获取到锁,而无需排队等待,即使等待队列中已有线程在排队。
4 常见锁类型
2.1 Synchronized
synchronized 能解决资源的原子性、可见性和有序性。底层实现依赖JVM的锁机制和对象头结构。JVM层面的锁,自动加锁和释放锁,非公平锁,设计到锁升级。
public class SynchronizedDemo {
int i = 0;
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.init1();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(synchronizedDemo.i);
}
public void init1() {
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(this::fun1);
threads[i].start();
}
}
/**
* 方法上锁
*/
synchronized void fun1() {
i++;
}
}
2.2 ReentranLock

浙公网安备 33010602011771号