synchronized
为什么要用synchronized
多线程有三大特性:原子性,有序性(重排序问题?)和可见性;其中原子性在程序上的体现就是使用synchronized,为了保证可见性是使用需要使用volatile.使用synchronized来保证程序的原子性
首先在讨论三大特性前需要知道java的内存模型和happen-before原则。内存模型与happen-before原则
牵扯出的问题:
什么是线程安全:我的理解就是如果保证多线程执行的结果和预期结果相同,就是线程安全的,否则就是线程不安全的
什么是JAVA内存模型?
在java内容中程序计数器,虚拟机栈,本地方法栈都是线程私有的区域,而方法区和java堆是线程共享的区域
因为cpu的处理速度与内存的读写速度之间有着着巨大的差距,所以在cpu和内存间添加一层缓存,每次现在cpu的缓存中,这个缓存区域的读写速度往往比内存的读写速度快很多,这样就能够缓解cpu和主存之间的速度差距。
所以每次cpu读写数据前,会先读取共享变量到本地内容,cpu再从本地内存中读取数据。这就是java的内存模型
这样也会产生数据不一致的情况,这是就要需要保证共享变量的数据可见性,即线程1修改了变量,则线程2再修改变量前已经得知变量已经被修改了。
应用synchronized
为了保证程序的原子性:我们在程序中使用synchronized关键字对程序进行锁定,表示在锁住的区域内只能有一个线程访问。
如何使用synchronized
- 使用synchronized修饰静态方法,针对类变量进行加锁。进入方法的前提是获得类的锁。
- 修饰实例方法,针对实例对象,在进入方法前要获得实例变量的锁。
- 修饰代码块。指定加锁的对象,在进入代码块钱要先获得指定对象的锁。
1 | public class SynchronizedTest { |
典型的使用synchronized的场景–单例模式的双重锁结构
在讨论双重锁的前,需要聊聊单例模式,单例模式通俗的来说一个类只能构造一个实例。针对的实现又两种:1.饿汉模式,2.懒汉模式
饿汉模式
1 | public class Singleton { |
这种方式不会影响到多线程的线程安全问题,因为类的装载机制在初始化对象的时候是保证不会有第二个线程进入的。但是有个很大的弊端是他不是lazy-loading
的,这回产生资源的浪费,比如创建完对象后,自始至终没用过。所以不推荐使用
懒汉模式
1 | public class Singleton { |
上面代码的第一种是不能保证线程安全的,多线程下会导致失效。而第二种使用静态内部类的方式实现能够实现线程安全得宜于类的加载机制,类似饿汉模式。但同时具有lazy-Loading
的特性。是常用的单例模式用法。
double-check 双重锁
1 | public class DoubleCheck { |
实际上double-check也是懒汉模式的一种,能够保证线程安全。很完美。。
synchronzied 底层实现
synchronized 同步语句块
的实现使用的是monitorenter
和monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized 修饰的
方法
并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
volatile
volatile是java最轻量级的同步机制。
volatile 特性
- volatile作用与变量,用来保证变量的在各线程之间的可见性,使用volatile修饰变量,表示变量不稳定,每次都需要写入,并重新获取.所以每次都是获取的都是主存内最新的变量的值.
- 禁止指令重排列优化
- volatile 只能保证可见性,不能保证原子性.
volatile和synachronized的区别
- volatile是线程同步的轻量级实现,所以说使用volatile的性能肯定要强于synchronized。
- volatile作用于变量,而synachronzied作用于方法和代码块。
- 多线程间使用volatile不会发生阻塞,而使用synachronized可能发生阻塞
- volatile保证变量在多线程间的可见性,而synchronized既能够保证可见性,又能保证原子性。