11-多线程(多线程-同步函数)
银行类:
储户类:
主函数中开两个线程:
运行结果:
但是多运行几次,发现顺序不太对:
如何找问题?
1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
我们就来一条一条找:
1,哪些代码是多线程运行代码:
2,共享数据。
3,多线程运行代码中哪些语句是操作共享数据的:
run方法中只有一句应该不是问题所在,add方法中有两句,我们来分析一下:
第一个线程进入add,给sum加了n,还没来的及打印,下一个就占用了cpu,也进入了add,做给sum加n的动作,它加完打印完之后,刚刚那个没来得及打印的线程才获得了cpu的执行权,继续打印,这个时候,就会导致打印的顺序不对。
为了验证,我们sleep一下:
编译运行:
天啦噜,问题好严重!
我们要解决问题!
编译运行:
问题解决啦。
那我们可以把这部分锁着吗?
也不是不可以,但又一个问题,就是如果李四进来了,得等到他把300块钱全存完,张三才能进来。
所以,哪些代码该同步,哪些代码不该同步,一定要搞清楚。
怎么搞清楚呢?就刚刚那三个步骤。
如何找问题?
1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
接下来要讲新知识点啦。
我们思考一下:
同步代码块是用来封装代码的,函数也是用来封装代码的,那它们有什么不同呢?
同步代码块相对比函数,是拥有了同步性,那我们也让函数具有同步性,这个点子怎么样?
其实超级简单,把synchronized关键作为修饰符放在函数上就OK啦:
总结一下:
同步有两种表现形式,第一个是同步代码块,第二个是同步函数。
12-多线程(多线程-同步函数的锁是this)
我们现在把之前卖票的程序也改成同步函数玩一下~
编译运行:
我们发现,一直是0线程在做这件事情,1、2、3线程根本没启动。
我们分析一下,0进来了,1、2、3被锁外面了,而0一进来就一直循环直到结束,1、2、3根本没有机会:
所以这么做不可以,因为没有搞清楚哪些需要同步,哪些不需要。
那该怎么写呢?单独写一个函数:
这个问题就解决啦。
但是另一个问题来了,我们这里已经没有object对象了,那同步函数用的是哪个锁?
show方法是不是需要被对象调用的呀?那同步函数能够用到的锁,就是调用它的对象:this。
因为函数需要被对象调用,那么函数都有一个所属对象引用,就是this。
所以同步函数使用的锁是this。
我们现在来验证一下:
使用两个线程来卖票。一个线程在同步代码块中,一个线程在同步函数中,都在执行卖票动作。
如果要是同步的话,就不会出现错误。
稍微修改一下代码:
同步代码块:
同步函数:
主函数总开启两个线程:
编译运行:
0线程也有,1线程也有,可是为什么全都是show?code没打印。
分析一下:现在有三个线程:主线程,0线程,1线程。
主线程启动后,创建了两个线程对象,然后开启了0线程,开启完之后,0线程会立刻执行吗?不一定,它先是处于临时状态,有了资格但是还没有执行权,因为这个时候是主线程持有执行权。主线程有可能瞬间把这几句话执行完:
瞬间几句话执行完后,flag值为false。
主线程执行完t2.start()之后,就结束了。
现在就剩两个线程:0线程和1线程,它们开始运行了。
因为flag值为false,所以0线程和1线程都去show中执行了。
那该怎么解决呢?
想让主线程执行完t1.start()之后先停一下,让0线程运行一会儿:
注意,在主线程睡眠的这10ms内,能运行的线程只有0线程,这个时候他就去执行同步代码块了。
过了10ms,主线程醒了。醒完以后,它继续往下执行,将flag置为假,开启了1线程,1线程就去执行同步函数show了。
此时0线程和1线程同时运行。
编译运行:
搞定!
但是这里出现错票了:
因为票数只能到1,出现卖0号票就错了。
所以不安全。
可是明明加了同步,怎么就能不安全呢?
加了同步还没解决,肯定是两个前提中至少有一个没满足。
前提:
1,必须是两个及两个以上的线程。满足了。
2,用的是同一个锁。不是。0线程用的obj锁,1线程用的this锁。
我们将同步代码块也改成this锁试一试:
编译运行:
安全啦!
通过这个小程序,我们也侧面验证了同步函数使用的锁是this,一箭双雕嘻嘻~
13-多线程(多线程-静态同步函数的锁是Class对象)
我们将show用static修饰:
tick也用static修饰:
编译运行:
我们发现,又出现0号票了,怎么会酱紫!
因此,静态的同步函数,用的锁肯定不是this了(因为静态方法中也不可以定义this),那么它用的锁是什么呢?
静态进内存时,内存中没有本类对象,到那时一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class。
所以将同步代码块的锁修改成Ticket.class试一下:
编译运行:
安全啦!
因此,静态的同步方法,使用的锁是该方法所在类的字节码文件对象:类名.class。
14-多线程(多线程-单例设计模式-懒汉式)
讲完了这些,我们回过头看一下之前讲过的单例设计模式——懒汉式。因为这个小知识点只有我们学完同步之后才能讲~
我们回顾一下单例设计模式:
饿汉式:
懒汉式:
这里有一个问题,如果多个线程并发访问这个getInstance,是不是有多条语句在操作共享数据s?(一条在判断,一条在赋值。)
加上synchronized就搞定了:
但是还有个问题,如果有很多个线程都要访问getInstance,是不是每个线程都要访问这个锁?所以懒汉式加了同步会比较低效。
怎么办呢?
这个时候我们可以这样干:
但是这样写和那样写不是没啥区别嘛,那再加一句:
我们分析一下:
A一进来,满足第一次s==null判断,拿到锁就进来了,进来之后A挂这里了:
B一进来,也满足第一次s==null判断,但是没拿到锁。
这个时候A继续执行s=new Single();并且在执行完之后就出去啦。
这时候B拿到了锁,也进来了。进行第二次s==null的判断,显然不满足。所以return s。
这时C进来了,进行第一次s==null的判断,不满足。而且C以后进来的线程,它们都不满足第一次s==null的判断,所以都不用再判断这个锁。
这样做,减少了判断锁的次数。
所以用双重判断,减少了判断锁的次数,稍微提高了懒汉式的效率。
但是不管怎样,饿汉式只有一句话就可以解决的问题,懒汉式都要用好几句。所以开发中还是写饿汉式好一些。
之所以讲这么详细,是因为在面试中经常会考到懒汉式。
比如饿汉式和懒汉式有什么不同?懒汉式的特点在于实例的延迟加载。
懒汉式的延迟加载有没有问题?有, 如果多线程访问时会出现安全问题。
怎么解决?用同步来解决。用同步代码块或者同步函数都可以,但是稍微有点低效。用双重判断可以稍微解决一下效率问题。
加同步的时候使用的锁是哪一个?该类所处的字节码对象。
所以,就这一个小问题可以考好多地方呢。
这个代码要会写哦:
一般有可能会要求:请写一个延迟加载的单例设计模式示例。就写这个~
15-多线程(多线程-死锁)
刚刚讲的this锁和字节码对象锁结论记住就行啦,代码有点麻烦,了解即可。但是单例设计模式的代码一定要会哦。
回到我们的进度中来。
同步还有一个弊端:死锁。这是同步出现之后会产生的一个现象。
何为死锁呢?
你持有一个锁,我也持有一个锁,我不放我的锁要到你那里面去运行,而你不放你的锁要到我这里面去运行。谁都不放,就导致了死锁。
死锁一产生,程序就挂那儿不动了。
通常死锁出现的原因是:同步中嵌套出现,而锁却不同。
我们稍微修改一下代码,让它出现这种情况:
编译运行,卡这里不动了:
我们分析一下:
这个时候就锁住了。
注意这样写不是教你写死锁哦,是要你一定要避免死锁~
再来一个稍微简单的死锁例子:
Test类实现Runnable接口:
为了方便起见,单独写了一个类装俩锁(锁是对象哦):
run方法中:
主函数中:
编译运行:
锁住了。
分析原因:
当然多运行几次偶尔也会出现和谐的情况。(但是这只是侥幸!)
面试的时候,有一道考题叫:请给我写一个死锁程序。这个考的就是对死锁的理解。能写出来,就说明对死锁理解的差不多了,避免死锁也不是问题了。
终于写完啦,吃午餐去辣!想吃面面~