首页 > 编程语言 >Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)

Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)

时间:2023-06-15 18:07:43浏览次数:58  
标签:Java synchronized thread Lock age lock2 lock1 线程 多线程


在上一节中,我们已经了解了Java多线程编程中常用的关键字synchronized,以及与之相关的对象 锁机制。这一节中,让 我们一起来认识JDK 5中新引入的并发框架中的锁机制。


我想很多购买了《Java程序员面试宝典》之类图书的朋友一定对下面 这个面试题感到非常熟悉:

问:请对比synchronized与java.util.concurrent.locks.Lock 的异同。
答案:主要相同点:Lock能完成synchronized所实现的所有功能
     主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放 锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

恩,让我们先鄙视一下应试教育。

言归正传,我们先来看一个多线程程序。它使用多个线程对一个Student对象进行访问,改变其中的变量值。 我们首先用传统的synchronized 机制来实现它:

public   class  ThreadDemo  implements  Runnable {

     class  Student {

         private   int  age  =   0 ;

         public   int  getAge() {
             return  age;
        }

         public   void  setAge( int  age) {
             this .age  =  age;
        }
    }
    Student student  =   new  Student();
     int  count  =   0 ;

     public   static   void  main(String[] args) {
        ThreadDemo td  =   new  ThreadDemo();
        Thread t1  =   new  Thread(td,  " a " );
        Thread t2  =   new  Thread(td,  " b " );
        Thread t3  =   new  Thread(td,  " c " );
        t1.start();
        t2.start();
        t3.start();
    }

     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {
        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
         synchronized  ( this ) { // (1)使用同一个ThreadDemo对象作为同步锁
            System.out.println(currentThreadName  +   "  got lock1@Step1! " );
             try  {
                count ++ ;
                Thread.sleep( 5000 );
            }  catch  (Exception e) {
                e.printStackTrace();
            }  finally  {
                System.out.println(currentThreadName  +   "  first Reading count: "   +  count);
            }

        }
       
        System.out.println(currentThreadName  +   "  release lock1@Step1! " );

         synchronized  ( this ) { // (2)使用同一个ThreadDemo对象作为同步锁
            System.out.println(currentThreadName  +   "  got lock2@Step2! " );
             try  {
                Random random  =   new  Random();
                 int  age  =  random.nextInt( 100 );
                System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);

                 this .student.setAge(age);

                System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +   this .student.getAge());

                Thread.sleep( 5000 );
            }  catch  (Exception ex) {
                ex.printStackTrace();
            }  finally {
                System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +   this .student.getAge());
            }

        }
        System.out.println(currentThreadName  +   "  release lock2@Step2! " );
    }
}
=



运行结果:


a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 76
thread a first  read age is: 76
thread a second read age is: 76
a release lock2@Step2!
c got lock1@Step1!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 35
thread c first  read age is: 35
thread c second read age is: 35
c release lock2@Step2!
b got lock1@Step1!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 91
thread b first  read age is: 91
thread b second read age is: 91
b release lock2@Step2!
成功生成(总时间: 30  秒)
————————————————



显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁,所以JVM优先使刚刚释放该锁的线程重新获得 该 锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是 30秒。



我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这 两个对象锁应该选择那些被所有线程所共享的对象。



那么好。我们把第二个同步块中的对象锁改为student(此处略去代码,读 者自己修改),程序运行结果为:



a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 73
thread a first  read age is: 73
c got lock1@Step1!
thread a second read age is: 73
a release lock2@Step2!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 15
thread c first  read age is: 15
b got lock1@Step1!
thread c second read age is: 15
c release lock2@Step2!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 19
thread b first  read age is: 19
thread b second read age is: 19
b release lock2@Step2!
成功生成(总时间: 21  秒)


从 修改后的运行结果来看,显然,由于同步块的对象锁不同了,三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之 后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟 的时间是两个线程同时工作的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线程执行第二个同步块的动作。相比较第一 个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/2n,其中n为 线程数。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个 synchronized块,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m- 1)n+1)/mn那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我 们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而 这种方案至少具备数学上的可行性理论支持。)



可见,使用不同的对象锁,在不同的同步块中完成任务,可以使性能大大提升。



很多人看到这不禁要问:这和新的Lock框 架有什么关系?



别着急。我们这就来看一看。



synchronized块的确不错,但是他有一些功能性的限制:


1. 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。


2.synchronized 块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些更适合使用 非块结构锁定的情况。



java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。



JDK 官方文档中提到:


ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized  方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。


ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ”



简单来说,ReentrantLock有一个与锁相关的获取计 数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。



ReentrantLock  类(重入锁)实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性 能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)



我们把 上面的例程改造一下:



public   class  ThreadDemo  implements  Runnable {

     class  Student {

         private   int  age  =   0 ;

         public   int  getAge() {
             return  age;
        }

         public   void  setAge( int  age) {
             this .age  =  age;
        }
    }
    Student student  =   new  Student();
     int  count  =   0 ;
    ReentrantLock lock1  = new ReentrantLock(false);
    ReentrantLock lock2 = new ReentrantLock(false );

     public   static   void  main(String[] args) {
        ThreadDemo td  =   new  ThreadDemo();
         for  ( int  i  =   1 ; i  <=   3 ; i ++ ) {
            Thread t  =   new  Thread(td, i  +   "" );
            t.start();
        }
    }

     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {
        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
        lock1.lock(); // 使用重入锁
        System.out.println(currentThreadName  +   "  got lock1@Step1! " );
         try  {
            count ++ ;
            Thread.sleep( 5000 );
        }  catch  (Exception e) {
            e.printStackTrace();
        }  finally  {
            System.out.println(currentThreadName  +   "  first Reading count: "   +  count);
            lock1.unlock();
            System.out.println(currentThreadName  +   "  release lock1@Step1! " );
        }

        lock2.lock(); // 使用另外一个不同的重入锁
        System.out.println(currentThreadName  +   "  got lock2@Step2! " );
         try  {
            Random random  =   new  Random();
             int  age  =  random.nextInt( 100 );
            System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);

             this .student.setAge(age);

            System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +   this .student.getAge());

            Thread.sleep( 5000 );
        }  catch  (Exception ex) {
            ex.printStackTrace();
        }  finally  {
            System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +   this .student.getAge());
            lock2.unlock();
            System.out.println(currentThreadName  +   "  release lock2@Step2! " );
        }

    }
}
————————————————



从上面这个 程序我们看到:



对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。并 且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的。



这说明两点问题:


1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码 人员自行掌握。


2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。


3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而 在finally中使用unlock方法。


转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/


细心的读者又发现了:



在我们的例程中,创建ReentrantLock实例的时候,我们的构造函数里面传递的参数是false。那么如果传递 true又回是什么结果呢?这里面又有什么奥秘呢?



请看本节的续 ———— Fair or Unfair? It is a question...


标签:Java,synchronized,thread,Lock,age,lock2,lock1,线程,多线程
From: https://blog.51cto.com/u_16065168/6493750

相关文章

  • java中相对路径,绝对路径问题总结
    java中相对路径,绝对路径问题总结2007-12-2000:191.基本概念的理解绝对路径:绝对路径就是你的主页上的文件或目录在硬盘上真正的路径,(URL和物理路径)例如:C:\xyz\test.txt代表了test.txt文件的绝对路径。http://www.sun.com/index.htm也代表了一个URL绝对路径。......
  • Java中使用Base64编码URL作为URL的参数
    Java中使用Base64编码URL作为URL的参数本文由arthinking发表于3年前|Java基础|暂无评论| 被围观8,004views+有时候我们需要使用一个URL作为URL地址的参数来传递,假如我们需要传递的URL含有参数,例如:/shopping/confirm.action?id=1&name=itzhai那么按......
  • javascript eval和JSON之间的联系
    本文着重解释eval函数和JSON数据格式之间的联系以及一些细节上的问题。如果您想详细了解eveval :https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Global_Functions/EvalJSON:http://www.json.org/eval函数的工作原理eval函数会评估一个给定的含有JavaScr......
  • 一些JavaScript单行代码
    数组去重从数组中删除所有重复值,实现方式非常多,我们这里就说最简单的方式,一行代码搞定:constuniqueArr=(arr)=>[...newSet(arr)];console.log(uniqueArr(["前端","js","html","js","css","html"]));//['前端','js',&......
  • 碎片化学习前端之JavaScript(JS 压缩图片)
    前言图片压缩是前端开发中常见的需求,目前前端主流的解决方案有:Canvas手动实现压缩以及第三方库压缩两种方案。Canvas手动实现压缩Canvas实现压缩主要原理是:将图片绘制到canvas上,然后通过调整canvas的宽高来实现压缩。functioncompressImage(file,maxWidth,maxHeight......
  • [转][Java]入门设置
    1、JDK使用1.8_3712、下载公司的settings.xml文件,覆盖到X:\maven\conf目录下3、修改settings.xml中的localRepository配置为本机资源位置4、在IDEA里设置JDK版本5、运行项目,会使用1XXX端口,公司自有应用端口范围:10000~199996、通过http://localhost:1XX......
  • Java 网络编程 —— RMI 框架
    概述RMI是Java提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都是Java程序RMI框架采用代理来负责客户与远程对象之间通过Socket进行通信的细......
  • 多线程
    概念程序:一段静态的代码进程:运行中的程序进程作为资源分配的单位线程:进程进一步细化为线程,是一个程序内部的一条执行路径实现方式继承thread创建一个继承Thread的子类子类中重写父类的run()方法创建子类的对象通过子类对象.start()启动线程实现runable接口创建......
  • 上传自己java项目到maven中央仓库pom
    前提首先的你项目需要在Gitee或者Github上有仓库我这里以Gitee是的yhchat-sdk-core仓库为例开始在sonatype上创建问题访问sonatype注册并登录创建一个问题概要填仓库名称描述随意写写GroupId填写自己的域名,如果没有域名的话,可以自行百度使用Gitee、Github的域名需......
  • 50基于java的智能停车场管理系统设计与实现
    ​>本章节给大家带来一个基于java的智能停车场管理系统设计与实现,可适用于java车辆管理,java停车场信息管理平台,小区停车管理平台,小区停车,物业停车管理,智慧停车场管理系统,智慧小区停车场平台,车辆AI识别,车辆识别。项目背景近年来,随着我国经济的快速发展,人们生活水平的不断提高,物......