多线程二-同步锁
关于线程安全问题的简述
多个线程做同一件事的时候
- 原子性:Syncronized,AtomicXXX,Lock
- 可见性:Syncronized,volatile
- 有序性:Syncronized,volatile
原子性问题
代码演示了两个线程分别调用incr()方法来对i进行累加,预期结果应该是20000,但是实际结果却是小于等于20000的值,这就是线程安全问题中原子性的体现。在这段代码中i++属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令组成。通过javap -v Demo.class
查看字节码指令如下:
public void incr();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #7 // Field i:I 访问变量i
5: iconst_1 //将整形常量1放入操作数栈
6: iadd //把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
7: putfield #7 // Field i:I 访问类变量复制给demo.i这个变量
10: return
LineNumberTable:
line 6: 0
line 7: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/caozz/demo2/thread/Demo;
这三个操作,如果要满足原子性,那么就需要保证某个线程在执行这个指令时,不允许其他线程干扰。然后实际上,确实存在该问题。简单来说就是将变量i加载后,被切换到其他线程,导致的问题。
代码如下:
package com.caozz.demo2.thread;
public class Demo {
int i;
public void incr(){
i++;
}
public static void main(String[] args) {
Demo demo = new Demo();
Thread[] threads = new Thread[2];
for (int j = 0; j < 2; j++) {
threads[j] = new Thread(() -> { //创建两个线程
for (int k = 0; k < 10000; k++) { //每个线程跑10000次
demo.incr();
}
});
threads[j].start();
}
try {
threads[0].join();
threads[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(demo.i);
}
}
Java中的同步锁syncronized
Markword对象头
对象在堆内存中的存储分布
- 对象标记,也就是markword对象头,四个字节,用于存储一些列的标记位,比如哈希值,锁信息,分代年龄 等
- 类元信息,即Klass Pointer,jdk8默认开启指针压缩后为4字节,可以使用参数
-XX:-UseCompressedOops
关闭指针压缩,关闭后长度为8位,其指向的位置是对象对应的class对象的内存地址 - 实例数据:包括对象的所有成员变量,大小由各个成员变量决定
- 对齐填充:并非必须,起到占位符作用。由于hotspot虚拟机的内存管理系统要求对象起始地址必须是8字节的整数倍,当对象实例数据部分没有对齐的话需要对其填充来补全。
markword分布
通过ClassLayout打印对象头
- 添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 测试代码
package com.caozz.demo2.thread;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class ClassLayoutTest {
Object obj = new Object();
public void testLock(){
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ClassLayoutTest classLayoutTest = new ClassLayoutTest();
System.out.println(ClassLayout.parseInstance(classLayoutTest).toPrintable());
System.out.println("-----------------------------------------------");
ClassLayoutTest classLayoutTest02 = new ClassLayoutTest();
new Thread(() -> {
classLayoutTest02.testLock();
}).start();
new Thread(() -> {
classLayoutTest02.testLock();
}).start();
new Thread(() -> {
classLayoutTest02.testLock();
}).start();
System.out.println(ClassLayout.parseInstance(classLayoutTest02).toPrintable());
}
}
- 结果
com.caozz.demo2.thread.ClassLayoutTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 18 c0 00 (00000000 00011000 11000000 00000000) (12589056)
12 4 java.lang.Object ClassLayoutTest.obj (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-----------------------------------------------
com.caozz.demo2.thread.ClassLayoutTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) a2 00 01 d1 (10100010 00000000 00000001 11010001) (-788463454)
4 4 (object header) c4 01 00 00 (11000100 00000001 00000000 00000000) (452)
8 4 (object header) 00 18 c0 00 (00000000 00011000 11000000 00000000) (12589056)
12 4 java.lang.Object ClassLayoutTest.obj (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 分析
锁状态为对象头第一部分的第一个字节后三位,上述结果第一个为001,第二个为010,根据markword分布,可知分别为无锁状态以及重量级锁状态
Synchronized锁升级
jdk1.6对锁的实现引入了大量的优化,如自旋锁,自适应自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销。锁主要存在四种状态:无锁,偏向锁,轻量级锁,重量级锁,他们会随着锁竞争的激烈程度而逐渐升级。这么设计的目的是减少重量级锁带来的性能开销。
默认情况下偏向锁是开启状态,偏向锁是在锁对象的对象头记录当前获取到该锁的线程ID,线程下次再来就可以直接获取锁了。当有第二个线程过来竞争锁,偏向锁就会升级为轻量级锁。轻量级锁底层是通过自旋来实现的,不会阻塞线程。如果自旋次数过多,则会升级为重量级锁,重量级锁会阻塞线程。
自旋锁是线程通过CAS获取预期的一个目标,如果没有获取到则循环获取,获取到了则表示获取到了锁。这个过程线程一直在运行相对而言没有使用太多的操作系统资源,比较轻量。
偏向锁的开启有个4秒的延迟,这么设计的原因是因为jvm自己有一些默认启动的线程。如果这时候就使用偏向锁,会在成偏向锁不断的升级和撤销,效率极低。当然,延迟也是可以通过参数设置-XX:BiasedLockingStartupDelay=0
CAS机制
CAS,Compare And Swap
,或compare and exchange
,compare and set
,比较交换的意思。它可以保证在多线程环境下对于一个变量修改的原子性。
原理如下图:
通过查看源码,可以知道它是一个native方法,然后去查看jvm源码unsafe.cpp:
cmpxchg:compare and exchange
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
cmpxchg的原子性 底层也是通过锁来保证的:atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Atomic实现原子性
由源码可以知道,他也是一个不断自旋来实现的
public final int getAndSet(int newValue) {
//private static final Unsafe U = Unsafe.getUnsafe();
return U.getAndSetInt(this, VALUE, newValue);
}
@IntrinsicCandidate
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, newValue));
return v;
}