热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

浅谈对java中锁的理解

本文主要讲述java中锁的相关知识。具有很好的参考价值,下面跟着小编一起来看下吧

在并发编程中,经常遇到多个线程访问同一个 共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,在java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的,因为对于共享资源属性访问是必要也是必须的,下文会有具体示例演示。

一.java中的锁

一般在java中所说的锁就是指的内置锁,每个java对象都可以作为一个实现同步的锁,虽然说在java中一切皆对象, 但是锁必须是引用类型的,基本数据类型则不可以 。每一个引用类型的对象都可以隐式的扮演一个用于同步的锁的角色,执行线程进入synchronized块之前会自动获得锁,无论是通过正常语句退出还是执行过程中抛出了异常,线程都会在放弃对synchronized块的控制时自动释放锁。 获得锁的唯一途径就是进入这个内部锁保护的同步块或方法 。

正如引言中所说,对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。

根据使用方式的不同一般我们会将锁分为对象锁和类锁,两个锁是有很大差别的,对象锁是作用在实例方法或者一个对象实例上面的,而类锁是作用在静态方法或者Class对象上面的。一个类可以有多个实例对象,因此一个类的对象锁可能会有多个,但是每个类只有一个Class对象,所以类锁只有一个。 类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定的是实例方法还是静态方法区别的 。

在java中实现锁机制不仅仅限于使用synchronized关键字,还有JDK1.5之后提供的Lock,Lock不在本文讨论范围之内。一个synchronized块包含两个部分:锁对象的引用,以及这个锁保护的代码块。如果作用在实例方法上面,锁就是该方法所在的当前对象,静态synchronized方法会从Class对象上获得锁。

二.synchronized使用示例

1.多窗口售票

假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是作为多个窗口的共享资源存在的,由于座位号是确定的,因此票上面的号码也是确定的,我们用多个线程来模拟多个窗口同时售票,首先在不使用synchronized关键字的情况下测试一下售票情况。

先将票本身作为一个共享资源放在单独的线程中,这种作为共享资源存在的线程很显然应该是实现Runnable接口,我们将票的总数num作为一个入参传入,每次生成一个票之后将num做减法运算,直至num为0即停止,说明票已经售完了,然后开启多个线程将票资源传入。

public class Ticket implements Runnable{
   private int num;//票数量
   private boolean flag=true;//若为false则售票停止
   public Ticket(int num){
   this.num=num;
   }
   @Override
   public void run() {
   while(flag){
   ticket();
   }
   }
   private void ticket(){
   if(num<=0){
   flag=false;
   return;
   }
   try {
   Thread.sleep(20);//模拟延时操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   //输出当前窗口号以及出票序列号
   System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
   }
  }
  public class MainTest {
   public static void main(String[] args) {
   Ticketticket = new Ticket(5);
   Threadwindow01 = new Thread(ticket, "窗口01");
   Threadwindow02 = new Thread(ticket, "窗口02");
   Threadwindow03 = new Thread(ticket, "窗口03");
   window01.start();
   window02.start();
   window03.start();
   }
  }

 程序的输出结果如下:

  窗口02售出票序列号:5
  窗口03售出票序列号:4
  窗口01售出票序列号:5
  窗口02售出票序列号:3
  窗口01售出票序列号:2
  窗口03售出票序列号:2
  窗口02售出票序列号:1
  窗口03售出票序列号:0
  窗口01售出票序列号:-1

从上面程序运行结果可以看出不但票的序号有重号而且出票数量也不对,这种售票系统比12306可要烂多了,人家在繁忙的时候只是刷不到票而已,而这里的售票系统倒好了,出票比预计的多了而且会出现多个人争抢做同一个座位的风险。如果是单个售票窗口是不会出现这种问题,多窗口同时售票就会出现争抢共享资源因此紊乱的现象,解决该现象也很简单,就是在ticket()方法前面加上synchronized关键字或者将ticket()方法的方法体完全用synchronized块包括起来。

//方式一
  private synchronized void ticket(){
   if(num<=0){
   flag=false;
   return;
   }
   try {
   Thread.sleep(20);//模拟延时操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
  }
  //方式二
  private void ticket(){
   synchronized (this) {
   if (num <= 0) {
   flag = false;
   return;
   }
   try {
   Thread.sleep(20);//模拟延时操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName() + "售出票序列号:" + num--);
   }
  }

再看一下加入synchronized关键字的程序运行结果:

  窗口01售出票序列号:5
  窗口03售出票序列号:4
  窗口03售出票序列号:3
  窗口02售出票序列号:2
  窗口02售出票序列号:1

从这里可以看出在实例方法上面加上synchronized关键字的实现效果跟对整个方法体加上synchronized效果是一样的。 另外一点需要注意加锁的时机也非常重要 ,本示例中ticket()方法中有两处操作容易出现紊乱,一个是在if语句模块,一处是在num–,这两处操作本身都不是原子类型的操作,但是在使用运行的时候需要这两处当成一个整体操作,所以synchronized将整个方法体都包裹在了一起。如若不然,假设num当前值是1,但是窗口01执行到了num–,整个操作还没执行完成,只进行了赋值运算还没进行自减运算,但是窗口02已经进入到了if语句模块,此时num还是等于1,等到窗口02执行到了输出语句的时候,窗口01的num–也已经将自减运算执行完成,这时候窗口02就会输出序列号0的票。再者如果将synchronized关键字加在了run方法上面,这时候的操作不会出现紊乱或者错误,但是这种加锁方式无异于单窗口操作,当窗口01拿到锁进入run()方法之后,必须等到flag为false才会将语句执行完成跳出循环,这时候的num就已经为0了,也就是说票已经被售卖完了,这种方式摒弃了多线程操作,违背了最初的设计原则-多窗口售票。

2.懒汉式单例模式

创建单例模式有很多中实现方式,本文只讨论懒汉式创建。在Android开发过程中单例模式可以说是最常使用的一种设计模式,因为它操作简单还可以有效减少内存溢出。下面是懒汉式创建单例模式一个示例:

public class Singleton {
   private static Singletoninstance;
   private Singleton() {
   }
   public static SingletongetInstance() {
   if (instance == null) {
   instance = new Singleton();
   }
   return instance;
   }
  }

如果对于多窗口售票逻辑已经完全明白了的话就可以看出这里的实现方式是有问题的,我们可以简单的创建几个线程来获取单例输出对象的hascode值。

  com.sunny.singleton.Singleton@15c330aa
  com.sunny.singleton.Singleton@15c330aa
  com.sunny.singleton.Singleton@41aff40f

在多线程模式下发现会出现不同的对象,这种单例模式很显然不是我们想要的,那么根据上面多窗口售票的逻辑我们在getInstance()方法上面加上一个synchronized关键字,给该方法加上锁,加上锁之后可以避免多线程模式下生成多个不同对象,但是同样会带来一个效率问题,因为不管哪个线性进入getInstance()方法都会先获得锁,然后再次释放锁,这是一个方面,另一个方面就是只有在第一次调用getInstance()方法的时候,也就是在if语句块内才会出现多线程并发问题,而我们却索性将整个方法都上锁了。讨论到这里就引出了另外一个问题,究竟是synchronized方法好还是synchronized代码块好呢? 有一个原则就是锁的范围越小越好 ,加锁的目的就是将锁进去的代码作为原子性操作,因为非原子操作都不是线程安全的,因此synchronized代码块应该是在开发过程中优先考虑使用的加锁方式。

public static SingletongetInstance() {
   if (instance == null) {
   synchronized (Singleton.class) {
   instance = new Singleton();
   }
   }
   return instance;
  }

这里也会遇到类似上面的问题,多线程并发下回生成多个实例,如线程A和线程B都进入if语句块,假设线程A先获得锁,线程B则等待,当new一个实例后,线程A释放锁,线程B获得锁后会再次执行new语句,同样不能保证单例要求,那么下面代码再来一个null判断,进行双重检查上锁呢?

public static SingletongetInstance() {
   if (instance == null) {
   synchronized (Singleton.class) {
   if(instance==null){
   instance = new Singleton();
   }
   }
   }
   return instance;
  }

该模式就是双重检查上锁实现的单例模式,这里在代码层面我们已经 基本 保证了线程安全了,但是还是有问题的, 双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现bug,而是归咎于java平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。 更为详细的介绍可以参考 Java单例模式中双重检查锁的问题 。所以单例模式创建比较建议使用恶汉式创建或者静态内部类方式创建。

3.synchronized不具有继承性

我们可以通过一个简单的demo验证这个问题,在一个方法中顺序的输出一系列数字,并且输出该数字所在的线程名称,在父类中加上synchronized关键字,子类重写父类方法测试一下加上synchronized关键字和不加关键字的区别即可。

public class Parent {
   public synchronized void test() {
   for (int i = 0; i <5; i++) {
   System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   }
   }
  }

子类继承父类Parent,重写test()方法.

public class Child extends Parent {
   @Override
   public void test() {
   for (int i = 0; i <5; i++) {
   System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   }
   }
  }

测试代码如下:

final Child c = new Child();
  new Thread() {
   public void run() {
   c.test();
   };
  }.start();
  new Thread() {
   public void run() {
   c.test();
   };
  }.start();

 输出结果如下:

  Parent Thread-0:0 Child Thread-0:0
  Parent Thread-0:1 Child Thread-1:0
  Parent Thread-0:2 Child Thread-0:1
  Parent Thread-0:3 Child Thread-1:1
  Parent Thread-0:4 Child Thread-0:2
  Parent Thread-1:0 Child Thread-1:2
  Parent Thread-1:1 Child Thread-0:3
  Parent Thread-1:2 Child Thread-1:3
  Parent Thread-1:3 Child Thread-0:4
  Parent Thread-1:4 Child Thread-1:4

通过输出信息可以知道,父类Parent中会将单个线程中序列号输出完成才会执行另一个线程中代码,但是子类Child中确是两个线程交替输出数字,所以synchronized不具有继承性。

4.死锁示例

死锁是多线程开发中比较常见的一个问题。若有多个线程访问多个资源时,相互之间存在竞争,就容易出现死锁。下面就是一个死锁的示例,当一个线程等待另一个线程持有的锁时,而另一个线程也在等待该线程锁持有的锁,这时候两个线程都会处于阻塞状态,程序便出现死锁。

public class Thread01 extends Thread{
   private Object resource01;
   private Object resource02;
   public Thread01(Object resource01, Object resource02) {
   this.resource01 = resource01;
   this.resource02 = resource02;
   }
   @Override
   public void run() {
   synchronized(resource01){
   System.out.println("Thread01 locked resource01");
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   synchronized (resource02) {
   System.out.println("Thread01 locked resource02");
   }
   }
   }
  }
  public class Thread02 extends Thread{
   private Object resource01;
   private Object resource02;
   public Thread02(Object resource01, Object resource02) {
   this.resource01 = resource01;
   this.resource02 = resource02;
   }
   @Override
   public void run() {
   synchronized(resource02){
   System.out.println("Thread02 locked resource02");
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   synchronized (resource01) {
   System.out.println("Thread02 locked resource01");
   }
   }
   }
  }
  public class MainTest {
   public static void main(String[] args) {
   final Object resource01="resource01";
   final Object resource02="resource02";
   Thread01thread01=new Thread01(resource01, resource02);
   Thread02thread02=new Thread02(resource01, resource02);
   thread01.start();
   thread02.start();
   }
  }

执行上面的程序就会一直等待下去,出现死锁。当线程Thread01获得resource01的锁后,等待500ms,然后尝试获取resource02的锁,但是此时resouce02锁已经被Thread02持有,同样Thread02也等待了500ms尝试获取resouce01锁,但是该所已经被Thread01持有,这样两个线程都在等待对方所有的资源,造成了死锁。

三.其它

关键字synchronized具有锁重入功能,当一个线程已经持有一个对象锁后,再次请求该对象锁时是可以得到该对象的锁的,这种方式是必须的,否则在一个synchronized方法内部就没有办法调用该对象的另外一个synchronized方法了。锁重入是通过为每个所关联一个计数器和一个占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时计数器会递减,直至减为0时锁才会被释放。

在声明一个对象作为锁的时候要注意字符串类型锁对象,因为字符串有一个常量池,如果不同的线程持有的锁是具有相同字符的字符串锁时,两个锁实际上同一个锁。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!


推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
  • 信息安全等级保护是指对国家秘密信息、法人和其他组织及公民的专有信息以及公开信息和存储、传输、处理这些信息的信息系统分等级实行安全保护,对信息系统中使用的信息安全产品实 ... [详细]
  • 无线认证设置故障排除方法及注意事项
    本文介绍了解决无线认证设置故障的方法和注意事项,包括检查无线路由器工作状态、关闭手机休眠状态下的网络设置、重启路由器、更改认证类型、恢复出厂设置和手机网络设置等。通过这些方法,可以解决无线认证设置可能出现的问题,确保无线网络正常连接和上网。同时,还提供了一些注意事项,以便用户在进行无线认证设置时能够正确操作。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 本文详细介绍了相机防抖的设置方法和使用技巧,包括索尼防抖设置、VR和Stabilizer档位的选择、机身菜单设置等。同时解释了相机防抖的原理,包括电子防抖和光学防抖的区别,以及它们对画质细节的影响。此外,还提到了一些运动相机的防抖方法,如大疆的Osmo Action的Rock Steady技术。通过本文,你将更好地理解相机防抖的重要性和使用技巧,提高拍摄体验。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文详细介绍了华为4GLTE路由器B310的外置天线安装和设置方法。通过连接电源和网线,输入路由器的IP并登陆设置页面,选择手动设置和手动因特网设置,输入ISP提供商的用户名和密码,并设置MTU值。同时,还介绍了无线加密的设置方法。最后,将外网线连在路由器的WAN口即可使用。 ... [详细]
  • 本文讨论了前端工程化的准备工作,主要包括性能优化、安全防护和监控等方面需要注意的事项。通过系统的答案,帮助前端开发者更好地进行工程化的准备工作,提升网站的性能、安全性和监控能力。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • MyBatis错题分析解析及注意事项
    本文对MyBatis的错题进行了分析和解析,同时介绍了使用MyBatis时需要注意的一些事项,如resultMap的使用、SqlSession和SqlSessionFactory的获取方式、动态SQL中的else元素和when元素的使用、resource属性和url属性的配置方式、typeAliases的使用方法等。同时还指出了在属性名与查询字段名不一致时需要使用resultMap进行结果映射,而不能使用resultType。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 如何修改路由器密码?路由器登录密码和无线密码的修改方法
    本文介绍了修改路由器密码的两种方法:一是修改路由器登录口令,需要进入路由器后台进行操作;二是修改无线连接密码,通过进入路由器后台的无线设置和无线安全设置进行修改。详细步骤包括复位处理、登录路由器后台、选择系统工具、填入用户名和用户密码、保存修改等。 ... [详细]
  • 本文介绍了2019年上半年内蒙古计算机软考考试的报名通知和考试时间。考试报名时间为3月1日至3月23日,考试时间为2019年5月25日。考试分为高级、中级和初级三个级别,涵盖了多个专业资格。报名采取网上报名和网上缴费的方式进行,报考人员可登录内蒙古人事考试信息网进行报名。详细内容请点击查看。 ... [详细]
author-avatar
广东狮子会_刘少杰
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有