用synchronized就一定线程安全吗?
用synchronized对方法进行同步,还真不一定线程安全,来看个简单的例子
上面的代码运行出来的结果:
看到没,并不是2000000,那么为什么f1和f3方法都用了synchronized关键字,然而并没有达到我们需要的结果呢?这需要从synchronized的原理开始讲起。
synchronized关键字有下面三种用法:
修饰实例方法:对当前实例加锁,进入方法需要获得当前实例的锁
修饰静态方法:对当前类对象加锁,进入静态方法需要获得当前类对象的锁
修饰代码块:对指定对象进行加锁,进入代码块需要获得指定对象的锁
那么上面三种方式有什么区别呢?这需要先理解下synchronized的底层语义。java中的同步是基于进入和退出管程(Moniter)对象来实现的,无论是显式同步(有明确的monitorenter和monitorexit指令,即同步代码块)还是隐式同步(同步方法,方法调用指令读取运行时常量池中的方法的ACC_SYNCHRONIZED标志来隐式实现的)。
先看下基于对象实现的,需要了解下java对象。在JVM中,对象在内存中的区域分成三部分:对象头,实例变量,填充数据。
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
对象头:是实现synchronized锁对象的基础。synchronized使用的锁对象是存储在Java对象头里的,其主要结构是由Mark Word 和 Class Metadata Address 组成。
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
了解了对象头,我们就可以知道为什么上面的代码虽然都使用了synchronized修饰,但是还是有线程安全问题,因为静态方法和实例方法锁的对象是不一致的(Monitor不是同一个),所以导致最终没有达到预期效果。