黑马程序员_一次多线程实践

    如果一个程序没有多线程,那有很多问题解决起来比较麻烦,但是虽然多线程带来了很多好处,也带来了很多需要注意的地方。

    就我个人而言,第一次真正的在程序中使用多线程是在上一个游戏中,那是个俄罗斯方块的程序,加载的时候就通过网络和本地读取记录。如果网络正常还好,网络不正常的话,由于连接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);
	}

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值