volatile既然不足以保证数据同步,那么就必须要引入锁来确保。互斥锁是最常见的同步手段,在并发过程中,当多条线程对同一个共享数据竞争时,它保证共享数据同一时刻只能被一条线程使用,其他线程只有等到锁释放后才能重新进行竞争。对于java开发人员,我们最熟悉的肯定就是用synchronized关键词完成锁功能,在涉及到多线程并发时,对于一些变量,你应该会毫不犹豫地加上synchronized去保证变量的同步性。
在C/C++可直接使用操作系统提供的互斥锁实现同步和线程的阻塞和唤起,与之不同的是,java要把这些底层封装,而synchronized就是一个典型的互斥锁,同时它也是一个JVM级别的锁,它的实现细节全部封装在JVM中实现,对开发人员只提供了synchronized关键词。根据锁的颗粒度,可以用synchronized对一个变量、一个方法、一个对象和一个类等加锁。被synchronized修饰的程序块经过编译后,会在前后生成monitorenter和monitorexit两个字节码指令,其中涉及到锁定和解锁对象的确定,这就要根据synchronized来确定了,假如明确指定了所对象,例如synchronized(变量)、synchronized(this)等,说明加解锁对象为变量或运行时对象。假如没有明确指定对象,则根据synchronized修饰的方法去找对应的锁对象,如修饰一个非静态方法表示此方法对应的对象为锁对象,如修饰一个静态方法则表示此方法对应的类对象为锁对象。当一个对象被锁住时,对象里面所有用synchronized修饰的方法都将产生堵塞,而对象里非synchronized修饰的方法可正常被调用,不收锁影响。
为了实现互斥锁,JVM的monitorenter和monitorexit字节码依赖底层操作系统的互斥锁来实现,java层面的线程与操作系统的原生线程有映射关系,这时如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,需要消耗很多处理器时间。如果可能,应该减少这样的切换,jvm一般会采取一些措施进行优化,例如在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
Synchronized还有另外一个重要的特性——可重入性。这个特性主要是针对当前线程而言的,可重入即是自己可以再次获得自己的内部锁,在尝试获取对象锁时,如果当前线程已经拥有了此对象的锁,则把锁的计数器加一,在释放锁时则对应地减一,当锁计数器为0时表示锁完全被释放,此时其他线程可对其加锁。可重入特性是为了解决自己锁死自己的情况,如下面伪代码:
public class DeadLock{
public synchronized void method1(){}
public synchronized void method2(){
this.method1();
}
public static void main(String[] args){
DeadLock deadLock=new DeadLock();
deadLock.method2();
}
}
这种情况其实也并非不常见,一个类中的同步方法调用另一个同步方法,假如synchronized不支持重入,进入method2方法时当前线程将尝试获取deadLock对象的锁,而method2方法里面执行method1方法时,当前线程又要去尝试获取deadLock对象的锁,这时由于不支持重入,它要去等deadLock对象的锁释放,把自己阻塞了,这就是自己锁死自己的现象。所以重入机制的引入,杜绝了这种情况的发生。
synchronized实现的是一个非公平锁,非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,当然也会产生线程饥饿现象。
synchronized最后一个特性(缺点)就是不可中断性,在所有等待的线程中,你们唯一能做的就是等,而实际情况可能是有些任务等了足够久了,我要取消此任务去干别的事情,此时synchronized是无法帮你实现的,它把所有实现机制都交给了JVM,提供了方便的同时也体现出了自己的局限性。
这节主要介绍java的synchronized关键词,包括它的作用及JVM及操作系统的底层实现,它的可重入性和不可中断性,它实现的是一个非公平的互斥锁,同时它也是一个悲观的并发策略,不管是否会产生竞争,任何的数据操作都必须要加锁。对synchronized深入全面的理解对理解tomcat中跟多线程并发相关的模块是很有帮助的。
喜欢研究java的同学可以交个朋友,下面是本人的微信号: