前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 并发编程之 Synchronized 关键字最全讲解

Java 并发编程之 Synchronized 关键字最全讲解

作者头像
帅飞
发布2019-01-22 17:24:49
4030
发布2019-01-22 17:24:49
举报
文章被收录于专栏:Java知其所以然

synchronized关键字

synchronized锁什么? 锁对象。

锁的对象包括:

  • this
  • 临界资源对象
  • Class 类对象。

synchronized 除了保障原子性外,其实也保障了可见性。因为 synchronized 无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。

synchronized 同步的缺点

  1. synchronized关键字同步的时候,等待的线程将无法控制,只能死等。
  2. synchronized关键字同步的时候,不保证公平性,因此会有线程插队的现象。

同步方法

代码语言:javascript
复制
synchronized T methodName(){//do smoething}

同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行。 也就是说当一个线程访问同步方法时,其他线程访问这个方法将会被阻塞(等待锁)。

同步代码块

用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方法执行一个较长时间的任务,那么 B 线程必须等待比较长的时间。这种情况下可以尝试使用 synchronized 同步代码块来解决问题。

同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。

技巧

为了尽可能的保证程序的性能,所以使用了同步块,在进行输出语句的调用时,并不会将当前对象锁定。众所周知,Java 在 I/O 方面的处理是比较慢的,因此在同步的语句当中,我们应当尽量的将 I/O 语句移出同步块(当然还包括一些其它处理较慢的语句)。 一句话:把需要同步的代码块包起来,注意不要把耗时的操作放在同步代码块中。比如打印输出、IO 操作等等。

锁定临界对象

代码语言:javascript
复制
public class SynchronizedDemo<T> {
    Object object = new Object();

    T methodName(){
        synchronized(object){        //do something
        }
    }
}

同步代码块在执行时,是锁定 object对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。

synchronized(非this对象 object)将 object 对象本身作为对象监视器

synchronized(非this对象 object),这个对象如果是实例变量的话,指的是对象的引用,只要对象的引用不变,即使改变了对象的属性,运行结果依然是同步的。

锁的是堆内存中的对象,而不是引用。

结论

1、当多个线程同时执行 synchronized(object){} 同步代码块时呈同步效果

2、当其他线程执行 object 对象中的 synchronized 同步方法时呈同步效果

3、当其他线程执行 object 对象方法中的 synchronized(this) 代码块时也呈同步效果

4、在定义同步代码块时,不要使用常量对象作为锁目标对象。比如字符串常量、整形等。

锁定当前对象

代码语言:javascript
复制
T methodName(){    synchronized(this){    //do something
    }
}

当锁定对象为this 时,相当于同步方法。

锁定Class对象(同步静态方法)

代码语言:javascript
复制
/**
 * 同步方法 - static
 * 静态同步方法,锁的是当前类型的类对象。在本代码中就是Test_02.class
 */package concurrent.t01;import java.util.concurrent.TimeUnit;public class Test_02 {
    private static int staticCount = 0;    public static synchronized void testSync4(){
        System.out.println(Thread.currentThread().getName() 
                + " staticCount = " + staticCount++);        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }    public static void testSync5(){        synchronized(Test_02.class){
            System.out.println(Thread.currentThread().getName() 
                    + " staticCount = " + staticCount++);
        }
    }

}

synchronized 还可以应用在静态方法上,如果这么写,则代表的是对当前 .java 文件对应的 Class 类加锁。

静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁。

synchronized 锁重入

代码语言:javascript
复制
public class Test_06 {

    synchronized void m1(){ // 锁this
        System.out.println("m1 start");        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end");
    }
    synchronized void m2(){ // 锁this
        System.out.println("m2 start");        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2 end");
    }    public static void main(String[] args) {        new Test_06().m1();
    }
}

关键字 synchronized 拥有锁重入的功能。所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的。 锁重入的实现是通过

同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。

这种锁重入的机制,也支持在父子类继承的环境中。 子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法。

发生异常自动释放锁

代码语言:javascript
复制
public class Test_08 {
    int i = 0;    synchronized void m(){
        System.out.println(Thread.currentThread().getName() + " - start");        while(true){
            i++;
            System.out.println(Thread.currentThread().getName() + " - " + i);            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {                // TODO Auto-generated catch block
                e.printStackTrace();
            }            if(i == 5){
                i = 1/0;
            }
        }
    }    public static void main(String[] args) {        final Test_08 t = new Test_08();        new Thread(new Runnable() {            @Override
            public void run() {
                t.m();
            }
        }, "t1").start();        new Thread(new Runnable() {            @Override
            public void run() {
                t.m();
            }
        }, "t2").start();
    }

}

运行结果:

t1、t2 俩个线程同时启动,t1 先拿到锁,t2 等待锁进入阻塞状态。当 t1 打印到 5 时,发生运行时异常,释放锁。t2 线程拿到锁开始执行任务,打印数据。

由此可以得出:

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

多方法调用原子性问题(业务)

代码语言:javascript
复制
public class Test_05 {    private double d = 0.0;    public synchronized void m1(double d){        try {            // 相当于复杂的业务逻辑代码。
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }        this.d = d;
    }    public double m2(){        return this.d;
    }    public static void main(String[] args) {
        final Test_05 t = new Test_05();        new Thread(new Runnable() {
            @Override            public void run() {
                t.m1(100);
            }
        }).start();
        System.out.println(t.m2());        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.m2());
    }

}

打印结果为:

0.0 100.0

由以上打印结果可以得出:

同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。

注意在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定统一个资源。

一般来说,商业项目中,不考虑业务逻辑上的脏读问题。在数据库上要考虑脏读。

锁的底层实现

Java 虚拟机中的同步(Synchronization)是基于进入和退出管程(Monitor)对象实现。同步方法并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

对象内存简图

对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。 实例变量:存放类的属性数据信息,包括父类的属性信息 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。 ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,以及 _Owner 标记。其中 _WaitSet 是用于管理等待队列(wait)线程的,_EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。线程状态图如下:

当多线程并发访问同一个同步代码时,首先会进入 _EntryList,当线程获取锁标记后,monitor 中的 _Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在 _EntryList 中继续阻塞。若执行线程调用 wait 方法,则monitor中的计数器执行赋值为0计算,并将 _Owner 标记赋值为 null,代表放弃锁,执行线程进如 _WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入 _EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor中的 _Owner 标记赋值为 null,且计数器赋值为0计算。

锁的种类

Java 中锁的种类大致分为

  • 偏向锁
  • 自旋锁
  • 轻量级锁
  • 重量级锁。

锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。

注意:锁只能升级,不能降级。

重量级锁

同步方法和同步代码块中解释的就是重量级锁。

偏向锁

是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在 Monitor 中有变量ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。

轻量级锁

过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。 两个线程也可能出现重量级锁。

自旋锁

是一个过渡锁,是偏向锁和轻量级锁的过渡。 当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。

总结

  • 每个对象都有一个锁。用来在多线程访问的时候实现同步。
  • synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。
  • 从执行效率的角度考虑,有时候我们未必要把整个方法都加上synchronized,而是可以采取 synchronized 块的方式,对会引起线程安全问题的那一部分代码进行 synchronized 就可以了。
  • 两个 synchronized 块之间具有互斥性
  • synchronized 块获得的是一个对象锁,换句话说,synchronized 块锁定的是整个对象。
  • Java 还支持对”任意对象”作为对象监视器来实现同步的功能。这个”任意对象”大多数是实例变量及方法的参数,使用格式为 synchronized(非this对象)
  • 同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-09-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java知其所以然 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • synchronized关键字
    • synchronized 同步的缺点
      • 同步方法
        • 同步代码块
          • 技巧
        • 锁定临界对象
          • synchronized(非this对象 object)将 object 对象本身作为对象监视器
        • 锁定当前对象
          • 锁定Class对象(同步静态方法)
            • synchronized 锁重入
              • 发生异常自动释放锁
                • 多方法调用原子性问题(业务)
                  • 锁的底层实现
                    • 对象内存简图
                  • 锁的种类
                    • 重量级锁
                    • 偏向锁
                    • 轻量级锁
                    • 自旋锁
                • 总结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档