java高并发编程系列三:线程安全与数据同步

前言:线程安全和数据同步

线程安全和数据同步还有锁,它是多线程中最复杂也是最重要的内容之一,在串行化的任务执行过程中,由于不存在资源的共享,线程安全的问题几乎不用考虑,但是串行化的程序,运行效率低下,不能最大化的利用CPU的计算能力,随着CPU的核数增加和计算速度的提升,串行化的任务执行显然是对资源的一种极大浪费,例如,B用户提交一个业务请求,只有等到A用户处理结束了才能操作,这样用户体验太差了。

无论是互联网还是企业级系统,在追求稳定的同时也在追求更高的吞吐量,对系统的开发者也提出了更大的要求,同时多线程的引入也带来了共享资源安全的隐患。

什么是共享资源呢,共享资源指的是多个线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就称为共享资源,如何保证多个线程访问到的资源是一致的,则被称为数据同步或者资源同步。

一。数据同步

1 数据不一致的引入

例子:叫号机程序,当业务受理数最大为500时,多次运行车程序,会出现3类数据不一致的问题:

a某个号码被忽略,线程的执行是由CPU时间片轮询调度的,当此时线程A和线程B都执行到了index=65的位置,其中线程A将index值修改为66未输出之前,进行等待,线程B获得执行,直接将index累加到了67,那么66值就被忽略了。

b某号码出现了多次,线程A执行index+1=55,等待,B获取执行权,由于A并没有更新index 的值,导致结果又出现55,所以出现了重复号码的情况。

c号码超过了最大值,当线程A,B都达到index=499的情况,并且都通过条件满足进入了执行增加逻辑,A短暂等待,B增加到500,B继续执行将500增加到了501,所以出现超出最大值的情况

2.同步关键字synncchronized

以上问题均是线程对共享资源同时操作中引起的,1.5以前引入的syncchronized,它提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。

jdk官方解释:synncchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么该对象的读写都将通过同步的方式进行。具体表现如下:

a.synncchronized提供了一种锁机制,能够确保共享变量的互斥访问,防止数据不一致的出现。

b.synncchronized关键字包括monitor enter和monitor exit 两个JVM指令,他能够保证任何时候线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是在缓存中,在monitor运行成功之后,共享变量被更新后的值必须刷入主内存中。

c。synncchronized必须严格遵守java happens-before规则,一个monitor exit执行成功之前必须monitor enter

synncchronized关键字可以用于代码块或方法进行修饰,而不能对class和变量进行修饰。

示例:

同步方法

同步代码块

修改叫号机程序输出正常:

3。深入syncchronized关键字

a.线程堆栈分析

syncchronized关键字提供一种互斥机制,也就是说在同一时刻只能有一个线程访问同步资源,执行的线程会获取与mutex关联的monitor锁,运行例子:

JVM命令如下:

在jconsole顶部可以看到进程号

使用jps命令查看java的个进程号

使用jstack 18028 >>123.txt 输出到文件。

如图,这里一般有两个运行参数,用来拍取内存快照,

他们的含义如下:

-l long listings,会打印出额(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况

-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)

以上代码中5个线程调用了accessResource,由于同步代码块互斥的原因,只有一个线程获取了mutex monitor的锁,其他线程进入阻塞状态,等待monitor锁的释放,只有一个线程是TIMED_WAITING,其他都是BLOCKED.

jstack 查看堆栈信息,Thread-0持有monitor的锁并处于休眠中(不会丢锁),那么其他线程无法进入这个方法中。

使用javap命令可以对Mutex进行反汇编,输出大量JVM类的指令(线程直接加载的指令),在指令中 monitor enter 和monitor exit是成对出现的,有事一个enter多个exit,一个exit走之前肯定对应的enter。

1>.Monitorenter

每个对象都有一个monitor关联,一个monitpr的lock的锁只能被一个线程在同一时间获得,住在一个线程尝试获得与对象关联的monitor的所有权可能出现的状况:

a如果monitor的计数器为0,标识还没有线程获得monitor的lock,某个线程得到以后计数加1,该线程获得了执行权。

b如果一个已经拥有monitor 的线程执行权重入,导致monitor计数器再次增加。

c如果monitor已经被其他线程拥有,其他线程获取该monitor锁时,会被陷入阻塞状态,知道monitor计数器变为0,才会再次尝试获取monitor的所有权。

2>monitorexit:

想要释放某个对象关联的monitor锁前提是获得了这个锁,释放的过程就是计数器减1,如果计数器的结果为0 ,那么该线程不在有锁的执行权,就是解锁。

4.使用syncchrnized需要注意的地方

a 与monitor锁关联的对象不能为空,每一个对象和monitor关联的对象都为null,monitor也无从谈起。

b synchromized 作用域太大

由于synchronized关键字存在排他性,所有的线程必须串行的经过synchronized保护的共享区域,如果synchronized的作用域越大,则代表的其效率越低,甚至会丧失并发的优势,

示例如下:以下的代码对整个线程的执行单元都进行了synchronized同步,从而丧失了并发的能力,这是不可取的,synchronized关键字应该尽可能的只作用于共享资源(数据的)读写区域。

c 以下构造了5个线程,也构造了5个Runable实例,Runnable实例作为逻辑执行单元传给Thread,然后,synchronized根本互斥不了与之对应的作用域,线程之间进行monitor lock 争抢只能发生在与monitor关联的同一个引用上,以下代码每一个线程的monitor的引用都是独立的,因此不能起到互斥的作用。5个实例5个新的monitor锁,所以不会互斥,如果使用同一个把锁变为类级别static。

d 多个锁的交叉锁导致死锁

多个锁的交叉很容易出现线程死锁的情况,程序并没有任何错误输出,但就是不工作。

5.This Monitor和class monitor详细介绍

a this monitor 对象锁

synchronized修饰了同一个实例对象的2个不同方法,与之对应的monitor是什么,2歌monitor是否一致呢

以下2个方法1和2都被synchronized修饰,启动2个线程分别访问1和2

查看jstack堆栈信息:

可见synchronizzed关键字同步类的不同实例方法,争抢的是同一个monitor的lock,而与之关联的引用是ThisMonitor的实例引用。如果method2修改为同步代码块,使用this的moitor,运行效果还是完全一样的。

b class monitor 类锁

运行例子,同一时刻只能有一个线程访问classMonitor的静态方法,使用jstack分析堆栈信息,使用synchronized同步某个类的不停静态方法,争抢的也是同一个monitor的lock锁。查看以下分析获得的monitor锁关联引用的是ClassMonitor.class的class的实例。

如果修改第二个同步代码块的方式,职责使用ClaaMonitor.class的实例作为monitor的lock。

6.程序死锁的原因及诊断。

a 程序死锁

1)交叉锁可导致程序出现死锁

线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁,(典型的哲学家吃面问题),这种情况最容易导致程序的死锁问题。

2)内存不足

当并发请求系统可用内存时,如果此时系统内存不足,则可能出现死锁的情况,例如:2个线程T1和T2,执行某个任务时,其中1已经获得10M内存,2获得了20M内存,如果每个线程的执行单元都需要20M内存,但剩余的内存刚好剩余20M,那么2个线程可能在这里彼此等待对方释放资源造成死锁。

3)一问一答式的数据交换

服务器开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求,仍然在等待一问一答式的数据交换,此时客户端和服务端都在等待双方的数据。

4)数据库锁

无论是数据库锁还是行级别锁,比如某个线程执行update语句退出了事物,其他线程访问该数据库时,都将陷入死锁。

5)文件锁

如果某个线程获得文件锁意外退出,其他读取文件的线程也将会进入死锁直到系统释放文件句柄资源。

6)死循环引起的死锁

程序由于代码原因或者某些异常处理不得当,进入了死循环,虽然查看线程堆栈信息不会发现任何死锁的迹象,但是,程序不工作,CPU占有率又居高不下,这种死锁又被称为系统假死,是最难排查的一种死锁,因为重现困难,进程对系统资源的使用量又达到了极限。想要做出dump的时候也是困难的。

b 程序死锁举例:

交叉不仅指写的代码出现了交叉的情况,如果使用了某个框架或者开源库,由于对源码API的不熟悉,也可能引起死锁,由于使用不当引起的死锁,排查的困难要高一些,所以框架或者开源库需要熟悉的。

还有HashMap不具备线程安全的能力,如果想要使用线程安全的map结构,请使用ConcurrentHashMap或者使用Collections.synchronizedMap来代替。

c 知道死锁的原因,我们可以借助诊断工具进行诊断。

1)交叉引起的死锁,打开jstack或者jconsole等工具,jstack-l PID (打印外锁信息)会直接发现死锁的信息。

一般交叉锁引起的死锁线程都会进入BLOCKED状态,CPU占用资源不高,很容易借助工具来发现。

2)死循环引起的死锁(假死),可以使用jstack,jconsole,jvisualvm工具来查看,但是不会给出明闲的提示,因为工作线程并未BLOCKED,而是出于RUNNABLE,CPU居高不下,甚至不能正好穿那个运行你的命令。

严格意义上来说,死循环会导致在程序假死,算不上真正的四搜索,但是线程对CPU消耗过多,导致其他线程等待CPU,内存资源也会陷入死锁等待。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181205G1R50000?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券