Java核心-并发中的线程同步与锁

一、线程安全问题的产生

线程安全问题:指的是在多线程编程中,同时操作同一个可变的资源之后,造成的实际结果与预期结果不一致的问题。

比如:A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

如果上面的内容您还没有理解,没关系,我们来看下面非安全线程的模拟代码:

public class ThreadSafeSample {

    public int number;

    public void add() {

        for (int i = 0; i < 100000; i++) {

            int former = number++;

            int latter = number;

            if (former != latter-1){

                System.out.printf("非相等 former=" +  former + " latter=" + latter);

            }

        }

    }

    public static void main(String[] args) throws InterruptedException {

        ThreadSafeSample threadSafeSample = new ThreadSafeSample();

        Thread threadA = new Thread(new Runnable() {

            @Override

            public void run() {

                threadSafeSample.add();

            }

        });

        Thread threadB = new Thread(new Runnable() {

            @Override

            public void run() {

                threadSafeSample.add();

            }

        });

        threadA.start();

        threadB.start();

        threadA.join();

        threadB.join();

    }

}

复制代码

我电脑运行的结果:非相等 => former=5555 latter=6061

可以看到,仅仅是两个线程的低度并发,就非常容易碰到 former 和 latter 不相等的情况。这是因为,在两次取值的过程中,其他线程可能已经修改了number.

二、线程安全的解决方案

线程安全的解决方案分为以下几个维度(参考《码出高效:Java开发手册》):

数据单线程可见(单线程操作自己的数据是不存在线程安全问题的,ThreadLocal就是采用这种解决方案);

数据只读;

使用线程安全类(比如StringBuffer就是一个线程安全类,内部是使用synchronized实现的);

同步与锁机制;

解决线程安全核心思想是:“要么只读,要么加锁”,解决线程安全的关键在于合理的使用Java提供的线程安全包java.util.concurrent简称JUC。

三、线程同步与锁

Java 5 以前,synchronized是仅有的同步手段,Java 5的时候增加了ReentrantLock(再入锁)它的语义和synchronized基本相同,比synchronized更加灵活,可以做到更多的细节控制,比如锁的公平性/非公平性指定。

3.1 synchronized

synchronized 是 Java 内置的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

3.1.1 synchronized 使用

synchronized 可以用来修饰方法和代码块。

3.1.1.1 修饰代码块

synchronized (this) {

    int former = number++;

    int latter = number;

    //...

}

复制代码

3.1.1.2 修饰方法

public synchronized void add() {

    //...

}

复制代码

3.1.2 synchronized 底层实现原理

synchronized 是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。在 Java 6 之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java 6的时候,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

3.1.2.1 偏向锁/轻量级锁/重量级锁

偏向锁是为了解决在没有多线程的访问下,尽量减少锁带来的性能开销。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

3.1.2.2 锁膨胀(升级)原理

Java 6 之后优化了 synchronized 实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,减低了锁带来的性能消耗,也就是我们常说的锁膨胀或者叫锁升级,那么它是怎么实现锁升级的呢?

锁膨胀(升级)原理:在锁对象的对象头里面有一个ThreadId字段,在第一次访问的时候ThreadId为空,JVM让其持有偏向锁,并将ThreadId设置为其线程id,再次进入的时候会先判断ThreadId是否尤其线程id一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会堵塞,执行一定次数之后就会升级为重量级锁,进入堵塞,整个过程就是锁膨胀(升级)的过程。

3.1.2.3 自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

3.1.2.4 乐观锁/悲观锁

悲观锁和乐观锁并不是某个具体的“锁”而是一种是并发编程的基本概念。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则与 Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

3.1.2.5 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

如果使用 synchronized 使用的是非公平锁,是不可设置的,这也是主流操作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很少会导致 “饥饿”发生。非公平锁的吞吐量大于公平锁。

非公平锁吞吐量大于公平锁的原因:

比如A占用锁的时候,B请求获取锁,发现被A占用之后,堵塞等待被唤醒,这个时候C同时来获取A占用的锁,如果是公平锁C后来者发现不可用之后一定排在B之后等待被唤醒,而非公平锁则可以让C先用,在B被唤醒之前C已经使用完成,从而节省了C等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。

3.2 ReentrantLock

ReentrantLock只能修饰代码块,使用ReentrantLock必须手动unlock释放锁,不然锁永远会被占用。

3.2.1 ReentrantLock 使用

ReentrantLock reentrantLock = new ReentrantLock(true); // 设置为true为公平锁,默认是非公平锁

reentrantLock.lock();

try {

}finally {

    reentrantLock.unlock();

}

复制代码

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

推荐阅读更多精彩内容