接深入浅出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);
当然这只是个解决方案,如何友好给给用户提示以及锁定那个文件就要根据具体情况而定了。