文章分类 » IT | 编程

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

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

问题

举个常用但没太注意过的例子 —— 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

 

Spring | UnexpectedRollbackException 异常(已解决)

项目中遇到了这样的异常:

1
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

项目概要

调用逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 最外层 Service
public class MainService {
	public void executeAAA() {
		try {
			...
			nestedService.executeBBB();
			...
		} catch (Exception e) {
			// 更新操作日志表
			updateTblOperationLog(e);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
pulblic class NestedService {
	public void executeBBB() {
		try {
			...
			nestedNestedService.sendMsg();
			...
		
		} catch (Exception e) {
			throws new BusinessException(e);
		}
	}
}
1
2
3
4
5
6
7
8
9
pulblic class NestedNestedService {
	public void sendMsg() throws BusinessException {
		try {
			...
		} catch (Exception e) {
			throws new BusinessException(e);
		}
	}
}

事务配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<aop:config proxy-target-class="true">
	<aop:pointcut id="servicePointcut" 
		expression="execution(* com.XXX..*service..*(..))" />
	<aop:advisor pointcut-ref="servicePointcut" advice-ref="txAdvice" order="1" />
</aop:config>
<tx:annotation-driven transaction-manager = "transactionManager" />
<tx:advice id="txAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<tx:method name="save*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="add*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="insert*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="update*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="delete*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="validate*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="calculate*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="execute*" propagation="REQUIRED" rollback-for="BusinessException"/>
		<tx:method name="find*" read-only="true" />
		<tx:method name="get*" read-only="true" />
		<tx:method name="load*" read-only="true" />
		<tx:method name="*" read-only="true"/>
	</tx:attributes>
</tx:advice>

出现这种没碰过的问题,第一反应当然就是 Google 了。
翻了很多 Blog 和 论坛,也尝试了很多办法,但都失败了。

尝试

原因已经找到:通过查看日志,知道问题是出现在调用 nestedNestedService.sendMsg() 时。由于对方服务不通,导致这个方法出现异常。

尝试1

总的来说就是事务嵌套的问题。网上说可以为每一个方法都起一个事务,修改事务配置文件:

1
<tx:method name="execute*" propagation="REQUIRED" rollback-for="BusinessException"/>

改为

1
<tx:method name="execute*" propagation="REQUIRES_NEW" rollback-for="BusinessException"/>

这个不行,还是会报一样的错误。

尝试2

考虑到出错的那个方法 sendMsg() 不在事务的预定义里面,属于 * 的范畴。

1
<tx:method name="*" read-only="true"/>

改成开启独立事务的模式(挂起前一个,当前方法新起一个事务):

1
<tx:method name="*" propagation="REQUIRES_NEW"/>

不行。

改成不用事务的,因为该用事务的前面定义的差不多了:

1
<tx:method name="*" propagation="NOT_SUPPORTED"/>

还是不行。报错说“事务是必须的”……

那就改成 SUPPORTS 的吧,意思是有事务就用,没有也没关系

1
<tx:method name="*" propagation="SUPPORTS"/>

还是不行。跟一开始一样的错。

* 不行,那就专门给以 send 开头的方法设置一个。
跟上面一样,尝试了 REQUIRES_NEWNOT_SUPPORTED ,都以失败告终。

解决办法

修改配置不行。那就只能从代码入手了。
看了这么多资料,做了这么多尝试,问题点差不多找到了:方法 nestedService.executeBBB() 抛出了 BusinessException 异常导致的。即使最外层的 Service 的 mainService.executeAAA() 做了 try-catch 也不行。

由于项目中的 Service 类里有很多 execute 开头的方法,所以不能随意的更改 rollback-for 配置。
事务里定义了如果遇到 BusinessException 就回滚。
怎样让它不回滚呢?
那就新增一个异常类吧。

  1. 新增异常类:
    1
    public class ServiceException extends Exception {}
  2. 修改 NestedNestedService 类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pulblic class NestedNestedService {
    	public void sendMsg() throws Exception {
    		try {
    			...
    		} catch (Exception e) {
    			//throws new BusinessException(e);
    			// 直接 throw ,就不再封装了
    			throws e;
    		}
    	}
    }
  3. 修改 NestedService 类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pulblic class NestedService {
    	public void executeBBB() {
    		try {
    			...
    			nestedNestedService.sendMsg();
    			...
    		
    		} catch (Exception e) {
    			// throws new BusinessException(e);
    			
    			// 换成 ServiceException 异常
    			throws new ServiceException(e);
    		}
    	}
    }

其实之前都是抱着试试看的态度。
问题就这样解决了。

 

神奇的递归

作为程序员不知道你有没有过跟我一样的感觉:有些算法的设计真精妙,真神奇!

举个例子,比如递归。

先考虑这样的一个程序题:将 1 到 10 依次累加。
你会怎么处理?

最容易想到的办法可能就是循环了。

1
2
3
4
5
// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
alert(acc); // result is 55

但如果不用循环呢?
还能怎么实现?

看看下面这块代码:

1
2
3
4
5
6
7
// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
    if (start > end)
        return acc;
    return sumRange(start + 1, end, acc + start)
}
alert(sumRange(1, 10, 0)); // result is also 55

这就是递归了。是不是很神奇?!

作为一个老程序员,写代码或看代码的时候,时不时的会有这样的时候:靠,代码还可以这么写啊,真妙!

via Mdeium

Hexo自问自答(Q&A)

Q01: 已有的博文可以重命名吗?

A : 可以。只要修改 source/_posts/xx.md 文件里的 title 即可。

注意:改MD的文件名是不行的。

Q02: 如何设置多标签

A : [tag1, tag2, tag3]

Eclipse | 同步时报 cannot open git-upload-pack(已解决)

在 Eclipse 中使用 egit 同步 GitHub 上的代码时报错:

网上翻了下,原来加一个配置就行了。

https://github.com/YoungZHU/CollectionCode4Java.git: cannot open git-upload-pack

打开 Eclipse,在工具栏中依次点开 Windows–>Preferences–>Team–>Git–>Configuration–>User Settings,然后点 Add Entry 新建一个键值对,
Key http.sslVerify
Value false

如图:

Eclipse配置截图

从 Hibernate 3 升级到 Hibernate 4 序列号(Sequence)不能用 String 了?(已解决)

从 Hibernate 3 升级到 Hibernate 4 ,出现了这样的错误:

1
2
3
4
5
6
org.hibernate.id.IdentifierGenerationException: Unknown integral data type for ids : java.lang.String
at org.hibernate.id.IdentifierGeneratorHelper.getIntegralDataTypeHolder(IdentifierGeneratorHelper.java:215)
at org.hibernate.id.SequenceGenerator.buildHolder(SequenceGenerator.java:150)
at org.hibernate.id.SequenceGenerator.generateHolder(SequenceGenerator.java:126)
at org.hibernate.id.SequenceGenerator.generate(SequenceGenerator.java:116)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:118)

原来,在 Hibernate 4 中如果主键的生成策略是 Sequence ,则要求数据库中对应的字段必须为数字(Number,Long 等)类型。
而当前系统中使用的是字符串类型,所以报错了。

网上参考了几篇文章,就一篇(见文末)还不错。在此基础上,覆写了部分方法,解决了问题。

首先是 HISequenceGenerator 类

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package org.young.hibernate.common;

import java.io.Serializable;

import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.id.IntegralDataTypeHolder;
import org.hibernate.id.SequenceGenerator;

/**
 * 继承 hibernate 里的 SequenceGenerator 类,
 * 覆写2个方法:
 * 	1. buildHolder()
 * 	2. generate(SessionImplementor session, Object obj)
 *
 * @author by youngz
 *      on 2016年7月24日
 *
 * Package&FileName: org.young.hibernate.common.HISequenceGenerator
 */
public class HISequenceGenerator extends SequenceGenerator {

	@Override
	public Serializable generate(SessionImplementor session, Object obj) {
		
		/*
		 * 源码
		 * 
		 * return generateHolder(session).makeValue();
		 */
//		Serializable id = generateHolder(session).makeValue();
		Number id = generateHolder(session).makeValue();
		
		/*
		 * 笔者曾自作聪明地以为可以省掉这一步,
		 * 继而这个方法就不用覆写,毕竟都是 Serializable ,
		 * 不管是数字(Number)还是字符串(String)
		 * 
		 * 但返回值却需要确定到String,因为Java类(hbm对象)的属性是String类型的
		 * 而 makeValue() 返回的 Number (这里的实际类型是 Long)
		 * 
		 * 所以 id 的类型直接定义成 Number (更直观),
		 * 而不是 Serializable
		 */
        if (getIdentifierType().getReturnedClass() == String.class){
        	//增加对String的判断
            return id.toString();
        }
        
        return id;
	}

	@Override
	protected IntegralDataTypeHolder buildHolder() {
		/*
		 * 源码
		 * 
		 * return IdentifierGeneratorHelper.getIntegralDataTypeHolder(this.identifierType.getReturnedClass());
		 */
		return HIIdentifierGeneratorHelper
				.getIntegralDataTypeHolder(getIdentifierType()
						.getReturnedClass());
	}
}

还有一个辅助类 HIIdentifierGeneratorHelper

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package org.young.hibernate.common;

import java.math.BigDecimal;
import java.math.BigInteger;

import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.id.IntegralDataTypeHolder;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * IdentifierGeneratorHelper ,final 类,不可继承
 *
 * @author by youngz
 *      on 2016年7月24日
 *
 * Package&FileName: org.young.hibernate.common.HIIdentifierGeneratorHelper
 */
public class HIIdentifierGeneratorHelper {

	public static IntegralDataTypeHolder getIntegralDataTypeHolder(
			Class integralType) {
		if ((integralType == Long.class) || (integralType == Integer.class)
				|| (integralType == Short.class)
				// 增加对String的判断
				|| (integralType == String.class)) {
			return new HIBasicHolder(integralType);
		}
		if (integralType == BigInteger.class)
			return new IdentifierGeneratorHelper.BigIntegerHolder();

		if (integralType == BigDecimal.class) {
			return new IdentifierGeneratorHelper.BigDecimalHolder();
		}

		throw new IdentifierGenerationException(
				"Unknown integral data type for ids : "
						+ integralType.getName());
	}

	public static class HIBasicHolder implements IntegralDataTypeHolder {
		private final Class exactType;
		private long value = 0L;

		public HIBasicHolder(Class exactType) {
			
			this.exactType = exactType;
			if ((exactType != Long.class) && (exactType != Integer.class)
					&& (exactType != Short.class)
					// 增加对String的判断
					&& (exactType != String.class)
					)
				throw new IdentifierGenerationException(
						"Invalid type for basic integral holder : " + exactType);
		}
		
		public long getActualLongValue() {
			return this.value;
		}

		public IntegralDataTypeHolder initialize(long value) {
			this.value = value;
			return this;
		}

		public IntegralDataTypeHolder initialize(ResultSet resultSet,
				long defaultValue) throws SQLException {
			long value = resultSet.getLong(1);
			if (resultSet.wasNull())
				value = defaultValue;

			return initialize(value);
		}

		public void bind(PreparedStatement preparedStatement, int position)
				throws SQLException {
			preparedStatement.setLong(position, this.value);
		}

		public IntegralDataTypeHolder increment() {
			checkInitialized();
			this.value += 1L;
			return this;
		}

		private void checkInitialized() {
			if (this.value == 0L)
				throw new IdentifierGenerationException(
						"integral holder was not initialized");
		}

		public IntegralDataTypeHolder add(long addend) {
			checkInitialized();
			this.value += addend;
			return this;
		}

		public IntegralDataTypeHolder decrement() {
			checkInitialized();
			this.value -= 1L;
			return this;
		}

		public IntegralDataTypeHolder subtract(long subtrahend) {
			checkInitialized();
			this.value -= subtrahend;
			return this;
		}

		public IntegralDataTypeHolder multiplyBy(IntegralDataTypeHolder factor) {
			return multiplyBy(IdentifierGeneratorHelper.extractLong(factor));
		}

		public IntegralDataTypeHolder multiplyBy(long factor) {
			checkInitialized();
			this.value *= factor;
			return this;
		}

		public boolean eq(IntegralDataTypeHolder other) {
			return eq(IdentifierGeneratorHelper.extractLong(other));
		}

		public boolean eq(long value) {
			checkInitialized();
			return (this.value == value);
		}

		public boolean lt(IntegralDataTypeHolder other) {
			return lt(IdentifierGeneratorHelper.extractLong(other));
		}

		public boolean lt(long value) {
			checkInitialized();
			return (this.value < value);
		}

		public boolean gt(IntegralDataTypeHolder other) {
			return gt(IdentifierGeneratorHelper.extractLong(other));
		}

		public boolean gt(long value) {
			checkInitialized();
			return (this.value > value);
		}

		public IntegralDataTypeHolder copy() {
			HIBasicHolder copy = new HIBasicHolder(this.exactType);
			copy.value = this.value;
			return copy;
		}

		public Number makeValue() {
			checkInitialized();
			if (this.exactType == Long.class)
				return Long.valueOf(this.value);
			
			// 增加对String的判断
			if (this.exactType == String.class) {
				return Long.valueOf(this.value);
			}

			if (this.exactType == Integer.class) {
				return Integer.valueOf((int) this.value);
			}

			return Short.valueOf((short) (int) this.value);
		}

		public Number makeValueThenIncrement() {
			Number result = makeValue();
			this.value += 1L;
			return result;
		}

		public Number makeValueThenAdd(long addend) {
			Number result = makeValue();
			this.value += addend;
			return result;
		}

		public String toString() {
			return "BasicHolder[" + this.exactType.getName() + "[" + this.value
					+ "]]";
		}

		public boolean equals(Object o) {
			if (this == o)
				return true;

			if ((o == null) || (super.getClass() != o.getClass())) {
				return false;
			}

			HIBasicHolder that = (HIBasicHolder) o;

			return (this.value == that.value);
		}

		public int hashCode() {
			return (int) (this.value ^ this.value >>> 32);
		}
	}
}

最后,修改 hibernate 配置文件里的主键生成策略

1
2
3
4
5
6
7
<id name="id" type="java.lang.String">
    <column name="ID" length="20" />
	<!-- 此处的 class 即刚刚新建的 Generator -->
    <generator class="org.young.hibernate.common.HISequenceGenerator" >
    	<param name="sequence">SEQ_ID</param>
    </generator>
</id>

本文的实验是在 hibernate-core-4.1.8.Final.jar 基础上完成。

可至 GitHub 上查看相关代码。

参考:

http://my.oschina.net/liyuj/blog/386582?fromerr=4h5OjIKZ

 

Java | 原来 serialVersionUID 的用处在这里

一直不太明白Java对象里 serialVersionUID 字段是做什么用的。
有或者没有,它们之间有差别吗?除了Eclipse里提示的那个黄色的警告。

今天终于知道,原来是在对象序列化这块有作用。

看个例子。

有这样一个对象 User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User implements Serializable {

	String username;
	int age;
	String gender;
	Date regDay;
	boolean martialStatus;

	@Override
	public String toString() {
		return "User [username=" + username + ", age=" + age + ", gender=" + gender + ", regDay=" + regDay
				+ ", martialStatus=" + martialStatus + "]";
	}

}

现在将这个对象序列化存储到硬盘上:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void testWriteObject() {
	User user = new User();
	
	user.username = "Jack";
	user.password = "1234567";
	user.age = 20;
	user.gender = "male";
	user.regDay = new Date();
	user.martialStatus = false;
	
	// 文件的后缀名不影响,无论是 txt,io,甚至是没有后缀
	ObjectSerialize.writeObject(user, "d:\\tmp\\obj");
}

注: ObjectSerialize 是笔者自己封装的简单的对对象序列化存取的工具类。
下同。

从对应的文件中读取对象信息:

1
2
3
4
public void testReadObject() {
	User user = (User) ObjectSerialize.readObject("d:\\tmp\\obj");
	System.out.println(user);
}

目前为止都是正常的。能够存储对象,也能够正确地读取对象信息。

假如,User对象又新增了一个国籍(nationality)字段,新的User类如下(主要差别是在第 8 行的新增字段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User implements Serializable {

	String username;
	int age;
	String gender;
	Date regDay;
	boolean martialStatus;
	String nationality; // 新增的字段

	@Override
	public String toString() {
		return "User [username=" + username + ", age=" + age + ", gender=" + gender + ", regDay=" + regDay
				+ ", martialStatus=" + martialStatus + ", nationality=" + nationality + "]";
	}

}

这个时候再去读区原来的对象文件,你会得到一个异常信息:

java.io.InvalidClassException: org.young.elearn.io.User; local class incompatible: stream classdesc serialVersionUID = 7967476135812239100, local class serialVersionUID = 905986497687499238

serialVersionUID 的问题!!!
文件里存储的User对象的 serialVersionUID 跟当前的User对象(User.class)的 serialVersionUID 不一致。因为如果一个Java对象没有指定 serialVersionUID ,那么系统(JVM)动态地指定一个。

为了解决这个问题,只要在定义User对象时指定 serialVersionUID 就可以了,即:

1
2
3
4
5
6
7
class User implements Serializable {

	private static final long serialVersionUID = 7967476135812239100L;

	// same code as before

}

详细的代码可参考:GitHub

 

Java | 原来 try 还可以这样用啊?!

习惯了这样的try:

1
2
3
4
5
6
try
{

} catch (Exception e)
{
}

看到了这样的try,觉得有点神奇。

1
2
3
4
5
try(...)
{
} catch (Exception e) 
{
}

原来这还有个专业术语, try-with-resources statement ,它会自动关闭括号内的资源(resources),不用手动添加代码 xx.close();了。

看个小例子:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package org.young.elearn;

import java.io.FileOutputStream;
import java.io.IOException;

import org.junit.Test;

/**
 * try-with-resources 的使用
 * try(resouces)
 * {
 * 
 * } catch (Exception e){}
 * 
 * 这里的resource会自动关闭
 * 
 * 1. resource 必须继承自 java.lang.AutoCloseable
 * 2. 定义和赋值必须都在try里完成
 *
 * @author by Young.ZHU
 *      on 2016年5月29日
 *
 * Package&FileName: org.young.elearn.TryWithResourcesTest
 */
public class TryWithResourcesTest {

/**
 * 验证一下资源是不是真的关闭了
 */
@Test
public void test() {

try (MyResources mr = new MyResources()) {
//mr.doSomething(4);
mr.doSomething(9);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}

/**
 * 编译错误:
 * The resource f of a try-with-resources statement cannot be assigned
 */
@Test
public void test2() {
try (FileOutputStream f = null;) {
//f = new FileOutputStream(new File(""));
} catch (IOException e) {
e.printStackTrace();
}
}

/**
 * 编译错误:
 * The resource type File does not implement java.lang.AutoCloseable
 */
@Test
public void test1() {
/*try (File file = new File("d:\\xx.txt");) {

} */
}

}

class MyResources implements AutoCloseable {

@Override
public void close() throws Exception {
System.out.println("resources are closed.");
}

public void doSomething(int num) throws Exception {
if (num % 2 == 0) {
System.out.println("it's OK.");
} else {
throw new Exception("Enter an even.");
}
}
}

 

以后使用 InputStreamOutputStream 之类的就方便一点了,具体可参考另一个例子:GitHub

注:要求JDK 7 以上

简明教程 | 从0到1搭建个人博客:Github + Hexo

Github 的使用

注册Github

注册地址:https://github.com/

安装Github客户端

配置Github

创建Github Pages

  1. 打开Github网站,在页面的右上角(头像旁边)有个+,点击,然后选择“New repository”。
  2. 填写Repository name
    Github Pages的Repository的名称格式是固定的:username.github.io。

    例如我的Github用户名是 youngzhu ,那么我的 Github Pages的Repository name就是 youngzhu.github.io ,即:https://github.com/youngzhu/youngzhu.github.io

  3. 点击下方的“Create repository”,完成创建。

购买域名

购买地址:GoDaddy ,支持支付宝支付。

2016-01-26 收到GoDaddy发来的邮件,说是支持中文了。15年底注册的时候还是英文。

不要问我为什么。大家(Google/知乎)都说好。

当然,如果你要选择其他的也可以。

介绍两个优惠码网址:

  1. http://www.gdcodecoupon.com/
  2. http://www.dute.me/

使用优惠码时,要注意:

  1. 优惠不了多少钱

    大概1刀。别看网站上很多5折/7折,很多不能用。

  2. 使用优惠码会影响到支付方式
    找啊找,找到一个优惠码居然可以省8刀!!!开心!!!
    可在支付的时候遇到了问题:我的VISA卡信息填了一遍又一遍,总是不对(它提示有问题,却又不说具体问题是什么……)。
    终于在它的帮助页找到了原因:原来这个优惠需要美区的信用卡……

设置DNS

使用DNSPod。

  1. 注册
    访问https://www.dnspod.cn
  2. 添加域名
    注册成功后,在左边的导航栏找到“域名解析”,点击,在网页右边区域会出现一个“添加域名”的按钮,点击,输入你要解析的域名,就是你刚过在GoDaddy上购买的域名。

    我的就是:youngzy.com

  3. 添加解析记录

    点进去之后,会发现已经有两个默认的记录了,不用管它。

    • 添加两条记录类型为 A 的记录
      记录值(记录指向的IP)是由Github提供:

      • 192.30.252.153
      • 192.30.252.154

        可至 GitHub Help查找最新的IP。

    • 添加一条记录类型为 CNAME 的记录
      • 主机记录:www
      • 记录类型:CNAME
      • 记录值:Github Pages 的 Repository name
      • 其他值:都用默认的就行

      如图:

  4. 修改域名DNS地址

    GoDaddy的可参考:DNSPod的帮助页

    其它的请参考:学会使用DNSPod,仅需三步 中的第二步。

Hexo的使用

关于Hexo的安装/部署/使用,官方有更详细的说明。详见文末 相关链接 部分。

将域名与Github Pages绑定

用 $HEXO_HOME 表示Hexo的源目录,即执行 hexo init 的目录。

  1. 在 $HEXO_HOME/source 目录下新建一个文件,文件名为 CNAME
  2. 编辑该文件,文件内容为你的域名。
    例如我的文件内容为:youngzy.com
    仅此而已,没有多余的内容。

就这样,域名和Github Pages通过CNAME绑定了。

发布

通过Hexo的命令,将博客发布到Github上。

hexo d -g

至此,博客搭建完成。
可以在地址栏中输入你的域名看看效果。Hexo默认会生成一篇文章(博客)。

感谢

Xuanwo

相关链接

Oracle中的NULL、’’(空字符串)以及’_’(空格)

在Oracle中使用 null''(空字符串),'_'(空格)时,有没有遇到问题?产生疑惑?

null’’(空字符串)是一个意思

注:
为了便于区分空字符串和空格,下面的示例均以 _ 代表空格

举个例子:

1
2
3
4
5
6
7
--建表
create table tbl_a (col_a varchar2(1), col_b int);  

--  造数据
insert into tbl_a values(‘_’, 1); --  插入空格
insert into tbl_a values(‘’, 2); --  插入空字符串
insert into tbl_a values(null, 3); --  插入NULL

以上SQL执行成功后,执行 select 来检查:

1
2
3
4
select count(*) from tbl_a; -- 结果是 3 
select count(*) from tbl_a where col_a = ‘_’;  -- 结果是 1 
select count(*) from tbl_a where col_a = ‘’;  -- 结果是 0 
select count(*) from tbl_a where col_a is null; -- 结果是 2

注意:
由于 ‘’ (空串)默认被转换成了 NULL,不能使用 = ‘’ 作为查询条件。也不能用 is ‘’。虽然不会有语法错误,但是不会有结果集返回。
只能用 is null
不等于就是 is not null

进一步验证:

1
select nvl(col_a, ‘a’) from tbl_a;

结果:

NVL(COL_A,’A’)
1
2 a
3 a

原来,在Oracle中,null’’(空字符串)是一个意思。


分析函数与NULL

在使用AVG,MAX,SUM,COUNT等函数时,为NULL的纪录会被忽略。

再插入几条数据:

1
2
3
4
5
insert into tbl_a values(null, null); --  插入NULL
-- 执行成功。
-- 再次证明,’’ 被当作了null处理.
-- 因为该字段是 int 类型,如果是字符串,执行会报错
insert into tbl_a values(‘a’, ‘’);

查看数据:

1
select * from tbl_a;

结果如下:

COL_A COL_B
_ 1
2
3
a

_ 代表空格
其余空白处表示 NULL

验证:

1
2
3
4
5
6
select AVG(col_b) from tbl_a; -- 结果为 2 ,NULL的纪录行忽略掉了
select MAX(col_b) from tbl_a; -- 结果为 3
select SUM(col_b) from tbl_a; -- 结果为 6
select COUNT(col_b) from tbl_a; -- 结果为 3 
select COUNT(col_a) from tbl_a; -- 结果为 2
select COUNT(*) from tbl_a; -- 结果为 5

排序时,NULL作为无穷大处理。

1
selectfrom tbl_a order by col_b desc ;

结果如下:

COL_A COL_B
a
3
2
_ 1

参考