02 线程安全与Synchronized

1. 什么是线程安全问题

线程的合理使用能够提升程序的处理性能,主要有两个方面,第一个是能够利用多核 cpu 以及超线程技术来实现线程的并行执行;第二个是线程的异步化执行相比于同步执行来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量。同时也带来了很多麻烦。如:多线程对于共享变量访问带来的安全性问题
一个变量 i,假如一个线程去访问这个变量进行修改,这个时候对于数据的修改和访问没有任何问题。但是如果多个线程对于这同一个变量进行修改,就会存在一个数据安全性问题。

对于线程安全性,本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。共享,是指这个数据变量可以被多个线程访问;可变,指这个变量的值在它的生命周期内是可以改变的。若共享变量对于多线程来说只读不写并不存在线程安全问题。

public class SynchronizedDemo {

    private static int count = 0;

    private static void countIncr()  {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                countIncr();
            }).start();
        }
        // 睡眠5秒,确保线程执行结束
        TimeUnit.SECONDS.sleep(5);
        System.out.println("count_result:" + count);
    }
}

通过结果发现count_result有时为98或100,不是一个固定不变的值,和我们期望的结果不一样。这就是多线程对共享变量的读写带来的安全性问题。

2. 多线程的数据安全性

2.1 如何保证数据安全性

问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使线程并行变成串行去访问共享数据,这样就不存问题了。

我们可以通过加锁来保证共享数据的安全问题。锁是处理并发的一种同步手段,而如果需要达到前面我们说的一个目的,那么这个锁一定需要实现互斥的特性。

java提供加锁的方法就是Synchroinzed关键字

2.2 Synchroinzed

通过Synchroinzed解决前面例子出现的线程安全问题。

    // 加锁保证线程安全问题
    private synchronized static void countIncr()  {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

synchronized实现同步的基础:java中的每一个对象都可以作为锁。具体表现为以下3种形式:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

2.3 锁是如何存储的

为了实现多线程的互斥性,那么这把锁需要哪些东西呢?

  • 锁需要一个东西来表示。
  • 需要记录锁的状态(获得锁是什么状态,无锁是什么状态)
  • 锁状态需要对多个线程共享

synchroinzed(lock)是基于lock对象来控制锁的,因此锁和这个lock对象有关。因此我们需要关注对象在JVM内存中是如何存储的。

从JVM规范中可以看出Synchroinzed在JVM的实现原理,JVM进入和退出Monior对象来实现方法同步和代码块同步,两者的实现细节不一样但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令实在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权即尝试获取对象的锁。

2.4 JAVA对象头

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

image-20200715232707602

Synchronized用的锁存在java对象头里的。

image-20200713230233355

java对象头里的Mark Word里默认存储对象的hashCode、分带年龄和锁标记位。32位JVM的Mark Word存储结构如下:

image-20200713230655649

在运行期间,Mark Word里面存储的数据会随着锁标志位的变化而变化。Mark Word可能变为存储以下4种数据

image-20200713230922739

可以看到,当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

2.5 用户态和内核态

平时我们所写的java程序是运行在用户空间的,因为我们的jvm对于操作系统来讲就是一个普通程序。用户空间的程序要执行读写硬盘、读写网络、读写内存等重要操作时必须经过操作系统内核来进行。

在JDK早期,Synchronized是重量级锁,每次申请锁都需要调用系统内核。需要从用户空间切换到内核空间,拿到锁后再将状态返回给用户空间。

2.6 CAS原理

2.6.1 什么是CAS

Compare and Swap,即比较再交换。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

2.6.2 CAS算法理解

CAS是一种无锁算法,CAS有3个操作数,内存值N,旧的预期值E,要修改的新值V。当且仅当预期值E和内存值N相同时,将内存值N修改为V。

存在ABA问题:一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A。此问题可以加入版本号解决,每次更新内存值后加入一个版本号进行区分。

image-20200715233141405

2.7 锁的升级与对比

java 1.6后为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁、轻量级锁。锁一共有4种状态,从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。锁升级后不能降级。

匿名偏向:锁对象线程ID为空,偏向锁的标识为1。

image-20200715221031149

2.7.1 偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

2.7.1.1 偏向锁的获取

  1. 首先获取锁对象的Markword,检查对象头中是否存储了当前线程的ID,如果存储了当前线程ID,表示当前线程已经获得了锁。

  2. 如果没有存储当前线程ID,锁对象处于可偏向状态(MarkWord中的偏向锁标识为1 且线程ID为空)。通过 CAS 操作,把当前线程的 ID写入到 MarkWord。

    • 如果 cas 成功,将对象头MarkWord的线程ID指向自己(变为T1|Epoch|1|01)。表示已经获得了锁对象的偏向锁,接着执行同步代码

      块。

    • 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁。

  3. 如果没有存储当前线程ID,锁对象处于已偏向状态(MarkWord中的偏向锁标识为1 且线程ID不为空)。当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

  4. 如果没有存储当前线程ID,且偏向锁标识为0,通过 CAS 操作,将对象头MarkWord的线程ID指向自己。

2.7.1.2 偏向锁的撤销

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程。
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。
image-20200715232215020

2.7.1.3 关闭偏向锁

偏向锁默认是启用的,但是它在应用程序启动几秒后才激活。如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定程序所有的锁通常情况下是处于竞争状态,可以通过JVM参数关闭偏向锁--XX:-UseBiasedLocking,那么程序默认会进入轻量级锁状态。

偏向锁为什么要延迟激活?

jvm在启动过程中是有大量的线程竞争资源的,这个时候启动偏向锁是没有意义的,所以延迟开启等待JVM启动。

openjdk提供了一个查看java对象布局的工具jol-core,来验证各个状态的MarkWord。注意关注锁标志位的变化

  1. JVM启动后创建对象

    此时偏向锁延迟开启还未启动,创建的对象为普通对象,加锁后直接变为轻量级锁。

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // 默认情况下偏向锁会延迟打开,此时偏向锁未启动
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    
    image-20200716002349395
  1. 延迟创建对象

    睡眠后创建对象,此时偏向锁已经开启,创建的对象为匿名偏向对象,加锁后为偏向锁。

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // 10后,偏向锁已启动
            TimeUnit.SECONDS.sleep(10);
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    
    image-20200716002725716
  1. 关闭延迟参数

    启动参数 -XX:BiasedLockingStartupDelay=0

    关闭偏向锁的延迟开启,创建的对象为匿名偏向对象,加锁后为偏向锁。结论和2相同

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // VM配置-XX:BiasedLockingStartupDelay=0关闭偏向锁的启动延迟
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    
    image-20200716003305913
  1. 关闭偏向锁

启动参数 -XX:-UseBiasedLocking,结论和1相同

public class MarkWordDemo {

    public static void main(String[] args) throws InterruptedException {
        // -XX:-UseBiasedLocking
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        System.out.println("-----------------");
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
}
image-20200716003444083

2.8.2 轻量级锁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342