如何理解和使用synchronized关键字

synchronized

为什么要用synchronized

多线程有三大特性:原子性,有序性(重排序问题?)和可见性;其中原子性在程序上的体现就是使用synchronized,为了保证可见性是使用需要使用volatile.使用synchronized来保证程序的原子性

首先在讨论三大特性前需要知道java的内存模型和happen-before原则。内存模型与happen-before原则

牵扯出的问题:

  1. 什么是线程安全:我的理解就是如果保证多线程执行的结果和预期结果相同,就是线程安全的,否则就是线程不安全的

  2. 什么是JAVA内存模型?

    在java内容中程序计数器,虚拟机栈,本地方法栈都是线程私有的区域,而方法区和java堆是线程共享的区域

    因为cpu的处理速度与内存的读写速度之间有着着巨大的差距,所以在cpu和内存间添加一层缓存,每次现在cpu的缓存中,这个缓存区域的读写速度往往比内存的读写速度快很多,这样就能够缓解cpu和主存之间的速度差距。

    所以每次cpu读写数据前,会先读取共享变量到本地内容,cpu再从本地内存中读取数据。这就是java的内存模型

    这样也会产生数据不一致的情况,这是就要需要保证共享变量的数据可见性,即线程1修改了变量,则线程2再修改变量前已经得知变量已经被修改了。

    应用synchronized

为了保证程序的原子性:我们在程序中使用synchronized关键字对程序进行锁定,表示在锁住的区域内只能有一个线程访问。

如何使用synchronized

  • 使用synchronized修饰静态方法,针对类变量进行加锁。进入方法的前提是获得类的锁。
  • 修饰实例方法,针对实例对象,在进入方法前要获得实例变量的锁。
  • 修饰代码块。指定加锁的对象,在进入代码块钱要先获得指定对象的锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedTest {

//synchronized的三种用法
//1.修饰实例方法 需要拥有实例对象的锁
public synchronized void hello(){
System.out.println("hello");
}
//2.修饰静态方法 需要拥有类对象的锁
public synchronized static void staticHello(){
System.out.println("staticHello");
}
public void blockHello(){
//3.修饰代码块 需要拥有【this】的锁
synchronized(this){
System.out.println("code block hello");
}
}
}

典型的使用synchronized的场景–单例模式的双重锁结构

在讨论双重锁的前,需要聊聊单例模式,单例模式通俗的来说一个类只能构造一个实例。针对的实现又两种:1.饿汉模式,2.懒汉模式

饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private Singleton(){};
//1. 使用静态变量
// private static Singleton singleton = new Singleton();
//2.使用静态代码块 未使用时造成内存空间浪费
private static Singleton singleton;
static {
singleton = new Singleton();
}
public Singleton newInstance(){
return singleton;
}
}

这种方式不会影响到多线程的线程安全问题,因为类的装载机制在初始化对象的时候是保证不会有第二个线程进入的。但是有个很大的弊端是他不是lazy-loading的,这回产生资源的浪费,比如创建完对象后,自始至终没用过。所以不推荐使用

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
private void Singleton(){}
private static Singleton singleton;

// 普通的线程不安全的
public Singleton newInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}

//2.静态内部类 一方面能够达到lazy-loading的效果,另一方面能够保证线程安全,因为jvm保证初始化的时候别的线程是不能进入的
private static class newSingleton{
private static final Singleton INSTANCE = new Singleton();
}

public Singleton newInstanceInnerClass(){
return newSingleton.INSTANCE;
}
}

上面代码的第一种是不能保证线程安全的,多线程下会导致失效。而第二种使用静态内部类的方式实现能够实现线程安全得宜于类的加载机制,类似饿汉模式。但同时具有lazy-Loading的特性。是常用的单例模式用法。

double-check 双重锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DoubleCheck {
private void DoubleCheck(){}
private volatile DoubleCheck doubleCheck; //使用volatile保证原子性,防止重排序
public DoubleCheck newInstance(){
if(doubleCheck == null){
// 当两个线程同时到这步 a先进同步块,在a进入后获得instance后,b获得锁,进入同步块,
// 这时候下一个判断就起到作用了,这时候的doubleCheck不为空,
// 直接return,否则又️新建了一个对象
synchronized (DoubleCheck.class){
if(doubleCheck == null){
doubleCheck = new DoubleCheck();
}
}
}
return doubleCheck;
}
}

实际上double-check也是懒汉模式的一种,能够保证线程安全。很完美。。

synchronzied 底层实现

synchronized 同步语句块的实现使用的是monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

volatile

volatile是java最轻量级的同步机制。

volatile 特性

  1. volatile作用与变量,用来保证变量的在各线程之间的可见性,使用volatile修饰变量,表示变量不稳定,每次都需要写入,并重新获取.所以每次都是获取的都是主存内最新的变量的值.
  2. 禁止指令重排列优化
  3. volatile 只能保证可见性,不能保证原子性.

volatile和synachronized的区别

  1. volatile是线程同步的轻量级实现,所以说使用volatile的性能肯定要强于synchronized。
  2. volatile作用于变量,而synachronzied作用于方法和代码块。
  3. 多线程间使用volatile不会发生阻塞,而使用synachronized可能发生阻塞
  4. volatile保证变量在多线程间的可见性,而synchronized既能够保证可见性,又能保证原子性。
-------------本文结束感谢您的阅读-------------

本文标题:如何理解和使用synchronized关键字

文章作者:NanYin

发布时间:2019年04月22日 - 12:04

最后更新:2020年03月20日 - 14:03

原始链接:https://nanyiniu.github.io/2019/04/22/2019-04-22-Synchronized%E5%85%B3%E9%94%AE%E5%AD%97%E6%80%BB%E7%BB%93/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。