为什么锁{this}不好?


484

MSDN文档说,

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

是“是否可以公开访问该实例的问题”。我想知道为什么吗?是因为锁的持有时间超过了必要?还是还有其他更阴险的原因?

Answers:


508

this在锁语句中使用这是一种不好的形式,因为通常是您无法控制的其他人可能会锁定该对象。

为了适当地计划并行操作,应格外小心,以考虑可能的死锁情况,并且锁入口点的数量未知会阻止这种情况。例如,任何引用对象的人都可以在对象设计者/创建者不知道它的情况下锁定它。这增加了多线程解决方案的复杂性,并可能影响其正确性。

私有字段通常是一个更好的选择,因为编译器将强制对其进行访问限制,并且它将封装锁定机制。使用this通过向公众公开您的锁定实现的一部分来违反封装。也不清楚是否将获得锁定,this除非已记录。即使那样,依靠文档来防止问题也不是很理想。

最后,存在一个普遍的误解,lock(this)实际上是修改了作为参数传递的对象,并以某种方式使其变为只读或不可访问。这是错误的。作为参数传递的对象lock仅用作。如果该键上已经有锁,则无法进行该锁;否则,将允许锁定。

这就是为什么在lock语句中使用字符串作为键很不好的原因,因为它们是不可变的,并且可以在应用程序的各个部分之间共享/访问。您应该改用私有变量,Object实例会做的很好。

运行以下C#代码作为示例。

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

控制台输出

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.

2
正如我所说:(1) Nancy在线程1中带有锁(this)。(2) SAME Nancy在线程2老化中,但仍锁定在线程1中-证明锁定的对象不是只读的。ALSO (2a)中,而在螺纹2,南西对象也被锁定在名称。(3)创建一个具有相同Name的DIFFERENT对象。 (4)进入thread3并尝试用Name锁定。(完成效果很大)但是“字符串是不可变的”,这意味着任何引用字符串“ Nancy Drew”的对象都在看内存中的相同字符串实例。因此,当object1锁定为相同值时,object2无法获得字符串锁定
Radarbob 2012年

使用标准变量代替lock(this)标准建议;重要的是要注意,这样做通常会使外部代码无法在方法调用之间保持与对象关联的锁。 这可能不是好事。允许外部代码在任意持续时间内保持锁定存在一定的危险,通常应设计类以使此类使用变得不必要,但并非总是可行的选择。举一个简单的例子,除非集合实现自己的ToArrayor ToList方法...
supercat 2013年

4
(与IEnumerable <T>扩展方法相对),希望线程获取集合快照的线程的唯一方法可能是枚举它,同时锁定所有更改。为了做到这一点,它必须有权访问任何会更改集合的代码所获取的锁。未能公开锁可能无法使程序定期执行集合的异步快照(例如,更新浏览集合的用户界面)。
2013年

there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false-我相信这些讨论都是关于CLR对象中的SyncBlock位的,所以正式来说这是正确的-锁定修改后的对象本身
sll

@Esteban,我绝对喜欢您的榜样,这太棒了。我有一个问题问你。方法NameChange(..)的代码结尾为:<code> if(Monitor.TryEnter(person.Name,10000)){。。。} else Monitor.Exit(person.Name); </ code>应该不以:<code> if(Monitor.TryEnter(person.Name,10000)){结束。。。Monitor.Exit(person.Name); } </ code>
AviFarah

64

因为如果人们可以得到您的对象实例(即您的this)指针,那么他们也可以尝试锁定同一对象。现在他们可能不知道您正在this内部锁定,因此这可能会导致问题(可能是死锁)

除此之外,这也是不好的做法,因为它“锁定”了太多

例如,您的成员变量可能为List<int>,而您实际上唯一需要锁定的是该成员变量。如果将整个对象锁定在函数中,则调用这些函数的其他事物将被阻止,等待锁定。如果这些函数不需要访问成员列表,则将导致其他代码等待,并毫无理由地降低应用程序的速度。


44
该答案的最后一段是不正确的。锁定不会以任何方式使对象变为不可访问或只读。Lock(this)不会阻止另一个线程调用或修改由此引用的对象。
Esteban Brenes

3
如果其他被调用的方法也做一个锁(this),它会这样做。我相信这就是他的意思。注意“如果您将整个对象锁定在函数中” ...
Herms,

@猎户座:更清楚。@Herms:是的,但是您不需要使用“ this”来实现该功能,例如,列表中的SyncRoot属性就可以达到这个目的,同时明确指出应该对该键进行同步。
Esteban Brenes

回复:锁定“太多”:这是决定锁定内容的一种很好的平衡方法。请注意,进行锁定涉及刷新缓存CPU的操作,并且价格昂贵。换句话说:不要锁定和更新每个单独的整数。:)
Zan Lynx

最后一段仍然没有意义。如果只需要限制对列表的访问,为什么其他功能如果不访问列表,则会具有锁?
约阿基姆·MH

44

看看MSDN主题线程同步(C#编程指南)

通常,最好避免锁定公共类型或应用程序无法控制的对象实例。例如,如果可以公开访问该实例,则lock(this)可能会出现问题,因为超出您控制范围的代码也可能会锁定该对象。这可能会导致两个或多个线程等待释放同一对象的死锁情况。与对象相反,锁定公共数据类型可能会导致问题,原因相同。锁定文字字符串特别危险,因为文字字符串由公共语言运行库(CLR)进行了中断。这意味着整个程序的任何给定字符串文字都有一个实例,在所有线程上的所有正在运行的应用程序域中,完全相同的对象代表文字。结果,在应用程序进程中任何位置上具有相同内容的字符串上放置的锁将锁定该字符串在应用程序中的所有实例。因此,最好锁定没有被阻止的私有成员或受保护成员。一些类提供专门用于锁定的成员。例如,数组类型提供SyncRoot。许多集合类型也提供SyncRoot成员。


34

我知道这是一个旧线程,但是由于人们仍然可以查找并依靠它,因此指出这lock(typeof(SomeObject))一点明显比差很重要lock(this)。话说回来; 衷心感谢Alan指出这lock(typeof(SomeObject))是不好的做法。

的实例System.Type是其中最通用,最粗糙的对象之一。至少,System.Type的实例是AppDomain的全局实例,.NET可以在AppDomain中运行多个程序。这意味着,如果两个完全不同的程序都试图在同一类型实例上获得同步锁,则它们甚至可能相互造成干扰,甚至产生死锁。

因此lock(this)并不是特别健壮的形式,可能会引起问题,并且出于所有提及的原因应始终引起人们的注意。然而,有广泛使用的,相对受人尊敬的且似乎稳定的代码(例如log4net)广泛使用了lock(this)模式,尽管我个人更希望看到这种模式更改。

但是却lock(typeof(SomeObject))打开了全新的蠕虫病毒罐。

物有所值。


26

...并且完全相同的参数也适用于此构造:

lock(typeof(SomeObject))

17
lock(typeof(SomeObject))实际上比lock(this)(stackoverflow.com/a/10510647/618649)差很多。
Craig 2013年

1
好吧,lock(Application.Current)甚至会更糟,但是谁会尝试这两种愚蠢的方法呢?lock(this)看起来是逻辑和简洁的,但其他示例则不然。
Zar Shardan

我不同意这lock(this)似乎是合乎逻辑和简洁的。这是一个非常粗略的锁定,任何其他代码都可能对您的对象进行锁定,从而可能会干扰您的内部代码。采取更多的粒度锁定,并采取更严格的控制。什么lock(this)确实有去为它是,它是一个很多好过lock(typeof(SomeObject))
Craig

8

想象一下,您在办公室有一个熟练的秘书,这是部门中的共享资源。偶尔,您会因为有任务而奔向他们,只是希望您的另一位同事尚未提出要求。通常,您只需要等待一小段时间。

由于关怀共享,您的经理决定客户也可以直接使用秘书。但这有一个副作用:客户在为该客户工作时甚至可能要求他们,并且您还需要他们执行部分任务。发生死锁,因为声明不再是层次结构。可以通过不允许客户首先要求他们来避免这种情况。

lock(this)如我们所见,这很糟糕。外部对象可能会锁定在该对象上,并且由于您不控制谁在使用该类,因此任何人都可以锁定该对象……这就是上述确切的示例。同样,解决方案是限制对象的曝光。但是,如果你有privateprotectedinternal类,你可能已经控制了谁是你的对象锁定,因为你相信你自己写的代码。所以这里的信息是:不要将其公开为public。另外,确保在类似情况下使用锁可以避免死锁。

完全相反的是锁定整个应用程序域中共享的资源-最坏的情况。这就像将您的秘书放到外面,然后允许所有人在那里要求他们。结果是非常混乱-或就源代码而言:这是一个坏主意;扔掉然后重新开始。那么我们该怎么做呢?

正如大多数人在此处指出的那样,类型在应用程序域中共享。但是我们可以使用更好的东西:字符串。原因是字符串被合并。换句话说:如果您在应用程序域中有两个内容相同的字符串,则它们的指针可能完全相同。由于指针用作锁定键,因此您基本上得到的是“为未定义的行为做准备”的同义词。

同样,您不应该锁定WCF对象,HttpContext.Current,Thread.Current,Singletons(通常)等。避免所有这些的最简单方法是? private [static] object myLock = new object();


3
实际上,拥有私人班级并不能解决这个问题。外部代码可以获得对私有类实例的引用...
Rashack

1
@Rashack在技术上是正确的(指出该点为+1)时,我的意思是,您应该控制谁锁定该实例。像这样返回实例可以打破这种情况。
atlaste 2013年

4

锁定在这个指针可以是坏的,如果你锁定在一个共享的资源。共享资源可以是静态变量,也可以是计算机上的文件,即该类的所有用户之间共享的资源。原因是,每次实例化类时,this指针将包含对内存中位置的不同引用。因此,锁定了这个在一次类的实例是不是锁定在不同的一个类的另一个实例。

查看此代码,了解我的意思。将以下代码添加到控制台应用程序的主程序中:

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

创建一个新的类,如下所示。

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random rand = new Random();
        Withdraw(rand.Next(1, 100) * 100);
    }
}

这是锁定的程序的运行。

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

这是锁定myLock的程序的运行。

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000

1
您的示例中要注意的是什么,例如您所显示的是不正确的。它很难发现使用Random rand = new Random();nvm 时出了什么问题我想我会看到它反复出现的Balance
Seabizkit

3

有一篇很好的文章 Microsoft®.NET运行时性能架构师Rico Mariani http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects

摘抄:

这里的基本问题是您不拥有类型对象,并且您不知道还有谁可以访问它。通常,依靠锁定未创建的对象并且不知道还有谁可以访问是一个非常糟糕的主意。这样做会导致死锁。最安全的方法是仅锁定私有对象。



2

这是一个更简单的说明(来自此处的问题34),为什么当您的类的使用者也尝试锁定该对象时,lock(this)不好并可能导致死锁。在下面,三个线程中只有一个可以继续进行,其他两个则处于死锁状态。

class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

要变通,此人使用Thread.TryMonitor(带有超时)而不是锁:

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks


据我所知,当我用的私有实例成员上的锁替换锁(this)时SomeClass,我仍然会遇到相同的死锁。同样,如果主类中的锁定是在Program的另一个私有实例成员上完成的,则会发生相同的锁定。因此,不确定此答案是否会引起误解和错误。在此处查看该行为:dotnetfiddle.net/DMrU5h
Bartosz

1

因为可以看到类实例的任何代码块也可以锁定该引用。您想隐藏(封装)锁定对象,以便只有需要引用它的代码才能引用它。关键字this指向当前的类实例,因此可以有许多东西引用它,并可以使用它进行线程同步。

需要说明的是,这很不好,因为其他一些代码块可能使用该类实例进行锁定,并且可能阻止您的代码及时获得锁定,或者可能导致其他线程同步问题。最好的情况:没有别的使用对您的类的引用来锁定。中例:某些东西使用对您的类的引用进行锁定,这会导致性能问题。最坏的情况:某些东西使用类的引用进行锁定,这会导致非常糟糕,非常微妙,非常难以调试的问题。


1

抱歉,伙计们,但我不同意将锁锁定可能导致死锁的说法。您会混淆两件事:僵局和饥饿。

  • 您不能在不中断线程之一的情况下取消死锁,因此陷入死锁后就无法退出
  • 线程之一完成工作后,饥饿将自动结束

是说明差异的图片。

结论如果线程饥饿对您来说不是问题,您
仍然可以安全使用lock(this)。您仍然需要记住,当线程(正在使线程lock(this)处于饥饿状态)在锁定对象的锁中结束时,它最终将以永恒的饥饿而结束;)


9
有所不同,但这与本讨论完全无关。结论的第一句话完全是错误的。
Ben Voigt

1
需要明确的是:我不是在辩护lock(this)-这种代码简直是错误的。我只是认为称之为死锁有点辱骂。
SOReader

2
链接到图像不再可用。:(您是否有机会重新引用它?Thx
VG1


1

以下是一些更易于遵循(IMO)的示例代码:(将在LinqPad中工作,引用以下命名空间:System.Net和System.Threading.Tasks)

要记住的一点是lock(x)基本上是语法糖,它的作用是使用Monitor.Enter,然后使用try,catch,finally块调用Monitor.Exit。请参阅:https//docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.enter(“备注”部分)

或使用C#lock语句(Visual Basic中为SyncLock语句),该语句将Enter和Exit方法包装在try ... finally块中。

void Main()
{
    //demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
    ClassTest test = new ClassTest();
    lock(test) //locking on the instance of ClassTest
    {
        Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Parallel.Invoke(new Action[]
        {
            () => {
                //this is there to just use up the current main thread. 
                Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
                },
            //none of these will enter the lock section.
            () => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
            () => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
        });
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i}  CurrentThread {Thread.CurrentThread.ManagedThreadId}");
    }

    public void DoWorkUsingMonitor(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        if (Monitor.TryEnter(this))
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Monitor.Exit(this);
        }
        else
        {
            Console.WriteLine($"Skipped lock section!  {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        }

        Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine();
    }
}

输出量

CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section!  2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13

请注意,线程#12永远不会因为其死锁而终止。


1
似乎DoWorkUsingThisLock没有必要使用第二个线程来说明问题?
Jack Lu)

您是不是要在主线程中锁定外部线程,一个线程只会等待另一个线程完成?然后将无效并行...我觉得我们需要更好的..现实世界的例子
Seabizkit

@Seabizkit,更新了代码以使其更加清晰。Parallel只是用来创建一个新线程并异步运行代码。实际上,第二个线程可能已通过多种方式(按钮单击,单独的请求等)调用。
Raj Rao

0

您可以建立一条规则,说一个类可以具有锁定在“ this”上的代码或该类中实例化该代码的任何对象。因此,如果不遵循模式,这只是一个问题。

如果您想保护自己免受不遵循此模式的代码的侵害,那么可以接受的答案是正确的。但是,如果遵循该模式,这不是问题。

锁的好处是效率。如果您有一个包含单个值的简单“值对象”,该怎么办。它只是一个包装,它实例化了数百万次。通过要求创建仅用于锁定的私有同步对象,您基本上将对象的大小增加了一倍,分配的数量也增加了一倍。当性能很重要时,这是一个优势。

当您不关心分配数量或内存占用时,出于其他答案中指出的原因,最好避免使用lock(this)。


-1

如果可以公开访问该实例,则会出现问题,因为可能还有其他请求正在使用同一对象实例。最好使用私有/静态变量。


5
不确定这会给男人带来什么,已经存在说同样的话的详细答案。
安德鲁·巴伯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.