理论与实践中的 C# 内存模型

转载自:https://msdn.microsoft.com/magazine/jj863136

这是该系列(包含两部分内容)的第一部分,这部分将以较长的篇幅介绍 C# 内存模型。 第一部分说明 C# 内存模型所做出的保证,并介绍促使其保证这些内容的代码模式;第二部分将详细说明如何在 Microsoft .NET Framework 4.5 的不同硬件体系结构上实现这些保证。

导致多线程编程具有复杂性的原因之一是编译器和硬件可能会悄然改变程序的内存操作,尽管其方式不会影响单线程行为,但可能会影响多线程行为。 请考虑以下方法:

void Init() {
  _data = 42;
  _initialized = true;
}

如果 _data 和 _initialized 是普通(即,非可变)字段,则允许编译器和处理器对这些操作重新排序,以便 Init 执行起来就像是用以下代码编写的:

void Init() {
  _initialized = true;
  _data = 42;
}

在编译器和处理器中存在可导致此类型重新排序的不同优化,我将在第 2 部分中讨论这些情况。

在单线程程序中,Init 中语句的重新排序不会改变程序的意义。 只要在该方法返回之前更新 _initialized 和 _data,采用何种分配顺序就没有差别。 在单线程程序中,没有可以观察更新之间状态的第二个线程。

但在多线程程序中,分配顺序的不同可能会产生影响,因为当 Init 处于执行状态时另一个线程可能会读取字段。 因此,在 Init 的重新排序后的版本中,另一个线程可能会遵守 _initialized=true 和 _data=0 的条件。

C# 内存模型是一组规则,描述允许和不允许的内存操作重新排序类型。 所有程序都应该根据在规范中定义的保证进行编写。

但是,即使允许编译器和处理器对内存操作进行重新排序,也不意味着它们在实际情况下会始终这样做。 根据这个抽象 C# 内存模型而包含“错误”的许多程序仍会在运行特定版本 .NET Framework 的特定硬件上正确执行。 值得注意的是,x86 和 x64 处理器仅在某些范围较窄的方案中对操作重新排序;同样,CLR 实时 (JIT) 编译器不会执行所允许的许多转换。

尽管您在编写新代码时应该对这个抽象的 C# 内存模型已心中有数,但理解这个内存模型在不同体系结构上的实际实现方式是很有用的,特别是在尝试理解现有代码的行为时。

根据 ECMA-334 的 C# 内存模型

标准 ECMA-334 C# 语言规范 (bit.ly/MXMCrN) 中提供了 C# 内存模型的权威定义。 我们将介绍在该规范中定义的 C# 内存模型。

内存操作重新排序根据 ECMA-334,当一个线程在 C# 中读取由其他线程写入到的某个内存位置时,阅读器可能会看到陈旧值。 此问题如图 1 所示。

图 1 存在内存操作重新排序风险的代码

public class DataInit {
  private int _data = 0;
  private bool _initialized = false;
  void Init() {
    _data = 42;            // Write 1
    _initialized = true;   // Write 2
  }
  void Print() {
    if (_initialized)            // Read 1
      Console.WriteLine(_data);  // Read 2
    else
      Console.WriteLine("Not initialized");
  }
}

假定在一个新的 DataInit 实例上并行(即,在不同线程上)调用了 Init 和 Print。 如果您查看 Init 和 Print 的代码,Print 似乎只能输出“42”或“Not initialized”。但是,Print 也可以输出“0”。

C# 内存模型允许在某一方法中对内存操作进行重新排序,只要单线程执行的行为不发生改变即可。 例如,编译器和处理器会自行对 Init 方法操作重新排序,如下所示:

void Init() {
  _initialized = true;   // Write 2
  _data = 42;            // Write 1
}

这一重新排序不会更改单线程程序中 Init 方法的行为。 但在多线程程序中,另一个线程可能会在 Init 已修改一个字段但未修改其他字段后读取 _initialized 和 _data 字段,随后进行重新排序可能会更改该程序的行为。因此,Print 方法最终可能会输出“0”。

Init 的重新排序并不是在这个代码示例中造成麻烦的唯一根源。 即使 Init 写入没有最终导致重新排序,也可能会改变 Print 方法中的读取:

void Print() {
  int d = _data;     // Read 2
  if (_initialized)  // Read 1
    Console.WriteLine(d);
  else
    Console.WriteLine("Not initialized");
}

就像写入的重新排序一样,这个改变对单线程程序没有影响,但可能会更改多线程程序的行为。 并且,就像写入的重新排序一样,读取的重新排序也可以导致 0 作为输出结果输出。

在本文的第 2 部分,我将详细介绍在不同硬件体系结构上时这些变化在实际中是如何发生以及为什么发生的。

可变字段 C# 编程语言提供可变字段,限制对内存操作重新排序的方式。 ECMA 规范规定,可变字段应提供获取--释放语义 (bit.ly/NArSlt)。

可变字段的读取具有获取语义,这意味着它不能与后续操作互换顺序。 此可变读取构成单向防护: 之前的操作可以通过,但之后的操作不能通过。 请考虑以下示例:

class AcquireSemanticsExample {
  int _a;
  volatile int _b;
  int _c;
  void Foo() {
    int a = _a; // Read 1
    int b = _b; // Read 2 (volatile)
    int c = _c; // Read 3
    ...
}
}

Read 1 和 Read 3 是不可变的,而 Read 2 是可变的。 Read 2 不能与 Read 3 互换顺序,但可与 Read 1 互换顺序。 图 2 显示了 Foo 正文的有效重新排序。

图 2 AcquireSemanticsExample 中读取的有效重新排序


int a = _a; // Read 1

int b = _b; // Read 2 (volatile)

int c = _c; // Read 3


int b = _b; // Read 2 (volatile)

int a = _a; // Read 1

int c = _c; // Read 3


int b = _b; // Read 2 (volatile)

int c = _c; // Read 3

int a = _a; // Read 1

另一方面,可变字段的写入具有释放语义,因此它不能与之前的操作互换顺序。 可变写入构成单向的防护,如下面的示例所示:

class ReleaseSemanticsExample
{
  int _a;
  volatile int _b;
  int _c;
  void Foo()
  {
    _a = 1; // Write 1
    _b = 1; // Write 2 (volatile)
    _c = 1; // Write 3
    ...
}
}

Write 1 和 Write 3 是非可变的,而 Write 2 是可变的。 Write 2 不能与 Write 1 互换顺序,但可与 Write 3 互换顺序。 图 3 显示了 Foo 正文的有效重新排序。

图 3 ReleaseSemanticsExample 中写入的有效重新排序


_a = 1; // Write 1

_b = 1; // Write 2 (volatile)

_c = 1; // Write 3


_a = 1; // Write 1

_c = 1; // Write 3

_b = 1; // Write 2 (volatile)


_c = 1; // Write 3

_a = 1; // Write 1

_b = 1; // Write 2 (volatile)

在本文后面的“通过可变字段发布”部分中,我将再次讨论这个获取-释放语义。

原子性 另一个要注意的问题是:在 C# 中,值不一定以原子方式写入内存。 请考虑以下示例:

class AtomicityExample {
  Guid _value;
  void SetValue(Guid value) { _value = value; }
  Guid GetValue() { return _value; }
}

如果一个线程反复调用 SetValue 并且另一个线程调用 GetValue,则 getter 线程可能会观察到 setter 线程从未写入的值。 例如,如果 setter 线程使用 Guid 值 (0,0,0,0) 和 (5,5,5,5) 交替调用 SetValue,则 GetValue 可能会观察到 (0,0,0,5)、(0,0,5,5) 或 (5,5,0,0), 即使从未使用 SetValue 分配上述任何值。

这一“撕裂”现象背后的原因在于,赋值“_value = value”在硬件级别并未以原子方式执行。 同样,_value 的读取也没有以原子方式执行。

C# ECMA 规范确保将以原子方式写入以下类型: 引用类型、bool、char、byte、sbyte、short、ushort、uint、int 和 float。 其他类型的值(包括用户定义的值类型)可在多个原子写入中写入内存。 因此,读取线程可能会观察到由含不同值的多个部分构成的撕裂值。

需要特别注意的一点是,如果在内存中没有正确排列值,则即使类型是以原子方式正常读取和写入的(例如 int),也可能会以非原子方式读取或写入。 通常,C# 将确保正确排列值,但用户能够使用 StructLayoutAttribute 类覆盖这个排列 (bit.ly/Tqa0MZ)。

不可重新排序优化 某些编译器优化可能会引入或消除某些内存操作。 例如,编译器可能会用单个读取替代对某个字段的反复读取。 同样,如果代码读取某个字段并且将值存储于一个本地变量中,然后反复读取该变量,则编译器可能会改为选择反复读取该字段。

因为 ECMA C# 规范没有排除非重新排序优化,所以可能会允许这样做。 实际上,如我在第 2 部分中所述,JIT 编译器确实会执行这些类型的优化。

线程通信模式

内存模型旨在实现线程通信。 在一个线程将值写入内存而另一个线程从内存进行读取时,内存模型将会指示读取线程可看到的值。

锁定 锁定通常是在线程之间共享数据的最简单方式。 如果您正确使用了锁,则基本上不必担心任何内存模型方面的麻烦。

在某一线程获取某个锁时,CLR 确保该线程将看到之前持有该锁的线程已进行的所有更新。 接下来,我们将向本文开头的示例添加锁定,如图 4 中所示。

图 4 使用锁定的线程通信

public class Test {
  private int _a = 0;
  private int _b = 0;
  private object _lock = new object();
  void Set() {
    lock (_lock) {
      _a = 1;
      _b = 1;
    }
  }
  void Print() {
    lock (_lock) {
      int b = _b;
      int a = _a;
      Console.WriteLine("{0} {1}", a, b);
    }
  }
}

添加 Print 和 Set 获取的锁提供了一个简单的解决方法。 现在,Set 和 Print 将相互以原子方式执行。 lock 语句确保 Print 和 Set 的正文将像是以某种连续顺序执行的,即使是从多个线程调用它们的。

图 5 中的图表显示一个可能的连续顺序,如果线程 1 调用 Print 三次,线程 2 调用 Set 一次并且线程 3 调用 Print 一次,则这个顺序就可能会发生。


图 5 使用锁定的顺序执行

在某一锁定的代码块执行时,保证会看到来自在该锁的连续顺序中该块之前的块的所有写入。 此外,保证不会看到来自在该锁的连续顺序中该块之后的块的任何写入。

简言之,锁隐藏了内存模型的所有不可预测性和复杂性问题: 如果您正确使用了锁,则不必担心内存操作的重新排序。 但是,请注意必须正确使用锁定。 如果只有 Print 或 Set 使用锁(或者 Print 和 Set 获取两个不同的锁),则内存操作可能会重新排序,而内存模型的复杂程度将恢复原状。

通过线程 API 发布 锁定是用于在线程之间共享状态的非常普遍和强大的机制。 通过线程 API 发布是针对并发编程的另一种常用模式。

阐释通过线程 API 进行发布的最简单方法是举例:

class Test2 {
  static int s_value;
  static void Run() {
    s_value = 42;
    Task t = Task.Factory.StartNew(() => {
      Console.WriteLine(s_value);
    });
    t.Wait();
  }
}

在您查看上述代码示例时,可能会预期“42”将输出到屏幕。 并且,实际上,您的直觉是正确的。 该代码示例确保输出“42”。

可能令人惊讶的是,甚至需要提及这个例子,但实际上,StartNew 的可能的实现方式将会允许输出“0”而不是“42”,至少在理论上是允许的。 毕竟,有两个通过非可变字段进行通信的线程,因此,可以对内存操作重新排序。 该模式显示在图 6 中的图表中。


图 6 通过非可变字段进行通信的两个线程

StartNew 实现必须确保对线程 1 上 s_value 的写入将不会移到 <start task t> 之后,并且确保从线程 2 上 s_value 进行的读取将不会移到 <task t starting> 的前面。 而实际上,StartNew API 真的保证了上述要求。

.NET Framework 中的所有其他线程 API(例如 Thread.Start 和 ThreadPool.QueueUserWorkItem)也提供类似的保证。 实际上,几乎每个线程 API 都必须具有某些屏障语义,以便正常发挥功能。 它们几乎从来不会记录下来,但通常只要考虑需要作出哪些保证以使该 API 发挥作用,就可以推断出它们。

通过类型初始化进行发布 将一个值安全地发布到多个线程的另一个方法是将该值写入静态初始值或静态构造函数中的静态字段。 请考虑以下示例:

class Test3
{
  static int s_value = 42;
  static object s_obj = new object();
  static void PrintValue()
  {
    Console.WriteLine(s_value);
    Console.WriteLine(s_obj == null);
  }
}

如果并行从多个线程调用 Test3.PrintValue,是否可确保每个 PrintValue 调用都输出“42”和“false”? 或者,其中一个调用是否也可能会输出“0”或“true”? 就像在前面的示例中一样,您得到了期望的行为: 每个线程都确保输出“42”和“false”。

到目前为止讨论的模式全都按您的预期发挥作用。 现在,我将要讲一些例子,其行为可能会出乎您的预料。

通过可变字段发布 可以通过将到目前为止所论述的三个简单模式与 .NET System.Threading 和 System.Collections.Concurrent 命名空间中的并发基元一起使用,生成许多并发程序。

我将要论述的模式十分重要,以至于可变关键字的语义就是围绕这个模式设计的。 实际上,记住可变关键字语义的最佳方式是记住此模式,而不是尝试记忆在本文前面介绍的抽象规则。

让我们从图 7 中的示例代码开始。 图 7 中的 DataInit 类具有两个方法 Init 和 Print;这两个方法都可以从多个线程调用。 如果没有对内存操作进行重新排序,则 Print 只能输出“Not initialized”或“42”,但有两个 Print 可以输出“0”的可能情形:

  • Write 1 和 Write 2 已重新排序。
  • Read 1 和 Read 2 已重新排序。

图 7 使用 Volatile 关键字

public class DataInit {
  private int _data = 0;
  private volatile bool _initialized = false;
  void Init() {
    _data = 42;            // Write 1
    _initialized = true;   // Write 2
  }
  void Print() {
    if (_initialized) {          // Read 1
      Console.WriteLine(_data);  // Read 2
    }
    else {
      Console.WriteLine("Not initialized");
    }
  }
}

如果 _initialized 未标记为可变的,则这两种重新排序都是允许的。 但在 _initialized 标记为可变时,这两种重新排序都不允许! 对于写入,您在一个普通写入后跟随一个可变写入,并且可变写入不能与之前的内存操作互换顺序。 对于读取,您在一个可变读取后跟随一个普通读取,并且可变读取不能与后续的内存操作互换顺序。

因此,Print 将永远不会输出“0”,即使使用 Init 对 DataInit 的新实例进行了并发调用。

请注意,如果 _data 字段是可变的,但 _initialized 不是,则允许这两种重新排序。 因此,记住此示例是记住可变语义的一个很好的方法。

迟缓初始化 通过可变字段进行发布的一个常见的变化形式是迟缓初始化。 图 8 中的示例说明了迟缓初始化。

图 8 迟缓初始化

class BoxedInt
{
  public int Value { get; set; }
}
class LazyInit
{
  volatile BoxedInt _box;
  public int LazyGet()
  {
    var b = _box;  // Read 1
    if (b == null)
    {
      lock(this)
      {
        b = new BoxedInt();
        b.Value = 42; // Write 1
        _box = b;     // Write 2
      }
    }
    return b.Value; // Read 2
  }
}

在这个示例中,LazyGet 始终保证返回“42”。但是,如果 _box 字段不是可变的,则出于两个原因将允许 LazyGet 返回“0”: 读取可能会被重新排序,或者写入可能会被重新排序。

为了进一步强调这一点,请考虑下面的类:

class BoxedInt2
{
  public readonly int _value = 42;
  void PrintValue()
  {
    Console.WriteLine(_value);
  }
}

现在,PrintValue 可以(至少在理论上可以)由于内存模型问题而输出“0”。 下面是 BoxedInt 的一个使用示例,它允许输出“0”:

class Tester
{
  BoxedInt2 _box = null;
  public void Set() {
    _box = new BoxedInt2();
  }
  public void Print() {
    var b = _box;
    if (b != null) b.PrintValue();
  }
}

因为该 BoxedInt 实例未正确发布(通过非可变字段 _box),所以,调用 Print 的线程可能会观察到部分构造的对象! 同样,使 _box 字段成为可变字段将解决这个问题。

联锁操作和内存屏障 联锁操作是原子操作,在许多情况下可用来减少多线程程序中的锁定。 请考虑下面这个简单的线程安全的计数器类:

class Counter
{
  private int _value = 0;
  private object _lock = new object();
  public int Increment()
  {
    lock (_lock)
    {
      _value++;
      return _value;
    }
  }
}

使用 Interlocked.Increment,您可以按照如下所示重新编写该程序:

class Counter
{
  private int _value = 0;
  public int Increment()
  {
    return Interlocked.Increment(ref _value);
  }
}

在使用 Interlocked.Increment 程序编写后,该方法应该更快地执行,至少在某些体系结构上会更快。 除了递增操作之外,Interlocked 类 (bit.ly/RksCMF) 还公开以下不同的原子操作的方法: 添加值、有条件地替换值、替换值和返回原始值等。

所有 Interlocked 方法都具有一个非常有趣的属性: 它们不能与其他内存操作互换顺序。 因此,无论是在联锁操作之前还是之后,没有任何内存操作可以通过联锁操作。

与 Interlocked 方法密切相关的一个操作是 Thread.MemoryBarrier,该操作可被视作虚拟联锁操作。 与 Interlocked 方法一样,Thread.Memory-Barrier 不能与任何之前或之后的内存操作互换顺序。 但与 Interlocked 方法不同的是,Thread.MemoryBarrier 没有负面影响;它只是约束内存重新排序。

轮询循环 轮询循环不是通常建议的模式,但有些遗憾的是,在实际中还会经常使用它。 图 9 显示一个中断的轮询循环。

图 9 中断的轮询循环

class PollingLoopExample
{
  private bool _loop = true;
  public static void Main()
  {
    PollingLoopExample test1 = new PollingLoopExample();
    // Set _loop to false on another thread
    new Thread(() => { test1._loop = false;}).Start();
    // Poll the _loop field until it is set to false
    while (test1._loop) ;
    // The previous loop may never terminate
  }
}

在这个示例中,主要线程循环轮询一个特定的非可变字段。 同时,帮助器线程设置该字段,但主要线程可能永远不会看到更新的值。

现在,如果 _loop 字段被标记为可变字段将怎么办? 这样做是否解决该问题? 一般的专家共识似乎是,不允许编译器将可变字段读取提升出循环,但 ECMA C# 规范是否作出这一保证有争议。

一方面,该规范仅指定可变字段遵守获取-释放语义,这似乎不足以禁止提升可变字段。 另一方面,该规范中的示例代码实际上轮询一个可变字段,这意味着该可变字段读取不能被提升出该循环。

在 x86 和 x64 体系结构上,PollingLoopExample.Main 通常将挂起。 JIT 编译器将只读取 test1._loop 字段一次,在寄存器中保存值,然后循环直至该寄存器值发生改变,这显然将永远不会发生。

但是,如果该循环正文包含一些语句,则 JIT 编译器将可能出于其他一些目的而需要寄存器,这样,每个迭代都可能最终重新读取 test1._loop。 因此,您可能最终会在现有程序中看到循环,这些循环将轮询非可变字段但碰巧会出现。

并发基元 大量并发代码可以从在 .NET Framework 4 中开始提供的高级并发基元中获益。 图 10 列出了一些 .NET 并发基元。

图 10 .NET Framework 4 中的并发基元

类型 说明
Lazy<> 迟缓初始化的值
LazyInitializer
BlockingCollection<> 线程安全集合
ConcurrentBag<>
ConcurrentDictionary<,>
ConcurrentQueue<>
ConcurrentStack<>
AutoResetEvent 用于协调不同线程的执行的基元
屏障
CountdownEvent
ManualResetEventSlim
监视
SemaphoreSlim
ThreadLocal<> 为每个线程承载单独值的容器

通过使用这些基元,您常常可以避免依赖于复杂方法(通过可变等)中的内存模型的低级别代码。

即将推出

到目前为止,我已经介绍了在 ECMA C# 规范中定义的 C# 内存模型,并且论述了定义内存模型的最重要的线程通信模式。

本文的第二部分将说明如何在不同体系结构上实际实现该内存模型,这对于理解实际真实世界中程序的行为很有帮助。

最佳实践

  • 您编写的所有代码都应该仅依赖于 ECMA C# 规范所作出的保证,而不依赖于在本文中说明的任何实现细节。
  • 避免不必要地使用可变字段。 大多数的时间、锁或并发集合 (System.Collections.Concurrent.*) 更适合于在线程之间交换数据。 在一些情况下,可以使用可变字段来优化并发代码,但您应该使用性能度量来验证所得到的利益胜过复杂性的增加。
  • 应该使用 System.Lazy<T> 和 System.Threading.LazyInitializer 类型,而不是使用可变字段自己实现迟缓初始化模式。
  • 避免轮询循环。 通常,您可以使用 BlockingCollection<T>、Monitor.Wait/Pulse、事件或异步编程,而不是轮询循环。
  • 尽可能使用标准 .NET 并发基元,而不是自己实现等效的功能。

Igor Ostrovsky 是 Microsoft 的一名高级软件开发工程师。 他从事并行 LINQ、任务并行库以及 Microsoft .NET Framework 中的其他并行库和基元方面的工作。 有关编程主题的 Ostrovsky 博客在 igoro.com 上提供。

衷心感谢以下技术专家对本文的审阅: Joe Duffy、Eric Eilebrecht、Joe Hoag、Emad Omara、Grant Richins、Jaroslav Sevcik 和 Stephen Toub

时间: 2024-10-09 23:47:28

理论与实践中的 C# 内存模型的相关文章

理论与实践中的 C# 内存模型,第 2 部分

转载自:https://msdn.microsoft.com/zh-cn/magazine/jj883956.aspx 这是介绍 C# 内存模型的系列文章的第二篇(共两篇). 正如在 MSDN 杂志十二月刊的第一篇文章 (msdn.microsoft.com/magazine/jj863136) 中所介绍的,编译器和硬件可能会悄然改变程序的内存操作,尽管其方式不会影响单线程行为,但可能会影响多线程行为. 例如,请考虑以下方法: void Init() {   _data = 42;   _ini

java中JVM虚拟机内存模型详细说明

java中JVM虚拟机内存模型详细说明 2012-12-12 18:36:03|  分类: JAVA |  标签:java  jvm  堆内存  虚拟机  |举报|字号 订阅 JVM的内部结构如下图: 一个优秀Java程序员,必须了解Java内存模型.GC工作原理,以及如何优化GC的性能.与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能提高整个应用程序的性能. 本文将从JVM内存模型.GC工作原理,以及GC的几个关键问题进行探讨,从

JVM理论:(一)JVM内存模型

一.JVM内存模型 1.程序计数器 线程私有,当前线程所执行的字节码的行号指示器,通过计数器来选取下条需要执行的字节码指令,分支.循环.跳转.异常处理.线程恢复等功能都依赖此功能,唯一没有规定OutOfMemoryError的区域,若执行的是Native方法,计数器值为空. 2.Java虚拟机栈 (1)线程私有,生命周期和线程相同. (2)栈与栈帧定义的区别: 每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,每调用一个方法就会为每个方法生成一个栈帧,一个方法从调用到完成对应着一个栈帧在虚拟

Cocos2d-x v3.11 中的新内存模型

Cocso2d-x v3.11 一项重点改进就是 JSB 新内存模型.这篇文章将专门介绍这项改进所带来的新研发体验和一些技术细节. 1. 成果 在 Cocos2d-x v3.11 之前的版本中,使用 JS 语言发布原生版本的用户可能多少都会遇到一个经典的问题:Invalid Native Object,或者遇到一些莫名其妙的 JS 对象失效的崩溃.而解决这些问题,我们给出的解决方案基本是使用 retain / release 来显式声明持有或释放对象,或者是在脚本层更合理得持有对象索引.而在 v

修复 Java 内存模型,第 1 部分——Brian Goetz

转自Java并发大师Brain Goetz:http://www.ibm.com/developerworks/cn/java/j-jtp02244/ (中文地址) http://www.ibm.com/developerworks/java/library/j-jtp02244/index.html (英文地址) 什么是 Java 内存模型,最初它是怎样被破坏的? 简介: 活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型(Java Memory Model, JMM

Java 理论与实践: 并发集合类

Java 理论与实践: 并发集合类 DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含了一些主要集合类型 List 和 Map 的高性能的.线程安全的实现.在本月的 Java理论与实践中,BrianGoetz向您展示了用 ConcurrentHashMap 替换 Hashtable 或 synchronizedMap ,将有多少并发程序获益.您可以在本文的 中与作者以及其他读者共享您的想法(您也可以点击文章顶部或者底部的 讨论进入论坛). 在Java

深入理解Java内存模型(七)——总结

处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照.JMM和处理器内存模型在设计时会对 顺序一致性模型做一些放松,因为如果完全按照顺序一致性模型来实现处理器和JMM,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影 响. 根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为下面几种类型: 放松程序中写-读操作的顺序,由此产生了total store ordering内存模型(简称为TS

Java 理论与实践: 处理 InterruptedException

捕捉到它,然后怎么处理它? 很多 Java™ 语言方法,例如 Thread.sleep() 和 Object.wait(),都可以抛出InterruptedException.您不能忽略这个异常,因为它是一个检查异常(checked exception).但是应该如何处理它呢?在本月的 Java 理论与实践中,并发专家 Brian Goetz 将解释 InterruptedException 的含义,为什么会抛出 InterruptedException,以及在捕捉到该异常时应该怎么做. 这样的

java内存模型二

并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信. 同步是指程序用于控制不同线程之间操作发生相对顺序的机制.在共享内存并发