Java多线程编程学习笔记---对线程安全和锁概念的一些理解

本文系原创,转载请注明出处

提到Java多线程开发,有开发经验的下意识的脑海就会浮现Thread,Runnable,ThreadPool之类的
这次的笔记起点并不是多线程,而是多线程环境下的线程安全问题和与之相对应的一个重要关键词
synchronized

一段代码是否正确,是否得当?这是一个一百人有一百个哈姆雷特的问题,笔者随手一段单例模式的代码
pubic class SingleSimple{
private SingleSimple instance;
public static singleSimple getSingleInstance(){
1.if(instance==null){
2.instance=new SingleSimple();
}
3.return instance;
}
}
再经典不过的单例模式,如果看到这,觉得这段代码很Ok啊,很经典啊,没有丝毫问题啊……
那么往下说的一切才是有意义的……

当我们习惯在单线程的情况下或者不管背后实现而使用框架(如Spring)提供的单例实现之后,初看
这代码,的确没什么问题,然而这个例子如果在被多个线程同时访问的时候还能经的起推敲么?

假设一个代码片段:
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
……
SingleSimple s=SingleSimple.getSingleInstance();
……
}
});
Thread t2=new Thread(new Runnable(){
@Override
public void run(){
……
SingleSimple s=SingleSimple.getSingleInstance();
……
}
});
t1.start();
t2.start();

我们取到的s对象真的就是同一个对象么?实则不然!简单分析多线程的逻辑
t1,t2在不做线程控制的状态下,他们是单纯的资源竞争者,竞争的激烈程度就是到每一个java指令
的运行(是单个java指令【一个栈帧】,而不是单条语句,虽然大部分情况下,一条语句大约等同于
一条指令),上述代码被标记了1,2,3三个部分,而t1,t2的直接竞争会极大概率出现一下情况:

t1:hiahia,1部分,我来了~~~
t1:hiahia,居然没人用,那2部分,我来……,哎呀我去,怎么被cpu给踢出去了……,肯定被t2
给顶了!等我杀回来!
t2:老子总算挤进来了!判断1,嗯,还是空!,执行2~!继续,搞定!
t1:mmp,我胡汉三又杀回来了!jvm的控制器告诉我上次进行到2了,好,执行完他……

于是,我们的单例模式被很轻易的攻破了……那么这段代码显然在多线程环境下显然没有达到
我们写代码的初心,对此有一个比较专业的描述:线程不安全

于是,“安全”成为了我们多线程开发的一个目标,而这种安全我们需要分步来看,即变量的可见性和
操作的原子性……
可见性:一个变量被赋予新值到这个变量值被刷新到内存到被取出看到修改后的值,这在jvm的执行中虽然只是一句话,却涵盖了几条指令(回忆前面说到的线程激烈竞争),如果在此期间指令被打断,那么新线程拿到的变量值就不是我们期望的修改后的指,这种情况,我们称之为变量不可见
原子性:仅仅是变量可见得以保证还不足以维持线程的安全,严谨的业务逻辑会要求一段代码的执行必须是串行不可被中断的(此处可以理解为语句上的执行安全),这种情况,我们称之为原子性

至此,锁机制要闪亮登场了,em……再此之前,需要插一个点
volitale关键字:轻量级保证可见性(留个伏笔,以后详述),因为这个关键字可以保证变量的可见性却不能保证原子性,总体上说并不能保证线程的安全……嗯,就先说到这里
如何同时保障可见性和原子性以达到线程安全呢?我们下意识的会想到锁,然后会蹦出两个关键词:synchronized和lock

首先说一说什么是锁,这是JVM中一个抽象的概念的具象化表现,那我们更具象化一些,把一个类看成一个房子,那么类里面的方法就可以看成是一个个房间,而变量(资源)就是房子里面的物品,也是线程们激烈争夺的东西!
在这个具象化的场景中首先再解释下线程不安全,即物品的不安全,在一个没有锁的房子里,物品当然是不安全的!即便这个物品能被看见(可见性)
那么保证物品的安全则必须要对房子或者房间上锁,并且只配发一把钥匙!
这里先说实例化的对象(静态对象是针对类,而实例化后针对的就是一个具体的实例)
synchronized在实例对象中就相当于JVM为这个房子的房间配发的一把锁(包含唯一的钥匙),任何一个房间被上锁(方法被synchronized修饰 )可以看成是整个房子被房门锁上(即对象被锁)。那么没有被修饰的房间呢?可以看成这个房间对外开了一个没有锁的窗子,所以激烈竞争的线程们依然可以登堂入室(当然仅限于这个房间)。那么一个线程安全的物品必然只能是通过解锁打开房门后才能获取到。
那么解释锁的互斥则可以套接这个场景……
现在我们来定义一个房子模型:
public Class House{
private int a=0;
private int b=0;
public void synchronized roomA(){
//something todo for a;
}
public void synchronized roomB(){
//something todo for b;
}
}
一个两居室的房子,房间A,B。分别安全的存放了物品a,b
接下来我们安排一个按照这个模型造的具体的房子(实例),并委托CPU来安排两个线程(现实情况下有多少个线程?呵呵…………)对房间A的a物品和房间B的b物品进行处理
House h=new House();
Thread th1 =new Thread(new Runnable(){
……
c.roomA();
c.roomB();
……
});
Thread th2 =new Thread(new Runnable(){
……
c.roomA();
c.roomB();
……
});
我们的线程th1,th2登场!
th1:干了一堆事,CPU通知我去A房间,嗯,我先到了A,A房门上放着锁和钥匙,Ok,我进入房间,对了,把House锁起来,这下安全了!
th2:干了一堆事,CPU通知我去A房间,嗯,哎呀我去,慢了一步,房门被锁了……好吧,我只能等等了
(时间逝去……)
th1:好了,搞定A了,没人争抢就是好,但是CPU催的紧啊,赶紧接着去B,哎,安全操作手册要求我得先打开房门,交钥匙……哎哎……
th2:起开,我瞄了好久了,哈哈!
th1:CPU太不公道了,又把我赶出来了,得想完成B房间的工作还得等th2干完活……
th2:我得麻利点儿,CPU今天心情好,干完A房间的活交钥匙也没赶我出去,得嘞,接着去B,拿钥匙开锁,锁House,th1,你就在外面呆着吧
(又一段时间过去)
th2:B房间也搞定了,交锁交钥匙,CPU还催着我去下家呢
th1:th2终于走了,哎,赶紧去B开锁……
以上的场景描述的是一个线程协作经常遇到的情况,即非静态类中,针对里面所有的非静态方法,都是互斥的!
因为非静态类下的所有非静态方法所用的锁其实是由具体实例提供的针对实例的内置锁,这把锁锁的不是具体的方法,而是具体的实例。
以此类推另一种情况,如果roomB不是用synchronized修饰的呢?那么可以看成roomB对外开了一个没有锁的窗子,那么被赶出去的th1当然会争分夺秒的跳窗而入喽……
所以,非静态类中的未被synchronized修饰的方法是不会与其他线程产生互斥的效果,试想还有其他的线程同样被分配到B房间的话……那么为了争夺B,线程们依然会跳窗而入进而陷入激烈的争夺之中
(住:肯定会有人继续问,如果roomA和roomB存在对相同物品的处理呢?即a的操作在roomA和roomB中都存在,房间模型也许不好进行具象化的描述,总不能让物品来个多重影分身吧,我只能说这种情况下,AB房间是对a这个物品是有个专门的通道来获取的,结果……可想而知,竞争会蔓延到这个通道中……安全?不存在的!)

那么我们还会引申出另外一个场景,这是个非常经典的场景,别着急,我们在编码的时候安排了th1和th2对RoomA和RoomB的操作顺序,上伪代码,房子构造改了
public Class House{
private int a=0;
private int b=0;
public void synchronized roomA(){
//something todo for a;
//something todo for b;
}
public void synchronized roomB(){
//something todo for b;
//something todo for a;
}
}
程序员就是这么任性,在程序的世界里,每个程序员都是独裁者
House h=new House();
Thread th1 =new Thread(new Runnable(){
……
c.roomA();
……
});
Thread th2 =new Thread(new Runnable(){
……
c.roomB();
……
});
故事再次开始:这次特别巧,th1和th2同时涌入房门
th1:干了一堆事,CPU通知我去A房间,嗯,我先到了A,A房门上放着锁和钥匙,Ok,我进入房间,对了,把House锁起来,这下安全了!
th2:干了一堆事,CPU通知我去B房间,嗯,我先到了B,B房门上放着锁和钥匙,Ok,我进入房间,对了,把House锁起来,这下安全了!
(时间逝去……)
th1:好了,a处理完了,接下来处理b了该,嘿!th2,b整完没,给我处理了!
th2:凭什么啊,我还等着你弄完我好处理a呢!
th1:你把b先给我,我处理完,把钥匙上交完你就可以处理a啦!这是规章制度!不这么办CPU得杀了我!
th2:去你的,我等你有一会儿了,你要是想要处理b,也得我把a处理完了上交钥匙你才能弄啊!不然我就要被CPU给剁了!
th1:…………
th2:…………
th1,th2:等着吧,咱两都得死这儿了,你看看这会儿的功夫,房子外面已经挤满了要干活的th345678……了
CPU:th1,th2,你们怎么还没干好活!看我怎么……,我次奥!报……报……报告独裁者,啊呸!总领事大人,不……不……不好了,死锁了又!
我:………………这tm只能我手动强制关闭进程了…………(所有的线程(更多的是无辜等待中的线程)都得消亡……)
对,这次的场景就是并发编程中程序员们闻之摇头,JVM内的灭顶之灾——死锁!
两个不同的线程对相同的资源或者原子业务模块操作时,在加锁的情况下,如果没有考虑周全,就会出现互相不能释放锁进而产生的死锁问题。
这是一个悲伤的故事……幸而是,我们完全可以有方法予以规避……
(太悲伤了,今天就写到这里吧,关于死锁的规避和静态类以及静态方法关于锁的互斥下一篇笔记我再来详细描述)

以上的观点仅仅是个人结合工作中所用和自己学习所领悟的一些想法,如果有缺陷之处希望同道中人积极指正。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值