在CLR Via C#一书中作者说了,他不推荐在任何时候使用lock,说实话,我们在很多时候还是会为了方便,在一些简单的场景中,还是会顺手就使用lock,那么为什么不建议使用lock呢?今天就来详细说说。
先上代码
代码环境为.Net4.6.1,因为.net6中Thread.CurrentThread.Abort()方法被弃用了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class TestServer
{
LockTest1 lockTest = new LockTest1();
public void TestStart()
{
LockTest1 lockTest = new LockTest1();
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(() => {
//lockTest.MethodLock();
lockTest.MethodMonitor();
// lockTest.MethodMonitorUp();
});
t.Start();
}
}
}
/// <summary>
/// 锁测试代码 基于.Net4.6.1
/// </summary>
public class LockTest1
{
private readonly static Object objLock = new Object();
private int j = 0;
/// <summary>
/// Lock方法
/// </summary>
public void MethodLock()
{
try
{
lock (objLock)
{
var x = Thread.CurrentThread.ManagedThreadId;
for (int i = 0; i < 10; i++)
{
Thread.Sleep(10);
j += 1;
Console.WriteLine($"================{j} 线程ID:{x.ToString("00")} ");
}
Console.WriteLine($"=====执行完成======{j} 线程ID:{x.ToString("00")} ");
}
}
finally
{
}
}
/// <summary>
/// Lock底层实现方法
/// </summary>
public void MethodMonitor()
{
try
{
var x = Thread.CurrentThread.ManagedThreadId;
//x 需要根据自己的线程进行修改
if (x == 10)
{
Thread.CurrentThread.Abort();
}
Monitor.Enter(this);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(10);
j += 1;
Console.WriteLine($"================{j} 线程ID:{x.ToString("00")} ");
}
Console.WriteLine($"=====执行完成======{j} 线程ID:{x.ToString("00")} ");
}
finally
{
try
{
//永远会退出
Monitor.Exit(this);
}
catch (Exception ex)
{
}
}
}
/// <summary>
/// 优化后的Lock底层实现方法
/// </summary>
public void MethodMonitorUp()
{
Boolean lockTaken = false;
try
{
var x = Thread.CurrentThread.ManagedThreadId;
//x 需要根据自己的线程进行修改
if (x == 10)
{
Thread.CurrentThread.Abort();
}
Monitor.Enter(this, ref lockTaken);
for (int i = 0; i < 10; i++)
{
Thread.Sleep(10);
j += 1;
Console.WriteLine($"================{j} 线程ID:{x.ToString("00")} ");
}
Console.WriteLine($"=====执行完成======{j} 线程ID:{x.ToString("00")} ");
}
finally
{
if (lockTaken)
{
Monitor.Exit(this);
}
}
}
}
}
- MethodMonitor可以看着lock的底层实现,注意看异常处理部分,无论如何,总是会释放锁,这样保证了锁的获取。但是,当线程突然被终止时,此时线程并没有获取锁,但是仍然要求释放锁,当然就会出现System.Threading.SynchronizationLockException:“从不同步的代码块中调用了对象同步方法。”异常。对于多线程来说,这样会使锁的状态被损坏,下一个线程会获取到一个损坏状态的锁,进而造成不安全的代码。而且try catch 也会在不同的编译器下影线程序性能。
- MethodMonitorUp代码中,添加lockTaken变量后,这个问题就可以被解决了,如果没有获取到锁,则不会要求释放锁。
- 由此我们也可以知道lock其实是一个moitor的语法糖.上面我们讲了lock语法糖的坏处,下面我们说说
moitor自己的问题。
下图展示了堆中的对象、它们的同步块索引以及CLR的同步块数组元素之间的关系。 - 由于同步索引是公开的,如下代码会是使线程池线程在LastTransaction中阻塞,所以要始终坚持使用私有锁,不使用公共锁.
internal sealed class Transaction{
private DateTime m_timeOfLastTrans;
public void PerformTransaction(){
Monitor.Enter(this);
m_timeOfLastTrans=DateTime.Now;
Monitor.Exit(this);
}
public DateTime LastTransaction{
get{
Monitor.Enter(this);
DateTime temp = m_timeOfLastTrans;
Monitor.Exit(this);
return temp;
}
}
}
public static void SomeMethod(){
var t=new Transaction();
//这个线程获取对象的公共锁
Monitor.Enter(t);
//让一个线程池线程显示 Lasttransaction时间
//注意:线程池线程会阻塞,直到 Somemethod调用了 Monitor.Exit!
ThreadPool.QueueUserWorkItem(o=>Console.WriteLine(t.LastTransaction));
//执行其他的代码
//释放锁
Monitor.Exit(t);
}
- 修改后的代码
internal sealed class Transaction{
//如果类是静态的 该锁静态即可
private readonly object m_lock = new object();
private DateTime m_timeOfLastTrans;
public void PerformTransaction(){
Monitor.Enter(m_lock);
m_timeOfLastTrans=DateTime.Now;
Monitor.Exit(m_lock);
}
public DateTime LastTransaction{
get{
Monitor.Enter(m_lock);
DateTime temp = m_timeOfLastTrans;
Monitor.Exit(m_lock);
return temp;
}
}
}
- Monitor还存在着许多问题
- 由于 Monitor的方法要获取一个 object,所以传递值类型会导致值类型被装箱,造成线程在已装箱对象上获取锁。每次调用 Monitor. Enter都会在一个完全不同的对象上获取锁,造成完全无法实现线程同步。
- 由于字符串可以留用所以两个完全独立的代码段可能在不知情的情况下获取对内存中的一个 String对象的引用。如果将这个String对象引用传给 Monitor的方法,两个独立的代码段现在就会在不知情的情况下以同步方式执行
- 锁定代理对象的引用时,锁的是代理对象,而不是实际对象