本文来源于实际项目。
项目需求:某段逻辑需要过滤注册用户,而每时每刻都可能会有新的注册用户加入进来。注册用户的存在与否是通过查询数据库表中是否存在记录判断的。由于不希望频繁的读数据库表,所以考虑定时从数据库加载一份用户列表到内存里,这样可以减少读库的次数并且可以提高查询的效率。
过滤用户逻辑代码简单抽象成下面的测试代码。
package test.java;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UserFilter implements Runnable {
private static IUserDAO dao = ServiceFactory.getUserDao();
public static Map<String, String> userIDMap = new HashMap<String, String>();
public static void start() {
// 将数据库表中的用户数据加载到内存(以HashMap存放)
// 赋值userIDMap
userIDMap = dao.queryAllUsers();
synchronized(userIDMap) {
// 已经赋值,唤醒所有处于等待状态的线程
userIDMap.notifyAll();
}
}
public static boolean isValidUser(String key) {
synchronized(userIDMap) {
if (userIDMap.size() == 0) {
try {
// 等待主线程调用start()方法对userIDMap赋值
userIDMap.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return userIDMap.containsKey(key);
}
}
@Override
public void run() {
if (isValidUser("test")) {
System.out.println("this is a valid user.");
}
}
public static void main(String[] args) {
// 主线程调用start方法
start();
ExecutorService exec = Executors.newCachedThreadPool();
// 使用线程池创建10个线程
for (int i = 0; i < 10; i++) {
exec.execute(new UserFilter());
}
}
}
这段代码希望完成的事情如下:
- 使用全局变量userIDMap,它是一个HashMap类型的变量。希望通过调用map.containsKey(String)方法判断用户是否为合法用户。
- 主线程调用start()方法,将所有用户列表从数据库里加载到内存里,并赋值给userIDMap。
- 用线程池创建10个UserFilter任务,UserFilter任务只做一件事情,即通过调用isValidUser(String)方法检查用户是否是合法用户
- isValidUser方法实现时会调用userIDMap.containsKey(String)方法,如果返回true,即userIDMap里存在相应的用户,表示用户合法。
- 在调用userIDMap.containsKey(String)方法之前需要保证userIDMap里已经存放了数据库中的所有用户列表。为此考虑使用java.lang.Object.wait()和java.lang.Object.notifyAll()方法。
- 在isValidUser方法开始处判断userIDMap中是否有值(if userIDMap.size()==0),如果userIDMap还是空的,则调用wait方法,让线程等待。
- 在start()方法中,将通过数据库dao调用查询接口返回的map赋值给userIDMap。之后调用notifyAll()方法唤醒所有等待的线程。
但是,测试发现,所有线程进入等待状态后都不能被正常唤醒。能看出问题出在哪儿么?
---------------------------------------------------------------------------------------------
在上面的代码中,我使用userIDMap作为调用wait()和notifyAll()方法的Object对象,并且调用放在了两个synchronized(userIDMap) 块中。
来看下wait()和notifyAll()方法的具体用法(下面是根据jdk中的方法注释概括出来的)
在某个线程方法中对wait()和notifyAll()的调用必须指定一个Object对象,而且该线程必须拥有该Object对象的monitor。最简单的获取到对象monitor的办法是,在对象上使用synchronized关键字。当调用wait()方法后,当前线程会释放掉对象锁,并进入sleep状态。其他线程在调用notifyAll()方法时必须使用同一个Object对象,notifyAll()方法成功调用后,所有在同一Obejct对象上等待的线程被唤醒。
这里有个很关键的点,即两个方法在不同线程里被调用时必须作用在同一个对象上。
然后再仔细看下上面代码中start()方法是怎么写的。
public static void start() {
// 将数据库表中的用户数据加载到内存(以HashMap存放)
// 赋值userIDMap
userIDMap = dao.queryAllUsers();
synchronized(userIDMap) {
// 已经赋值,唤醒所有处于等待状态的线程
userIDMap.notifyAll();
}
}
是的,userIDMap被赋值了!导致下面的synchronized作用到另一个对象上面,即使该对象现在也叫userIDMap。这里的本意是想将查询得到的用户列表放入全局维护的userIDMap中,通过赋值虽然可以实现这个需求,但却让usreIDMap引用了一个全新的对象。从这个上下文看,为了保证userIDMap引用同一个对象,需要考虑其他的途径。
那么,如何将一个Map的值复制到另一个Map?我最初想到了下面两种方式:
- 直接使用赋值语句。会使得左侧变量引用新的对象。
- 使用 Map.clear() 方法清除 map中的数据,然后 Map.putAll(Map)
很遗憾一开始使用了“简洁”一点的赋值操作,导致花了很长时间排查bug。
最后对代码做如下修改,问题解决:
public static void start() {
// 修改后的userIDMap“赋值”方式
userIDMap.clear();
userIDMap.putAll(userIDMap);
// userIDMap = dao.queryAllUsers();
synchronized(userIDMap) {
// 已经赋值,唤醒所有处于等待状态的线程
userIDMap.notifyAll();
}
}
分享到:
相关推荐
死锁通常与这些方法的不当使用有关,但不是唯一原因。 - **选项E**:正确。即使在单线程应用程序中,如果同步块使用不当也可能导致死锁。 - **选项F**:正确。`Thread.yield()`只会影响线程的调度,并不能解决由同步...
- E正确,单线程应用程序通过不当使用`synchronized`块也可能会发生死锁。 - F正确,`Thread.yield()`仅能让当前线程放弃CPU使用权,并不能解决死锁问题。 ##### 题目3 **题目描述**: 给出以下代码段,哪项陈述...
`wait`方法用于使当前线程等待,直到另一个线程调用该对象的`notify`或`notifyAll`方法唤醒它。`sleep`方法则会让当前线程暂停指定的时间,但不会释放锁。`wait`和`notify`方法必须在同步上下文中使用,而`sleep`...
主要通过wait()、notify()和notifyAll()方法实现,这些方法位于Object类中,必须在同步块或同步方法内使用,以防止死锁和不一致的状态。此外,Java还提供了高级的并发工具,如Semaphore(信号量)、CyclicBarrier...
- **wait()**:是`Object`类的方法,使当前线程放弃对象锁并进入等待池,直到收到`notify()`或`notifyAll()`信号才会重新竞争对象锁。 #### 十三、Java 是否支持 goto - **结论**:Java 不支持传统的`goto`语句。 ...
16.4.9 防止错误的使用wait、notify、notifyAll方法 371 16.5 获取当前正在运行的线程 372 16.6 volatile关键字的含义与使用 372 16.7 小结 373 第17章 高级线程开发 374 17.1 线程池的使用 374 17.1.1...
- 使用分段锁技术,将数据分成若干段,每段使用一个锁。 - **应用场景**: - 适合大量读取操作,少量写入操作的场景。 #### 二十二、volatile关键字的理解 - **作用**: - 保证了变量的可见性和禁止指令重排序。...
Java并发编程还包括线程间的通信,如`wait()`, `notify()`和`notifyAll()`方法,它们是`Object`类的方法,用于线程之间的协作。然而,这些方法使用不当可能导致死锁、活锁和饥饿等问题,因此需要谨慎使用,并结合`...