简介
在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。
参考资料