多线程编程:聊天室升级版
?
?????? 还记得上一章的聊天室吗,不如说是单向发送器,我只能单方面的发送信息给客户端,不能接收消息,不能看到其他室友的发送的信息,我想要的是一个真正的聊天室,在发送信息的同时能看到我和其他室友发送的聊天记录,这里就不得不一边发送一边接收,意味着在用户与GUI交互的同时不能打断它,需要一个单独的执行空间来执行接收功能,就必须引进一个新的线程(Thread)。
?
一、创建线程
????? 创建一个线程其实很简单,Java就内置有多线程的功能,通过下面代码就可以创建一个线程并执行:
?
Thread t = new Thread(); t.start();
????? 新的线程对象被创建启动,它是一个独立的调用执行空间。但是有一个问题,我创建新的线程后总需要它做点什么吧,什么都不做的话我干嘛要它出生,所以它需要一个任务来体现它存在的意义。
?
二、线程的任务
??????可以这样理解,把线程看作是一个职员,作为一个职员肯定要有它的工作任务,而怎样去定义它的工作任务呢?Java语言中通过把Runnable放进线程来安排一个职员的工作任务,比如:
//因为Runnable是一个接口,所以写一个类实现它,该接口只有一个方法run(); Runnable myJob = new MyJobRunnable(); //将任务安排给这个职员(线程) Thread t = new Thread( myJob ); //启动线程后,将会自动执行任务的第一个方法run(), t.start();
?????? Runnable是一个接口,就好比一个任务模板,实现了该接口的类都可以作为线程的任务被执行,当然你得把工作具体任务放进run()方法里。
?
三、线程的状态
????? 上面我知道一个线程怎样被创建并且给它安排一个工作,是不是当线程对象一旦被创建或执行,该线程就立马开工工作,答案是我也不知道,这得看看Java虚拟机线程调度机制的心情,它可以让你立马开工或者稍作等待或者开工中途暂停休息。
下面这个例子可以很明显的看出:
?
public class ThreadTest implements Runnable { /** * 循环执行 */ public static void main(String[] args) { for( int i = 1; i<10; i++ ){ Thread t = new Thread(new ThreadTest()); t.start(); System.out.println("main线程被执行"); } } @Override public void run() { System.out.println("新线程被执行"); } }
?
?
运行结果: main线程被执行 main线程被执行 main线程被执行 main线程被执行 新线程被执行 新线程被执行 main线程被执行 main线程被执行 main线程被执行 新线程被执行 main线程被执行 新线程被执行 main线程被执行 main线程被执行 新线程被执行 新线程被执行 新线程被执行 新线程被执行 新线程被执行 新线程被执行
????? 注意上面的运行结果,有时候主线程会先执行完毕,有时候新线程会先执行完毕,也有可能主线程执行到一半暂停新线程又开始执行。
????? 那么我可以控制一个线程的执行状态吗?sleep()这个方法可以让一个线程暂时睡一会,比如sleep(2000),这会让线程睡眠2秒,但是值得注意,当2秒后是否该线程就会立刻进入执行状态,答案当然是不知道,之前就说了这得看Java虚拟机线程调度机制的心情,你可以让线程强制睡眠,但是苏醒后是否立刻执行或者等一会再执行我无法控制。
?
四、线程的缺陷-并发性问题
????? 这种缺陷来自一个可能发生的状况:当两个或两个以上线程同时调用同一对象数据时,特别是getter和setter存取数据时,就有可能发生并发性的问题。举个例来说明,先看代码:
?
public class LostUpdate implements Runnable { private int money = 10; @Override public void run() { for( int m=1; m<=5; m++ ){ money = money -1; System.out.println(Thread.currentThread().getName()+"查询余额: "+money); } } public static void main(String[] args) { LostUpdate update = new LostUpdate(); Thread t1 = new Thread(update); t1.setName("张三"); Thread t2 = new Thread(update); t2.setName("王四"); t1.start(); t2.start(); } }
运行结果: 张三查询余额: 9 张三查询余额: 7 王四查询余额: 8 王四查询余额: 6 王四查询余额: 5 王四查询余额: 4 王四查询余额: 3 张三查询余额: 2 张三查询余额: 1 张三查询余额: 0
?????? 代码很简单,张三和王四共同拥有一个银行账号,分5次每次取出1元后查看余额。从运行结果很明显就给他俩带来了疑惑,在张三第二次查询余额后是7,当他进行第三次操作后他本该以为余额应该是6,结果变成了2,他不知道王四在中途已经取走了4元,同理王四也会遇到这样问题。这就是并发性带来的问题。
?
五、解决并发性问题-synchronized同步锁
?????? 上一个例子中,张三怎样才能避免在他分5次每次取1元的操作时,王四对账户进行操作呢!很简单,他需要一把锁,在他进行操作时将账户上锁并带走钥匙,王四在没有钥匙当然无法对账户进行操作。用关键字synchronized来给一个方法或一个代码块上锁,在被它修饰的方法或代码中只容许同一时段只有当前线程结束后才会释放钥匙给下一个线程。看看加了同步锁的上面例子:
public class LostUpdate implements Runnable { private int money = 10; @Override public void run() { synchronized(this){ for( int m=1; m<=5; m++ ){ money = money -1; System.out.println(Thread.currentThread().getName()+"查询余额: "+money); } } } public static void main(String[] args) { LostUpdate update = new LostUpdate(); Thread t1 = new Thread(update); t1.setName("张三"); Thread t2 = new Thread(update); t2.setName("王四"); t1.start(); t2.start(); } }
?
运行结果: 张三查询余额: 9 张三查询余额: 8 张三查询余额: 7 张三查询余额: 6 张三查询余额: 5 王四查询余额: 4 王四查询余额: 3 王四查询余额: 2 王四查询余额: 1 王四查询余额: 0
????? 看似如果把所有方法都加上同步锁就可以都避免多线程的并发问题,当然不行,在考虑多线程安全性的同时我也得考虑同步化带来的额外成本,同步锁的程序会有查询钥匙等性能上的损耗,其次加上同步锁的方法会强制后面的线程排队等待使程序运行变慢,而最糟糕的是多个线程多个对象的调用可能带来死锁现象。所以,原则上只做最少量的同步化。
????? 这里有个概念——死锁:这个概念解释起来有点绕,?A线程在对象M的同步化方法中需要调用B线程正在调用的N对象同步化方法,B线程在N对象的同步化方法中需要调用A线程正在调用的M对象同步化方法,由于AB线程在同步化方法中都没有结束就不会释放同步锁,只有这样你等我,我等你一直循环下去。
?
六、聊天室升级版
来升级一下之前的聊天室,让它看起来是个真正的聊天室。
服务端:
public class ChatRoomServer { ServerSocket ss; ArrayList<Socket> outSockets; public void go(){ outSockets = new ArrayList<Socket>(); try { ss = new ServerSocket(5000); System.out.println("没跳过线程继续执行"); while( true ){ Socket socket = ss.accept(); outSockets.add(socket); Thread clientThread = new Thread(new AccpetMessage(socket)); clientThread.start(); } } catch (Exception e) { e.printStackTrace(); } } /** * 内部类-多线程处理接收客户端发送请求的操作实现 * @author Administrator * */ public class AccpetMessage implements Runnable{ Socket sClient; BufferedReader br; PrintWriter pw; String message = ""; public AccpetMessage( Socket sClient ) { this.sClient = sClient; try { br = new BufferedReader(new InputStreamReader(sClient.getInputStream())); } catch (Exception e) { e.printStackTrace(); } } @Override public void run() { while( true ){ try { message = Thread.currentThread().getName()+"说:"+br.readLine(); System.out.println(message); for( Socket sc : outSockets ){ pw = new PrintWriter(sc.getOutputStream()); pw.println(message); pw.flush(); } } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) { new ChatRoomServer().go(); } }
?客户端:
?
public class ChatRoomClient { public JTextField mywords; public JTextArea sharRoom; public PrintWriter pw; public BufferedReader br; Socket socket; public void go(){ JFrame myFrame = new JFrame("My Chat Room"); JPanel myPanel = new JPanel(); mywords = new JTextField(20); sharRoom = new JTextArea(8,25); JScrollPane scroller = new JScrollPane(sharRoom); JButton sendB = new JButton("发送"); sendB.addActionListener(new SendButtonListener()); myPanel.add(scroller); myPanel.add(mywords); myPanel.add(sendB); myFrame.getContentPane().add(BorderLayout.CENTER,myPanel); setConnection(); Thread readerThread = new Thread(new AcceptServerMessage()); readerThread.start(); myFrame.setSize(400, 500); myFrame.setVisible(true); } public void setConnection(){ try { socket = new Socket("127.0.0.1",5000); pw = new PrintWriter(socket.getOutputStream()); br = new BufferedReader(new InputStreamReader(socket.getInputStream())); } catch (Exception e) { e.printStackTrace(); } } public class SendButtonListener implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { pw.println(mywords.getText()); pw.flush(); mywords.setText(""); mywords.requestFocus(); } } public class AcceptServerMessage implements Runnable{ @Override public void run() { while( true ){ String message = ""; try { if ( (message = br.readLine()) != null ){ sharRoom.append(message + "\r\n"); } } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { } }
测试类:
public class SS { /**先启动服务端后执行测试类 * @param args */ public static void main(String[] args) { ChatRoomClient c1 = new ChatRoomClient(); c1.go(); ChatRoomClient c2 = new ChatRoomClient(); c2.go(); } }
?
原文:http://ranji13.iteye.com/blog/2294081