什么是线程安全

线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

产生线程不安全的原因

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

多线程同时执行下面的代码可能会出错:

public class Counter {
    protected long count = 0;

    public void add(long value){
        this.count = this.count + value;
    }
}

想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存

观察线程A和B交错执行会发生什么:

   this.count = 0;
   A:   读取 this.count 到一个寄存器 (0)
   B:   读取 this.count 到一个寄存器 (0)
   B:   将寄存器的值加2
   B:   回写寄存器值(2)到内存. this.count 现在等于 2
   A:   将寄存器的值加3
   A:   回写寄存器值(3)到内存. this.count 现在等于 3

两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。

竞态条件 & 临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

共享资源

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。

局部变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

public void someMethod(){
  long threadSafeInt = 0;
  threadSafeInt++;
}

局部的对象引用

上面提到的局部变量是一个基本类型,如果局部变量是一个对象类型呢?对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内,所有的对象都存在共享堆中,所以对于局部对象的引用,有可能是线程安全的,也有可能是线程不安全的。

那么怎样才是线程安全的呢?如果在某个方法中创建的对象不会被其他方法或全局变量获得,或者说方法中创建的对象没有逃出此方法的范围,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:

public void someMethod(){
  LocalObject localObject = new LocalObject();
  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

上面样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象,始终在someMethod()方法内部。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。

对象成员对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }
}

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}

注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。

当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

现在两个线程都有自己单独的NotThreadSafe对象,访问的不是同一资源,不满足竞态条件,是线程安全的。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。

判断资源对象是否是线程安全

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象,数组,文件,数据库连接,套接字等等。Java中我们无需主动销毁对象,所以“销毁”指不再有引用指向对象。

注意即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:

检查记录X是否存在,如果不存在,插入X

如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:

线程1检查记录X是否存在。检查结果:不存在
线程2检查记录X是否存在。检查结果:不存在
线程1插入记录X
线程2插入记录X

同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

不可变的共享资源

当多个 线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。

我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

如果你需要对ImmutableValue类的实例进行操作,如添加一个类似于加法的操作,我们不能对这个实例直接进行操作,只能创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}

请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。

引用不是线程安全的!

重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:

public void Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用,因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,多个线程访问Calculator实例时仍可通过setValue()和add()方法改变它的状态,因此Calculator类不是线程安全的。

换句话说:ImmutableValue类是线程安全的,但使用它的类则不一定是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。

要使Calculator类实现线程安全,将getValue()setValue()add()方法都声明为同步方法即可。

Java中实现线程安全的方法

在Java多线程编程当中,提供了多种实现Java线程安全的方式:

  • 最简单的方式,使用Synchronization关键字:Java Synchronization介绍
  • 使用java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
  • 使用java.util.concurrent.locks 包中的锁
  • 使用线程安全的集合ConcurrentHashMap
  • 使用volatile关键字,保证变量可见性(直接从内存读,而不是从线程cache读)
时间: 2024-11-02 15:45:48

什么是线程安全的相关文章

新人一枚,请问怎样从B线程调用A线程的内容?

问题描述 新人一枚,请问怎样从B线程调用A线程的内容? 具体要求:写一个线程A,得到0-10的随机数,再写一个线程B来了调用线程A!!!!! 解决方案 int a;//全局变量,如果多个线程同步访问需要加锁 A...//线程A B...//线程B 代码结构大致如上,访问的时候,因为变量a是全局数据,所以在线程A和B中都可以直接访问,就好像C中的任何函数都可以访问和使用全局变量一样 解决方案二: 放是全局变量来交换数据

并发集合(三)使用阻塞线程安全的列表

使用阻塞线程安全的列表 列表(list)是最基本的集合.一个列表中的元素数量是不确定的,并且你可以添加.读取和删除任意位置上的元素.并发列表允许不同的线程在同一时刻对列表里的元素进行添加或删除,而不会产生任何数据不一致的问题. 在这个指南中,你将学习如何在你的并发应用程序中使用阻塞的列表.阻塞列表与非阻塞列表的主要区别是,阻塞列表有添加和删除元素的方法,如果由于列表已满或为空而导致这些操作不能立即进行,它们将阻塞调用的线程,直到这些操作可以进行.Java包含实现阻塞列表的LinkedBlocki

[转载]Linux 线程实现机制分析

  自从多线程编程的概念出现在 Linux 中以来,Linux 多线应用的发展总是与两个问题脱不开干系:兼容性.效率.本文从线程模型入手,通过分析目前 Linux 平台上最流行的 LinuxThreads 线程库的实现及其不足,描述了 Linux 社区是如何看待和解决兼容性和效率这两个问题的.   一.基础知识:线程和进程 按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位.在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开

你真的了解:IIS连接数、IIS并发连接数、IIS最大并发工作线程数、应用程序池的队列长度、应用程序池的最大工作进程数 吗?

原文:你真的了解:IIS连接数.IIS并发连接数.IIS最大并发工作线程数.应用程序池的队列长度.应用程序池的最大工作进程数 吗? IIS连接数   一般购买过虚拟主机的朋友都熟悉购买时,会限制IIS连接数,这边先从普通不懂代码用户角度理解IIS连接数 顾名思义即为IIS服务器可以同时容纳客户请求的最高连接数,准确的说应该叫"IIS限制连接数" 这边客户请求的连接内容包括: 1.网站html请求,html中的图片资源,html中的脚本资源,其他需要连接下载的资源等等,任何一个资源的请求

我的代码是怎么进入死循环的,为什么有两个线程运行一会就唤不醒了

问题描述 classResource{privateStringname;privateintcount=1;privatebooleanflag=false;publicsynchronizedvoidset(Stringname){while(flag){notify();try{wait();}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName()+"....循环唤醒..."

线程基础之JAVA和C++0x的特性

译文连接   译文地址  译者:衣着时   校对:丁一    (有兴趣参与试译或校对的同学,请加入并发网试译者QQ群:369468545) JAVA特性 JAVA线程通常是一个带有run()方法的java.lang.Thread的子类,然后调用这个子类对象的start()方法.我们之前定义过,数据竞争是因为两个线程同时访问内存单元,在JAVA中,内存单元是一个对象字段或数组元素. 由于JAVA旨在支持运行不受信任代码作为受信任的应用程序的一部分,必须限制不受信任代码的数据争用造成的破坏.因此不允

ios-在背景线程中处理数据

问题描述 在背景线程中处理数据 我写了一方法如下: - (BOOL)shouldDoSomeWork { BOOL result = // here I need do hard work with data in background thread and return result so main thread should wait until the data is calculated and then return result; return result;} 如何实现? 解决方案

线程同步 卖票问题 三个车站卖七张票

问题描述 线程同步 卖票问题 三个车站卖七张票 正确的写法:public class mainclass { public static void main(String[] args) { Train A=new Train(); Train B=new Train(); Train C=new Train(); A.start(); B.start(); C.start(); } } class Train extends Thread{ public static int ticket=7

在非UI线程处理Bitmap

http://my.oschina.net/ryanhoo/blog/88344 译者:Ryan Hoo 来源:https://developer.android.com/develop/index.html 译者按: 在Google最新的文档中,提供了一系列含金量相当高的教程.因为种种原因而鲜为人知,真是可惜!Ryan将会细心整理,将之翻译成中文,希望对开发者有所帮助.         本系列是Google关于展示大Bitmap(位图)的官方演示,可以有效的解决内存限制,更加有效的加载并显示图

基本线程同步(四)在同步代码中使用条件

在同步代码中使用条件 在并发编程中的一个经典问题是生产者与消费者问题,我们有一个数据缓冲区,一个或多个数据的生产者在缓冲区存储数据,而一个或多个数据的消费者,把数据从缓冲区取出. 由于缓冲区是一个共享的数据结构,我们必须采用同步机制,比如synchronized关键字来控制对它的访问.但是我们有更多的限制因素,如果缓冲区是满的,生产者不能存储数据,如果缓冲区是空的,消费者不能取出数据. 对于这些类型的情况,Java在Object对象中提供wait(),notify(),和notifyAll()