在Java编程中,为了保证线程安全,有3种不同的思路
1、互斥同步:包括synchronized和lock等。
2、非阻塞同步:如AtomicInteger的increaseAndGet()方法等。
3、无同步:如ThreadLocal方案。
本文介绍使用synchronized实现同步的方法。
1、修饰方法
synchronized static方法:锁加在类上;synchronized 普通方法:锁加在对象上。由此可以知道:
- 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
- 当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
- 如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
- 如果一个线程执行一个对象的synchronized普通方法,另外一个线程需要执行这个对象所属类的synchronized static方法,此时不会发生互斥现象,因为访问synchronized static方法占用的是类锁,而访问synchronized普通方法占用的是对象锁,所以不存在互斥问题。
2、修饰代码块:代码如下所示。
synchronized(synObject) { ...... }
当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使得其他线程无法同时访问该代码块。synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。修饰代码块与修饰方法相比,更加灵活,可以选择方法中需要同步的代码块进行同步。
无论synObject是不是this,对于被同步的对象而言,不同方法之间的相互影响,类似于1中描述:如果是某对象的一个同步代码块被执行,那么该对象的所有同步代码块、同步方法,都要等该代码块执行完才能够执行;同步方法与之类似。
3、synchronized还可以修饰类,如下所示。
synchronized(xxx.class) { ...... }
就像synchronized(syncObject)与synchronized普通方法作用类似,synchronized(xx.class)与synchronized static方法作用类似,是加在类上的锁。同样,synchronized(xx.class)与synchronized static方法,如果是对同一类的锁,也会互斥。
4、对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
5、原理解析:在Java中,每一个对象都拥有一个锁标记(monitor),多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。每个类应该也有这个标记。这就解释了为什么同一个对象的不同synchronized方法和synchronized代码块是互斥的,因为他们共用一个锁;同样解释了类层面的互斥。
对于synchronized代码块,对应的字节码是monitorenter和monitorexit;synchronized方法对应的字节码仍然是synchronized。monitorenter和monitorexit字节码指令需要reference类型的参数,当Java程序中没有明确指定时,JVM会取实例对象或Class对象作为锁对象。
注意:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
6、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。而Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。所以synchronized是Java语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。