线程安全性:num++操作为什么也会出问题?

  线程的安全性可能是非常复杂的,在没有充足同步的情况下,由于多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果(非预期的)。下面的Tools工具类的plus方法会使计数加一,为了方便,这里的num和plus()都是static的:

public class Tools {

    private static int num = 0;

    public  static int plus() {
        num++;
        return num;
    }

}

  我们再编写一个任务,调用这个plus()方法并输出计数:

public class Task implements Runnable {

    @Override
    public void run(){
        int num = Tools.plus();
        System.out.println(num);
    }
}

  最后创建10个线程,驱动任务:

public class Main {

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

  输出:

2
4
3
1
5
6
7
8
9
10

  看起来一切正常,10个线程并发地执行,得到了0累加10次的结果。我们把10次改为10000次:

public class Main {

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

  输出:

...
9994
9995
9996
9997
9998

  在我的电脑上,这个程序只能偶尔输出10000,为什么?

  问题在于,如果执行的时机不对,那么两个线程会在调用plus()方法时得到相同的值,num++看上去是单个操作,但事实上包含三个操作:读取num,将num加一,将计算结果写入num。由于运行时可能多个线程之间的操作交替执行,因此这多个线程可能会同时执行读操作,从而使它们得到相同的值,并将这个值加1,结果就是,在不同的线程调用中返回了相同的数值。

A线程:num=9→→→9+1=10→→→num=10
B线程:→→→→num=9→→→9+1=10→→→num=10

  如果把这个操作换一种写法,会看的更清晰,num加一后赋值给一个临时变量tmp,并睡眠一秒,最后将tmp赋值给num:

public class Tools {

    private static int num = 0;

    public static int plus() {
        int tmp = num + 1;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = tmp;
        return num;
    }

}

  这次我们启动两个线程就能看出问题:

public class Main {

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

  启动程序后,控制台1s后输出:

1
1
A线程:num=0→→→0+1=1→→→num=1
B线程:→num=0→→→0+1=1→→→num=1

  上面的例子是一种常见的并发安全问题,称为竞态条件(Race Condition),在多线程环境下,plus()是否会返回唯一的值,取决于运行时对线程中操作的交替执行方式,这并不是我们希望看到的情况。

  由于多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量,线程会由于无法预料的数据变化而发生错误。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生彼此干扰。幸运的是,java提供了各种同步机制来协同这种访问。

  将plus()修改为一个同步方法,同一时间只有一个线程可以进入该方法,可以修复错误:

public class Tools {

    private static int num = 0;

    public synchronized static int plus() {
        int tmp = num + 1;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = tmp;
        return num;
    }

}

  控制台先后输出:

1
2

  这时如果将plus()方法改为num++,驱动10000个线程去执行,也可以保证每次都能输出到10000了。

  除了安全性,多线程程序还有可能出现活跃性问题(死锁等),性能问题(上下文切换等),这些问题我们后续再详细说明。

  

时间: 2024-11-06 01:32:03

线程安全性:num++操作为什么也会出问题?的相关文章

Java线程安全性中的对象发布和逸出

发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外的代码.比如return一个对象,或者作为参数传递到其他类的方法中. 什么是逸出?如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性. 概念我们知道了,可我们要关注什么地方呢?我们要关注的时候就是逸出问题,在不该发布该对象的地方就

VC和gcc在保证函数static变量线程安全性上的区别

VC和gcc不同,不能保证静态变量的线程安全性.这就给我们的程序带来了很大的安全隐患和诸多不便.这一点应该引起我们的重视!尤其是在构造函数耗时比较长的时候,很可能给程序带来意想不到的结果.本文从测试代码开始,逐步分析原理,最后给出解决方案. 多线程状态下,VC不能保证在使用函数的静态变量的时候,它的构造函数已经被执行完毕,下面是一段测试代码: class TestStatic { public: TestStatic() { Sleep(1000*10); m_num = 999; } publ

第二章:线程安全性——java并发编程实战

一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问状态变量时使用同步机制 完全由线程安全类构造的程序也不一定是线程安全的,线程安全类中也可以包含非线程安全的类 一.什么是线程安全性 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的.(因为线程安全类中封装了必要的同步代码) 一个无状态的类是线程安全的.无状态类是指不

《Java并发变成实践》读书笔记---第二章 线程安全性

什么是线程安全性 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问.从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据."共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以发生变化.所以编写线程安全的代码更侧重于如何防止在数据上发生不受控的并发访问. 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误

线程安全性

多线程程序中,如果控制不好,经常会出现各种的问题,有时候问题在调试或者测试的时候就会暴露出来,最要命的是程序部署一段时间之后才出现各种怪异现象,感到头疼?需要补充Java并发的知识了.该读书笔记系列以<Java并发编程实战>为基础,同时会参考网络上一些其他的资料,和大家一起学习Java并发编程的各个方面.这方面也是笔者比较薄弱的地方,理解不对的地方请留言或者邮件指出,同时也欢迎讨论.邮箱:[email protected] 线程安全性这章,将从如下几个方面入手,描述探讨: 1.什么是对象的状态

ConcurrentHashMap和 CopyOnWriteArrayList提供线程安全性和可伸缩性 以及 同步的集合类 Hashtable 和 Vector Collections.synchronizedMap 和 Collections.synchronizedList 区别缺点

ConcurrentHashMap和 CopyOnWriteArrayList提供线程安全性和可伸缩性 DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含了一些主要集合类型 List 和 Map 的高性能的.线程安全的实现.在本月的 Java理论与实践中,BrianGoetz向您展示了用 ConcurrentHashMap 替换 Hashtable 或 synchronizedMap ,将有多少并发程序获益. 在Java类库中出现的第一个关联的集合类

[Java Concurrency in Practice]第二章 线程安全性

线程安全性 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享和可变的状态的访问. 对象的状态是指存储在状态变量(例如实例或静态域)中的数据.对象的状态可能包括在其他依赖对象的域.例如,某个HashMap的状态不仅存储在HashMap对象本身,还存储在许过Map.Entry对象中.在对象的状态中包含了任何可能影响其外部可见行为的数据. "共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以发生变化. 一个对象是否需

浅析HashMap与ConcurrentHashMap的线程安全性

本文要解决的问题: 最近无意中发现有很多对Map尤其是HashMap的线程安全性的话题讨论,在我的理解中,对HashMap的理解中也就知道它是线程不安全的,以及HashMap的底层算法采用了链地址法来解决哈希冲突的知识,但是对其线程安全性的认知有限,故写这篇博客的目的就是让和我一样对这块内容不熟悉的小伙伴有一个对HashMap更深的认知. 哈希表 在数据结构中有一种称为哈希表的数据结构,它实际上是数组的推广.如果有一个数组,要最有效的查找某个元素的位置,如果存储空间足够大,那么可以对每个元素和内

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 package com.roocon.thread.t3; public class Sequence { private int value; public int getNext(){ return value++; } public static void main(String[] args) { S