我有一个Web应用程序,我正在使用Oracle数据库,我有一个基本上像这样的方法:
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
if (!methodThatChecksThatObjectAlreadyExists) {
storemyObject() //pseudo code
}
// Have to do a lot other saving stuff, because it either saves everything or nothing
commit() // pseudo code to actually commit all my changes to the database.
}
现在没有任何类型的同步,所以n个线程当然可以自由地访问这个方法,当2个线程进入这个方法同时检查时会出现问题,当然还没有任何东西,然后它们都可以提交事务,创建一个重复的对象。
我不想在我的数据库中使用唯一的密钥标识符来解决这个问题,因为我认为我不应该抓住SQLException
。
我也无法在提交之前检查,因为有几个检查不仅1
,这将需要相当长的时间。
我对锁和线程的经验是有限的,但我的想法基本上是将这个代码锁定在它接收的对象上。我不知道例如说我收到一个Integer对象,并且我用值1锁定我的Integer,这只会阻止具有值为1的另一个Integer的线程进入,以及所有其他带有value != 1
的线程可以自由进入吗?这是它的工作原理吗?。
此外,如果这是如何工作的,那么锁定对象如何比较?它是如何确定它们实际上是同一个对象的?关于这一点的好文章也将受到赞赏。
你会如何解决这个问题?。
答案 0 :(得分:5)
你的想法很好。这是简单/天真的版本,但它不太可行:
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
synchronized (theObjectIwantToSave) {
if (!methodThatChecksThatObjectAlreadyExists) {
storemyObject() //pseudo code
}
// Have to do a lot other saving stuff, because it either saves everything or nothing
commit() // pseudo code to actually commit all my changes to the database.
}
}
此代码使用对象本身作为锁。但它必须是相同的对象(即objectInThreadA == objectInThreadB)才能工作。如果两个线程正在一个彼此的副本的对象上运行 - 例如,具有相同的“id”,那么您将需要同步整个方法:
public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) ...
这当然会大大降低并发性(使用该方法,吞吐量将一次降至一个线程 - 要避免)。
或者找到一种基于保存对象获取相同锁定对象的方法,如下所示:
private static final ConcurrentHashMap<Object, Object> LOCKS = new ConcurrentHashMap<Object, Object>();
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
synchronized (LOCKS.putIfAbsent(theObjectIwantToSave.getId(), new Object())) {
....
}
LOCKS.remove(theObjectIwantToSave.getId()); // Clean up lock object to stop memory leak
}
这是推荐的最后一个版本:它将确保共享相同“id”的两个保存对象使用相同的锁对象锁定 - 方法ConcurrentHashMap.putIfAbsent()
是线程安全的,因此“这将起作用”,它只需要objectInThreadA.getId().equals(objectInThreadB.getId())
才能正常工作。此外,getId()的数据类型可以是任何东西,包括原语(例如int
),因为java autoboxing。
如果您为对象覆盖equals()
和hashcode()
,那么您可以使用对象本身而不是object.getId()
,这将是一个改进(感谢@TheCapn指出这一点)
此解决方案仅适用于一个JVM。如果您的服务器是群集的,那么整个不同的球类游戏和java的锁定机制将无法帮助您。您将不得不使用群集锁定解决方案,这超出了本答案的范围。
答案 1 :(得分:3)
这是一个改编自And360对Bohemian答案的评论的选项,试图避免竞争条件等。虽然我更喜欢我other answer对这个问题的评论,但是:
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
// it is no advantage of using ConcurrentHashMap, since we synchronize access to it
// (we need to in order to "get" the lock and increment/decrement it safely)
// AtomicInteger is just a mutable int value holder
// we don't actually need it to be atomic
static final HashMap<Object, AtomicInteger> locks = new HashMap<Integer, AtomicInteger>();
public static void saveSomethingImportantToDataBase(Object objectToSave) {
AtomicInteger lock;
synchronized (locks) {
lock = locks.get(objectToSave.getId());
if (lock == null) {
lock = new AtomicInteger(1);
locks.put(objectToSave.getId(), lock);
}
else
lock.incrementAndGet();
}
try {
synchronized (lock) {
// do synchronized work here (synchronized by objectToSave's id)
}
} finally {
synchronized (locks) {
lock.decrementAndGet();
if (lock.get() == 0)
locks.remove(id);
}
}
}
您可以将这些分解为帮助方法“获取锁定对象”和“释放锁定”,或者也可以将其拆分为清除代码。这种感觉比我的other answer感觉更加笨拙。
答案 2 :(得分:2)
// there is no synchronized weak hash map, apparently
// and Collections.synchronizedMap has no putIfAbsent method, so we use synchronized(locks) down below
WeakHashMap<Integer, Integer> locks = new WeakHashMap<>();
public void saveSomethingImportantToDataBase(DatabaseObject objectToSave) {
Integer lock;
synchronized (locks) {
lock = locks.get(objectToSave.getId());
if (lock == null) {
lock = new Integer(objectToSave.getId());
locks.put(lock, lock);
}
}
synchronized (lock) {
// synchronized work here (synchronized by objectToSave's id)
}
// no releasing needed, weakref does that for us, we're done!
}
以及如何使用上述风格系统的更具体的例子:
static WeakHashMap<Integer, Integer> locks = new WeakHashMap<>();
static Object getSyncObjectForId(int id) {
synchronized (locks) {
Integer lock = locks.get(id);
if (lock == null) {
lock = new Integer(id);
locks.put(lock, lock);
}
return lock;
}
}
然后在其他地方使用它:
...
synchronized (getSyncObjectForId(id)) {
// synchronized work here
}
...
这个工作的原因基本上是,如果两个具有匹配键的对象进入关键区块,则第二个将检索第一个已经使用的锁定(或者留下的锁定并且没有GC&#39 ; ed)。但是,如果它未被使用,则两者都将保留该方法并删除它们对锁对象的引用,因此可以安全地收集它。
如果你有一个有限的&#34;已知的尺寸&#34;您想要使用的同步点(最终不必减小大小),您可以避免使用HashMap并使用ConcurrentHashMap,而使用putIfAbsent方法可能更容易理解。
答案 3 :(得分:1)
我的意见是你没有遇到真正的线程问题。
最好让DBMS自动分配一个非冲突的行ID。
如果需要使用现有的行ID,则将它们存储为线程局部变量。 如果不需要共享数据,则不要在线程之间共享数据。
http://download.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html
在应用程序服务器或Web容器中,Oracle dbms在保持数据一致方面要好得多。
“插入行时,许多数据库系统会自动生成唯一的键字段.Oracle数据库在序列和触发器的帮助下提供相同的功能.JDBC 3.0引入了自动生成的键功能的检索,使您可以检索此类生成的值。在JDBC 3.0中,增强了以下接口以支持检索自动生成的密钥功能....“
http://download.oracle.com/docs/cd/B19306_01/java.102/b14355/jdbcvers.htm#CHDEGDHJ
答案 4 :(得分:1)
如果您偶尔会出现过度同步(即不需要时按顺序完成工作),请尝试以下方法:
IdLock类:
public class IdLock {
private Object[] locks = new Object[10000];
public IdLock() {
for (int i = 0; i < locks.length; i++) {
locks[i] = new Object();
}
}
public Object getLock(int id) {
int index = id % locks.length;
return locks[index];
}
}
及其用途:
private idLock = new IdLock();
public void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
synchronized (idLock.getLock(theObjectIwantToSave.getId())) {
// synchronized work here
}
}
答案 5 :(得分:0)
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
synchronized (theObjectIwantToSave) {
if (!methodThatChecksThatObjectAlreadyExists) {
storemyObject() //pseudo code
}
// Have to do a lot other saving stuff, because it either saves everything or nothing
commit() // pseudo code to actually commit all my changes to the database.
}
}
synchronized关键字会锁定您想要的对象,以便其他任何方法都无法访问它。
答案 6 :(得分:0)
我认为你别无选择,只能选择一个你似乎不想做的解决方案。
在您的情况下,我认为objectYouWantToSave上的任何类型的同步都不会起作用,因为它们基于Web请求。因此,每个请求(在其自己的线程上)很可能拥有它自己的对象实例。即使它们在逻辑上被认为是相等的,但这对于同步来说无关紧要。
答案 7 :(得分:0)
synchronized关键字(或其他同步操作)必须但不足以解决您的问题。您应该使用数据结构来存储使用的整数值。在我们的示例中使用了HashSet。不要忘记从hashset中删除太旧的记录。
private static HashSet <Integer>isUsed= new HashSet <Integer>();
public synchronized static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
if(isUsed.contains(theObjectIwantToSave.your_integer_value) != null) {
if (!methodThatChecksThatObjectAlreadyExists) {
storemyObject() //pseudo code
}
// Have to do a lot other saving stuff, because it either saves everything or nothing
commit() // pseudo code to actually commit all my changes to the database.
isUsed.add(theObjectIwantToSave.your_integer_value);
}
}
答案 8 :(得分:0)
要回答有关锁定整数的问题,简短答案为否 - 它不会阻止具有相同值的另一个Integer实例的线程进入。答案很长:取决于你如何获得Integer - by构造函数,重用一些实例或者valueOf(使用一些缓存)。无论如何,我不会依赖它。
可行的工作解决方案是使方法同步:
public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
if (!methodThatChecksThatObjectAlreadyExists) {
storemyObject() //pseudo code
}
// Have to do a lot other saving stuff, because it either saves everything or nothing
commit() // pseudo code to actually commit all my changes to the database.
}
这可能不是性能方面的最佳解决方案,但保证可以正常工作(请注意,如果您不在群集环境中),直到找到更好的解决方案。
答案 9 :(得分:0)
private static final Set<Object> lockedObjects = new HashSet<>();
private void lockObject(Object dbObject) throws InterruptedException {
synchronized (lockedObjects) {
while (!lockedObjects.add(dbObject)) {
lockedObjects.wait();
}
}
}
private void unlockObject(Object dbObject) {
synchronized (lockedObjects) {
lockedObjects.remove(dbObject);
lockedObjects.notifyAll();
}
}
public void saveSomethingImportantToDatabase(Object theObjectIwantToSave) throws InterruptedException {
try {
lockObject(theObjectIwantToSave);
if (!methodThatChecksThatObjectAlreadyExists(theObjectIwantToSave)) {
storeMyObject(theObjectIwantToSave);
}
commit();
} finally {
unlockObject(theObjectIwantToSave);
}
}