白话并发冲突与线程同步(2)——Monitor、lock和死锁

转载 2012年06月01日 17:32:45
     竞赛暂时胜过它的目的,永远如此。对于要建立殖民地的殖民主义者,生活的意义就在于征服。士兵看不起移民,但是,征服的目的不就是要让移民定居下来吗?因此,在进步的狂热中,我们把人招来修铁路,建工厂,钻油井。但是,我们不是记得很清楚,我们进行的这些建设是服务人类的。……真理,对于一些人来说就是建造房子,而对于另一些人来说就是居住。
                                                                                    ——圣埃克絮佩里
                                                                                          摘自《人的大地》


1-2-3 和比尔盖茨的一些往事

在上一篇里我们说道,1-2-3写了一段程序,并且在使用了2个线程分别执行foo1()和foo2()之后,程序的结果就不对了。

class Program
{
   
 static int n = 0
;
   
 static void
 foo1()
    {
       
 for (int i = 0; i < 1000000000; i++) //
 10 浜?/span> 
        {
               
 int a =
 n;
                n
 = a + 1
;
        }
        Console.WriteLine(
"foo1() complete n = {0}"
, n);
    }
   
 static void
 foo2()
    {
       
 for (int j = 0; j < 1000000000; j++) //
 10 浜?/span> 
        {
               
 int a =
 n;
                n
 = a + 1
;
        }
        Console.WriteLine(
"foo2() complete n = {0}"
, n);
    }
   
 static void Main(string
[] args)
    {
       
 new Thread
(foo1).Start();
       
 new Thread
(foo2).Start();
    }
}

究其原因,就是因为Windows总是不问青红皂白随随便便就把我的线程给停掉了。例如,上面的那个程序很可能会以下面的顺序来执行(黄色底色的代码属于第一个线程,绿色底色的代码属于第二个线程):


这样,第一、第二个线程里面的循环各自执行了3次,n的值是3,而不是我们期望的6。
所以呢,我就打算建议比尔盖茨在C#里加一个关键字:

对foo2()也做同样的修改,这样,就可以确保程序以下图所示的顺序执行了:


如果这个建议被微软接受,它将创造两个记录:
  1. 它将是C#里面第一个中文关键字。
  2. 它将是C#里面最长的关键字。

可是,比尔盖茨听了我的建议之后,却把眉毛皱成了个大疙瘩,叹道:“大哥,不行呀。你知道,Windows里会同时运行着上千个线程,且不说那些居心不良的病毒和木马,就是那些干正经事的线程,谁又能保证在你那个超长关键字里包裹的代码不会运行个二、三十秒?CPU可只有一个,在那个线程运行的二、三十秒里,整个Windows都会一动不动的,不知情的用户还以为是Windows又挂掉了,最后挨骂的可是兄弟我呦!”

“不过,”比尔又接着说,“我可以提供另一种方案来达到同样的效果。我可以让线程1里面的指定代码块不执行完,线程2就一直处于阻塞(ThreadState.WaitSleepJoin)状态。”

要达到这个效果,需要使用.net里的两个函数。

Monitor.Enter(n); // 尝试获取对n的控制权。如果n没主儿,则成功获取了n的控制权;如果n已经有主儿了,则此线程阻塞,死等。
Monitor.Exit(n); // 释放对n的控制权。等待着n的那个阻塞中的线程将获取n的控制权,并从阻塞状态变成运行状态。

可以把n想像成WC里的一个蹲位,线程1 Enter了之后,其它线程就不能Enter了,只能干等着,直到线程1 Exit,下一个等着的线程才能Enter,之后才能继续办事。如果一个线程Enter了之后迟迟不Exit(例如Enter了之后,发生了异常,比如忘了带SZ),就是所谓的“占着MK不LS”了。(一边吃午饭一边看贴的兄弟对不住啦~~)

使用 Monitor

现在就可以在我的代码里使用Monitor了。

class Program
{
   
 static int n = 0
;
   
 static void
 foo1()
    {
       
 for (int i = 0; i < 1000000000; i++) // 10 亿
 
        {
            Monitor.Enter(n);
           
 int a =
 n;
            n
 = a + 1
;
            Monitor.Exit(n);
        }
        Console.WriteLine(
"foo1() complete n = {0}"
, n);
    }

   
 static void
 foo2()
    {
       
 for (int j = 0; j < 1000000000; j++) // 10 亿
 
        {
           
 Monitor.Enter(n);
           
 int a = n;
            n
 = a + 1
;
           
 Monitor.Exit(n);
        }
        Console.WriteLine(
"foo2() complete n = {0}", n);
    }
   
 static void Main(string
[] args)
    {
       
 new
 Thread(foo1).Start();
       
 new
 Thread(foo2).Start();
    }
}

这段代码很可能会以下图所示的顺序执行(黄色底色的代码属于线程1,绿色底色的代码属于线程2。下图演示了线程1循环2次,线程2循环1次,n的值为3):


如果我们把上图之中与Monitor相关的行和演示线程状态的行去掉,就可以得到下图:


怎么样?和我的那个超长关键字的效果一样吧?

不过,如果你尝试运行上面那个代码,就会发现它根本无法通过编译!这是因为Monitor.Enter()只接受类型为Object的参数。那么,可不可以写 Monitor.Enter(
(Object)n); 呢?它确实能够通过编译,但是这样岂不是要装箱20亿次?所以千万别这么写。没法子了,我们只能再声明一个Object类型的变量,专门用于这两个线程的同步。
class Program
{
   
 static int n = 0
;
   
 static object mk = new object();
 
   
 static void foo1()
    {
       
 for (int i = 0; i < 1000000000; i++) // 10 亿
 
        {
            Monitor.Enter(mk);
           
 int a =
 n;
            n
 = a + 1
;
            Monitor.Exit(mk);
        }
        Console.WriteLine(
"foo1() complete n = {0}"
, n);
    }
   
 static void
 foo2()
    {
       
 for (int j = 0; j < 1000000000; j++) // 10 亿
 
        {
            Monitor.Enter(mk);
           
 int a =
 n;
            n
 = a + 1
;
            Monitor.Exit(mk);
        }
        Console.WriteLine(
"foo2() complete n = {0}"
, n);
    }
   
 static void Main(string
[] args)
    {
       
 new
 Thread(foo1).Start();
       
 new
 Thread(foo2).Start();
    }
}

这段代码在我的赛扬800的机器上运行时间为3分零6秒。

lock 关键字

在C#里面有一个lock关键字,它其实是一个语法糖。

小贴士:在VB里与lock等价的关键字是SyncLock。用法是

SyncLock (mk)
   
 Dim a As Integer =
 n
    n
 = a + 1
 
End SyncLock

死锁 

还有比占着MK不LS更恶劣的行径么?有,那就是吃着碗里的望着锅里的。在下面的这段代码中,线程1喜欢先占着mk1然后在mk2里办事;线程2呢,喜欢先占着mk2,然后在mk1里办事,要是这两个活宝碰到一起……

class Program
{
   
 static object mk1 = new object
();
   
 static object mk2 = new object
();
   
 static void
 foo1()
    {
       
 for (int i = 0; i < 100; i++
)
        {
            Monitor.Enter(mk1);
            Console.WriteLine(
"i={0} 线程1:\"先占着mk1,再去mk2里办事。\""
, i);
            Monitor.Enter(mk2);
            Console.WriteLine(
"i={0} 线程1:\"进入了mk2,办事\""
, i);
            Monitor.Exit(mk2);
            Console.WriteLine(
"i={0} 线程1:\"办完事了,离开mk2\""
, i);
            Monitor.Exit(mk1);
            Console.WriteLine(
"i={0} 线程1:\"办完事了,离开mk1\""
, i);
        }
    }
   
 static void
 foo2()
    {
       
 for (int j = 0; j < 100; j++
)
        {
            Monitor.Enter(mk2);
            Console.WriteLine(
"j={0} 线程2:\"先占着mk2,再去mk1里办事。\""
, j);
            Monitor.Enter(mk1);
            Console.WriteLine(
"j={0} 线程2:\"进入了mk1,办事\""
, j);
            Monitor.Exit(mk1);
            Console.WriteLine(
"j={0} 线程2:\"办完事了,离开mk1\""
, j);
            Monitor.Exit(mk2);
            Console.WriteLine(
"j={0} 线程2:\"办完事了,离开mk2\""
, j);
        }
    }
   
 static void Main(string
[] args)
    {
       
 new
 Thread(foo1).Start();
       
 new
 Thread(foo2).Start();
    }
}

运行这段代码,可以得到这样的结果:


如上图所示,当程序恰巧以“线程1 Enter mk1 -> 线程2 Enter mk2 -> 线程1 想要Enter mk2 发现 mk2 已经被占用,线程1阻塞 -> 线程2 想要Enter mk1 发现 mk1 己经被占用,线程2阻塞”这个顺序执行时,线程1等待线程2释放mk2,线程2等待线程1释放mk1,两个线程双双陷入阻塞状态,直到山无棱、天地合…… 这就是死锁

白话并发冲突与线程同步(2)——Monitor、lock和死锁

白话并发冲突与线程同步(2)——Monitor、lock和死锁 转自http://www.cnblogs.com/1-2-3/archive/2008/06/02/colloquialism-t...
  • zephyr_be_brave
  • zephyr_be_brave
  • 2013年10月27日 21:21
  • 482

并发插入引发的死锁问题排查

一.业务背景我们现在的业务是一款数据产品,有不少实时计算和爬取来的数据都汇总到大数据仓库、数据挖掘平台ODPS上。然后应用在读取这些数据时,这些数据会先导入到并发读能力更强,适合结构查询的mysql上...
  • qq_16681169
  • qq_16681169
  • 2017年06月16日 23:52
  • 1318

Monitor实例--tryenter解除死锁

在多线程编程里面,经常出现的两个问题:一个是共享资源(异步之后的同步),另外一个是死锁。用到lock和monitor .enter(obj)进行资源同步时,如果控制不好如意出现死锁现象:lock(A)...
  • Folish_Audi
  • Folish_Audi
  • 2014年07月03日 10:28
  • 1350

通过Android trace文件分析死锁ANR

对于从事Android开发的人来说,遇到ANR(Application Not Responding)是比较常见的问题。一般情况下,如果有ANR发生,系统都会在/data/anr/目录下生成trace...
  • oujunli
  • oujunli
  • 2013年06月15日 22:26
  • 37540

C#中Monitor和Lock以及区别

1.Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,当然在使用过程中为了避免获取锁之后因为异常,致锁...
  • gu263278505
  • gu263278505
  • 2016年05月11日 10:58
  • 723

HashMap死锁原因及替代方案

1、首先我们需要简单地了解一下HashMap数据结构  HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算  法通过key算出这...
  • fhzaitian
  • fhzaitian
  • 2016年05月26日 11:14
  • 2177

使用apache log解决高并发下log4j引起大量线程block问题

由于项目用户量比较大,测试同事采用两百并发进行测试,在测试查过程中,查看jvm 虚拟机发现很多java.lang.Thread.State: BLOCKED (on object monitor) ...
  • Mr_Smile2014
  • Mr_Smile2014
  • 2016年09月01日 17:21
  • 7755

PostgreSQL高并发单行更新发生死锁 2015

这么简单的一条SQL,100个并发时居然会发生死锁,太不可思议了。 发生死锁的SQL update_smallrange.sql: \setrandom id 1 10000 ...
  • baidu_18607183
  • baidu_18607183
  • 2015年10月24日 10:09
  • 1016

java 内在锁(intrinsic lock)或者监视器锁(monitor lock)

在JVM的规范中,有这么一些话:  “在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁,锁住了一个对象,就是获得对象相关联的...
  • fujunsfzh
  • fujunsfzh
  • 2016年12月08日 14:19
  • 1603

聊聊高并发(三)锁的一些基本概念

理解并发编程的一些基本概念很重要,给我们思考问题指明一个基本的方向。这篇说一说锁的一些基本概念。 在通常情况下我们说的锁都指的是“互斥”锁,因为在还存在一些特殊的锁,比如“读写锁”,不完全是互斥...
  • ITer_ZC
  • ITer_ZC
  • 2014年10月08日 16:32
  • 5304
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:白话并发冲突与线程同步(2)——Monitor、lock和死锁
举报原因:
原因补充:

(最多只允许输入30个字)