如何处理 Jfinal 线程不安全的问题

最近我们发现在多并发情况下,发现Jfinal会有线程不安全的情况。我们先按照之前的文档做如下处理

public class SafeTestCase {

	public static SafeTestCase me = new SafeTestCase();

     public void signInTask(final int userId) {
		try {
			boolean isSigned = checkSignIn(userId, RuleID);
			Db.tx(() -> {
				if (!isSigned) {
					System.out.println(userId + "加积分");
					addScore(userId,RuleID, RuleScore);
				}
				return true;
			});

		} catch (Exception e) {

		}
	}

	private static final String lock = "lock";
	public void signInTaskLock(final int userId) {
		try {
			synchronized (lock) {
				boolean isSigned = checkSignIn(userId, RuleID);
				Db.tx(() -> {
					if (!isSigned) {
						System.out.println(userId + "加积分");
						addScore(userId,RuleID, RuleScore);
					}
					return true;
				});
			}

		} catch (Exception e) {

		}
	   }
	}

业务逻辑,一个用户一天只能签到一次并增加积分,isSigned为true时说明用户已经签到。

第一个方法是按照官方文档方法进行数据存储处理的,

第二个方法是我们按照加锁处理线程安全。

然后在controller层对以上两个方法进行调用看结果。

@ActionKey("safeTest")
public void safeTest() {
		int userId = getParaToInt();
		SafeTestCase.me.signInTask(userId);
		renderText("ok");
	}
	
@ActionKey("safeTestLock")
public void safeTestLock() {
		int userId = getParaToInt();
		SafeTestCase.me.signInTaskLock(userId);
		renderText("ok");
	}

以下是测试类:

@Testpublic void safeCaseTest() {
		int userId = 30000;
		String url="http://localhost:8080/safeTest/"+userId;
		String urlLock="http://localhost:8080/safeTestLock/"+(userId+1);
		ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i = 0; i<=10;i++){
            executorService.execute(new Runnable() {
				@Override
				public void run() {
					HttpKit.get(url);
					HttpKit.get(urlLock);
				}
			});
        }
 
        try {
			Thread.sleep(200000);
			System.exit(0);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        
}

结果发现第一种方法会出现多个重复记录,第二种方法由于加了锁没有出现重复记录。

麻烦波神看下,如果要做线程安全的话,如何运用框架处理,而不是加锁。

评论区

郭浩伟

2019-03-13 10:58

围观,等待大神解答

红星

2019-03-13 13:34

你这个本质是并发访问问题 。并不是线程安全的问题。解决并发访问出现重复记录的问题随便搜一下,解决方法很多。

JFinal

2019-03-13 13:47

1:假定有 A、B 两个并发线程

2:没加锁的那个版本,当 A 线程执行完下面代码得到 isSigned 后被操作系统挂起:
boolean isSigned = checkSignIn(userId, RuleID);
A 线程得到的 isSigned 是 false,并且在得到后立即被操作系统挂起

3:这时,线程 B 追上来,也执行到了 boolean isSigned = checkSignIn(userId, RuleID) 这行代码,也得到一个 false 值

4:接下来,A、B 继续向下执行,必然会引起重复签到和加分

这个问题显然是与 jfinal 无关的,是 java 的多线程基础问题。解决办法除了你的加锁版本以外,还可以通过数据库事务来解决,不要将 isSigned 读到内存中来,而是要让所有动作一气呵成,全部转化成数据库操作,大致如下:
Db.tx(() -> {
String sql = "(update user set score=score + 1 where id not in(select userId from user_signed where createDate = ?)";

int n1 = Db.update(sql, 今天日期);

// 插入一条签到数据
int n2 = Db.update("insert into user_signed ...");

return n1 > 0 && n2 > 0;
});

上面假定了你要更新的表是 user 的 score 字段,仅演示了加 1,假定签到表为 user_signed

JFinal

2019-03-13 13:48

注意看一下,上面有关 user 表与 user_signed 表的操作全在一个事务中,并不是像你的代码那样将一个 signed 读到内存,而且你读到内存的数据库操作与后面的数据操作还不在事务中,肯定要出问题

goodsense

2019-03-13 15:27

多谢波神的回答,由于我们业务比较复杂,不是简单sql能实现的。还是用加锁的方式解决线程安全的问题。希望波神能在框架里面增加些线程安全的工具类,实现Jfinal在高并发环境下避免脏数据的问题。

JFinal

2019-03-13 15:35

@goodsense 这个在框架层面是解决不了的,除非用 JDK 的 synchronized、lock 机制做一套 API 供大家在业务中使用,类似于一个多线程同步框架,这样来弄就与 JDK 提供的那套机制没多大差别了

注意一下,目前你的解决方案是不支持集群的,如果你用多个 JVM 跑你现在的同一个项目,即便用锁也是不行的,因为这个锁只是在同一个 JVM 中有效

使用我给的方案,由于是用的数据库事务机制,支持集群

goodsense

2019-03-15 09:15

@JFinal 好的,谢谢波神

热门反馈

扫码入社