线程安全定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
线程安全的实现方法
保证线程安全最常见的一种并发手段是互斥同步,互斥是方法,同步是目的。
第一种:在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译后会在同步快前后形成monitorenter和monitorexit指令。
后续锁问题都是基于以下示例代码分析,设计的method1()和method2()都需要互斥同步,method1()内部调用method2(),是为了验证锁可重入问题:
public class Mysync
{
static Object obj = new Object();
public static void method1(){
synchronized (obj)
{
System.out.println("method1……");
method2();
}
}
public static void method2(){
synchronized (obj)
{
System.out.println("method2……");
}
}
public static void main(String[] args) {
Mysync.method1();
}
}
先用javap -verbose Mysync.class > Mysync.txt命令生成字节码,截取method1()的字节码文件内容如下:可以看到第5行生成monitorenter指令,第18和23行生成了monitorexit指令,这两个指令都需要一个reference类型的参数来指定要锁定和解锁的对象。我们这里明确指定了锁对象是obj,如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class类来作为锁对象。
为什么18和23行monitorexit出现了两次呢?
这是因为加锁之后,正常处理结束需要解锁,异常处理也需要解锁,否则锁不释放会造成死锁,所以会有两条解锁命令,但实际只会执行一次。
#javap -verbose Mysync.class > Mysync.txt
public static void method1();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: getstatic #13 // Field obj:Ljava/lang/Object;
3: dup
4: astore_0
5: monitorenter
6: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #26 // String method1……
11: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: invokestatic #34 // Method method2:()V
17: aload_0
18: monitorexit
19: goto 25
22: aload_0
23: mmethod2onitorexit
24: athrow
25: return
在代码进入method1()时,如果能获得对象锁,锁计数器加1,代码执行结束,锁计数器减1.当计数器为0时,释放锁;如果获取锁失败,那就阻塞等待,直到对象锁被另一个线程释放为止。
jvm规范对monitorenter和monitorexit描述中有两点需要注意:
1、分析以上代码,method1()获得了对象锁,调用method2()时,会不会造成死锁呢?
答案是:不会!synchronized同步块对于同一条线程而言是可重入的,执行method2()需要获取锁时,发现该对象被自己的线程已经锁住,这时候锁计算器加1,执行代码,执行结束,锁计算器减1,不会出现自己把自己锁死的问题。
2、对象锁被其他线程占用,当前想要获得锁的线程需阻塞等待。而java的synchronized互斥锁的实现,是基于操作系统互斥锁机制,所以阻塞等待要进行用户态和核心态切换,是个很耗性能的操作,要慎重使用synchronized互斥锁。
第二种:除了synchronized外,JUC(java.util.concurrent)包还提供了重入锁ReentrantLock。
两者的不同点:ReentrantLock显式的获得、释放锁,synchronized隐式获得释放锁;synchronized不需要我们手动解锁,而ReentrantLock需要自己在finally中调用unlock()方法手动解锁。
ReentrantLock可响应中断、可轮回,,为处理锁的不可用性提供了更高的灵活性,synchronized是不可以响应中断的;
ReentrantLock是API级别的,synchronized是JVM级别的;
ReentrantLock可以实现公平锁,而synchronized释放锁后,线程竞争无序,可能导致线程饥饿(先到的线程一直得不到锁);
ReentrantLock通过Condition可以绑定多个条件;
底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略;
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
通过Lock可以知道有没有成功获取锁,而synchronized无法得知获得锁情况;
Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。
通过以上对比,ReentrantLock可能更加灵活一些,但是从性能考虑,JDK1.6以后对synchronized做了很多优化锁,实际中还是提倡synchronized来进行同步。