文章标签 » 多线程

如何在Spring管理的事务中使用多线程

keywords:Java, Spring, Transactional, Multi-threading, ExecutorService

为什么要用多线程?

1
2
3
4
5
6
7
8
9
10
11
12
public class BigService {

    public void execute() {
        doSomething();

	List entities = buildManyManyEntities();
	save(entities);

	doOthers();
    }

}

如上的业务代码,经测试大部分时间花在了 save 方法上(耗时:15/22,数据量:5000)。

于是想到了用多线程(entities 数据之间互不影响)。

于是改成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BigService {

    public void execute() {
	doSomething();

	List entities = buildManyManyEntities();
	// 使用多线程存储
	saveInParallel(entities);

	doOthers();
    }

    void save(Object entities) {
	// 直接存入数据库
	// 单个对象和集合都可以
    }

    void saveInParallel(List entities) {
	int cpus = RunTime.getRunTime().availableProcessors();
	ExecuteService exec = Executors.newFixedThreadPool(cpus);

	try {
	    for (final Object each : entities) {   
		exec.execute(new Runnable() {
			public void run() {
			    save(each);
			}
		});
	    }
	} finally {
	    exec.shutdown(); // 关闭资源
	}
    }
}

自我感觉良好。结果一测试发现表里没数据。

网上查了一下,找到了原因:新的线程没受Spring控制(项目中事务是由Spring控制的)。

知道了问题,就有了方向。意料之外,情理之中地在Stack Overflow(链接见文末)找到了答案。关键点在于Spring中的Lookup,详述如下。

关键步骤在于由Spring来控制线程类的实例化。

新建多线程工具类:MultiThreadHelper.java 。

1
2
3
4
5
public abstract class MultiThreadHelper {
    // 必须是抽象方法且无参
    // 详见文末Spring官方文档Lookup部分
    public abstract DAOTask createDAOTask();
}

新建线程类:DAOTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DAOTask implements Runnable {
    private DAO dao;
    private Object entity;

    public DAOTask(DAO dao) {
	this.dao = dao;
    }

    public void addEntity(Object entity) {
	this.entity = entity;
    }

    public void run() {
	dao.save(entity);
    }
}

新建Spring的配置文件: multi.xml 。

1
2
3
4
5
6
7
8
<bean id="multiThreadHelper" class="MultiThreadHelper">
    <lookup-method name="createDAOTask" bean="daoTask"/>
</bean>

<!-- 注意要用prototype,每次都生成一个新的 -->
<bean id="daoTask" class="DAOTask" scope="prototype">
    <constructor-arg ref="dao"/>
</bean>

新的BigService类(省略了Spring配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class BigService {

    private MultiThreadHelper multiThreadHelper; // 通过Spring注入

    public void execute() {
	doSomething();

	List entities = buildManyManyEntities();
	// 使用多线程存储
	saveInParallel(entities);

	doOthers();
    }

    void save(Object entities) {
	// 直接存入数据库
	// 单个对象和集合都可以
    }

    void saveInParallel(List entities) {
	int cpus = RunTime.getRunTime().availableProcessors();
	ExecuteService exec = Executors.newFixedThreadPool(cpus);

	try {
	    for (final Object each : entities) {   
		// 一定要调用这个Lookup方法生成线程
		DAOTask task = getMultiThreadHelper().createDAOTask();
		task.addEntity(each);
		exec.execute(task);
	    }
	} finally {
	    exec.shutdown(); // 关闭资源
	}
    }
}

最后需要注意的是,要把 run 方法纳入事务范围。传播类型REQUIREDREQUIRES_NEW 没看出明显差别。

大功告成!

时间从本来的22s左右,减少为4s左右。当然,这仅仅是响应时间(从调用开始到调用结束),不代表数据都已经存入库中了(通过日志可以看出来)。

线程数量的选择

之前看过的一本书(《Java并发编程实战》)里说:线程数量等于服务器的CPU数量+1时能实现最优的利用率。

于是分别测试了N+1,N,N-1三种情况,差别不大,N-1的略胜一筹。不知道是不是仅仅因为需要创建的线程数少了,导致响应时间变短。

说明:这里的N指CPU的数量,测试中是4 。

问题

这里存在一个问题:事务不能跟主线程一起统一管理,即如果主线程中发生了错误多线程写入的数据不会回滚。

不过有个小技巧——先删后插,每次调这个方法前都先删一下。

如有更好的方法,欢迎留言讨论。

参考链接

 

多线程与线程安全(实例讲解)

什么情况下需要关注线程安全问题?
就是多个线程会对某个变量同时执行读/写操作的时候。

问题

举个常用但没太注意过的例子 —— SimpleDateFormat 类。
这个类是JDK里面封装的,用来对日期进行格式化。提供 parse(String dateStr), format(Date date) 等方法。

注:上面提到的方法都在其父类 DateFormat 中。

翻开源代码就会发现,它有一个实体变量 calendar (也是在父类 DateFormat 中)。上面提到的 parse 、format 等方法会对这个变量执行 clear() 、set…(…) 等操作(具体操作请查看源代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class DateFormat extends Format {

    /**
     * The {@link Calendar} instance used for calculating the date-time fields
     * and the instant of time. This field is used for both formatting and
     * parsing.
     *
     * <p>Subclasses should initialize this field to a {@link Calendar}
     * appropriate for the {@link Locale} associated with this
     * <code>DateFormat</code>.
     * @serial
     */
    protected Calendar calendar;

    ...

}

这样,如果有一个类型为 SimpleDateFormat 的公共变量,就要小心了,这个变量不是线程安全的,多个线程间的数据可能会串了…几乎是一定会串。

例如,有这样一个日期的工具类,就是将日期转换成特定的字符串格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.DateUtil
 */
public class DateUtil {

	public final static String PATTERN_YMD = "yyyy-MM-dd";
	
	private static SimpleDateFormat formatter = new SimpleDateFormat();
	
	public static String getFormattedDate(Date date) {
		formatter.applyPattern(PATTERN_YMD);
		return formatter.format(date);
	}
}

这是测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 测试类
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.Main
 */

public class Main {

	public static void main(String[] args) {
		// 昨天的日期
		Calendar calYesterday = Calendar.getInstance();
		calYesterday.setTime(new Date());
		calYesterday.add(Calendar.DAY_OF_MONTH, -1);
		ShowDate yesterday = new ShowDate("YESTERDAY", calYesterday.getTime());
		
		// 今天的日期
		ShowDate today = new ShowDate("TODAY", new Date());
		
		Thread t1 = new Thread(yesterday);
		Thread t2 = new Thread(today);
		
		/*
		 * start() 
		 * 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
		 * run() 
		 * 就和普通的成员方法一样,可以被重复调用。单独调用s的话,会在当前线程中执行run(),而并不会启动新线程!
		 */
		t1.start();
		t2.start();
	}
}

还有一个线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * 一个简单的线程的实现类
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.ShowDate
 */
class ShowDate implements Runnable {

	private String desc;
	private Date date;
	
	public ShowDate(String desc, Date date) {
		this.desc = desc;
		this.date = date;
	}

	@Override
	public void run() {

		for (int i = 0; i < 1000; i++) {
			if (i % 30 == 0) {
				try {
					Thread.sleep(500L);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(desc + ": " + DateUtil.getFormattedDate(date));
		}
	}
}

按照设想,以今天(2016-11-27)为例,应该是 YESTERDAY: 2016-11-26TODAY: 2016-11-27 这两个字符串交叉着,各自打印 1000 遍。
可事实是这样吗?
运行一下看看。
截图
结果简直惨不忍睹,第一行就出错了(当然,这个可能需要一定的“运气”)。“今天”一会儿 26,一会儿 27。“昨天”也一样,一会儿 26,一会儿 27。
这就是线程不安全导致的问题了。
该怎么解决呢??

解决办法(一) —— synchronized

比较容易想到的就是这个关键字了:synchronized 。在学校就是这么学的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 进化的 DateUtil
 * 使用同步块,synchronized
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.SyncDateUtil
 */
public class SyncDateUtil {

	public final static String PATTERN_YMD = "yyyy-MM-dd";
	
	private static SimpleDateFormat formatter = new SimpleDateFormat();
	
	public static String getFormattedDate(Date date) {
		synchronized(formatter) {
			
			formatter.applyPattern(PATTERN_YMD);
			return formatter.format(date);
		}
	}
}

只要把线程类 —— ShowDate 的第 30 行的工具类替换一下就行,DateUtil 换成 SyncDateUtil ,即:

1
2
//	System.out.println(desc + ": " + DateUtil.getFormattedDate(date));
	System.out.println(desc + ": " + SyncDateUtil.getFormattedDate(date));

在运行试试!
不管执行多少遍,都不会出现前面说的串数据的问题了。

解决办法(二) —— ThreadLocal

除了 synchronized 还有 ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 进化的 DateUtil —— ThreadLocal
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.ThreadLocalDateUtil
 */
public class ThreadLocalDateUtil {

	public final static String PATTERN_YMD = "yyyy-MM-dd";
	
	private static ThreadLocal<DateFormat> formatter = new ThreadLocal<DateFormat>() {
		protected DateFormat initialValue() {
			return new SimpleDateFormat(PATTERN_YMD);
		}
	};
	
	public static String getFormattedDate(Date date) {
		return formatter.get().format(date);
	}
}

ThreadLocal 还有另一种实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * 进化的 DateUtil —— ThreadLocal 的另一种实现
 *
 * @author by youngz
 *      on 2016年11月27日
 *
 * Package&FileName: org.young.thread.ThreadLocalDateUtil2
 */
public class ThreadLocalDateUtil2 {

	public final static String PATTERN_YMD = "yyyy-MM-dd";
	
	private static ThreadLocal<DateFormat> formatter = new ThreadLocal<DateFormat>();
	
	public static DateFormat getDateFormat() {
		DateFormat df = formatter.get();
		
		if (null == df) {
			df = new SimpleDateFormat(PATTERN_YMD);
			formatter.set(df);
		}
		
		return df;
	}
	
	public static String getFormattedDate(Date date) {
		return getDateFormat().format(date);
	}
}

详细代码可参照 https://github.com/youngzhu/CollectionCode4Java/tree/master/src/org/young/thread

参考:
http://www.cnblogs.com/doit8791/p/4093808.html