美文网首页多线程
Java基础day11笔记:多线程同步函数|synchroniz

Java基础day11笔记:多线程同步函数|synchroniz

作者: 楠楠喜欢泡枸杞 | 来源:发表于2018-12-13 12:07 被阅读0次

    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方法中:

        主函数中:

        编译运行:

        锁住了。

        分析原因:

        当然多运行几次偶尔也会出现和谐的情况。(但是这只是侥幸!)

        面试的时候,有一道考题叫:请给我写一个死锁程序。这个考的就是对死锁的理解。能写出来,就说明对死锁理解的差不多了,避免死锁也不是问题了。

        终于写完啦,吃午餐去辣!想吃面面~

相关文章

网友评论

    本文标题:Java基础day11笔记:多线程同步函数|synchroniz

    本文链接:https://www.haomeiwen.com/subject/dcdvhqtx.html