java并发修行之基础篇:线程安全

前言

在互联网应用广泛的今天,软件并发已经成为目前软件开发的必备基础。java作为一门成熟的语言,其拥有着极其高效的并发机制,是目前大中型企业的常用开发语言。想要开发大规模应用,java并发已成为java程序猿们的必备基础技能。

从今天开始,开启java并发修行之路。

什么是线程安全性

线程的安全性总是难以定义的。
在阅读《java并发编程实战》的过程中觉得说的很好:

在线程安全性的定义中,最核心的概念就是正确性。

何为正确性?

某个类的行为与其规范完全一致。

通常我们并不规定类的规范,于是我们通俗对正确性的理解是,单线程的类的行为是按照我们“所见”来运行的,我们确保其可信,“所见即所知”。

于是给出线程安全性的定义:

当多个线程访问某个类时,这个类始终都能表现出正确的行为。

某个对象保证多线程环境下共享、可修改的状态的正确性,那么即是线程安全。

换个角度分析:

  • 当某个类单线程条件都不是正确的,那么其肯定不是线程安全的。
  • 无状态对象一定线程安全
  • 状态不共享或不可修改,即不存在线程安全问题。

如何做到线程安全

线程安全要保证:

  1. 原子性。保证一组相关操作在竞态条件下保证结果可靠。
  2. 可见性。当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性。避免指令重排序。

解释一下上面三个安全特性:

原子性

关键词:一组相关操作竞态条件

先解释竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

简单的说,其本质就是基于了一个错误的状态去判断或执行计算。

上代码举例。比如一个多线程累加并打印偶数的程序。

程序A:

典型的竞态条件无处理:

public class EchoEvenNumService implements Runnable {
    private int num;
    @Override
    public void run() {
        if (num % 2 == 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print(num + "\t");
        }

        num++;
    }
}

运行main程序:

public static void main(String[] args) {
    EchoEvenNumService service = new EchoEvenNumService();
    for (int i = 0; i < 100; i++) {
        new Thread(service).start();
    }
}

结果是可想而知的,奇数偶数都有:

0   1   1   2   4   3   2   1   1   9   9   10  9   9   9   9   10  9   18  18  18  20  21  22  20  20  19 ...

程序B

有些同学会觉得,num改成线程安全的类型(AtomicInteger)就可以了,可是事实是这样吗?修改程序A:

public class EchoEvenNumService implements Runnable {
    private AtomicInteger num = new AtomicInteger(0);
    @Override
    public void run() {
        if (num.get() % 2 == 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print(num.get() + "\t");
        }
        num.incrementAndGet();
    }
}

运行main方法后,你会发现还是奇数偶数都有:

0   0   2   3   4   5   5   5   5   7   9   6   6   13  14  14  14  15  18  14  17  20  20  17  21  25  25  26  28  28  29  30 ...

这么写是因为没有理解一组相关操作。在上面的程序中,实际上需要做到三个操作:1.num.get() % 2 == 0判断。2.打印偶数。3.num递增。

即时上面三个操作各做各的做到了原子性,但是整体并不是原子性,程序依旧会错误。

程序C

做到整体的原子性,加锁同步。修改程序A:

public class EchoEvenNumService implements Runnable {
    private int num;
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            lock.lock();
            if (num % 2 == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(num + "\t");
            }
            num++;
        } finally {
            lock.unlock();
        }
    }
}

运行main方法后,程序终于保证了正确性:

0   2   4   6   8   10  12  14  16  18  20  22  24  26  28  30  32  34  36  38  40  42  44  46  48  50  52  54  56  58  60  62  64  66  68  70  72  74  76  78  80  82  84  86  88  90  92  94  96  98  

可见性

保证共享变量有效。
这里需要简单提一下Java的内存模型:


image

看图说话:

  • 主内存(Main Memory),存储所有变量。
  • 工作内存(Working Memory),保存了该线程使用到的变量的主存副本拷贝。
  • 线程、工作内存、主存三者关系:线程对变量的所有操作(读写等)都必须在工作内存中进行,而不能之间读写主内存中的变量。

如此可见,如果程序没有保证可见性,会使一部分线程读取到的是工作内存中的值(并不一定准确),导致程序不正确执行。

如何保证可见性?手段:加锁,volatile修饰。

有序性

解释下“重排序”现象:

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。

比如赋值两个变量:

private int a;
private int b;

线程A对a,b进行赋值,代码中的逻辑是这样的:

a = 1;
b = 2;

线程A在运行时,对a变量赋值发现a变量在主存中被其他线程加锁不能访问,线程A并不会等待锁释放,它会去尝试获取b变量,当b变量没有被占用时,线程A的执行过程就会变成这样:

b = 2;
a = 1;

这就是JVM内部优化导致的“指令重排序”。

重排序可能导致一些重要的状态值的读取顺序改变导致程序异常甚至会死循环发生OOM。

比如如下程序:

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

这个程序的诡异之处在于,ReaderThread可能永远看不到ready值,更诡异的是ReaderThread的输出可能是0,ReaderThread只读到了ready的值但没有读到number值。这一切“归功于”神奇的“重排序”。

解决方式:同步。

总结

本文java并发修行的第一篇,重在基础。本文简单讲解了线程安全的“定义”,以及线程安全的一些基础概念。核心在于线程并发处理共享变量时的三点保证:原子性可见性有序性。细细体会之,后续准备从源码层面详细对这三点进行分析。

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

推荐阅读更多精彩内容