奇怪的Java多线程行为
我在一个由REST API组成的应用程序中工作,该应用程序公开了几个端点,其中一个是加载系统数据的初始化操作,可以在应用程序生命周期中多次调用,删除现有数据并加载新数据。其余端点对数据执行某些操作,并向系统中引入一些附加数据,这些数据在调用初始化端点时将被擦除。考虑到应用程序应该在并发场景下一致地工作,我决定基于奇怪的Java多线程行为,java,multithreading,testing,concurrency,locking,Java,Multithreading,Testing,Concurrency,Locking,我在一个由REST API组成的应用程序中工作,该应用程序公开了几个端点,其中一个是加载系统数据的初始化操作,可以在应用程序生命周期中多次调用,删除现有数据并加载新数据。其余端点对数据执行某些操作,并向系统中引入一些附加数据,这些数据在调用初始化端点时将被擦除。考虑到应用程序应该在并发场景下一致地工作,我决定基于可重入写锁实现一个同步器类,其中只有初始化操作将锁定写锁,其余操作将锁定读锁。然而,当涉及到测试我的实现时,我面临着一种奇怪的行为,我想有些事情我不是很了解,不确定是关于我的实现还是测试
可重入写锁
实现一个同步器
类,其中只有初始化操作将锁定写锁,其余操作将锁定读锁。然而,当涉及到测试我的实现时,我面临着一种奇怪的行为,我想有些事情我不是很了解,不确定是关于我的实现还是测试本身
public class LockBasedSynchronizer implements Synchronizer {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
@Override
public void startNonBlocking() {
lock.readLock().lock();
}
@Override
public void endNonBlocking() {
lock.readLock().unlock();
}
@Override
public void startBlocking() {
lock.writeLock().lock();
}
@Override
public void endBlocking() {
lock.writeLock().unlock();
}
}
class LockBasedSynchronizerTest {
private LockBasedSynchronizer synchronizer;
@BeforeEach
void setUp() {
synchronizer = new LockBasedSynchronizer();
}
@Test
void itDoesntBlockNonBlockingOperations() {
List<String> outputScrapper = new ArrayList<>();
List<Worker> threads = asList(
new NonBlockingWorker(synchronizer, outputScrapper, 300, "first"),
new NonBlockingWorker(synchronizer, outputScrapper, 100, "second"),
new NonBlockingWorker(synchronizer, outputScrapper, 10, "third")
);
threads.parallelStream().forEach(Worker::run); // this way it works
// threads.forEach(Worker::run); // this way it fails
assertThat(outputScrapper).containsExactly("third", "second", "first");
}
@AllArgsConstructor
abstract class Worker extends Thread {
protected Synchronizer synchronizer;
private List<String> outputScraper;
private int delayInMilliseconds;
private String id;
protected abstract void lock();
protected abstract void unlock();
@Override
public void run() {
try {
System.out.println(format("Starting [%s]", id));
sleep(delayInMilliseconds);
lock();
outputScraper.add(id);
unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class NonBlockingWorker extends Worker {
public NonBlockingWorker(Synchronizer synchronizer, List<String> outputScraper, int delayInMilliseconds, String id) {
super(synchronizer, outputScraper, delayInMilliseconds, id);
}
@Override
protected void lock() {
synchronizer.startNonBlocking();
}
@Override
protected void unlock() {
synchronizer.endNonBlocking();
}
}
公共类基于锁的同步器实现同步器{
私有最终ReentrantReadWriteLock锁=新的ReentrantReadWriteLock();
@凌驾
public void startNonBlocking(){
lock.readLock().lock();
}
@凌驾
公共void endNonBlocking(){
lock.readLock().unlock();
}
@凌驾
public void startBlocking(){
lock.writeLock().lock();
}
@凌驾
公共void endBlocking(){
lock.writeLock().unlock();
}
}
类LockBasedSynchronizerTest{
专用锁式同步器;
@之前
无效设置(){
同步器=新的基于锁的同步器();
}
@试验
void itDoesntBlockNonBlockingOperations(){
List outputscarser=new ArrayList();
列表线程=asList(
新的非阻塞工人(同步器,输出刮板,300,“第一”),
新的非阻塞工人(同步器,输出刮板,100,“秒”),
新的非阻塞工人(同步器,输出刮板,10,“第三”)
);
threads.parallelStream().forEach(Worker::run);//这样工作
//threads.forEach(Worker::run);//这样会失败
资产(输出报废者)。实际包含(“第三”、“第二”、“第一”);
}
@AllArgsConstructor
抽象类工作线程扩展{
受保护的同步器;
私有列表输出刮刀;
私有整数延迟毫秒;
私有字符串id;
受保护的抽象空锁();
受保护的抽象无效解锁();
@凌驾
公开募捐{
试一试{
System.out.println(格式(“起始[%s]”,id));
睡眠(延迟毫秒);
锁();
outputScraper.add(id);
解锁();
}捕捉(中断异常e){
e、 printStackTrace();
}
}
}
类NonBlockingWorker扩展了Worker{
公共非阻塞辅助程序(同步器同步器,列表输出刮刀,整数延迟毫秒,字符串id){
超级(同步器,输出刮刀,延迟毫秒,id);
}
@凌驾
受保护的无效锁(){
同步器。启动非阻塞();
}
@凌驾
受保护的无效解锁(){
synchronizer.endNonBlocking();
}
}
正如您所看到的,当我通过并行流并行生成工作线程时,它会按预期工作,但是如果我按顺序运行它们,它们不会按预期工作,并且会按照与运行相同的顺序执行。看起来它们只使用一个线程执行。有人能解释为什么吗
另外,我脑海中关于这个实现的另一个疑问是,如果阻塞操作正在等待写锁,而非阻塞操作没有停止到达,那么会发生什么。它会无限期地等待,直到非阻塞操作停止到达吗
除此之外,如果你能为暴露的问题想出更好的解决方案,建议是非常受欢迎的。首先,
线程的主要问题是forEach(Worker::run)
实际上并不同时运行任何东西,你只需遍历Worker
的列表并调用它们的run()
方法,一个接一个
现在再谈几点
一般来说,不要扩展线程
。而是创建一些可运行的
或可调用的
或其他一些接口来在线程中运行,通常是通过执行器
。您并没有试图重新定义线程
,您只是试图同时运行一些东西
另外,对于您现在拥有的代码,您没有使用任何扩展线程的方法,因为您只是为流中的工作者
应用run()
方法,而流中的工作者
可以是流或列表中任何类的任何方法
我认为最好的方法是让工作者
实现可运行
,而不是扩展线程
,并将这些工作者
送入执行器
。或者简单地将运行()
应用于并行流()的元素
也可以,我想,只要知道你在做什么。首先,线程的主要问题是,forEach(Worker::run)
实际上并不同时运行任何东西,你只是在遍历你的Worker
列表,然后一个接一个地调用它们的run()
方法
现在再谈几点
一般来说,不要扩展线程
。而是创建一些可运行的
或可调用的
或其他一些接口,通常通过执行器
在线程中运行。您不会试图重新定义线程
threads.forEach(Worker::start);
Thread.sleep(1000);