如果一个程序没有多线程,那有很多问题解决起来比较麻烦,但是虽然多线程带来了很多好处,也带来了很多需要注意的地方。
就我个人而言,第一次真正的在程序中使用多线程是在上一个游戏中,那是个俄罗斯方块的程序,加载的时候就通过网络和本地读取记录。如果网络正常还好,网络不正常的话,由于连接mysql数据库需要等待超时,在超时之前,是堵塞的,也就是说需要等待超时之后界面才会出现。所以当时就想起来了用多线程。
我使用的是最简单的多线程,通过线程池来运行。此线程池由GameControl 统一管理
private final static int MAX_THREAD = 40;
/** 线程池 **/
private final static ExecutorService POOL = Executors
.newFixedThreadPool(MAX_THREAD);
但是有时其他类中也需要新的线程。第一种是准备将pool变成public,反正是静态常量,
第二种方法是将新建线程的行为变成GameControl的一个静态函数;
<span style="white-space:pre"> </span>/**
* 对外提供的使用新线程处理事务的方法
*/
public static void execute(Runnable run) {
pool.execute(run);
}
当时为了学习下多线程下的安全问题,不试不知道,一试吓一跳。在单线程下安安全全运行的程序,在多线程下简直是漏洞百出;最后我在贴代码,过长了。
简单的介绍下,当时测试是一个数据类,是用于从本地保存记录,和读取记录的类。有一个List<Player> players的成员,使用的是ArrayList容器。save(Player player),和load()两个接口方法。给save,和load加上同步,但是发现在多线程下还是会出现异常
以下是当时的测试方法
public static void main(String[] args) throws Exception {
// 线程池子最大线程数为40
DataDisk disk = new DataDisk();
DataDisk disk2 = new DataDisk();
// 无序测试调用两个方法
for (int i = 0; i < 50; i++) {
Player player = new Player("测试:" + i, i);
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk.saveData(player);
// System.out.println(Thread.currentThread().getName());
}
});
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk.loadData().toString();
// System.out.println(Thread.currentThread().getName());
}
});
}
Thread.sleep(10000);
// 自动关闭
System.exit(1);
}
}
查看第一个异常代码,百度了下,是由于容器迭代错误造成的。后来思考发觉是.toString()惹得祸。因为当时为了查看是否读取正常使用了System.out.println(players)方法,此方法需要迭代ArrayList容器,而在当时多线程下,存储和载入两个方法都会调用List的方法迭代。而外部的显示与内部并不是同步的,所以会出现异常。当时想的解决办法是要么自己写一个底层容器,加上同步。第二个方法是使用线程安全的vector;
更换底层容器后,在单实例下确实是线程安全了,但是当多实例多进程时出现时,又出现了异常。
我知道多实例对象不能使用对象锁。当时恰好players是静态成员变量,于是使用这个锁来同步代码块。后来发现其中排序容器要么出现空指针,要么又是迭代错误,后来才发现还是players的错误,因为load数据时会改写players,所以这锁也就无从谈起了,老老实实使用类文件当同步锁,正常了。
还有好像是虚拟机问题,导致我还有一个文件io异常怎么都无法解决。
当我猛地点新建进程时,如果新建虚拟机出现堵塞(出现一个对话框)时,就会出现各种各样的奇葩问题,空指针,数组越界,io未知结尾。
我预计是由于系统资源未准备好,就开始运行,导致一系列问题。
不过我是进行的高强度测试,正常来说读取本地文件不会很频繁的。
以下完整代码,需要自己实现Player类,很简单的,一个名字一个分数,然后不需要实现Data接口,基本就可以单机运行了。线程池自己建立
package dao;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
import control.GameControlImpl;
import dto.Player;
/**
* 这时一个英语读取和储存本地记录的数据类
* <p>
* 由于使用多线程,会出现同时存储和读取的状态
* <p>
* 所以使用同步锁
* <p>
*
* @author wzg1015
*
*/
public class DataDisk implements Data {
private static String SAVE_PATH = "data/save.dat";
private static int MAX_RECODES = 20;
private static List<Player> players;
private static final File FILE = new File(SAVE_PATH);
/**
* 测试用构造函数
*
* @throws IOException
*/
private DataDisk() {
players = new Vector<Player>();
if (!FILE.exists()) {
// 文件不存在则新建文件
try {
FILE.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
}
/** 读取配置用构造 **/
public DataDisk(HashMap<String, String> prameters) {
SAVE_PATH = prameters.get("savepath");
MAX_RECODES = Integer.parseInt(prameters.get("maxCount"));
players = new Vector<Player>();
if (!FILE.exists()) {
// 文件不存在则新建文件
try {
FILE.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
}
/**
* 读取本地记录
*
* @throws IOException
* @throws ClassNotFoundException
*/
@SuppressWarnings("unchecked")
@Override
public List<Player> loadData() {
synchronized (DataDisk.class) {
@SuppressWarnings("resource")
ObjectInputStream is = null;
try {
is = new ObjectInputStream(new FileInputStream(SAVE_PATH));
Object obj = is.readObject();
/** 为防止一次错误存储导致后期一直不能正确读取,进行类型检查 **/
if (obj.getClass().equals(players.getClass())) {
players = (List<Player>) obj;
} else {
// 重新生成本地记录
System.err.println("本地记录损坏,重新新建文件");
FILE.createNewFile();
}
} catch (EOFException e) {
/** 第一次无记录时会出现的正常状况 **/
System.err.println("DataDisk.loadData()" + "未读到数据,跳过");
return players == null ? new Vector<Player>() : players;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null)
try {
is.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
return players == null ? new Vector<Player>() : players;
}
}
/**
* 保存一个玩家的信息
*
* @param player
* 玩家
* @throws IOException
*/
@Override
public void saveData(Player player) {
synchronized (DataDisk.class) {
if (player == null) {
System.err.println("传销sssssssssssssssssss");
return;
}
/** 将玩家增加到当前列表,并进行重排,删除工作 **/
players.add(player);
Collections.sort(players);
while (players.size() > MAX_RECODES) {
players.remove(MAX_RECODES);
}
@SuppressWarnings("resource")
ObjectOutputStream os = null;
try {
/** 保存本地记录 **/
os = new ObjectOutputStream(new FileOutputStream(SAVE_PATH));
os.writeObject(players);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null)
try {
os.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
}
}
// 标记先后顺序
static int test = 0;
/**
*
* 虽然此类使用不多,为学习多线程,进行多线程测试
*
* 普通测试: <br>
* 出现问题1:文件不存在,初始化新建文件<br>
* 2:inputStream读取异常,由于是新建文件,没有记录,直接返回当前记录<br>
* <p>
* 测试多线程<br>
* 1.单数存储数据多线程无影响<br>
* 2.读取数据时出现异常,基本原因为成员变量的ArrayList为线程不安全的<br>
* 在调用其toString等需要迭代工作时,会出现异常,<br>
* 而这是上层锁不好锁的。<br>
* 暂时3种方案:1.写一个自己的底层数据类,使用自己基本数据,在访问数据的最底层方法上添加锁<br>
* 2.使用线程安全的Vector 3.提供给外面的数据的get方法为深度clone
*
* 3.由于此对象使用不频繁,更换底层容器为Vector。<br>
* 4.ConcurrentModificationException异常<br>
* 5.开启多个进程时,如果点击过于频繁时,出现本地记录损坏异常<br>
*
*
* <p>
* 在非多实例进程非频繁调用的条件下,是线程安全的
* <p>
*
* <p>
* 6.多对象,当有多个实例对象存在时,由于所用的锁是对象锁,导致异常<br>
* <p>
* 7.解决方案:1.使用单实例设计方案(本游戏适用,虽然本程序中此对象的创建是通过反射创建的,<br>
* 但可以考虑在抽象类中定义一个获取实例对象的方法,通过该方法来获取单实例对象)。<br>
* 8.使用锁。由于本程序读和写都需要修改对象,所以使用普通互斥锁(实验成功)<br>
*
*
* 9.当相当频繁开启新进程时出 "pool-1-thread-1" java.lang.NullPointerException,不可恢复性异常<br>
* 原因:通过打印得知内部含有空指针Null,而且被储存到了文件内,导致再次读取,此null还在里面。<br>
* 相当诡异的错误,方案1.在入口处检查player==null(解决)<br>
* 预计原因为1.Player刚刚创建,还未初始化完成,就被添加进了容器。
* 2.Player创建完成后,添加线程还在等待中,就开始了下一轮迭代,此时恰好垃圾回收机检测到<br>
* 此对象无引用,于是清除掉了,然后形成一个空指针。(由于不是很了解底层,所以是猜的)<br>
* 结论:同步锁原因参见10
*
* 10.java.lang.ArrayIndexOutOfBoundsException,以上的后续问题<br>
* 像容器未完全创建完毕就被输出到了输出流,导致存储了一个错误的对象<br>
* [name:测试:0----1, null, null, null, null, null, null, null, null, null,
* null, null, null, name:测试:1---1]<br>
* 结论:同步锁使用错误,开始使用players静态变量,由于此变量也会发生变化,(新的new Vector)<br>
* 导致 不同步,进而导致Collection.sort,出现错误。private static Lock 也无用<br>
*
* java.io.StreamCorruptedException: unexpected end of block data
* java.io.StreamCorruptedException: invalid type code: 00
* 同时超过5个进程开始出现多次文件损坏,应该跟系统的缓冲写入有关。
*
* @throws Exception
**/
public static void main(String[] args) throws Exception {
// 线程池子最大线程数为40
DataDisk disk = new DataDisk();
DataDisk disk2 = new DataDisk();
// 无序测试调用两个方法
for (int i = 0; i < 50; i++) {
Player player = new Player("测试:" + i, i);
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk.saveData(player);
// System.out.println(Thread.currentThread().getName());
}
});
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk.loadData().toString();
// System.out.println(Thread.currentThread().getName());
}
});
}
for (int i = 0; i < 50; i++) {
Player player = new Player("测试:" + i, i);
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk2.saveData(player);
// System.out.println(Thread.currentThread().getName());
}
});
GameControlImpl.execute(new Runnable() {
@Override
public void run() {
disk2.loadData().toString();
// System.out.println(Thread.currentThread().getName());
}
});
}
Thread.sleep(10000);
// 自动关闭
System.exit(1);
}
}