如果多个线程在做同一件事情,或者共享同一个资源变量时,就会产生线程安全星问题,此时就需要保证三要素:
用一个案例来说明原子性问题,在下面的案例中,我们创建了两个线程,都去调用incr()方法来对i这个变量进行叠加,每个线程都调用10000次,预期的结果是20000,但是实际结果却是小于20000的值
- /**
- * 原子性问题
- */
- public class StomicityDemo {
- int i = 0;
-
- public void incr() {
- i++;
- }
-
- public static void main(String[] args) {
- StomicityDemo demo = new StomicityDemo();
- Thread[] threads = new Thread[2];
- for (int j = 0; j < threads.length; j++) {
- // 创建两个线程
- threads[j] = new Thread(()->{
- // 每个线程跑10000次
- for (int i = 0; i < 10000; i++) {
- demo.incr();
- }
- });
- threads[j].start();
- }
- try {
- threads[0].join();
- threads[1].join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(demo.i);
- }
- }
- 复制代码
这个就是典型的线程安全问题中原子性问题的体现,就如原子性的定义说的,当一个线程在执行的过程中不被其他线程打断,我们得到的值应该就是20000,但是现在小于20000,说明其中一个线程在执行的过程,被其他线程打断了,导致结果不是预期值,具体是怎么被打断的呢?
在上面的代码中,i++是属于java高级语言中的编程指令,而这些指令最终可能会有多条cpu指令来组成,我们通过javap -v StomicityDemo.class查看字节码指令如下:
- public incr()V
- L0
- LINENUMBER 13 L0
- ALOAD 0
- DUP
- GETFIELD zhl.thread.threadDemo1/StomicityDemo.i : I // 访问变量i
- ICONST_1 // 将整形常量1放入操作数栈
- IADD // 把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
- PUTFIELD zhl.thread.threadDemo1/StomicityDemo : I // 访问类字段(类变量),复制给Demo.i这个变量
- 复制代码
这三个操作,如果满足原子性,那么就需要保证某个线程在执行这个指令时,不允许其他线程干扰,然而实际上却存在这个问题
前面我们说过,一个cpu核心在同一时刻只能执行一个线程,如果线程数量远远大于cpu的核心数,就会发生线程的切换,这个切换动作可以发生在任何一条cpu指令执行完之前。
对于i++这三个cpu指令来说,如果线程A先拿到执行权限,执行指令GETFIELD将i=0加载到寄存器中后,做了线程切换,假设切换到了线程B,此时线程B同样执行指令GETFIELD将i=0加载到寄存器中,然后线程B按下图顺序执行剩余指令,执行完后再切换到线程A中,此时线程A接着之前未执行完的指令继续往下执行,就会导致最终的结果时1,而不是2.

这就是在多线程环境下,存在原子性问题,那么怎么解决呢?
认真观察上面这个图,表面上是多线程对于同一个变量的操作,实际上是 i++ 这行代码,它不是原子的,所以才导致多线程环境下出现这个问题。
也就是说我们只要保存,i++这个指令在运行期间,在同一时刻只能由一个线程来访问,就可以解决这个问题。可以加Synchronized这个关键字来解决。
Synchronized由三种方式来实现加锁,不同的修饰类型,代表锁的控制粒度。
修饰实力方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
- /**
- * Synchronized修饰实例方法
- */
- public class SynchronizedDemo {
- // 共享资源
- static int num = 0;
-
- // 修饰实力方法
- public synchronized void incr() {
- num++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- SynchronizedDemo demo1 = new SynchronizedDemo();
- SynchronizedDemo demo2 = new SynchronizedDemo();
-
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo1.incr();
- }
- });
-
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo2.incr();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(num);
- }
- }
- 复制代码
以上代码num的输出小于20000,虽然使用了synchronized修改了incr()方法,但是是通过不同的实例demo1和demo2来调用的incr()方法,这就意味着存在两个不同实例的对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2使用的是不同的对象锁,因此线程安全是无法保证的。
-
- /**
- * Synchronized修饰实例方法
- */
- public class SynchronizedDemo1 {
- // 共享资源
- static int num = 0;
-
- // 修饰实例方法
- public synchronized void incr() {
- num++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- SynchronizedDemo1 demo = new SynchronizedDemo1();
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo.incr();
- }
- });
-
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo.incr();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(num);
- }
- }
-
- 复制代码
以上代码num的输出结果为20000,synchronized修饰了incr方法,线程t1和t2在执行时,是通过同一个实例对象demo来调用的incr方法,两个线程调用的是同一实例锁,其中一个线程拿到锁后另一个线程无法进入到实例方法,保证了线程的安全性问题。
静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
- **
- * Synchronized修饰实例方法
- */
- public class SynchronizedDemo2 {
- // 共享资源
- static int num = 0;
-
- // 作用于静态方法
- public static synchronized void incr() {
- num++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- incr();
- }
- });
-
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- incr();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(num);
- }
- }
- 复制代码
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。
- /**
- * Synchronized修饰代码块
- */
- public class SynchronizedDemo1 {
- // 共享资源
- static int num = 0;
-
- // 修饰代码块
- public void incr() {
- // this,当前实例对象锁
- // synchronized (this){
- // num++;
- // }
- // class对象锁
- synchronized (SynchronizedDemo1.class){
- num++;
- }
-
- }
-
- public static void main(String[] args) throws InterruptedException {
- SynchronizedDemo1 demo1 = new SynchronizedDemo1();
- SynchronizedDemo1 demo2 = new SynchronizedDemo1();
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo1.incr();
- }
- });
-
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 10000; i++) {
- demo2.incr();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(num);
- }
- }
- 复制代码
如果传入的是this,表示修饰的是当前实例对象锁,如果两个不同的对象demo1和demo2调用incr方法,存在线程安全问题
如果我们传入的是SynchronizedDemo1.class,表示class对象锁,此时两个不同的对象demo1和demo2调用incr方法,如果一个线程拿到锁,另一个线程就需要等待,不存在线程安全问题。
当前我们也可以传入其他,比如new一个新的对象等等。
- // 实例锁
- Object object = new Object();
- synchronized (object){
- num++;
- }
- // class锁
- synchronized(Lock.class){
- num++;
- }
- 复制代码
总之,是否能保证线程的原子性,主要取决于synchronized的作用范围。
synchronized到底帮我们做了什么,为什么能解决原子性。
在没有加锁之前,多个线程去调用incr()方法时,没有任何限制,都是可以同时拿到这个i值进行++操作的,但是当加了synchronized锁之后,线程A和B就由并行执行变成了串行执行。

synchronized时如何实现锁的,以及锁的信息存储在哪里?就拿上图来说,如果线程A拿到锁,线程B怎么知道当前锁被占了,这个地方一定会有一个标记来实现,而这个标记一定存储在某个地方。
synchronized加锁在对象上,锁标记就在这个对象的对象头(markword)中。

在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)

为了更加直观的看到对象的存储和实现,我们可以使用jol查看对象的内存布局。
添加jol依赖
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.9</version>
- </dependency>
- 复制代码
编写单元测试,在不加锁的情况下,对象头信息打印
- public class LayoutDemo {
- Object o = new Thread();
-
- public static void main(String[] args) {
- LayoutDemo layoutDemo = new LayoutDemo();
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- }
- }
- 复制代码
输入内容如下
- zhl.thread.threadDemo1.LayoutDemo 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 java.lang.Object LayoutDemo.o (object)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-
- 复制代码
在jdk1.5之前一直是一种重量级锁的状态,也就是每次加锁和释放锁都直接和操作系统打交道。
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级,需要的注意的是,锁的升级是不可逆的。

这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无所的实现。

接下来,我们通过下面的例子来演示一下,通过加锁之后继续打印对象布局信息,来关注对象头里面的变化。
- public class LayoutDemo {
- Object o = new Thread();
-
- public static void main(String[] args) {
- LayoutDemo layoutDemo = new LayoutDemo();
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- synchronized (layoutDemo){
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- }
- }
- }
- 复制代码
得到的对象布局信息如下,对照上面markword状态变化图
- // 在未加锁之前,对象头中的第一个字节最后三位为 [001], 其中最后两位 [01]表示无锁,第一位[0]也表示无锁
- zhl.thread.threadDemo1.LayoutDemo 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 java.lang.Object LayoutDemo.o (object)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- // 下面部分是加锁之后的对象布局变化
- // 其中在前4个字节中,第一个字节最后三位都是[000], 后两位00表示轻量级锁,第一位为[0],表示当前不是偏向锁状态。
- zhl.thread.threadDemo1.LayoutDemo object internals:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) c8 f2 7f 56 (11001000 11110010 01111111 01010110) (1451225800)
- 4 4 (object header) 0c 00 00 00 (00001100 00000000 00000000 00000000) (12)
- 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 java.lang.Object LayoutDemo.o (object)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 复制代码
按上面说的,锁的升级是基于线程竞争情况,来实现从偏向锁到轻量级锁再到重量级锁的升级,但是此处明明没有线程的竞争,为什么是轻量级锁呢?
默认情况下,偏向锁的开启是有延迟的,默认是4秒。这么设计的目的是因为jvm虚拟机自己有一些默认的启动线程,这些线程里面有很多的synchronized代码,这些synchronized代码启动的时候就会出发竞争,如果使用偏向锁,就会造成偏向锁不断进行锁升级和撤销,效率低下。
通过这个jvm参数可以将延迟设置为0
-XX:BiasedLockingStartupDelay=0
再次运行下面代码
- public class LayoutDemo {
- Object o = new Thread();
-
- public static void main(String[] args) {
- LayoutDemo layoutDemo = new LayoutDemo();
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- synchronized (layoutDemo){
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- }
- }
- }
- 复制代码
得到如下的对象布局,可以看到对象头中的高位第一个字节最后三位数为【101】,表示当前为偏向锁。
这里的第一个对象和第二个对象的锁状态都是101,是因为偏向锁打开状态下,默认会有配置匿名的对象获取偏向锁
- zhl.thread.threadDemo1.LayoutDemo object internals:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) 01 00 00 00 (00000101 00000000 00000000 00000000) (1)
- 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
- 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 java.lang.Object LayoutDemo.o (object)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
-
- zhl.thread.threadDemo1.LayoutDemo object internals:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) c8 f2 7f 56 (00000101 00110000 01001010 00000011) (1451225800)
- 4 4 (object header) 0c 00 00 00 (00001100 00000000 00000000 00000000) (12)
- 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 java.lang.Object LayoutDemo.o (object)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 复制代码
在竞争激烈的情况下,线程一直无法获得锁得情况下,就会升级到重量级锁。
通过两个线程来模拟竞争得场景
- public class LayoutDemo {
- public static void main(String[] args) {
- LayoutDemo layoutDemo = new LayoutDemo();
- Thread t1 = new Thread(()->{
- synchronized (layoutDemo){
- System.out.println("t1 lock ing");
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- }
- });
- t1.start();
- synchronized (layoutDemo){
- System.out.println("main lock ing");
- System.out.println(ClassLayout.parseInstance(layoutDemo).toPrintable());
- }
- }
- }
- 复制代码
从结果可以看出,在竞争的情况下锁的标记为 [010] ,其中所标记 [10]表示重量级锁
- main lock ing
- zhl.thread.threadDemo1.LayoutDemo object internals:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) 7a c6 a0 a1 (01111010 11000110 10100000 10100001) (-1583298950)
- 4 4 (object header) fd 01 00 00 (11111101 00000001 00000000 00000000) (509)
- 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 (loss due to the next object alignment)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
- t1 lock ing
- zhl.thread.threadDemo1.LayoutDemo object internals:
- OFFSET SIZE TYPE DESCRIPTION VALUE
- 0 4 (object header) 7a c6 a0 a1 (01111010 11000110 10100000 10100001) (-1583298950)
- 4 4 (object header) fd 01 00 00 (11111101 00000001 00000000 00000000) (509)
- 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
- 12 4 (loss due to the next object alignment)
- Instance size: 16 bytes
- Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 复制代码
CAS这个在synchronized底层用的非常多,他的全称有两种
就是比较并交换的意思。它可以保证在多线程环境下对于一个变量修改的原子性。
CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(E)以及期待更新的值(N)。
