接深入淺出Java多線程系列(1),本文主要解決的問題是:
如何使其Swing程序只能運行一個實例?
拋開Swing, 我們的程序是通過java 命令行啟動一個進程來執行的,該問題也就是說要保證這個進程的唯一性,當然如果能夠訪問系統的接口,得到進程的信息來判斷是否已有進程正在運行,不就解決了嗎?但是如何訪問系統的接口呢?如何要保證在不同的平臺上都是OK的呢?我的思路是用文件鎖,當然我相信肯定有更好的方法,呵呵,希望讀者能夠指出。
文件鎖是JDK1.4 NIO提出的,可以在讀取一個文件時,獲得文件鎖,這個鎖應該是系統維護的,JVM應該是調用的系統文件鎖機制,例子如下:
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
/**
*
* @author vma
*/
public class temp1 {
public static void main(String args[]) throws FileNotFoundException, InterruptedException, IOException{
RandomAccessFile r = new RandomAccessFile("d://testData.java","rw");
FileChannel temp = r.getChannel();
FileLock fl = temp.lock();
System.out.println(fl.isValid());
Thread.sleep(100000);
temp.close();
}
當代碼獲得鎖后:我們試圖編輯這個文件是就會:
如果在啟動一個Java Main方法時:
public class temp2 {
public static void main(String args[]) throws FileNotFoundException, InterruptedException, IOException{
RandomAccessFile r = new RandomAccessFile("d://testData.java","rw");
FileChannel temp = r.getChannel();
FileLock fl = temp.tryLock();
System.out.println(fl== null);
temp.close();。
返回的結束是 ture , 也就是得不到文件的鎖。
這就是對于進程唯一性問題我的解決思路,通過鎖定文件使其再啟動時得不到鎖文件而無法啟動。
說到這里,跟今天Swing中的EDT好像還沒有關系,對于Swing程序,Main方法中一般像這樣:
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager
.getCrossPlatformLookAndFeelClassName());
} catch (Exception e) {
}
//Create the top-level container and add contents to it.
JFrame frame = new JFrame("SwingApplication");
SwingApplication app = new SwingApplication();
Component contents = app.createComponents();
frame.getContentPane().add(contents, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
啟動Jframe后,Main線程就退出了,上面獲得文件鎖,并持有鎖的邏輯往哪里寫呢? 有人會說事件分發線程EDT,真的嗎?
由于我沒有做過Swing的項目,僅僅做過個人用的財務管理小軟件,還沒有深入理解過EDT,不管怎么說先把那段邏輯加到EDT,
怎么加呢 用SwingUtilities
static void |
invokeAndWait(Runnable doRun)
Causes doRun.run() to be executed synchronously on
the AWT event dispatching thread. |
static void |
invokeLater(Runnable doRun)
Causes doRun.run() to be executed asynchronously on the AWT
event dispatching thread. |
加上去以后怎么界面沒有任何反應了呢?
代碼如下:
package desktopapplication1;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class SwingApplication {
private static String labelPrefix = "Number of button clicks: ";
private int numClicks = 0;
public Component createComponents() {
final JLabel label = new JLabel(labelPrefix + "0 ");
JButton button = new JButton("I'm a Swing button!");
button.setMnemonic(KeyEvent.VK_I);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
numClicks++;
label.setText(labelPrefix + numClicks);
}
});
label.setLabelFor(button);
/*
* An easy way to put space between a top-level container and its
* contents is to put the contents in a JPanel that has an "empty"
* border.
*/
JPanel pane = new JPanel();
pane.setBorder(BorderFactory.createEmptyBorder(30, //top
30, //left
10, //bottom
30) //right
);
pane.setLayout(new GridLayout(0, 1));
pane.add(button);
pane.add(label);
return pane;
}
public static void main(String[] args) throws InterruptedException {
try {
UIManager.setLookAndFeel(UIManager
.getCrossPlatformLookAndFeelClassName());
} catch (Exception e) {
}
//Create the top-level container and add contents to it.
JFrame frame = new JFrame("SwingApplication");
SwingApplication app = new SwingApplication();
Component contents = app.createComponents();
frame.getContentPane().add(contents, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
try {
SwingUtilities.invokeAndWait(new getFileLock());
} catch (InvocationTargetException ex) {
ex.printStackTrace();
}
}
}
class getFileLock implements Runnable{
public void run() {
try {
RandomAccessFile r = null;
try {
r = new RandomAccessFile("d://testData.java", "rw");
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}
FileChannel temp = r.getChannel();
FileLock fl = null;
try {
fl = temp.lock();
} catch (IOException ex) {
Logger.getLogger(getFileLock.class.getName()).log(Level.SEVERE, null, ex);
}
System.out.println(fl.isValid());
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
temp.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
打個斷點看看怎么了,斷點就在這里
Thread.sleep(Integer.MAX_VALUE); 看看那個線程暫停了 看圖片:
看到了吧,我們寫的那個
getFileLock 是由AWT-EventQueue-0 線程執行,看右下角調用關系, EventDispathThread 啟動 Run方法, 然后pumpEvents 取事件,然后從EventQueue取到InvocationEvent 執行Dispath
Dispath調用的就是我們在
getFileLock寫的
run() 方法, JDK代碼如下:
public void dispatch() {
if (catchExceptions) {
try {
runnable.run();
}
catch (Throwable t) {
if (t instanceof Exception) {
exception = (Exception) t;
}
throwable = t;
}
}
else {
runnable.run();
}
if (notifier != null) {
synchronized (notifier) {
notifier.notifyAll();
}
}
}
runnable.run();
而如何將我們寫的
getFileLock加入的那個EventQueue中的呢?當然是
SwingUtilities.invokeAndWait(new getFileLock());
看JDK代碼:
public static void invokeAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
if (EventQueue.isDispatchThread()) {
throw new Error("Cannot call invokeAndWait from the event dispatcher thread");
}
class AWTInvocationLock {}
Object lock = new AWTInvocationLock();
InvocationEvent event =
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable, lock,
true);
synchronized (lock) {
Toolkit.getEventQueue().postEvent(event);
lock.wait();
}
Toolkit.getEventQueue().postEvent(event);把我們寫的
getFileLock 塞進了EventQueue.
這下讀者對EDT有個認識了吧。
1. EDT 只有一個線程, 雖然getFileLock是實現Runnable接口,它調用的時候不是star方法啟動新線程,而是直接調用run方法。
2. invokeAndWait將你寫的getFileLock塞到EventQueue中。
3. Swing 事件機制采用Product Consumer模式 EDT不斷的取EventQueue中的事件執行(消費者)。其他線程可以將事件塞入EventQueue中,比如鼠標點擊Button是,將注冊在BUttion的事件塞入EventQueue中。
所以我們將getFileLock作為事件插入進去后 EDT分發是調用Thread.sleep(Integer.MAX_VALUE)就睡覺了,無暇管塞入EventQueue的其他事件了,比如關閉窗體。
所以絕對不能將持有鎖的邏輯塞到EventQueue,而應該放到外邊main線程或者其他線程里面。
提到
invokeAndWait,還必須說說invokelater 這兩個區別在哪里呢?
invokeAndWait與invokelater區別: 看JDK代碼:
public static void invokeLater(Runnable runnable) {
Toolkit.getEventQueue().postEvent(
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable));
}
public static void invokeAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
if (EventQueue.isDispatchThread()) {
throw new Error("Cannot call invokeAndWait from the event dispatcher thread");
}
class AWTInvocationLock {}
Object lock = new AWTInvocationLock();
InvocationEvent event =
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable, lock,
true);
synchronized (lock) {
Toolkit.getEventQueue().postEvent(event);
lock.wait();
}
Throwable eventThrowable = event.getThrowable();
if (eventThrowable != null) {
throw new InvocationTargetException(eventThrowable);
}
}
invokelater:當在main方法中調用SwingUtils.invokelater,后,把事件塞入EventQueue就返回了,main線程不會阻塞。
invokeAndWait: 當在Main方法中調用SwingUtils.invokeAndWait 后,看代碼片段:
synchronized (lock) {
Toolkit.getEventQueue().postEvent(event);
lock.wait();
}
main線程獲得lock 后就wait()了,直到事件分發線程調用lock對象的notify喚醒main線程,否則main 就干等著吧。
這下明白了吧!
總之,對于我們問題最簡單的方法就是是main線程里,或者在其他線程里處理。
最后的解決方案是:
package desktopapplication1;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;
public class SwingApplication {
private static String labelPrefix = "Number of button clicks: ";
private int numClicks = 0;
public Component createComponents() {
final JLabel label = new JLabel(labelPrefix + "0 ");
JButton button = new JButton("I'm a Swing button!");
button.setMnemonic(KeyEvent.VK_I);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
numClicks++;
label.setText(labelPrefix + numClicks);
}
});
label.setLabelFor(button);
/*
* An easy way to put space between a top-level container and its
* contents is to put the contents in a JPanel that has an "empty"
* border.
*/
JPanel pane = new JPanel();
pane.setBorder(BorderFactory.createEmptyBorder(30, //top
30, //left
10, //bottom
30) //right
);
pane.setLayout(new GridLayout(0, 1));
pane.add(button);
pane.add(label);
return pane;
}
public static void main(String[] args) throws InterruptedException {
try {
UIManager.setLookAndFeel(UIManager
.getCrossPlatformLookAndFeelClassName());
} catch (Exception e) {
}
Thread t = new Thread(new getFileLock());
t.setDaemon(true);
t.start();
//Create the top-level container and add contents to it.
JFrame frame = new JFrame("SwingApplication");
SwingApplication app = new SwingApplication();
Component contents = app.createComponents();
frame.getContentPane().add(contents, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
class getFileLock implements Runnable{
public void run() {
try {
RandomAccessFile r = null;
try {
r = new RandomAccessFile("d://testData.java", "rw");
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}
FileChannel temp = r.getChannel();
try {
FileLock fl = temp.tryLock();
if(fl == null) System.exit(1);
} catch (IOException ex) {
ex.printStackTrace();
}
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
temp.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
在Main方法里啟動一個Daemon線程,持有鎖,如果拿不到鎖,就退出
if(fl == null) System.exit(1);
當然這只是個解決方案,如何友好給給用戶提示以及鎖定那個文件就要根據具體情況而定了。