java concurrency: synchronized vs lock

16 篇文章 0 订阅
16 篇文章 0 订阅

简介

        在java的多线程编程中,需要考虑的最多的情况莫过于线程之间的同步和通信了。在线程的同步机制中,最常用的莫过于synchronized和lock。从更深层次的比较来说,他们有什么特点呢?在开发的时候到底哪种方式比较合适?我们就详细的了解一下吧。

synchronized简介

        一提起synchronized,似乎太简单了。在任何需要多线程访问的情况下,如果要对所访问的数据进行限制,保证每次只有一个线程可以操作该数据,我们可以在数据或者方法部分加一个synchronized。synchronized主要有两种使用的方式,一种是在一个方法内部用synchronized封装的代码块,可以称之为synchronized声明,还有一种是synchronized方法,主要是用于修饰一个方法。

两者的使用方式分别如下:

 

synchronized声明:

 

synchronized(lockObject)
{
    // put your stuff here
}

 

 

synchronized方法:

 

synchronized void doSomething()
{
      // Here is the business code.
}

 

光看这两种使用方式,似乎太简单了,没什么好说的。在更深层次里,jvm用了一些特别的手法来实现synchronized的特性。

 

更进一步分析

Monitor

        synchronized在jvm内部的实现是通过一种monitor的机制。synchronized在编译后会生成monitorenter和monitorexit这两个字节码。这两个字节码都需要一个引用类型的参数来指定要加锁和解锁的对象。如果synchronized指明了参数对象,也就是采用synchronized声明的方式,则该对象就是要加锁和后续解锁的对象。如果synchronized修饰的是方法,则根据方法对应的对象或者类来获取加锁和解锁对象。如果该方法是某个对象的,则对对象进行操作,如果是类方法,则获取该方法所在类的Class对象。

        系统生成的monitorenter和monitorexit正好封装了我们要同步操作的那部分代码块。

        我们来看一个示例,分别采用synchronized声明和synchronized方法实现。

下面是synchronized方法实现的代码:

class Prompter
{
    int delay;

    Prompter(int d)
    {
        if(d <= 0) d = 1;
        delay = d;
    }

    synchronized void display(String msg)
    {
        for(int i = 0; i < msg.length(); i++)
        {
            System.out.print(msg.charAt(i));

            if(Character.isWhitespace(msg.charAt(i)))
            {
                try
                {
                    Thread.sleep(delay * 1000);
                }
                catch(InterruptedException exc)
                {
                    return;
                }
            }
        }
        System.out.println();
    }
}

class UsePrompter implements Runnable
{
    Prompter prompter;
    String message;

    UsePrompter(Prompter p, String msg)
    {
        prompter = p;
        message = msg;
        new Thread(this).start();
    }

    public void run()
    {
        prompter.display(message);
    }
}

class SyncDemo
{
    public static void main(String[] args)
    {
        Prompter p = new Prompter(1);

        UsePrompter promptA = new UsePrompter(p, "One Two Three Four");
        UsePrompter promptB = new UsePrompter(p, "Left Right Up Down");
    }
}

 

我们定义了设定同步的地方在Prompter的display方法。两个线程分别调用Prompter对象的display方法。这时,如果我们反编译生成的Prompter class文件,会生成如下的结果:

 

Compiled from "SyncDemo.java"
class Prompter {
  int delay;

  Prompter(int);
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iload_1       
       5: ifgt          10
       8: iconst_1      
       9: istore_1      
      10: aload_0       
      11: iload_1       
      12: putfield      #2                  // Field delay:I
      15: return        

  synchronized void display(java.lang.String);
    Code:
       0: iconst_0      
       1: istore_2      
       2: iload_2       
       3: aload_1       
       4: invokevirtual #3                  // Method java/lang/String.length:()I
       7: if_icmpge     55
      10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      13: aload_1       
      14: iload_2       
      15: invokevirtual #5                  // Method java/lang/String.charAt:(I)C
      18: invokevirtual #6                  // Method java/io/PrintStream.print:(C)V
      21: aload_1       
      22: iload_2       
      23: invokevirtual #5                  // Method java/lang/String.charAt:(I)C
      26: invokestatic  #7                  // Method java/lang/Character.isWhitespace:(C)Z
      29: ifeq          49
      32: aload_0       
      33: getfield      #2                  // Field delay:I
      36: sipush        1000
      39: imul          
      40: i2l           
      41: invokestatic  #8                  // Method java/lang/Thread.sleep:(J)V
      44: goto          49
      47: astore_3      
      48: return        
      49: iinc          2, 1
      52: goto          2
      55: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      58: invokevirtual #10                 // Method java/io/PrintStream.println:()V
      61: return        
    Exception table:
       from    to  target type
          32    44    47   Class java/lang/InterruptedException
}

 现在,我们再看看另外一个synchronized声明的版本的代码:

class Prompter
{
    int delay;

    Prompter(int d)
    {
        if(d <= 0) d = 1;
        delay = d;
    }

    public void display(String msg)
    {
		synchronized(this)
		{
		    for(int i = 0; i < msg.length(); i++)
		    {
		        System.out.print(msg.charAt(i));

		        if(Character.isWhitespace(msg.charAt(i)))
		        {
		            try
		            {
		                Thread.sleep(delay * 1000);
		            }
		            catch(InterruptedException exc)
		            {
		                return;
		            }
		        }
		    }
		    System.out.println();
		}
    }
}

class UsePrompter implements Runnable
{
    Prompter prompter;
    String message;

    UsePrompter(Prompter p, String msg)
    {
        prompter = p;
        message = msg;
        new Thread(this).start();
    }

    public void run()
    {
        prompter.display(message);
    }
}

class SyncDemo
{
    public static void main(String[] args)
    {
        Prompter p = new Prompter(1);

        UsePrompter promptA = new UsePrompter(p, "One Two Three Four");
        UsePrompter promptB = new UsePrompter(p, "Left Right Up Down");
    }
}

代码几乎和前面的一样,唯一的差别就是将原来的synchronized display方法修改成了synchronized(this)的声明。这样,我们相当于对Prompter对象加锁。

再看看对Prompter class反编译的结果:

Compiled from "SyncDemo.java"
class Prompter {
  int delay;

  Prompter(int);
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: iload_1       
       5: ifgt          10
       8: iconst_1      
       9: istore_1      
      10: aload_0       
      11: iload_1       
      12: putfield      #2                  // Field delay:I
      15: return        

  public void display(java.lang.String);
    Code:
       0: aload_0       
       1: dup           
       2: astore_2      
       3: monitorenter  
       4: iconst_0      
       5: istore_3      
       6: iload_3       
       7: aload_1       
       8: invokevirtual #3                  // Method java/lang/String.length:()I
      11: if_icmpge     62
      14: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1       
      18: iload_3       
      19: invokevirtual #5                  // Method java/lang/String.charAt:(I)C
      22: invokevirtual #6                  // Method java/io/PrintStream.print:(C)V
      25: aload_1       
      26: iload_3       
      27: invokevirtual #5                  // Method java/lang/String.charAt:(I)C
      30: invokestatic  #7                  // Method java/lang/Character.isWhitespace:(C)Z
      33: ifeq          56
      36: aload_0       
      37: getfield      #2                  // Field delay:I
      40: sipush        1000
      43: imul          
      44: i2l           
      45: invokestatic  #8                  // Method java/lang/Thread.sleep:(J)V
      48: goto          56
      51: astore        4
      53: aload_2       
      54: monitorexit   
      55: return        
      56: iinc          3, 1
      59: goto          6
      62: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      65: invokevirtual #10                 // Method java/io/PrintStream.println:()V
      68: aload_2       
      69: monitorexit   
      70: goto          80
      73: astore        5
      75: aload_2       
      76: monitorexit   
      77: aload         5
      79: athrow        
      80: return        
    Exception table:
       from    to  target type
          36    48    51   Class java/lang/InterruptedException
           4    55    73   any
          56    70    73   any
          73    77    73   any
}
 

如果我们仔细去看两种方式反编译后的结果,会发现除了synchronized声明中增加了一个monitorenter和monitorexit外,几乎没什么差别。两者有这么一个细微的差别是在于对于synchronized方法,jvm自动帮我们去获取绑定该方法的对象锁了,而对于我们指定的对象,jvm会生成monitorenter和monitorexit的字节码。

可重入

        jvm编译后的monitor还有一个很重要的特性就是支持可重入。它表示,在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把所的计数器加1.在执行monitorexit时将锁的计数器减1,当计数器为0时,锁就被释放了。这样一个好处就是如果当前获取到锁的线程它再次去访问到该锁锁定的部分时可以直接方法,只需要对锁计数器加1,而不至于还要被阻塞。它这种可重入性有一个典型的好处,见如下代码:

 

public class Super
{
    public synchronized void doSomething()
    {
        ...
    }
}

public class SubClass extends Super
{
    public synchronized void doSomething()
    {
        System.out.println(toString() + ": calling something");
        super.doSomething();
    }
}
 在这里,父类和子类都同步限制了doSomething方法。子类要访问父类的doSomething方法。如果锁不是可重入的话,要调用父类的方法就会被阻塞。而且,要解决这个问题也会比较麻烦。

 

lock

        java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

        ReentrantLock类的使用方法如下所示:

Lock lock = new ReentrantLock();
lock.lock();
try { 
  // update object state
}
finally {
  lock.unlock(); 
}
        我们如果要使用ReentrantLock类,必须要用try,finally块的方式来使用,在try里面更新数据,在finally里面释放锁。

        除了上面的那些差别,ReentrantLock的加锁机制也和synchronized几乎一样,它也是可重入的,通过同样的计数器机制来获取和释放锁。

 

两者的比较

        一个非常常见的说法就是,Lock实现了synchronized的所有功能,同时提供了更加高级和更加细粒度的控制。比如ReentrantLock就有如下几项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。从以往的比较来看,ReentrantLock的性能比较synchronized相对要好一些。随着JDK6以及后续一些版本的优化,他们的差别已经很小了。至少从官方来看还是比较倾向于使用synchronized。

参考资料

 

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

Thread synchronization

深入理解Java虚拟机

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值