Java Swing开发中的线程安全

Python014

Java Swing开发中的线程安全,第1张

SwingAPI的设计目标是强大 灵活和易用 非凡地 我们希望能让程序员们方便地建立新的Swing组件 不论是从头开始还是通过扩展我们所提供的一些组件 出于这个目的 我们不要求Swing组件支持多线程访问 相反 我们向组件发送请求并在单一线程中执行请求 本文讨论线程和Swing组件 目的不仅是为了帮助你以线程安全的方式使用SwingAPI 而且解释了我们为什么会选择现在这样的线程方案 本文包括以下内容

单线程规则 Swing线程在同一时刻仅能被一个线程所访问 一般来说 这个线程是事件派发线程 规则的例外 有些操作保证是线程安全的 事件分发 假如你需要从事件处理或绘制代码以外的地方访问UI 那么你可以使用SwingUtilities类的invokeLater要求在事件派发线程中执行某些代码 这个方法会立即返回 不会等待代码执行完毕 invokeAndWait行为与invokeLater类似 除了这个方法会等待代码执行完毕 一般地 你可以用invokeLater来代替这个方法 下面是一些使用这几个API的例子 请同时参阅《TheJavaTutorial》中的 BINGOexample 尤其是以下几个类 CardWindow ControlPane Player和OverallStatusPane

使用invokeLater方法你可以从任何线程调用invokeLater方法以请求事件派发线程运行特定代码 你必须把要运行的代码放到一个Runnable对象的run方法中 并将此Runnable对象设为invokeLater的参数 invokeLater方法会立即返回 不等待事件派发线程执行指定代码 这是一个使用invokeLater方法的例子

 RunnabledoWorkRunnable=newRunnable }

SwingUtilities invokeLater使用invokeAndWait方法invokeAndWait方法和invokeLater方法很相似 除了invokeAndWait方法会等事件派发线程执行了指定代码才返回 在可能的情况下 你应该尽量用invokeLater来代替invokeAndWait 假如你真的要使用invokeAndWait 请确保调用invokeAndWait的线程不会在调用期间持有任何其他线程可能需要的锁

这是一个使用invokeAndWait的例子

 voidshowHelloThereDialogthrowsException }SwingUtilities invokeAndWait}

类似地 假设一个线程需要对GUI的状态进行存取 比如文本域的内容 它的代码可能类似这样

 voidprintTextField throwsException }SwingUtilities invokeAndWaitSystem out println}

假如你能避免使用线程 最好这样做 线程可能难于使用 并使得程序的debug更困难 一般来说 对于严格意义下的GUI工作 线程是不必要的 比如对组件属性的更新 不管怎么说 有时候线程是必要的 下列情况是使用线程的一些典型情况 执行一项费时的任务而不必将事件派发线程锁定 例子包括执行大量计算的情况 会导致大量类被装载的情况 和为网络或磁盘I/O而阻塞的情况 重复地执行一项操作 通常在两次操作间间隔一个预定的时间周期 要等待来自客户的消息 你可以使用两个类来帮助你实现线程 SwingWorker 创建一个后台线程来执行费时的操作 Timer 创建一个线程来执行或多次执行某些代码 在两次执行间间隔用户定义的延迟 使用SwingWorker类SwingWorker类在SwingWorker java中实现 这个类并不包含在Java的任何发行版中 所以你必须单独下载它 SwingWorker类做了所有实现一个后台线程所需的肮脏工作 虽然许多程序都不需要后台线程 后台线程在执行费时的操作时仍然是很有用的 它能提高程序的性能观感

SwingWorkersanexampleofusingSwingWorker 要使用SwingWorker类 你首先要实现它的一个子类 在子类中 你必须实现construct方法还包含你的长时间操作 当你实例化SwingWorker的子类时 SwingWorker创建一个线程但并不启动它 你要调用你的SwingWorker对象的start方法来启动线程 然后start方法会调用你的construct方法 当你需要construct方法返回的对象时 可以调用SwingWorker类的get方法 这是一个使用SwingWorker类的例子

  //在main方法中 finalSwingWorkerworker=newSwingWorker }worker start //在动作事件处理方法中 JOptionPane showMessageDialog)

当程序的main方法调用start方法 SwingWorker启动一个新的线程来实例化ExpensiveDialogComponent main方法还构造了由一个窗口和一个按钮组成的GUI 当用户点击按钮 程序将阻塞 假如必要 阻塞到ExpensiveDialogComponent创建完成 然后程序显示一个包含ExpensiveDialogComponent的模式对话框 你可以在MyApplication java找到整个程序 使用Timer类Timer类通过一个ActionListener来执行或多次执行一项操作 你创建定时器的时候可以指定操作执行的频率 并且你可以指定定时器的动作事件的监听者 启动定时器后 动作监听者的actionPerformed方法会被调用来执行操作 定时器动作监听者定义的actionPerformed方法将在事件派发线程中调用 这意味着你不必在其中使用invokeLater方法 这是一个使用Timer类来实现动画循环的例子

 publicclassAnimatorApplicationTimer extendsJFrameimplementsActionListener publicvoidstartAnimationelse } publicvoidstopAnimation publicvoidactionPerformed }

在一个线程中执行所有的用户界面代码有这样一些优点 组件开发者不必对线程编程有深入的理解 像ViewPoint和Trestle这类工具包中的所有组件都必须完全支持多线程访问 使得扩展非常困难 尤其对不精通线程编程的开发者来说 最近的一些工具包如SubArctic和IFC 都采用和Swing类似的设计 事件以可预知的次序派发 invokeLater排队的runnable对象从鼠标和键盘事件 定时器事件 绘制请求的同一个队列派发 在一些组件完全支持多线程访问的工具包中 组件的改变被变化无常的线程调度程序穿插到事件处理过程中 这使得全面测试变得困难甚至不可能 更低的代价 尝试小心锁住临界区的工具包要花费实足的时间和空间在锁的治理上 每当工具包中调用某个可能在客户代码中实现的方法时 工具包都要保存它的状态并释放所有锁 以便客户代码能在必要时获得锁 当控制权交回到工具包 工具包又必须重新抓住它的锁并恢复状态 所有应用程序都不得不负担这一代价 即使大多数应用程序并不需要对GUI的并发访问 这是的SubArcticJavaToolkit的对在工具包中支持多线程访问的问题的描述 我们的基本信条是 当设计和建造多线程应用程序 尤其是那些包括GUI组件的应用程序时 必须保证极端小心 线程的使用可能会很有欺骗性 在许多情况下 它们表现得能够极好的简化编成 使得设计 专注于单一任务的简单自治实体 成为可能 在一些情况下它们的确简化了设计和编码 然而 在几乎所有的情况下 它们都使得调试 测试和维护的困难大大增加甚至成为不可能 无论大多数程序员所受的练习 他们的经验和实践 还是我们用来帮助自己的工具 都不是能够用来对付非决定论的 例如 全面测试在bug依靠于时间时是几乎不可能的 尤其对于Java来说 一个程序要运行在许多不同类型的机器的操作系统平台上 并且每个程序都必须在抢先和非抢先式调度下都能正常工作 由于这些固有的困难 我们力劝你三思是否绝对有使用线程的必要 尽管如此 有些情况下使用线程是必要的 所以subArctic提供了一个线程安全的访问机制

lishixinzhi/Article/program/Java/gj/201311/27616

并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务。

而Java并发则由多线程实现的。

在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中。(当然这是比较笼统的说法,线程之间是可以交互的,他们也不一定是串行。)

多线程的存在就是压榨cpu,提高程序性能,还能减少一定的设计复杂度(用现实的时间思维设计程序)。

这么说来似乎线程就是传说中的银弹了,可事实告诉我们真正的银弹并不存在。

多线程会引出很多难以避免的问题, 如死锁,脏数据,线程管理的额外开销,等等。更大大增加了程序设计的复杂度。

但他的优点依旧不可替代。

死锁和脏数据就是典型的线程安全问题。

简单来说,线程安全就是: 在多线程环境中,能永远保证程序的正确性。

只有存在共享数据时才需要考虑线程安全问题。

java内存区域:

其中, 方法区和堆就是主要的线程共享区域。那么就是说共享对象只可能是类的属性域或静态域。

了解了线程安全问题的一些基本概念后, 我们就来说说如何解决线程安全问题。我们来从一个简单的servlet示例来分析:

public class ReqCounterServlet extends HttpServlet{    private int count = 0

public void doGet(HttpServletRequest request,

HttpServletResponse response) throws IOException, ServletException {

count++

System.out.print("当前已达到的请求数为" + count)

}

public void doPost(HttpServletRequest request,

HttpServletResponse response) throws IOException, ServletException {        // ignore    }

}

1. 了解业务场景的线程模型

这里的线程模型指的是: 在该业务场景下, 可能出现的线程调用实况。

众所周知,Servlet是被设计为单实例,在请求进入tomcat后,由Connector建立连接,再讲请求分发给内部线程池中的Processor,

此时Servlet就处于一个多线程环境。即如果存在几个请求同时访问某个servlet,就可能会有几个线程同时访问该servlet对象。如图:

线程模型,如果简单的话,就在脑海模拟一下就好了,复杂的话就可以用纸笔或其他工具画出来。

2. 找出共享对象

这里的共享对象就很明显就是ReqCounterServlet。

3. 分析共享对象的不变性条件

不变性条件,这个名词是在契约式编程的概念中的。不变性条件保证类的状态在任何功能被执行后都保持在一个可接受的状态。

这里可以引申出, 不可变对象是线程安全的。(因为不可变对象就没有不变性条件)

不变性条件则主要由对可变状态的修改与访问构成。

这里的servlet很简单, 不变性条件大致可以归纳为: 每次请求进入时count计数必须加一,且计数必须正确。

在复杂的业务中, 类的不变性条件往往很难考虑周全。设计的世界是险恶的,只能小心谨慎,用测量去证明,最大程度地减少错误出现的几率。

4. 用特定的策略解决线程安全问题。

如何解决的确是该流程的重点。目前分三种方式解决:

第一种,修改线程模型。即不在线程之间共享该状态变量。一般这个改动比较大,需要量力而行。

第二种,将对象变为不可变对象。有时候实现不了。

第三种,就比较通用了,在访问状态变量时使用同步。 synchronized和Lock都可以实现同步。简单点说,就是在你修改或访问可变状态时加锁,独占对象,让其他线程进不来。

这也算是一种线程隔离的办法。(这种方式也有不少缺点,比如说死锁,性能问题等等)

其实有一种更好的办法,就是设计线程安全类。《代码大全》就有提过,问题解决得越早,花费的代价就越小。

是的,在设计时,就考虑线程安全问题会容易的多。

首先考虑该类是否会存在于多线程环境。如果不是,则不考虑线程安全。

然后考虑该类是否能设计为不可变对象,或者事实不可变对象。如果是,则不考虑线程安全

最后,根据流程来设计线程安全类。

设计线程安全类流程:

1、找出构成对象状态的所有变量。

2、找出约束状态变量的不变性条件。

3、建立对象状态的并发访问管理策略。

有两种常用的并发访问管理策略:

1、java监视器模式。  一直使用某一对象的锁来保护某状态。

2、线程安全委托。  将类的线程安全性委托给某个或多个线程安全的状态变量。(注意多个时,这些变量必须是彼此独立,且不存在相关联的不变性条件。)