在 Java 中避免同步(this)?
Posted
技术标签:
【中文标题】在 Java 中避免同步(this)?【英文标题】:Avoid synchronized(this) in Java? 【发布时间】:2010-10-01 08:24:44 【问题描述】:每当 SO 上出现关于 Java 同步的问题时,有些人非常急切地指出应该避免使用 synchronized(this)
。相反,他们声称,最好锁定私有引用。
一些给定的原因是:
some evil code may steal your lock(这个很受欢迎,还有一个“意外”变种) all synchronized methods within the same class use the exact same lock, which reduces throughput you are (unnecessarily) exposing too much information包括我在内的其他人认为,synchronized(this)
是一个经常使用的习语(也在 Java 库中),安全且易于理解。不应该避免它,因为您有一个错误并且您不知道多线程程序中发生了什么。换句话说:如果它适用,那么就使用它。
我有兴趣看到一些真实的例子(没有 foobar 的东西),当 synchronized(this)
也可以完成这项工作时,避免锁定 this
是更可取的。
因此:您是否应该始终避免使用synchronized(this)
并将其替换为对私有引用的锁定?
一些进一步的信息(在给出答案时更新):
我们正在讨论实例同步 隐式(synchronized
方法)和显式形式的 synchronized(this)
都被考虑
如果您引用 Bloch 或其他有关该主题的权威,请不要忽略您不喜欢的部分(例如 Effective Java,线程安全项目:通常它是实例本身的锁,但是有例外。)
如果您需要除synchronized(this)
之外的锁定粒度,那么synchronized(this)
不适用,所以这不是问题
【问题讨论】:
我还想指出上下文很重要 - “通常它是实例本身的锁定”位在有关记录条件线程安全类的部分中,当你'重新公开锁。换句话说,这句话适用于您已经做出此决定的情况。 在没有内部同步的情况下,当需要外部同步时,锁往往是实例本身,Bloch 基本上是这么说的。那么,为什么在“this”上锁定的内部同步也不是这种情况呢? (文档的重要性是另一个问题。) 在扩展粒度和额外的 CPU 缓存和总线请求开销之间进行权衡,因为锁定外部对象很可能需要在 CPU 缓存之间修改和交换单独的缓存行(参见 MESIF和 MOESI)。 我认为,在防御性编程的世界中,您不是通过成语而是通过代码来防止错误。当有人问我“您的同步优化程度如何?”时,我想说“非常”而不是“非常”,除非其他人不遵循这个习惯用法。 【参考方案1】:同步包括 3 个部分:原子性、可见性和有序性
同步块是非常粗略的同步级别。它按照您的预期强制执行可见性和排序。但是对于原子性,它并没有提供太多的保护。原子性需要程序的全球知识而不是本地知识。 (这使得多线程编程非常困难)
假设我们有一个类Account
具有方法deposit
和withdraw
。它们都是基于这样的私有锁同步的:
class Account
private Object lock = new Object();
void withdraw(int amount)
synchronized(lock)
// ...
void deposit(int amount)
synchronized(lock)
// ...
考虑到我们需要实现一个更高级别的类来处理传输,如下所示:
class AccountManager
void transfer(Account fromAcc, Account toAcc, int amount)
if (fromAcc.getBalance() > amount)
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
假设我们现在有 2 个帐户,
Account john;
Account marry;
如果Account.deposit()
和Account.withdraw()
仅使用内部锁锁定。当我们有 2 个线程工作时,这将导致问题:
// Some thread
void threadA()
john.withdraw(500);
// Another thread
void threadB()
accountManager.transfer(john, marry, 100);
因为threadA
和threadB
可以同时运行。并且线程B完成条件检查,线程A退出,线程B再次退出。这意味着即使他的账户没有足够的钱,我们也可以从约翰那里提取 100 美元。这会破坏原子性。
您可能会建议:那为什么不将withdraw()
和deposit()
添加到AccountManager
中呢?但是根据这个提议,我们需要创建一个多线程安全的Map
,它从不同的帐户映射到它们的锁。我们需要在执行后删除锁(否则会泄漏内存)。我们还需要确保没有其他人直接访问Account.withdraw()
。这会引入很多细微的错误。
正确且最惯用的方法是在Account
中公开锁。并让AccountManager
使用锁。但是在这种情况下,为什么不直接使用对象本身呢?
class Account
synchronized void withdraw(int amount)
// ...
synchronized void deposit(int amount)
// ...
class AccountManager
void transfer(Account fromAcc, Account toAcc, int amount)
// Ensure locking order to prevent deadlock
Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;
synchronized(firstLock)
synchronized(secondLock)
if (fromAcc.getBalance() > amount)
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
简单地说,私有锁不适用于稍微复杂的多线程程序。
(转自https://***.com/a/67877650/474197)
【讨论】:
【参考方案2】:我在 2019 年的两分钱,尽管这个问题本可以解决。
如果您知道自己在做什么,锁定“this”还不错,但在幕后锁定“this”是(不幸的是,方法定义中的同步关键字允许这样做)。
如果您确实希望您的类的用户能够“窃取”您的锁(即阻止其他线程处理它),您实际上希望所有同步方法在另一个同步方法正在运行时等待,依此类推。 它应该是有意的和深思熟虑的(并因此记录下来以帮助您的用户理解它)。
更详细地说,反过来说,如果您锁定一个不可访问的锁(没有人可以“偷”您的锁,您完全可以控制并且很快...)。
对我来说问题是方法定义签名中的 synchronized 关键字让程序员很容易不考虑锁定什么是非常重要的事情如果你不考虑不想在多线程程序中遇到问题。
不能争辩说“通常”您不希望您班级的用户能够做这些事情,或者“通常”您想要......这取决于您正在编码的功能。您无法制定拇指规则,因为您无法预测所有用例。
考虑例如使用内部锁的 printwriter,但如果人们不希望输出交错,则很难从多个线程中使用它。
您的锁是否可以在类外部访问是您作为程序员的决定,具体取决于类具有什么功能。它是 api 的一部分。例如,您不能从 synchronized(this) 移到 synchronized(provateObjet) 而不冒破坏使用它的代码更改的风险。
注意 1:我知道您可以通过使用显式锁定对象并公开它来实现同步(this)“实现”的任何内容,但我认为如果您的行为有据可查并且您实际上知道锁定 'this' 是没有必要的意思。
注 2:我不同意如果某些代码意外窃取了您的锁,这是一个错误,您必须解决它。这在某种程度上与说我可以将所有方法公开,即使它们不打算公开也是一样的论点。如果有人“不小心”称我打算成为私有方法,则它是一个错误。为什么首先要启用这个事故!!!如果窃取你的锁的能力对你的班级来说是一个问题,那就不要允许它。就这么简单。
【讨论】:
【参考方案3】:当您使用 synchronized(this) 时,您将类实例用作锁本身。这意味着当 线程 1 获取锁时,线程 2 应该等待。
假设如下代码:
public void method1()
// do something ...
synchronized(this)
a ++;
// ................
public void method2()
// do something ...
synchronized(this)
b ++;
// ................
方法一修改变量a和方法二修改变量b,应该避免两个线程同时修改同一个变量,确实如此。但是,当 thread1 修改 a 和 thread2 修改 b 时,它可以在没有任何竞争条件的情况下执行。
不幸的是,上面的代码不允许这样做,因为我们对锁使用相同的引用;这意味着即使线程不处于竞争状态,它们也应该等待,显然代码会牺牲程序的并发性。
解决方法是对两个不同的变量使用2个不同的锁:
public class Test
private Object lockA = new Object();
private Object lockB = new Object();
public void method1()
// do something ...
synchronized(lockA)
a ++;
// ................
public void method2()
// do something ...
synchronized(lockB)
b ++;
// ................
上面的例子使用了更细粒度的锁(2 个锁而不是一个(lockA 和 lockB 用于变量 a 和 b 分别)并因此允许更好的并发性,另一方面它变得比第一个示例更复杂......
【讨论】:
这是非常危险的。您现在已经介绍了客户端(此类用户的)锁定排序要求。如果两个线程以不同的顺序调用method1()和method2(),它们很可能会死锁,但是这个类的用户并不知道会出现这种情况。 “同步(this)”未提供的粒度超出了我的问题范围。你的锁定字段不应该是最终的吗? 为了产生死锁,我们应该从 A 同步的块调用 B 同步的块。daveb,你错了...... 据我所见,此示例中没有死锁。我接受它只是伪代码,但我会使用 java.util.concurrent.locks.Lock 的实现之一,如 java.util.concurrent.locks.ReentrantLock【参考方案4】:避免使用synchronized(this)
作为锁定机制:这会锁定整个类实例并可能导致死锁。在这种情况下,重构代码以仅锁定特定的方法或变量,这样整个类就不会被锁定。 Synchronised
可以在方法级别使用。
下面的代码没有使用synchronized(this)
,而是显示了如何锁定一个方法。
public void foo()
if(operation = null)
synchronized(foo)
if (operation == null)
// enter your code that this method has to handle...
【讨论】:
【参考方案5】:我只想提一个可能的解决方案,用于在没有依赖关系的代码的原子部分中使用唯一私有引用。您可以使用带锁的静态 Hashmap 和名为 atomic() 的简单静态方法,该方法使用堆栈信息(完整的类名和行号)自动创建所需的引用。然后你可以在同步语句中使用此方法,而无需编写新的锁对象。
// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic()
StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point
StackTraceElement exepoint = stack[2];
// creates unique key from class name and line number using execution point
String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber());
Object lock = locks.get(key); // use old or create new lock
if (lock == null)
lock = new Object();
locks.put(key, lock);
return lock; // return reference to lock
// Synchronized code
void dosomething1()
// start commands
synchronized (atomic())
// atomic commands 1
...
// other command
// Synchronized code
void dosomething2()
// start commands
synchronized (atomic())
// atomic commands 2
...
// other command
【讨论】:
【参考方案6】:这实际上只是对其他答案的补充,但是如果您对使用私有对象进行锁定的主要反对意见是它会使您的类变得混乱,其中包含与业务逻辑无关的字段,那么 Project Lombok 有 @Synchronized
来生成编译时的样板:
@Synchronized
public int foo()
return 0;
编译成
private final Object $lock = new Object[0];
public int foo()
synchronized($lock)
return 0;
【讨论】:
【参考方案7】:视情况而定。 如果只有一个或多个共享实体。
查看完整的工作示例 here
一个小小的介绍。
线程和可共享实体 多个线程可以访问同一个实体,例如多个连接线程共享一个消息队列。由于线程同时运行,因此可能会被另一个数据覆盖,这可能是一个混乱的情况。 因此,我们需要某种方法来确保一次只能由一个线程访问可共享实体。 (并发)。
同步块 synchronized() 块是一种确保可共享实体并发访问的方法。 一、打个比方 假设洗手间内有两个人 P1、P2(线程)一个洗脸盆(可共享实体),并且有一扇门(锁)。 现在我们希望一个人一次使用洗脸盆。 一种方法是在门被锁时由 P1 锁门 P2 等到 p1 完成他的工作 P1开锁 那么只有p1可以使用洗脸盆。
语法。
synchronized(this)
SHARED_ENTITY.....
“this”提供了与类关联的内在锁(Java 开发人员设计了 Object 类,使每个对象都可以作为监视器工作)。 当只有一个共享实体和多个线程 (1: N) 时,上述方法可以正常工作。 N 个可共享实体-M 个线程 现在想想一个洗手间里有两个脸盆,只有一扇门的情况。如果我们使用前面的方法,那么一次只有 p1 可以使用一个脸盆,而 p2 将在外面等待。由于没有人使用 B2(洗脸盆),这是一种资源浪费。 更明智的方法是在洗手间内创建一个较小的房间,并为每个洗脸盆提供一扇门。这样,P1 可以访问 B1,P2 可以访问 B2,反之亦然。
washbasin1;
washbasin2;
Object lock1=new Object();
Object lock2=new Object();
synchronized(lock1)
washbasin1;
synchronized(lock2)
washbasin2;
查看更多关于线程的信息----> here
【讨论】:
【参考方案8】:-
尽可能使您的数据不可变(
final
变量)
如果您无法避免跨多个线程共享数据的突变,请使用高级编程结构 [例如粒度Lock
API]
锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要先获取锁。
使用实现Lock
接口的ReentrantLock
的示例代码
class X
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m()
lock.lock(); // block until condition holds
try
// ... method body
finally
lock.unlock()
锁定优于同步(this)的优势
使用同步方法或语句会强制所有锁的获取和释放以块结构的方式发生。
锁实现通过提供
-
获取锁的非阻塞尝试 (
tryLock()
)
尝试获取可中断的锁 (lockInterruptibly()
)
尝试获取可能会超时的锁 (tryLock(long, TimeUnit)
)。
Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,例如
-
保证订购
不可重入使用
死锁检测
看看这个关于各种类型Locks
的SE问题:
Synchronization vs Lock
您可以通过使用高级并发 API 而不是同步块来实现线程安全。该文档page 提供了良好的编程结构来实现线程安全。
Lock Objects 支持可简化许多并发应用程序的锁定习惯用法。
Executors 定义用于启动和管理线程的高级 API。 java.util.concurrent 提供的 Executor 实现提供了适合大规模应用的线程池管理。
Concurrent Collections 可以更轻松地管理大量数据集合,并且可以大大减少同步需求。
Atomic Variables 具有最小化同步并有助于避免内存一致性错误的功能。
ThreadLocalRandom(在 JDK 7 中)提供从多个线程高效生成伪随机数。
请参阅java.util.concurrent 和java.util.concurrent.atomic 包以了解其他编程结构。
【讨论】:
【参考方案9】:这取决于您要执行的任务,但我不会使用它。另外,首先检查您想要完成的线程保存是否无法通过 synchronize(this) 完成?还有一些不错的locks in the API 可能会对您有所帮助:)
【讨论】:
【参考方案10】:我将分别介绍每一点。
一些邪恶的代码可能会偷走你的锁(这个很流行,也有一个 “意外”变体)
我更担心意外。这相当于this
的这种使用是您的类的公开接口的一部分,应该记录在案。有时需要其他代码使用您的锁的能力。 Collections.synchronizedMap
之类的东西也是如此(参见 javadoc)。
同一类中的所有同步方法都使用完全相同的 锁,会降低吞吐量
这是过于简单化的想法;只是摆脱synchronized(this)
并不能解决问题。吞吐量的正确同步需要更多考虑。
您(不必要地)暴露了太多信息
这是#1 的变体。 synchronized(this)
的使用是您界面的一部分。如果您不想/不需要这种暴露,请不要这样做。
【讨论】:
1. “同步”不是您班级公开界面的一部分。 2. 同意 3. 见 1. Essentially synchronized(this) is 暴露,因为这意味着外部代码会影响您的类的操作。所以我断言你必须将它记录为接口,即使语言没有。 类似。请参阅 Collections.synchronizedMap() 的 Javadoc——返回的对象在内部使用 synchronized(this),他们希望消费者利用这一点来为像迭代这样的大规模原子操作使用相同的锁。 事实上 Collections.synchronizedMap() 内部没有使用 synchronized(this),它使用了一个私有的最终锁对象。 @Bas Leijdekkers: the documentation 明确指出同步发生在返回的地图实例上。有趣的是,keySet()
和 values()
返回的视图不会锁定(他们的)this
,而是地图实例,这对于获得所有地图操作的一致行为很重要。锁定对象被分解为变量的原因是,子类SynchronizedSortedMap
需要它来实现锁定原始地图实例的子地图。【参考方案11】:
我认为在 Brian Goetz 的一本书《Java Concurrency In Practice》中对为什么这些技术都是你掌握的重要技术有很好的解释。他非常清楚地说明了一点——你必须使用相同的锁“EVERYWHERE”来保护你的对象的状态。同步方法和对象上的同步通常是齐头并进的。例如。 Vector 同步其所有方法。如果您有一个向量对象的句柄并且要执行“如果不存在则放置”,那么仅仅 Vector 同步它自己的各个方法并不能保护您免受状态损坏。您需要使用 synchronized (vectorHandle) 进行同步。这将导致每个拥有向量句柄的线程都获取相同的锁,并将保护向量的整体状态。这称为客户端锁定。事实上,我们知道向量确实同步(this)/同步其所有方法,因此在对象 vectorHandle 上同步将导致向量对象状态的正确同步。仅仅因为您使用线程安全集合就相信您是线程安全的,这是愚蠢的。这正是 ConcurrentHashMap 显式引入 putIfAbsent 方法的原因——使此类操作具有原子性。
总结
-
在方法级别同步允许客户端锁定。
如果您有一个私有锁定对象 - 它会使客户端锁定成为不可能。如果您知道您的课程没有“如果不存在则放置”类型的功能,这很好。
如果您正在设计一个库,那么在此同步或同步该方法通常更明智。因为您很少能够决定如何使用您的课程。
如果 Vector 使用了私有锁对象 - 就不可能获得“如果不存在则放置”的权利。客户端代码永远不会获得私有锁的句柄,从而破坏了使用 EXACT SAME LOCK 保护其状态的基本规则。
正如其他人所指出的那样,在此方法或同步方法上进行同步确实存在问题 - 有人可能会获得锁而永远不会释放它。所有其他线程将继续等待锁被释放。
所以要知道自己在做什么,并采用正确的方法。
有人争辩说,拥有私有锁对象可以为您提供更好的粒度 - 例如。如果两个操作不相关 - 它们可以由不同的锁保护,从而提高吞吐量。但我认为这是设计气味而不是代码气味 - 如果两个操作完全不相关,为什么它们是 SAME 类的一部分?一个班级俱乐部为什么要完全不相关的功能?可能是实用程序类?嗯 - 一些通过同一实例提供字符串操作和日历日期格式的工具? ...至少对我来说没有任何意义!!
【讨论】:
【参考方案12】:正如这里已经说过的,同步块可以使用用户定义的变量作为锁对象,当同步函数只使用“this”时。当然,您也可以对应该同步的功能区域进行操作,等等。
但是每个人都说同步函数和使用“this”作为锁定对象覆盖整个函数的块之间没有区别。这不是真的,区别在于两种情况下都会生成的字节码。在同步块使用的情况下,应分配局部变量,该变量包含对“this”的引用。因此,我们将拥有更大的函数大小(如果您只有少数函数,则无关紧要)。
您可以在此处找到有关差异的更详细说明: http://www.artima.com/insidejvm/ed2/threadsynchP.html
由于以下观点,同步块的使用也不好:
synchronized 关键字在一个方面非常有限:当退出同步块时,所有等待该锁的线程必须解除阻塞,但只有其中一个线程获得锁;所有其他人都看到锁定已被占用并返回阻塞状态。这不仅仅是浪费了大量的处理周期:通常,解除阻塞线程的上下文切换还涉及从磁盘分页内存,这非常、非常、昂贵。
有关此领域的更多详细信息,我建议您阅读这篇文章: http://java.dzone.com/articles/synchronized-considered
【讨论】:
【参考方案13】:锁用于可见性或保护某些数据免受并发修改可能导致竞争。
当您只需要使原始类型操作成为原子操作时,可以使用AtomicInteger
等可用选项。
但是假设您有两个彼此相关的整数,例如x
和y
坐标,它们彼此相关并且应该以原子方式更改。然后你会用同一个锁来保护它们。
锁应该只保护彼此相关的状态。不多也不少。如果在每个方法中使用synchronized(this)
,那么即使类的状态不相关,即使更新不相关的状态,所有线程也会面临争用。
class Point
private int x;
private int y;
public Point(int x, int y)
this.x = x;
this.y = y;
//mutating methods should be guarded by same lock
public synchronized void changeCoordinates(int x, int y)
this.x = x;
this.y = y;
在上面的例子中,我只有一种方法可以同时改变x
和y
,而不是两种不同的方法,因为x
和y
是相关的,如果我给出了两种不同的方法来改变x
和y
分开,那么它就不是线程安全的。
这个例子只是为了演示而不一定是它应该实现的方式。最好的方法是让它IMMUTABLE。
现在与Point
示例相反,@Andreas 已经提供了一个TwoCounters
示例,其中状态受两个不同的锁保护,因为状态彼此无关。
使用不同锁保护不相关状态的过程称为Lock Striping或Lock Splitting
【讨论】:
【参考方案14】:不同步this的原因是有时你需要不止一个锁(第二个锁经常在经过一些额外的思考后被移除,但你仍然需要它处于中间状态)。如果你锁定了this,你总是要记住两个锁中的哪一个是this;如果你锁定一个私有对象,变量名会告诉你。
从读者的角度来看,如果你看到锁定在this上,你总是要回答两个问题:
-
这个保护什么样的访问?
一把锁真的够用吗,不是有人介绍了bug吗?
一个例子:
class BadObject
private Something mStuff;
synchronized setStuff(Something stuff)
mStuff = stuff;
synchronized getStuff(Something stuff)
return mStuff;
private MyListener myListener = new MyListener()
public void onMyEvent(...)
setStuff(...);
synchronized void longOperation(MyListener l)
...
l.onMyEvent(...);
...
如果两个线程在BadObject
的两个不同实例上开始longOperation()
,它们会获取
他们的锁;当调用l.onMyEvent(...)
时,我们遇到了死锁,因为两个线程都不能获取另一个对象的锁。
在这个例子中,我们可以通过使用两个锁来消除死锁,一个用于短操作,一个用于长操作。
【讨论】:
在这个例子中获得死锁的唯一方法是BadObject
A 在 B 上调用 longOperation
,传递 A 的 myListener
,反之亦然。并非不可能,但相当复杂,支持我之前的观点。【参考方案15】:
使用 synchronized(this) 的一个很好的例子。
// add listener
public final synchronized void addListener(IListener l) listeners.add(l);
// remove listener
public final synchronized void removeListener(IListener l) listeners.remove(l);
// routine that raise events
public void run()
// some code here...
Set ls;
synchronized(this)
ls = listeners.clone();
for (IListener l : ls) l.processEvent(event);
// some code here...
正如您在此处看到的,我们在此使用 synchronize 以轻松地与那里的一些同步方法进行长时间(可能是 run 方法的无限循环)的协作。
当然,在私有字段上使用同步可以很容易地重写它。但有时,当我们已经设计了一些带有同步方法的设计时(即我们派生自的遗留类,synchronized(this) 可能是唯一的解决方案)。
【讨论】:
任何对象都可以用作这里的锁。它不需要是this
。它可能是一个私有字段。
正确,但是这个例子的目的是展示如何正确同步,如果我们决定使用方法同步。【参考方案16】:
如果您已经决定:
您需要做的就是锁定 当前对象;和 你想 锁定它的粒度小于 一个完整的方法;那我看不到 synchronizezd(this) 的禁忌。
有些人故意在方法的整个内容中使用 synchronized(this)(而不是标记方法已同步),因为他们认为“读者更清楚”实际上正在同步哪个对象。只要人们做出明智的选择(例如,了解这样做实际上是在方法中插入了额外的字节码,这可能会对潜在的优化产生连锁反应),我并不认为这有什么问题.您应该始终记录程序的并发行为,因此我认为“'同步'发布行为”的论点没有那么引人注目。
关于你应该使用哪个对象的锁的问题,我认为在当前对象上同步没有任何问题如果你正在做的事情的逻辑以及你的类通常如何预期的话使用。例如,对于集合,您在逻辑上期望锁定的对象通常是集合本身。
【讨论】:
“如果这是逻辑所期望的......”也是我试图表达的观点。我没有看到总是使用私有锁的意义,尽管普遍的共识似乎是它更好,因为它不会伤害并且更具防御性。【参考方案17】:虽然我同意不要盲目地遵守教条规则,但“盗锁”场景对您来说是否如此古怪?线程确实可以“从外部”(synchronized(theObject) ...
) 获取对象上的锁,从而阻塞等待同步实例方法的其他线程。
如果您不相信恶意代码,请考虑此代码可能来自第三方(例如,如果您开发某种应用服务器)。
“偶然”的版本似乎不太可能,但正如他们所说,“做出一些防白痴的东西,就会有人发明一个更好的白痴”。
所以我同意它取决于班级做什么学派。
编辑 eljenso 的前 3 个 cmets:
我从未遇到过锁窃取问题,但这是一个想象的场景:
假设您的系统是一个 servlet 容器,我们正在考虑的对象是 ServletContext
实现。它的getAttribute
方法必须是线程安全的,因为上下文属性是共享数据;所以你把它声明为synchronized
。我们还假设您基于容器实现提供公共托管服务。
我是您的客户,在您的网站上部署我的“好”servlet。碰巧我的代码包含对getAttribute
的调用。
伪装成另一个客户的黑客在您的站点上部署了他的恶意 servlet。它在init
方法中包含以下代码:
假设我们共享相同的 servlet 上下文(只要两个 servlet 位于同一虚拟主机上,规范就允许),我对 getAttribute
的调用将永远锁定。黑客在我的 servlet 上实现了 DoS。
如果getAttribute
在私有锁上同步,这种攻击是不可能的,因为第 3 方代码无法获取此锁。
我承认该示例是人为设计的,并且对 servlet 容器的工作方式过于简单化了,但恕我直言,它证明了这一点。
所以我会根据安全考虑做出我的设计选择:我是否可以完全控制有权访问实例的代码?线程无限期地持有一个实例的锁会有什么后果?
【讨论】:
it-depends-on-what-the-class-does:如果它是一个“重要”对象,那么锁定私有引用?其他实例锁定就足够了吗? 是的,盗锁场景对我来说似乎有些牵强。每个人都提到它,但谁真正做过或经历过它?如果你“不小心”锁定了一个你不应该锁定的对象,那么这种情况就有一个名称:这是一个错误。修复它。 此外,锁定内部引用并不能免于“外部同步攻击”:如果您知道代码的某个同步部分等待外部事件发生(例如文件写入,值DB,计时器事件)您也可以安排它阻止。 让我承认我是那些白痴中的一员,尽管我在年轻时就这样做了。我认为通过不创建显式锁定对象,代码更简洁,而是使用另一个需要参与监视器的私有最终对象。我不知道对象本身对自己进行了同步。你可以想象随之而来的 hijinx...【参考方案18】:java.util.concurrent
包大大降低了我的线程安全代码的复杂性。我只有轶事证据可以继续,但我看到的大多数使用 synchronized(x)
的工作似乎是重新实现锁、信号量或闩锁,但使用较低级别的监视器。
考虑到这一点,使用这些机制中的任何一种进行同步都类似于在内部对象上进行同步,而不是泄漏锁。这是有益的,因为您可以绝对确定您通过两个或更多线程控制进入监视器。
【讨论】:
【参考方案19】:嗯,首先要指出的是:
public void blah()
synchronized (this)
// do stuff
在语义上等价于:
public synchronized void blah()
// do stuff
这是不使用synchronized(this)
的原因之一。你可能会争辩说你可以围绕synchronized(this)
块做一些事情。通常的原因是尽量避免必须进行同步检查,这会导致各种并发问题,特别是double checked-locking problem,它只是说明了进行相对简单的检查线程安全是多么困难。
私有锁是一种防御机制,这绝不是一个坏主意。
此外,正如您所提到的,私有锁可以控制粒度。对一个对象的一组操作可能与另一组完全无关,但synchronized(this)
将相互排除对所有对象的访问。
synchronized(this)
真的没有给你任何东西。
【讨论】:
"synchronized(this) 真的没有给你任何东西。"好的,我用同步(myPrivateFinalLock)替换它。这给了我什么?你说它是一种防御机制。我受到什么保护? 保护您免受外部对象对“this”的意外(或恶意)锁定。 我完全不同意这个答案:锁应该始终保持尽可能短的时间,这正是您想要围绕一个“做事”的原因同步块而不是同步整个方法。 在同步块之外做事总是善意的。关键是人们很多时候都犯了这个错误,甚至没有意识到这一点,就像在双重检查锁定问题中一样。通往地狱的道路是用善意铺就的。 我总体上不同意“X 是一种防御机制,这绝不是一个坏主意。”由于这种态度,有很多不必要的臃肿代码。【参考方案20】:C# 和 Java 阵营对此似乎有不同的共识。我见过的大多数 Java 代码使用:
// apply mutex to this instance
synchronized(this)
// do work here
而大多数 C# 代码选择更安全的方式:
// instance level lock object
private readonly object _syncObj = new object();
...
// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
// do work here
C# 习惯用法当然更安全。如前所述,不能从实例外部对锁进行恶意/意外访问。 Java 代码也有这种风险,但随着时间的推移,Java 社区似乎已经被安全性稍差但更简洁的版本所吸引。
这并不是对 Java 的挖苦,只是反映了我在这两种语言上工作的经验。
【讨论】:
也许因为 C# 是一门年轻的语言,他们从 Java 阵营中发现的不良模式中学到了更好的代码?单身人士也少吗? :) 嘿嘿。很可能是真的,但我不会上钩!我可以肯定地说,C# 代码中有更多的大写字母;) 不是真的(说得好听)【参考方案21】:简答:您必须了解差异并根据代码做出选择。
长答案:一般来说,我宁愿尽量避免 synchronize(this) 以减少争用,但私有锁会增加您必须注意的复杂性。因此,为正确的工作使用正确的同步。如果您对多线程编程没有那么丰富的经验,我宁愿坚持实例锁定并阅读该主题。 (也就是说:仅使用 synchronize(this) 不会自动使您的类完全线程安全。)这不是一个简单的话题,但是一旦您习惯了,答案是否使用 synchronize(this) 与否是自然而然的。
【讨论】:
你说这取决于你的经验,我理解正确吗? 首先,这取决于您要编写的代码。只是说当你转向不使用 synchronize(this) 时可能需要更多经验。【参考方案22】:不,你不应该总是。但是,当对特定对象有多个关注点时,我倾向于避免它,而这些关注点只需要相对于它们自身是线程安全的。例如,您可能有一个具有“标签”和“父”字段的可变数据对象;这些需要是线程安全的,但更改一个不需要阻止另一个被写入/读取。 (实际上,我会通过声明 volatile 字段和/或使用 java.util.concurrent 的 AtomicFoo 包装器来避免这种情况。
一般来说,同步有点笨拙,因为它会锁定一个大锁,而不是考虑如何允许线程相互工作。使用synchronized(this)
甚至更加笨拙和反社会,因为它表示“当我持有锁时,没有人可以更改本课程的任何东西”。您实际上需要多久执行一次?
我宁愿拥有更细粒度的锁;即使您确实想阻止一切更改(也许您正在序列化对象),您也可以获取所有锁来实现相同的目标,而且这种方式更加明确。当您使用synchronized(this)
时,不清楚您要同步的确切原因,或者可能有什么副作用。如果你使用synchronized(labelMonitor)
,或者更好的labelLock.getWriteLock().lock()
,那么很清楚你在做什么以及你的临界区的影响是有限的。
【讨论】:
【参考方案23】:我认为第一点(其他人使用您的锁)和第二点(所有方法都不必要地使用同一个锁)可以在任何相当大的应用程序中发生。尤其是当开发人员之间没有良好的沟通时。
这不是一成不变的,这主要是一个良好实践和防止错误的问题。
【讨论】:
以上是关于在 Java 中避免同步(this)?的主要内容,如果未能解决你的问题,请参考以下文章
Effective java 第十章 并发 避免过度同步 读书笔记