勇哥注:
《多线程安全》这个系列会持续写下去,它是我的一个弱点,有兴趣的朋友可以选择性看看。
上节说到lock锁关键字,它实际上是Monitor的语法糖。
lock锁定的是一个内存地址的引用。
lock必须锁定一个引用类型的变量。
锁定的变量msdn推荐是下面这样的:
private static readonly object lockobj = new object();
这里为什么必须这样是有玄机的。我们来依次看几个例子。
(一)锁定null
null在定义的时候当引用类型,执行的时候会报错误。
这里需要注意的是,有时候这样用vs是不会报错的,因为线程会把异常吃掉,这个时候你用try catch把它括起来就可以看到异常。
这可能跟vs版本或者异常设置有关系。
(二)锁定的变量改为public
见代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private static readonly object lockobj = new object();
private void button1_Click(object sender, EventArgs e)
{
var list1 = new List<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (lockobj)
{
list1.Add(i);
}
});
}
Thread.Sleep(6000);
Console.WriteLine(list1.Count);
}
private void button2_Click(object sender, EventArgs e)
{
test1.show();
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (test1.lockobj)
{
Console.WriteLine($"{i},{k},start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
public class test1
{
//注意这里是public
public static readonly object lockobj = new object();
public static void show()
{
for(int i=0;i<5;i++)
{
int k = i;
Task.Run(() =>
{
lock(lockobj)
{
Console.WriteLine($"{i},{k},test1start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},test1end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
}
这里lock后,执行次序就变成先start,再end
这里由于lock后程序的执行变为单线程模式。先是10号线程工作,它完成之间其它线程需要排除。
如果没有lock,是乱序的。
如果lock的变量置为public, 则两个线程之间是相互阻塞的,而不是并发的。
这个就是public的潜在问题。
因此如果两个线程想并发,请各自锁自己的变量,不要锁相同的变量。
(三)锁定的变量去掉static
把上面的代码改一下:
public class test2
{
//注意这里没有static
public readonly object lockobj = new object();
public void show(int pos)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (lockobj)
{
Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
private void button3_Click(object sender, EventArgs e)
{
var t1= new test2();
t1.show(1);
var t2 = new test2();
t2.show(2);
}
程序结果单次调用内部肯定是顺序执行的,即先有start, 才有end,因为有lock
问题是两次调用之间可以并发吗?
答案是可以。
因为没有static后是成员变量,实例化后两个lockobj 变量是两个对象。
如果你希望多个类的实例共用一个变量,就要加static
执行结果中箭头所示线程10和15同时start,证明两次调用间是并发关系。
(四)锁定的变量为string
由于字符串也是引用类型,能不能锁字符串呢?
答案是最好不要!!
我们把代码改下,试完你就知道了。
private void button4_Click(object sender, EventArgs e)
{
var t1 = new test3();
t1.show(1);
var t2 = new test3();
t2.show(2);
}
public class test3
{
//注意这里字符串,它也是引用类型
public readonly string lockobj = "勇哥";
public void show(int pos)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (lockobj)
{
Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
从执行结果上来看,两次调用间是不能并发的。
这个原因是C#的字符串是类似享元模式的。
虽然lockobj 是两个类的实例的成员变量,理论是不同的两个变量。lock不是锁定变量而是锁定变量的引用,但是由于享元模式,这些变量的引用是同一个对象。
就算是有1000个test3类的实例,在堆里面只装着一个“勇哥”。
有关C#字符串的享元模式的知识点,参见下面的贴子:
(四)范型类的问题
上代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private static readonly object lockobj = new object();
private void button1_Click(object sender, EventArgs e)
{
//泛型类在类型参数相同的情况下,它是相同的类
//泛型类在类型参数不同的情况下,它是不同的类
test1<int>.show(1);
test1<int>.show(2);
test1<test2>.show(3);
}
}
public class test1<T>
{
private static readonly object lockobj = new object();
public static void show(int index)
{
for(int i=0;i<5;i++)
{
int k = i;
Task.Run(() =>
{
lock(lockobj)
{
Console.WriteLine($"{i},{k},test1start[{index}]...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},test1end[{index}]...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
public class test2
{
public readonly object lockobj = new object();
public void show(int pos)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (lockobj)
{
Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
}
对于下面的调用,我们想知道的问题是: 1和3能不能并发?
test1<int>.show(1);
test1<int>.show(2);
test1<test2>.show(3);
答案是可以。
因为泛型类在类型参数类型相同的情况下,它是相同的类。
泛型类在类型参数不同的情况下,它是不同的类。
这意味着1和3的锁变量lockobj 虽然是static的,但是却有两个不同的副本。
(五)this的问题
this当前类的实例,如果锁this
则t1与t2之间可以并发吗?
答案当然是可以。
private void button2_Click(object sender, EventArgs e)
{
var t1 = new test3();
t1.show(1);
var t2 = new test3();
t2.show(2);
}
public class test3
{
public void show(int pos)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (this)
{
Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
}
}
}
再来看个递归调用的问题
当t1.show(1)后,代码会产生锁死的问题吗?
答案是不会。
private void button2_Click(object sender, EventArgs e)
{
var t1 = new test3();
t1.show(1);
}
public class test3
{
int js = 0;
public void show(int pos)
{
for (int i = 0; i < 5; i++)
{
js++;
int k = i;
Task.Run(() =>
{
lock (this)
{
Console.WriteLine($"{i},{k},[{pos}]start...[{Thread.CurrentThread.ManagedThreadId}]");
Thread.Sleep(1000);
Console.WriteLine($"{i},{k},[{pos}]end...[{Thread.CurrentThread.ManagedThreadId}]");
}
});
if (this.js < 5)
{
this.show(pos);
}
else
{
break;
}
}
}
}
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

