java是线程安全的吗

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java是线程安全的吗相关的知识,希望对你有一定的参考价值。

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。(Vector,HashTab;le)
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。(ArrayList,LinkedList,HashMap等)
概念:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
例子:
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B
得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0
(注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加
Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
安全性:
线程安全性不是一个非真即假的命题。 Vector 的方法都是同步的,因为java会有相应的机制是同一时刻只有一个线程对这个变量操作。并且
Vector 明确地设计为在多线程环境中工作。但是它的线程安全性是有限制的,即在某些方法之间有状态依赖(类似地,如果在迭代过程中 Vector
被其他线程修改,那么由 Vector.iterator() 返回的
iterator会抛出ConcurrentModifiicationException)。
对于 Java 类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的是在编写类时尽量记录下它们的线程安全行为。
Bloch 给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。这种系统有其局限性
– 各类之间的界线不是百分之百地明确,而且有些情况它没照顾到 –
但是这套系统是一个很好的起点。这种分类系统的核心是调用者是否可以或者必须用外部同步包围操作(或者一系列操作)。下面几节分别描述了线程安全性的这五种类别。
不可变
不可变的对象一定是线程安全的,并且永远也不需要额外的同步。因为一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。Java
类库中大多数基本数值类如 Integer 、 String 和 BigInteger 都是不可变的。
需要注意的是,对于Integer,该类不提供add方法,加法是使用+来直接操作。而+操作是不具线程安全的。这是提供原子操作类AtomicInteger的原。
线程安全
线程安全的对象具有在上面“线程安全”一节中描述的属性 –
由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排线程都不需要任何额外的同步。这种线程安全性保证是很严格的 –
许多类,如 Hashtable 或者 Vector 都不能满足这种严格的定义。
有条件的线程安全
有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历由 Hashtable
或者 Vector 或者返回的迭代器 – 由这些类返回的 fail-fast
迭代器假定在迭代器进行遍历的时候底层集合不会有变化。为了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。通常,独占性的访问是由对锁的同步保证的
– 并且类的文档应该说明是哪个锁(通常是对象的内部监视器(intrinsic monitor))。
如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,而且还要记录必须防止哪些操作序列的并发访问。用户可以合理地假设其他操作序列不需要任何额外的同步。
线程兼容
线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。这可能意味着用一个 synchronized
块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的(就像 Collections.synchronizedList()
一样)。也可能意味着用 synchronized
块包围某些操作序列。为了最大程度地利用线程兼容类,如果所有调用都使用同一个块,那么就不应该要求调用者对该块同步。这样做会使线程兼容的对象作为变量实例包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。
许多常见的类是线程兼容的,如集合类 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 类 Connection 和 ResultSet 。
线程对立
线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。线程对立类的一个例子是调用 System.setOut() 的类。
参考技术A 存在竞争的线程不安全,不存在竞争的线程就是安全的 参考技术B 你可以这样理解:ThreadLocal是线程安全的,session具有唯一性

Java HashSet 是只读的线程安全的吗?

【中文标题】Java HashSet 是只读的线程安全的吗?【英文标题】:is Java HashSet thread-safe for read only? 【发布时间】:2011-07-19 18:49:17 【问题描述】:

如果我通过 Collections.unmodifiableSet() 运行了一个 HashSet 实例,它是线程安全的吗?

我问这个是因为 Set 文档指出它不是,但我只是执行读取操作。

【问题讨论】:

另见***.com/questions/20039470/… 【参考方案1】:

来自 Javadoc:

请注意,此实现不同步。如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须对外同步

阅读不会修改集合,因此你很好。

【讨论】:

除了创建 HashSet 并向其添加值,因此它不为空是有用的,写入它。所以这个答案具有误导性。 执行类似 Collections.unmodifiableMap( /* call or code that created a new HashMap here */ ) 将创建一个填充的、不可修改的映射(确保没有人更改它并且线程只从它读取)。 @Raedwald 我不认为这个答案具有误导性,因为它清楚地说明了修改集合,通过修改它并不意味着我们应该添加、删除或更新集合中的任何元素。【参考方案2】:

HashSet 如果以只读方式使用,将是线程安全的。这并不意味着您传递给Collections.unmodifiableSet()任何 Set 都是线程安全的。

想象一下contains 的这种幼稚实现,它缓存了最后检查的值:

Object lastKey;
boolean lastContains;

public boolean contains(Object key) 
   if ( key == lastKey ) 
      return lastContains;
    else 
      lastKey = key;
      lastContains = doContains(key);
      return lastContains;
   

显然这不是线程安全的。

【讨论】:

从地图示例更改为总集示例以更适用于问题。【参考方案3】:

这将是线程安全的,但这只是因为Collections.unmodifiableSet() 在内部以安全的方式(通过final 字段)发布目标Set

请注意,诸如“只读对象始终是线程安全的”之类的一般陈述是不正确的,因为它们没有考虑操作重新排序的可能性。

(理论上)有可能,由于操作重新排序,在对象完全初始化并填充数据之前,对该只读对象的引用将对其他线程可见。为了消除这种可能性,您需要以安全的方式发布对对象的引用,例如,将它们存储在 final 字段中,就像 Collections.unmodifiableSet() 所做的那样。

【讨论】:

+1,一般来说“只读对象总是线程安全的”这样的说法是不正确的 为什么 final 关键字在操作重新排序方面使这个“只读”线程安全? JVM 会因此而采取不同的行动吗? @Asaf:是的,final 字段是特殊情况,请参阅java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5 包装器安全发布是否记录?如果不是,那么假设它这样做是不安全的。虽然我无法想象内部没有使用 final 字段的实现。【参考方案4】:

如果你不改变它,每个数据结构都是线程安全的。

因为你必须改变一个 HashSet 才能初始化它,所以必须在初始化集合的线程和所有读取线程之间同步一次。您只需一次 次。例如,当您将对不可修改集的引用传递给一个以前从未接触过它的新线程时。

【讨论】:

这并不完全正确。看看我的答案,看看它不正确的例子。仍然存在突变,但用户无法知道;它是封装状态的突变。 事实并非如此。至少在Java中。你不改变的对象和 immutable 对象之间是有区别的。这两个只有不可变对象自动线程安全。对象在以下情况下是不可变的:1)它在构造后无法修改 2)它的所有字段都是最终的 3)此引用在构造期间没有转义 @jmg 字符串绝对是不可变的。您奇怪的结论是基于这样一个事实,即特定方法不是引用透明的,因为它取决于另一个隐式参数。这是方法的属性,而不是对象本身。 @jmg 也读取全局变量不是副作用,它根本不是引用透明的。但是,您可以读取此变量并将其作为参数提供,因此读取当前 Locale 并将其提供给 String.toUpperCase 使 String 函数具有引用透明性。 @jmg @Mark_Peters 字符串不是不可变的,但它是线程安全的。它有一个缓存的哈希码,其设置具有竞争条件。尽管哈希函数具有引用透明性,但这场竞赛是良性的。【参考方案5】:

我不相信它是线程安全的,因为你运行 Collections.unmodifiableSet()。即使 HashSet 完全初始化并且您将其标记为不可修改,但这并不意味着这些更改将对其他线程可见。更糟糕的是,在没有同步的情况下,允许编译器重新排序指令,这可能意味着读取线程不仅可以看到丢失的数据,而且还可以看到处于奇怪状态的哈希集。因此,您将需要一些同步。我相信解决这个问题的一种方法是将哈希集创建为 final 并在构造函数中完全初始化它。这是一篇关于 JMM http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 的好文章。阅读有关最终字段如何在新 JMM 下工作的部分?

查看正确构造的字段值的能力很好,但如果字段本身是一个引用,那么您还希望您的代码查看它指向的对象(或数组)的最新值。如果您的字段是 final 字段,这也是有保证的。因此,您可以拥有一个指向数组的最终指针,而不必担心其他线程会看到数组引用的正确值,但会看到数组内容的错误值。同样,这里的“正确”是指“在对象的构造函数结束时是最新的”,而不是“可用的最新值”。

【讨论】:

正如在别处指出的那样,Collections.unmodifiable 视图安全地发布集合的状态,就像创建不可修改的包装器时一样(发生在之前)。但随后的更改不是,应该放弃原来的参考。【参考方案6】:

是的,并发读取访问是安全的。这是文档中的相关句子:

如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须在外部进行同步。

它表明只有at least one线程修改它时才需要同步。

【讨论】:

不,它没有说明。【参考方案7】:

如果共享内存永远不会改变,您可以随时读取而无需同步。使集合不可修改只会强制执行无法写入的事实。

【讨论】:

这是非常非常错误的。 必须在原始写入和读取之间存在某种发生前的关系,否则无法保证除了默认值之外的任何内容都会被看到。在 x86 架构上,您通常会看到一些东西,这与任何类型的保证都不相同。

以上是关于java是线程安全的吗的主要内容,如果未能解决你的问题,请参考以下文章

Java 中的 volatile int 是线程安全的吗?

Java HashSet 是只读的线程安全的吗?

Java HashSet 是只读的线程安全的吗?

一道非常棘手的 Java 面试题:i++ 是线程安全的吗?

Java面试题:Servlet是线程安全的吗?

java面试 (12)- Valiolate原理?是线程安全的吗?