Lock锁定对象最佳做法研究

本文详细介绍了C#中的lock关键字如何确保线程安全,避免竞态条件,通过示例解释了lock(this)的问题及最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。如果其他线程尝试进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
 
线程处理(C# 和 Visual Basic) 这节讨论了线程处理。

lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。ThreadInterruptedException 引发,如果 Interrupt 中断等待输入 lock 语句的线程。
 
通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
 
如果实例可以被公共访问,将出现 lock (this) 问题。


如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。


由于进程中使用同一字符串的任何其他代码都将共享同一个锁,所以出现 lock("myLock") 问题。

 
最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

在 lock 语句的正文不能使用 等待 关键字。

下例使用线程和 lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。

// using System.Threading;

class Account
{
    private Object thisLock = new Object();
    int balance;

    Random r = new Random();

    public Account(int initial)
    {
        balance = initial;
    }

    int Withdraw(int amount)
    {

        // This condition never is true unless the lock statement
        // is commented out.
        if (balance < 0)
        {
            throw new Exception("Negative Balance");
        }

        // Comment out the next line to see the effect of leaving out
        // the lock keyword.
        lock (thisLock)
        {
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Amount to Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
                return amount;
            }
            else
            {
                return 0; // transaction rejected
            }
        }
    }

    public void DoTransactions()
    {
        for (int i = 0; i < 100; i++)
        {
            Withdraw(r.Next(1, 100));
        }
    }
}

class Test
{
    static void Main()
    {
        Thread[] threads = new Thread[10];
        Account acc = new Account(1000);
        for (int i = 0; i < 10; i++)
        {
            Thread t = new Thread(new ThreadStart(acc.DoTransactions));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
    }
}

为什么要lock,lock了什么?


当我们使用线程的时候,效率最高的方式当然是异步,即各个线程同时运行,其间不相互依赖和等待。
但当不同的线程都需要访问某个资源的时候,就需要同步机制了,也就是说当对同一个资源进行读写的时候,
我们要使该资源在同一时刻只能被一个线程操作,以确保每个操作都是有效即时的,也即保证其操作的原子性。
lock是C#中最常用的同步方式,格式为lock(objectA){codeB} 。


lock(objectA){codeB} 看似简单,实际上有三个意思,这对于适当地使用它至关重要:
1. objectA被lock了吗?没有则由我来lock,否则一直等待,直至objectA被释放。
2. lock以后在执行codeB的期间其他线程不能调用codeB,也不能使用objectA。
3. 执行完codeB之后释放objectA,并且codeB可以被其他线程访问。


二. lock(this)怎么了?


我们看一个例子:
using System;
using System.Threading;

namespace Namespace1
{
    class C1
    {
        private bool deadlocked = true;

        //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
        public void LockMe(object o)
        {
            lock (this)
            {
                while(deadlocked)
                {
                    deadlocked = (bool)o;
                    Console.WriteLine("Foo: I am locked :(");
                    Thread.Sleep(500);
                }
            }
        }

        //所有线程都可以同时访问的方法
        public void DoNotLockMe()
        {
            Console.WriteLine("I am not locked :)");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            C1 c1 = new C1();

            //在t1线程中调用LockMe,并将deadlock设为true(将出现死锁)
            Thread t1 = new Thread(c1.LockMe);
            t1.Start(true);
            Thread.Sleep(100);

            //在主线程中lock c1
            lock (c1)
            {
                //调用没有被lock的方法
                c1.DoNotLockMe();
                //调用被lock的方法,并试图将deadlock解除
                c1.LockMe(false);
            }
        }
    }

在t1线程中,LockMe调用了lock(this), 也就是Main函数中的c1,这时候在主线程中调用lock(c1)时,必须要等待t1中的lock块执行完毕之后才能访问c1,
即所有c1相关的操作都无法完成,于是我们看到连c1.DoNotLockMe()都没有执行。


把C1的代码稍作改动:
    class C1
    {
        private bool deadlocked = true;
        private object locker = new object();

        //这个方法用到了lock,我们希望lock的代码在同一时刻只能由一个线程访问
        public void LockMe(object o)
        {
            lock (locker)
            {
                while(deadlocked)
                {
                    deadlocked = (bool)o;
                    Console.WriteLine("Foo: I am locked :(");
                    Thread.Sleep(500);
                }
            }
        }

        //所有线程都可以同时访问的方法
        public void DoNotLockMe()
        {
            Console.WriteLine("I am not locked :)");
        }
    }

这次我们使用一个私有成员作为锁定变量(locker),在LockMe中仅仅锁定这个私有locker,而不是整个对象。
这时候重新运行程序,可以看到虽然t1出现了死锁,DoNotLockMe()仍然可以由主线程访问;
LockMe()依然不能访问,原因是其中锁定的locker还没有被t1释放。


关键点:
1. lock(this)的缺点就是在一个线程(例如本例的t1)通过执行该类的某个使用"lock(this)"的方法(例如本例的LockMe())锁定某对象之后,
导致整个对象无法被其他线程(例如本例的主线程)访问 - 因为很多人在其他线程(例如本例的主线程)中使用该类的时候会使用类似lock(c1)的代码。
2. 锁定的不仅仅是lock段里的代码,锁本身也是线程安全的。
3. 我们应该使用不影响其他操作的私有对象作为locker。
4. 在使用lock的时候,被lock的对象(locker)一定要是引用类型的,如果是值类型,
将导致每次lock的时候都会将该对象装箱为一个新的引用对象(事实上如果使用值类型,C#编译器(3.5.30729.1)在编译时就会给出一个错误)。

<think>首先,用户的问题是:“在编程中定义锁(lock)时应该使用哪种对象?请问定义lock锁使用什么对象编程实现线程同步”根据提供的引用内容,我需要回答在定义锁时应该使用哪种对象来实现线程同步。引用内容来自于不同的来源,涉及Java、C#和线程同步。关键引用:-[^1]和[^2]讨论了Java中的锁和线程同步,提到了Lock接口和synchronized关键字。-[^3]和[^4]讨论了C#中的lock,强调它用于线程同步,只能锁定引用类型对象。-[^5]是一个Java示例,使用synchronized块锁定对象,如synchronized(this)。具体点:-引用[^1]:提到Lock接口用于线程同步,优于synchronized的一些方面。-引用[^2]:解释了Lock对象作为synchronized的替代。-引用[^3]:C#中的lock关键字,需要锁定引用类型对象。C#中的工作原理是通过对象锁。-引用[^4]:强调lock只能锁定引用类型,而不能是值类型,因为它是基于对象的。-引用[^5]:Java示例,使用synchronized(this),其中this是当前对象实例。总结核心点:-在Java中,可以使用synchronized关键字锁定对象(如this),或使用Lock接口的实现,如ReentrantLock。-在C#中,lock关键字用于锁定对象,它必须是引用类型。用户的问题是通用的“编程中”,但由于引用偏向Java和C#,我应该基于此回答。通用原则:在编程中,定义一个锁时,应该使用一个对象作为锁。这个对象通常是一个引用类型(如类实例),以确保多个线程可以竞争同一个锁。具体到Java:-在synchronized块中,使用的对象可以是任意非空对象。常见的包括this(当前对象)、类对象或特定锁对象。-在使用java.util.concurrent.locks.Lock接口时,实现如ReentrantLock需要一个对象实例来表示锁。在C#中:-lock语句需要一个引用类型对象作为参数。所以,回答时:定义锁时,应该使用一个对象。这个对象必须是引用类型(不是值类型),并且通常是一个实例对象、静态对象或特定创建的锁对象最佳实践:-避免锁定公共对象或字符串字面量,可能导致死锁问题。-推荐使用私有对象专门作为锁对象,以避免外部干扰。例如:-Java中:privatefinalObjectlock=newObject();//然后synchronized(lock){...}-C#中:privatereadonlyobjectlockObj=newobject();//然后lock(lockObj){...}或者使用Java的Lock接口。用户的问题是“定义lock锁”,可能指lock关键字或锁对象。在上下文中,引用[^3]和[^4]明确提到C#的lock需要引用类型对象。类似地,在Java中,synchronized也需要对象。在Java中,有java.util.concurrent.locks.Lock,这不是锁在对象上,而是实现锁的对象。回顾用户问题:“定义lock锁使用什么对象”意思是如何定义锁时使用的对象。在编程中,实现线程同步的锁时,使用的对象应该是:-一个实例级别的私有对象,以确保线程安全。-引用类型。现在,回答结构:1.清晰结构化回答。2.根据系统指令,使用中文回答。3.行内数学用$...$,但问题中没有数学,所以可能不需要。4.独立公式用$$...$$,如果涉及数学。5.生成相关问题。6.回答中引用的段落末尾自然地添加引用标识。7.保证回答真实可靠。步骤:-先解释什么是锁对象。-基于引用,说明推荐的对象类型。-给出示例。-引用内容:自然添加[^1],[^3],等。相关问题生成:像示例一样,在§§相关问题§§部分。相关生成:-什么是锁的粒度和最佳实践?-不同编程语言的锁实现差异?具体化。回答提纲:1.简要介绍锁在编程中的作用。2.解释定义锁时使用的对象:必须是引用类型对象。3.举例说明(Java或C#)。4.基于引用添加引用标识。5.在最后生成相关问题。相关问题:-例如:1.在Java中使用Lock接口与synchronized的区别是什么?[^1]2.如何避免锁的死锁问题?3.C#中的lock关键字的限制有哪些?[^3]确保语言中文。</think> ### 定义锁对象以实现线程同步 在编程中,实现线程同步的锁机制时,使用的对象必须是一个**引用类型对象**(reference type),这是因为锁机制依赖于对象的内在锁(也称为监视器锁)来管理线程的互斥访问。引用类型对象(如类实例、数组或自定义对象)在内存中有唯一的标识,使多个线程可以竞争同一个锁。值类型(value type)不能被用作锁对象,因为它们可能被复制或存储在栈上,无法提供全局可见的同步点[^3][^4]。 #### 为什么必须使用引用类型对象? - **互斥保证**:锁机制通过对象的内置锁(在JVM或CLR中)实现。当一个线程获得锁时,其他线程会被阻塞,直到锁释放。引用类型对象确保所有线程共享同一锁状态。 - **内存模型兼容性**:值类型(如int、struct)在栈上分配,每个线程可能有自己的副本,这无法保证线程间的同步。而引用类型在堆上分配,所有线程引用同一实例[^3][^4]。 - **避免错误**:锁定值类型会导致未定义行为,例如在C#中使用lock锁定值类型会编译错误;在Java中,synchronized块只能作用于非空对象[^3][^4]。 #### 推荐的对象类型和使用方式 在主流编程语言中,定义锁时推荐使用以下类型的对象: - **私有实例对象**:创建一个专用的、私有的引用类型对象作为锁。这是一种最佳实践,因为它减少锁冲突风险,并保持代码封装性。 - **静态对象**:如果需要跨多个实例共享锁,使用静态(static)引用类型对象。 - **避免公共对象**:不要锁定公开可访问的对象(如this或公共字符串),否则可能导致死锁或性能问题(多个无关代码竞争同一锁)[^1][^4][^5]。 ##### 具体示例(基于Java和C#) - **Java示例**(使用synchronized或Lock接口): 在Java中,synchronized块需要一个对象作为锁。推荐定义一个私有final对象: ```java public class Counter { private int count = 0; private final Object lock = new Object(); // 专用私有锁对象,引用类型 public void increment() { synchronized(lock) { // 使用lock对象同步 count++; } } } ``` 或者使用Lock接口的实现(如ReentrantLock),它本身就是一个对象,提供更灵活的锁控制: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Counter { private int count = 0; private final Lock lock = new ReentrantLock(); // Lock对象实例化 public void increment() { lock.lock(); // 获取锁 try { count++; } finally { lock.unlock(); // 释放锁 } } } ``` 在这个示例中,ReentrantLock是一个引用类型对象,提供比synchronized更细粒度的控制,如超时和中断支持[^1][^2]。 - **C#示例**(使用lock关键字): 在C#中,lock语句必须作用于一个引用类型对象: ```csharp public class Counter { private int count = 0; private readonly object lockObj = new object(); // 私有只读锁对象,引用类型 public void Increment() { lock(lockObj) // 使用lockObj同步 { count++; } } } ``` 锁定值类型(如int)会导致编译错误,验证了引用类型的要求[^3][^4]。 #### 最佳实践总结 - **选择对象**:始终使用私有或受保护的引用类型对象(如new Object()或自定义类实例)。这确保锁的独占性和可控性。 - **锁粒度**:使用细粒度锁(例如多个小锁而非一个大锁)以提高性能,避免热点问题[^1]。 - **避免常见陷阱**:不要锁定this、字符串字面量或可序列化对象,因为它们可能被共享而不安全(如在Java中锁定字符串池)[^4][^5]。 - **跨语言一致性**:在Java和C#等语言中,尽管API不同(如Java的synchronized vs. Lock,C#的lock),但核心原则相同——锁对象必须是引用类型[^1][^3][^4]。 通过正确选择锁对象,您可以安全地实现线程同步,防止数据竞争,并保持代码高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值