Java多线程显式锁与隐式锁的不一样的详细解读

前言

那我在搜索多线程有关于锁的知识的时候发现,基本上所有的博客内容都是出自同一篇文章,啊,知识就是这样传播开来的。那我就觉得我既然也学习了这个知识点那就做点不一样的,当然我也很希望大家都能转载我这篇博客,留言说一声就行,点个赞再走也不错,能收藏那就是极好不过了,不转载学习讨论留言评论区大家一起探讨也是极好的。
好,开始我们的学习,那在开始本次的学习中我希望大家能够了解,什么是锁,锁的定义概念是什么,锁它要什么用,锁它会在哪里去使用呢?
其实我们生活中到处都有锁的身影,自行车锁,车锁,门锁,还有你们那难以打开的心锁。。。等等。对吧,那我们都知道锁的存在,那谁能说一下锁它为什么要存在呢?好,没人说,那我说啊,那不管是上面的哪个锁是不是都是在防止盗窃,防止我们的财产不被他人恶意侵占,防止不好的事情发生。那锁的存在就给了我们一定程度上的安全感没错吧。那这也是Java中锁的意义所在。那我们的显式锁与隐式锁的意义也就是在Java多线程的任务执行中保护我们线程能够好好的,不被阻断的进行下去。

那谈到了锁就必然会存在安全问题。我们也经常听到别人说线程安全不安全的,注意,面试官也经常问线程安全的奥。 那既然涉及到了这个知识点,那我们先来聊一聊什么是线程安全。

线程安全

什么是线程安全?

多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全。
不知道大家打过架没有?反正我肯定是没有打过架了,好好学生一枚的说!(我都是往死里打。),好继续说回来。那假设你们家你有个姐姐,没错,总是欺负你的那个女人,她每次都会在你们吃饭的时候和你抢饭吃,不是穷,没饭吃,而是总抢你的饭吃。你没有听错,她就是总抢你的饭吃。(那时候我还小,打不过她,到了10+了,更打不过了,女孩发育早的说。别给我说,什么姐姐最好了,那我小的时候最怕的就是她,很烦,打也打不过,你放弃,她还穷追不舍,拿话讽刺你。啊,那种感觉我现在回想起来还是有点气。)。好,回归到打架的阶段,那她总是要和你吃同一口,抢一个食,那是不是你们就筷子会打架,最后你被打。对吧,那这就是不安全。

源码案例解释

public class Demo7 {     public static void main(String[] args) {         //线程不安全         Runnable run = new Ticket();         new Thread(run).start();         new Thread(run).start();         new Thread(run).start();     }      static class Ticket implements Runnable{         //票数         private int count=10;         @Override         public void run() {             while (count>0){                 //卖票                 System.out.println("正在准备卖票");                 try {                     Thread.sleep(1000);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 count--;                 System.out.println("出票成功,余票:"+count);             }         }     } } 

没错,就是市面上的例子,我就不浪费心思了,反正知识点都是一样的。
好,我们来瞧一瞧在线程不安全下的运行结果。
Java多线程显式锁与隐式锁的不一样的详细解读
通过上面的测试结果,三个线程,同时抢票,有时候会抢到同一张票?那在这里我先说一个情况,我这次的线程运行中没有打印出余票为负数的情况的,但是这种情况也是存在的,因为在第一个线程出票为0之后,那第二个第三个线程还在继续执行,也就出现了余票为-1,-2的现象。那我们为了能够更好地理解这个卖票的过程,我们用画图来讲一下。

Java多线程显式锁与隐式锁的不一样的详细解读
注意,重点来了
那假设我们的票count卖着卖着卖的就剩一张了,那A继续到while判断,count=1,是大于0的,那继续往下,当走到我们的try-catch的时候就会耗费一些时间,当然,sleep方法会消耗时间(sleep方法是放大了犯错的几率),但是即使没有,也有可能因为时间偏的丢失,导致B抢到了,那这个时候B也进行判断,往下执行,之后C也执行了,也就是说,我们的三个线程,同时处在任务中,那A走完打印是不是余票0,之后B再打印那就是-1,C就是-2了,这也会死为什么有可能出现-2的原因。
好,这里引入一个知识点:

时间偏指的是CPU分出来的一个个的时间。即抢到的时间的概率越大。

这里就又涉及到我们的线程调度问题,那这里就不再多说了,想知道的可以自主查找或者留言评论。那我们其中的抢占调度就是我们Java使用的调度机制,当CPU空闲以后,CPU会主动抛出时间偏,由各个线程争抢,抢到了就去执行。
那通过以上的案例也让我们更好的理解到线程安全,那怎么解决我们的线程安全问题就是这次博客的真正重点了。

解决线程安全问题方法

1、隐式锁:同步代码块

被标记的锁对象,Java中任何对象都可以传进来,任何对象都可以打标记

public class Demo8 {     public static void main(String[] args) {         //线程不安全         //解决方案1. 同步代码块         //格式: synchronized(锁对象){         //         // }         Runnable run = new Ticket();         new Thread(run).start();         new Thread(run).start();         new Thread(run).start();     }      static class Ticket implements Runnable{         //票数         private int count=10;         private Object  o= new Object();         @Override         public void run() {             //Object o= new Object();             while (count>0){                 synchronized (o){                     if (count>0){                         //卖票                         System.out.println("正在准备卖票");                         try {                             Thread.sleep(1000);                         } catch (InterruptedException e) {                             e.printStackTrace();                         }                         count--;                         System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count+"张"+",时间:"+System.currentTimeMillis());                     }else {                         break;                     }                 }             }         }     } } 

为了让大家能够直观的感受到打印的时间间隔,我们测试运行结果如下:
Java多线程显式锁与隐式锁的不一样的详细解读
通过图片我们可以发现,同一时间,抢票的间隔差不多都是1000ms,为什么,不是说多线程吗?
因为在抢票的方法上,增加了public class Demo9 { public static void main(String[] args) { //解决方案2. 同步方法 //线程不安全 Runnable run = new Ticket(); new Thread(run).start(); new Thread(run).start(); new Thread(run).start(); } static class Ticket implements Runnable{ //票数 private int count=10; @Override public void run() { while (true){ boolean flag = sale(); if (!flag){ break; } } } public synchronized boolean sale(){ //this //Ticket.class /** * 以方法为单位进行加锁。在方法名称前面加上修饰符synchronized, * 它的锁是this,调用该方法的对象就是锁, * 但当方法被静态修饰的时候锁对象就是类名.class。 * 而创建(new)了多个对象以后,每个线程执行的对象互不相同,就不会由排队的情况, * 因为它们的锁不一样,给判断各自的锁 * */ if (count>0) { //卖票 System.out.println("正在准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count+"张"+",时间:"+System.currentTimeMillis()); return true; } return false; } } }

方法为单位进行加锁。在方法名称前面加上修饰符synchronized,它的锁是this,调用该方法的对象就是锁,但当方法被静态修饰的时候锁对象就是类名.class。而创建(new)了多个对象以后,每个线程执行的对象互不相同,就不会由排队的情况,因为它们的锁不一样,给判断各自的锁。
测试结果是一样的,就不再多发图了,这里只是向大家展示两种不同方式。

3、显式锁:Lock

API的链接:最新版API JDK11版本中文释义
Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁
首先我们来看一下Lock的API:
Java多线程显式锁与隐式锁的不一样的详细解读
看完就一个感受,灵活啊,这也太活了,想锁就锁,想开就开,这不是任意妄为?但是CSDN也不是法外之地,所以不要太活。
那在我们进行源码操作之前,我们先来看一下它的构造方法。
Java多线程显式锁与隐式锁的不一样的详细解读
那我们其实主要就是进行上锁与解锁。,剩下的呢就由大家自行探索了。
上锁的介绍:
Java多线程显式锁与隐式锁的不一样的详细解读解锁的介绍:
Java多线程显式锁与隐式锁的不一样的详细解读
好,上源码:

/**  * 同步代码块和同步方法都属于隐式锁  * 线程同步:Lock  */ public class Demo10 {     public static void main(String[] args) {         //线程不安全         //解决方案3. 显示锁Lock 子类 ReentrantLock         Runnable run = new Ticket();         new Thread(run).start();         new Thread(run).start();         new Thread(run).start();      }      static class Ticket implements Runnable{         //票数         private int count=10;         //显示锁         private Lock l = new ReentrantLock();//1、 创建锁          //private Lock l = new ReentrantLock(true);//fair参数为true就表示公平锁         @Override         public void run() {             while (true){                 l.lock();//2、 上锁                 if (count>0){                     //卖票                     System.out.println("正在准备卖票");                     try {                         Thread.sleep(1000);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     count--;                     System.out.println("出票成功,余票:"+count);                 }else {                     break;                 }                 l.unlock();//3、解锁             }         }     } } 

测试结果是一样的:
Java多线程显式锁与隐式锁的不一样的详细解读

总结

那其实从开始到现在了,想必大家对二者的区别也是有了一定的了解。

  1. 所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
    那其实对于手动操作的lock锁我更偏爱,因为它可以随意指定位置,我们在之前的API中也可以看出,我们甚至可以在执行的进程中去上锁中断它,就是这么无敌。

  2. 这也是为什么,在各博客说的等待是否可中断?
    1、synchronized是不可中断的。除非抛出异常或者正常运行完成 。
    2、 Lock可以中断的。中断方式:

    1. :调用设置超时方法tryLock(long timeout ,timeUnit unit) 2. :调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断 
  3. 加锁的时候是否可以公平?
    synchronized:非公平锁
    lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。
    Java多线程显式锁与隐式锁的不一样的详细解读true:公平锁
    false:非公平锁

  4. 从性能分析
    synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
    但是我现在还是建议使用lock,未来是未来,现在那就使最优的啊,但是我们也要对synchronized有所了解就行了,到了用的时候咱也不怵。

好了,在观看了大量的精品复制博客后,我也摘抄了不少,但是学习一定要有自己的思考,这是我想说的。

版权声明:玥玥 发表于 2021-07-26 21:49:34。
转载请注明:Java多线程显式锁与隐式锁的不一样的详细解读 | 女黑客导航