并发编程之同步锁_为机器舞的博客-程序员宅基地

技术标签:   synchronized  高并发  1024程序员节  并发编程  

并发编程之同步锁

1 线程的安全性

并发给我们带来的问题就是,当多个线程操作同一个数据的时候,往往不能得到我们预期的结果,造成这个问题的原因是什么呢?其实就是该数据对多个线程没有可见性,这些线程不能有序性的去操作这个公共的数据,操作数据时还不是原子操作,所以导致预期结果不一致。因此我们可以总结出线程的安全性的三个体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Synchronized、AtomicXXX、Lock)。
  • 可见性:一个线程对主内存进行了修改,可以及时被其他线程观察到(Synchronized、volatile)。
  • 有序性:如果两个线程不能从 happens-before原则观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(Synchronized、volatile)。

1.1 原子性问题

在下面的代码中,演示了两个线程分别去调用demo.incr()方法来对变量i进行叠加,预期结果应应该是20000,但实际结果却是小于等于20000的值。

 public class Demo {
    
    int i = 0;
    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(() -> {
     
              	// 每个线程跑10000次
				for (int k=0;k<10000;k++) {
     
	            	demo.incr();
                }
			});
            threads[j].start();
        }
        try {
    
            threads[0].join();
            threads[1].join();
        } catch (InterruptedException e) {
    
            e.printStackTrace();
		}
        System.out.println(demo.i);
    }
}

1.1.1 问题的原因

这个就是典型的线程安全问题中原子性问题的体现了。

在上面的代码中,i++是属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令来组成,i++最终会生成3条指令,通过javap -v xxx.class查看字节码指令如下:

 public incr()V
   	L0
    LINENUMBER 13 L0
    ALOAD 0
    DUP
    GETFIELD com/gupaoedu/pb/Demo.i : I     // 访问变量i
    ICONST_1                                // 将整形常量1放入操作数栈
		IADD                                    // 把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
    PUTFIELD com/gupaoedu/pb/Demo.i : I     // 访问类字段(类变量),复制给Demo.i这个变量

这三个操作如果要满足原子性,那就需要保证线程在执行这个指令时,不允许其他线程干扰。

1.1.2 图解问题本质

一个CPU核心在同一时刻只能执行一个线程,如果线程数量远远大于CPU核心数,那么就会发生线程的切换,这个切换动作可以发生在任何一个CPU指令执行完之前。

对于i++这三个cpu指令来说,如果线程A在执行指令1之后,做了线程切换,切换到了线程B,线程B同样执行i++的三个CPU的指令,执行顺序如下图所示,就会导致结果是1,而不是2。

请添加图片描述

这就是在多线程环境下,存在的原子性问题,那么应该怎么解决这个问题呢?

从上面的图中可以看出,表面上是多个线程对于同一个变量的操作,实际上是i++这行代码它不是原子性的,所以才导致了在多线程环境下出现这样的问题。

也就是说,我们只需要保证i++这个指令运行期间,在同一时刻只能由一个线程来访问,就可以解决这个问题了。所以我们需要用到同步锁Synchronized来解决。

2 Synchronized的基本应用

Synchronized有三种加锁方式,不同的修饰类型,代表锁的控制粒度:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前需要获得当前实例的锁。
  • 静态方法,作用于当前类对象加锁,进入同步代码前需要获得当前类对象的锁。
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前需要获得给定对象的锁。

2.1 锁的实现模型理解

Synchronized能帮我们做些什么呢?为什么能解决原子性问题?

在没有加锁之前,多个线程去调用incr()方法时,没有任何限制,可以同时拿到这个i的值进行i+1操作,但是当加了Synchronized锁之后,线程A和线程B就由并行执行变成了串行执行了。

请添加图片描述

2.2 Synchronized的原理

Synchronized是如何实现锁的?锁的信息是存储在哪里?就拿上面的图来说,线程A抢到了锁,线程B怎么知道当前的锁已经被抢占了,这个地方一定会有一个标记来实现,而这个标记一定存储在某个地方。

2.2.1 Markword对象头

Markword是对象头的意思,简单理解,就是一个对象,在JVM内存中的存储形式。

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

请添加图片描述

  • mark-word:对象标记字段占4个字节,用于存储一些列的标记位,比如:哈希值、轻量级锁的标记位、偏向锁的标记位、分代年龄等。
  • Klass Pointer:Class对象的类型指针,Jdk1.8默认开启指针压缩后为4个字节,关闭压缩指针后(-xx:-UseCompressedOops),长度为8字节。其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址。
  • 对象实际数据:包括对象的所有成员变量,大小由各个成员变量决定,比如byte占1个字节、int占4个字节。
  • 对齐:最后这段空间补全并非必须,仅仅为了起到占位符的作用。由于Hotspot虚拟机的内存管理系统要去对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数。因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

2.2.2 通过ClassLayout打印对象头

为了更加直观的看到对象的存储和实现,我们使用JOL查看对象的内存布局。

  • 添加jol依赖

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>
    
  • 编写测试代码,在不加锁的情况下,打印对象头信息

public class Demo {
    
    Object o=new Object();
    public static void main(String[] args) {
    
				Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
        		System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    }
}
  • 输出内容
com.test.Demo 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 Demo.o
(object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

2.3 Synchronized锁的升级

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁主要存在四个状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。

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

请添加图片描述

  • 默认情况下偏向锁是开启状态,偏向的线程ID是0,偏向一个Anonymous BiasedLock。

  • 如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把markword的线程ID改为当前抢占锁的线程ID。

  • 如果有线程竞争,这个时候会撤销偏向锁,升级为轻量级锁,线程在自己的栈帧中会创建一个LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢占到锁。

  • 如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数设置),或者自旋线程数超过CPU核心数的一倍,在1.6之后,加入了自适应自旋Adapative Self Spinning,JVM会根据上次竞争的情况来自动控制自旋的时间。

  • 升级到重量级锁,向操作系统申请资源,然后线程被挂起进入到等待队列。

2.3.1 轻量级锁的获取

我们通过例子演示一下,通过加锁之后打印对象布局信息,来关注对象头里面的变化。

public class Demo {
    
	Object o=new Object();
    public static void main(String[] args) {
    
		Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
      	System.out.println(ClassLayout.parseInstance(demo).toPrintable()); 
      	synchronized (demo){
    
    		System.out.println(ClassLayout.parseInstance(demo).toPrintable());
		}
	} 
}

得到的对象布局信息如下

 // 在未加锁之前,对象头中的第一个字节最后三位为 [001], 其中最后两位 [01]表示无锁,第一位[0]也表示无锁
com.test.Demo 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 Demo.o
(object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
// 下面部分是加锁之后的对象布局变化
// 其中在前4个字节中,第一个字节最后三位都是[000], 后两位00表示轻量级锁,第一位为[0],表示当前不是偏向锁状态。
com.gupaoedu.pb.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)													 d8 f0
d5 02 (11011[000] 11110000 11010101 00000010) (47575256)
      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 Demo.o
(object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Process finished with exit code 0

这里会有疑惑,不是说锁的升级是基于线程竞争情况来实现升级,从偏向锁到轻量级锁再到重量级锁的吗?为什么这里没有线程竞争,他的锁标记是轻量级锁呢?答案需要在偏向锁的获取以及原理里寻找。

2.3.2 偏向锁的获取

默认情况下,偏向锁的开启有个延迟,默认是4s,为什么要这么设计呢?

因为JVM虚拟机自己有一些默认启动的线程,这些线程里有很多的Synchronized代码,这些Synchronized代码在启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁升级和撤销,效率较低。

通过JVM参数-XX:BiasedLockingStartupDelay=0可以将延迟设置为0。

再次运行代码

public class Demo {
    
	Object o=new Object();
    public static void main(String[] args) {
    
		Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
      	System.out.println(ClassLayout.parseInstance(demo).toPrintable()); 
      	synchronized (demo){
    
    		System.out.println(ClassLayout.parseInstance(demo).toPrintable());
		}
	} 
}

得到如下对象布局,可以看到对象头的高位第一个字节最后三位数为[101],表示当前为偏向锁状态。

 com.test.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00
00 00 (00000101 00000000 00000000 00000000) (5)                            
		  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 Demo.o
(object)
 Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
com.gupaoedu.pb.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
			0     4 									 (object header)													 05 30
4a 03 (00000101 00110000 01001010 00000011) (55193605) 
   		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 Demo.o
(object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

这里的第一个对象和第二个对象的锁状态都是101,是因为偏向锁打开状态下,默认会有配置匿名的对象获得偏向锁。

2.3.3 重量级锁获取

在竞争比较激烈的情况下,线程一直无法获取到锁的时候,就会升级到重量级锁。

下面的案例,通过两个线程来模拟竞争的场景。

 public static void main(String[] args) {
    
    Demo testDemo = new Demo();
    Thread t1 = new Thread(() -> {
    
        synchronized (testDemo){
    
        	System.out.println("t1 lock ing");
 			System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
        }
    });
    t1.start();
    synchronized (testDemo){
    
        System.out.println("main lock ing");
        System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
    }
}

从结果可以看出,在竞争的情况下锁的标记为[010],其中标记[10]表示重量级锁。

 com.test.Demo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)													 8a 20 5e 26
(10001010 00100000 01011110 00100110) (643702922)
      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        (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
com.gupaoedu.pb.Demo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)													 8a 20 5e 26
(10001010 00100000 01011110 00100110) (643702922)
      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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

3 CAS

CAS在Synchronized的底层用的比较多,他的全程有两种:

  • Compare and swap
  • Compare and exchange

就是比较并交换的意思,它可以保证在多线程环境下对于一个变量修改的原子性。

CAS原理很简单,包含三个值:当前内存值(V)、预期原来的值(E)、期待更新的值(N)。

请添加图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_36401695/article/details/120941759

智能推荐

上交大 2011 二次方程计算器_dixiang1123的博客-程序员宅基地

题目:输入关于x的二次方程表达式(系数为整数),输出两个解(由小到大输出,保留两位小数);如果无解,则输出“No Solution”。思路:先确定等号位置,分成左右两个字符串,从中分别提取系数,再综合。然后求解。提取系数的过程如图:过程:介绍两个函数:atoi()函数可以识别"+""-"号,并正确转化成int。string str="-2980";string s...

只重写equals()但不重写hashCode会有什么后果?_1024276449的博客-程序员宅基地_不重写hashcode有什么影响

只重写equals()但不重写hashCode会有什么后果?1.如果判断两个数如果hashCode相同则equals不一定相同,反而equals相同则hashCode则一定相同。2.那么我们只重写equals()但不重写hashCode会有什么后果?如果我们不将我们重写equals方法的类放到HashSet等散列表中时则不会有什么影响,但如果放到我们的散列表中时我们的散列表则会优先比较HashCode所以可能会产生错误。...

Struts2中的设计模式_7潜伏7的博客-程序员宅基地_struts2设计模式

设计模式(Design pattern)是经过程序员反复实践后形成的一套代码设计经验的总结。设计模式随着编程语言的发展,也由最初的“编程惯例”逐步发展成为被反复使用、并为绝大多数程序员所知晓的、完善的理论体系。我们使用设计模式(Design pattern)的初衷,是使代码的重用度提高、让代码能够更容易被别人理解以及保证代码的可靠性。毫无疑问,在程序中使用设计模式无论是对于程序员自身还是对于应用程

Assemble UVALive - 3971 组装电脑_Nicolas Lee的博客-程序员宅基地

Recently your team noticed that the computer you use to practice for programming contests is notgood enough anymore. Therefore, you decide to buy a new computer. To make the ideal computer ...

ubuntu 16.06 编译 vlc for android_qq_15361657的博客-程序员宅基地

1、https://www.ubuntu.com/download      下载 ubuntu16.042、vmware workstation配置虚拟机3、下载android studio,下载sdk ndk4、配置sdk,ndk5、配置bash.profile6、git7、sh compile.sh

如何发布ArcGIS Server离线地图(google 瓦片)_weixin_44922969的博客-程序员宅基地

说明本案例实现内容:GoogleEarth瓦片地图的获取、在ArcGIS Server Manger中发布下载好的影像瓦片数据。工具准备1、BIGEMAP地图下载器http://www.bigemap.com/reader/download/2、ARCGIS10.2 http://pan.baidu.com/s/1i5uMzU93、ARCGIS SERVE...

随便推点

cf 1154G Minimum Possible LCM_二分抄代码的博客-程序员宅基地

...这题关键在他的a[i]&lt;=1e7那么我们知道lcm(a,b)=a*b/gcd(a,b);那么我们只要枚举每一个因数d,不管他是不是gcd然后找出能被这个d整除的最小的两个数字a,b那么对于这个因数d,tmp=a*b/d,ans=min(ans,tmp)由于我们枚举了1-1e7所有的质因子,所以就算a*b/d不是lcm,但之后总会枚举到a*b/gcd(a,b)而使...

java excel 加密_Java 加密/解密Excel_丸子里里的博客-程序员宅基地

概述设置excel文件保护时,通常可选择对整个工作簿进行加密保护,打开文件时需要输入密码;或者对指定工作表进行加密,即设置表格内容只读,无法对工作表进行编辑。另外,也可以对工作表特定区域设置保护,即设置指定区域可编辑或者隐藏数据公式,保护数据信息来源。无需设置文档保护时,可撤销密码保护,即解密文档。下面,将通过java程序演示以上加密、解密方法的实现。示例大纲1. Excel工作簿1.1 加密工作...

Spring Cloud 核心组件 Dubbo-Nacos_m0_37567301的博客-程序员宅基地

Spring Cloud 核心组件 Dubbo-Nacos作者:DecaMinCow博客:http://blog.csdn.net/m0_37567301邮箱:decamincow#gmail.com (#-&gt;@)Dubbo 介绍阿里研发的 RPC 框架注册中心为 nacos 的 dubbo 示例1. Provider依赖文件&lt;dependency&gt; &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt; &l

android接收list对象数组,Android - ToDoList(定制ArrayAdapter)_席佳益的博客-程序员宅基地

ToDoList(定制ArrayAdapter)本文地址:http://blog.csdn.net/caroline_wendy/article/details/21401907前置项目参见:http://blog.csdn.net/caroline_wendy/article/details/21330733环境: Android Studio 0.5.1ArrayAdapter使用泛型(模...

STS on Eclipse 3.6_咔啡的博客-程序员宅基地

EngineeringChristian DupuisJuly 01, 2010Last week the Eclipse Foundation released the much anticipated next version of Eclipse. You can download Eclipse 3.6 aka Helios from SpringSource’s member distribution page. Also check out the New &amp; Noteworthy

js读取服务器html文件,【未解决】js中将html内容保存到服务器上的本地的html文件..._Arsd的博客-程序员宅基地

【背景】之前已经实现了:网页中,点击某个按钮,可以调用到js获得到KindEditor的html的内容:function submitGoodsContent(){var kindeditor = window.editor;// 取得HTML内容html = kindeditor.html();console.log(html);}商品名:在此输入新产品的介绍内容提交当前页面现在希望实现,不仅仅...

推荐文章

热门文章

相关标签