文章目录
- 前言
- 一:悲观锁
- 1.1:synchronized
- 1.2:Lock
- 二:乐观锁
- 2.1:AtomicInteger
- 2.2:CAS算法
- 2.2.1:ABA问题
- 2.2.2:解决ABA问题
- 2.2.2:解决ABA问题
前言
锁的种类很多,我们今天就来梳理一下。Java中的好多锁系列之悲观锁、乐观锁。
一:悲观锁
悲观的认为所有的线程都会导致数据错误,每一个线程都需要排队等待。优点:数据一致性,缺点:效率低
1.1:synchronized
- jdk内置的锁,熟悉native的朋友会知道,synchronized是调用了jr.jar里的放
- 灵魂的拷问,他是怎么实现的。答案对象头object head,通过Object Header对象头
Java对象的对象头由 Mark Word 和 Klass pointer 两部分组成,
mark word存储了同步状态、标识、hashcode、GC状态等等。
Klass pointer存储对象的类型指针,该指针指向它的类元数据
值得注意的是,如果应用的对象过多,使用64位的指针将浪费大量内存。64位的JVM比32位的JVM多耗费50%的内存。
我们现在使用的64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。
以64位操作系统为例,对象头存储内容图例。
简单介绍一下各部分的含义
lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:Java GC标记位对象年龄,4位最大是15,所以minor Gc的次数就是15次。
identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向线程Monitor的指针。
输出的第一行内容和锁状态内容对应
unused:1 | age:4 | biased_lock:1 | lock:2
0 0000 0 01 代表A对象正处于无锁状态
第三行中表示的是被指针压缩为32位的klass pointer
第四行则是我们创建的A对象属性信息 1字节的boolean值
第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段
- 对象锁:synchronized
锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念。
synchronized是对类的**当前实例(当前对象)**进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。
import java.util.concurrent.TimeUnit;
public class Test1 {
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
Test1 test = new Test1();
new Thread(test::start, "线程A").start();
new Thread(test::end, "线程B").start();
}
}
执行结果:
线程A,start
线程B,end
- 类锁:static synchronized
该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
StaticTest a = new StaticTest();
StaticTest b = new StaticTest();
new Thread(a::start, "线程A").start();
new Thread(b::end, "线程c").start();
}
执行结果:
线程A,end
线程c,start
更改start、end方法为static后,执行结果:
线程c,start
线程A,end
- synchronized+static synchronized
public class StaticTest {
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public static synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
StaticTest a = new StaticTest();
StaticTest b = new StaticTest();
new Thread(() -> a.start(), "线程c").start();
new Thread(() -> a.end(), "线程A").start();
}
}
执行结果:
线程A,end
线程c,start
synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问。
1.2:Lock
未完待续
二:乐观锁
2.1:AtomicInteger
public class VersionTest {
final static int LOOP = 10000;
int count = 0;
public void add() {
count++;
System.out.println(Thread.currentThread().getName() + " add number :" + count);
}
public void subtract() {
count--;
System.out.println(Thread.currentThread().getName() + " subtract number :" + count);
}
public static void main(String args[]) throws InterruptedException {
VersionTest test = new VersionTest();
for (int i = 0; i < VersionTest.LOOP; i++) {
new Thread(() -> test.add()).start();
new Thread(() -> test.subtract()).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(test.count);
}
}
执行结果:
Thread-19996 add number :1
Thread-19997 subtract number :0
Thread-19998 add number :1
Thread-19999 subtract number :0
Thread-19962 add number :1
Thread-19971 subtract number :0
Thread-19975 subtract number :-1
Thread-19881 subtract number :-2
-2
public class AtomicIntegerTest {
final static int LOOP = 10000;
AtomicInteger count = new AtomicInteger(0);
public void add() {
count.addAndGet(1);
System.out.println(Thread.currentThread().getName() + " add number :" + count.get());
}
public void subtract() {
count.decrementAndGet();
System.out.println(Thread.currentThread().getName() + " subtract number :" + count.get());
}
public static void main(String args[]) throws InterruptedException {
AtomicIntegerTest test = new AtomicIntegerTest();
for (int i = 0; i < AtomicIntegerTest.LOOP; i++) {
new Thread(() -> test.add()).start();
new Thread(() -> test.subtract()).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(test.count.get());
}
}
执行结果:
Thread-19989 subtract number :0
Thread-19988 add number :1
Thread-19973 subtract number :0
Thread-19990 add number :1
Thread-19999 subtract number :0
Thread-19996 add number :1
Thread-19995 subtract number :0
0
2.2:CAS算法
compare and swap比较与交换,实现原理:
当前主内存变量的值V,线程本地变量预期值A(主内存变量V的副本),线程本地待更新值B。当需要更新变量值的时候,会先获取到内存变量值V然后跟预期值A进行比较,如果相同则更新为B,如果不同,线程本地变量预期值A更新为主内存变量V的值。
2.2.1:ABA问题
当前主内存变量的值V,线程本地变量预期值A,线程本地待更新值B
A1线程、A2线程获取主内存变量100,A1线程将主内存100更新为更新值80,A3线程获取主内存变量80,将主内存80更改为更新值100,此时A2的预期值和主内存比较相等,认为主内存的值没有改变过,更新自己的待更新值。
这儿说一下自己的看法,好多博客说的存钱问题如:
假设有个线程A去判断账户里的钱此时是15,满足条件,直接+20,这时候卡里余额是35.但是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,线程B去执行扫描账户的时候,发现它又小于20,又用过cas给它加了20。
如上的举例论证一度让我怀疑人生,我觉得最后的结果没有问题的,即使加版本号多线程依然会再加20的
链表的例子才是正解:
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B;
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A;
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,C、D丢掉了。
所以场景很重要!
2.2.2:解决ABA问题
即使加版本号多线程依然会再加20的
链表的例子才是正解:
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B;
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A;
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,C、D丢掉了。
所以场景很重要!
2.2.2:解决ABA问题
Java并发包提供了一个带有标记的原子引用类AtomicStampedReference,它通过控制变量值的版本来保证CAS的正确性。每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。