201871010132--张潇潇--《面向对象程序设计(java)》第十六周学习总结
项目 |
内容 |
这个作业属于哪个课程 |
https://www.cnblogs.com/nwnu-daizh |
这个作业的要求在哪里 |
https://www.cnblogs.com/nwnu-daizh/p/12031970.html |
作业学习目标 |
(1) 掌握Java应用程序的打包操作; (2) 掌握线程概念; (3) 掌握线程创建的两种技术。 (4)学习应用程序的GUI |
Thread 类的静态 sleep 方法将暂停给定的毫秒数。调用 Thread.sleep 不会创建一个新线程, sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。 sleep 方法可以抛出一个 InterruptedException 异常。
java.lang.Thread 1.0
static void sleep(long millis)
休眠给定的毫秒数。
参数:millis 休眠的毫秒数
不要调用 Thread 类或 Runnable 对象的 run 方法。直接调用 run 方法,只会执行同一个线程中的任务,并不会启动新线程。应该调用 Thread.start 方法。这个方法将创建一个执行 run 方法的新线程。
java.lang.Thread 1.0
Thread(Runnable target)
构造一个新线程,用于调用给定target的run()方法。
void start()
启动这个线程,将引发调用run()方法。这个方法将立即返回,并且新线程将并行运行。
void run()
调用关联Runnable的run方法。
java.lang.Runnable 1.0
void run()
必须覆盖这个方法,并在这个方法中提供所要执行的任务指令。
14.2 中断线程
当线程的 run 方法执行方法体重最后一条语句后,并经由执行 return 语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。在Java的早期版本中,还有一个 stop 方法,其他线程可以调用它终止线程。但是,这个方法现在已经被弃用了。
有一种可以强制线程终止的方法。然而, interrupt 方法可以用来请求终止线程。
当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每一个线程都具有的 boolean 标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
调用 Thread.currentThread().isInterrputed() 方法获得当前线程的中断状态是否被置位。但是,如果线程被阻塞,就无法检测中断状态。这是产生 InterruptedException 异常的地方。当在一个被阻塞的线程(调用 sleep 或 wait )上调用 interrupt 方法时,阻塞调用将会被 Interrupt Exception 异常中断(存在不能被中断的阻塞 I/O 调用,应该考虑选择可中断的调用)。
没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后,继续执行,而不理会中断。但是,更普通的情况是,线程将简单地将中断作为一个终止的请求。
如果在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法), isInterrpted 检测既没有必要也没有用处。如果在种蒜状态被置位时调用 sleep 方法,它不会休眠。相反,它将清除这一状态(!)并抛出 InterrputedException 。因此,如果你的循环调用 sleep ,不会检测中断状态,相反,需要捕获 InterrputedException 异常。
有两个非常类似的方法, interrupted 和 isInterrupted 。 Interrupted 方法是一个静态方法,它检测当前的线程是否被中断。而且,调用 interrupted 方法会清除该线程的中断状态。另一方面, isInterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。
java.lang.Thread 1.0
void interrupt()
向线程发送中断请求。线程的中断状态将设置为 true 。如果目前该线程被一个 sleep 调用阻塞,那么, InterruptedException 异常被抛出。
static boolean interrupted()
测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用-它将当前线程的中断状态重置为false。
boolean isInterrupted()
测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
static Thread currentThread()
返回代表当前执行线程的 Thread 对象。
14.3 线程状态
线程可以有如下6种状态:
New (新创建)
Runnable (可运行)
Blocked (被阻塞)
Waiting (等待)
Timed waiting (计时等待)
Terminated (被终止)
14.3.1 新创建线程
当用new操作符创建一个新线程,如 new Thread(r) ,该线程还没有开始运行。这意味着它的状态是 new 。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基本工作要做。
14.3.2 可运行线程
一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。( Java 的规范说明没有将它作为一个单独状态。一个正在运行中的线程仍然处于可运行状态。)
一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级。
在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。
14.3.3 被阻塞线程和等待线程
当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。
当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrect 库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时,就会出现这种情况。实际上,被阻塞状态与等待状态是由很大不同的。
有几个方法有一个超时参数。调用它们导致线程进入计时等待( timed waiting )状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait 、 Thread.join 、 Lock.tryLock 以及 Condition.awit 的计时版。
当一个线程被阻塞或等待时(或终止时),另一个线程被调度为运行状态。当一个线程被重新激活(例如,因为超时期满或成功地获得一个锁),调度器检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行。
线程状态图
线程因如下两个原因之一而被终止:
因为 run 方法正常退出而自然死亡。
因为一个没有捕获的异常终止了 run 方法而意外死亡。
特别是,可以调用线程的 stop 方法杀死一个线程。该方法抛出 ThreadDeath 错误对象,由此杀死线程。但是, stop 方法已过时,不要在自己的代码中调用这个方法。
java.lang.Thread 1.0
void join()
等待终止指定的线程。
void join(long millis)
等待指定的线程死亡或者经过指定的毫秒数。
Thread.State getState() 5.0
得到这一线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING或TERMINATED之一。
void stop()
停止该线程。这一方法已过时。
void suspend()
暂停这一线程的执行。这一方法已过时。
void resume()
恢复线程。这一方法仅仅在调用suspend()之后调用。这一方法已过时。
14.4 线程属性
线程的各种属性,其中包括:线程优先级、守护线程、线程组以及处理未捕获异常的处理器。
14.4.1 线程优先级
在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 )与 MAX_PRIORITY (定义为 10 )之间的任何值。 NORM_PRIORITY 被定义为 5 。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优先级被映射到宿主主机平台的优先级上,优先级个数也许更多,也许更少。
Windows 有 7 个优先级别。一些 Java 优先级将映射到相同的操作系统优先级。在 Sun 为 Linux 提供的 Java 虚拟机,线程的优先级被忽略-所有线程具有相同的优先级。
java.lang.Thread 1.0
void setPriority(int newPriority)
设置线程的优先级。优先级必须在Thread.MIN_PRIORITY与Thread.MAX_PRIORITY之间。一般使用Thread.NORM_PRIORITY优先级。
static int MIN_PRIORITY
线程的最小优先级。最小优先级的值为1。
static int NORM_PRIORITY
线程的默认优先级。默认优先级为5。
static int MAX_PRIORITY
线程的最高优先级。最高优先级的值为10。
static void yield()
导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。
14.4.2 守护线程
可以通过 t.setDaemon(true) 将线程转换为守护线程( daemon thread )。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
java.lang.Threed 1.0
void setDaemon(boolean isDaemon)
标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。
14.4.3 未捕获异常处理器
线程的 run 方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何 catch 子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个被用于未捕获异常的处理器。
该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有一个方法。
void uncaughtException(Thread t,Throwable e)
可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。
线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。
ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:
1)如果该线程组有父线程组,那么父线程组的 uncaughtException 方法被调用。
2)否则,如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器,则调用该处理器。
3)否则,如果 Throwable 是 ThreadDeath 的一个实例,什么都不做。
4)否则,线程的名字以及 Throwable 的栈跟踪被输出到 System.err 上。
java.lang.Thread 1.0
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() 5.0
设置或获取未捕获异常的默认处理器。
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() 5.0
设置或获取未捕获异常的处理器。如果没有安装处理器,则将线程组对象作为处理器。
java.lang.Thread.UncaughtExceptionHandler 5.0
void uncaughtException(Thread t,Throwable e)
当一个线程因未捕获异常而终止,按规定要将客户报告记录到日志中。
参数:t 由于未捕获异常而终止的线程
e 未捕获的异常对象
java.lang.ThreadGroup 1.0
void uncaughtException(Thread t,Throwable e)
如果有父线程组,调用父线程组的这一方法;或者,如果 Thread 类有默认处理器,调用该处理器,否则,输出栈踪迹到标准错误流上(但是,如果 e 是一个 ThreadDeath 对象,栈踪迹是被禁用的。 ThreadDeath 对象 stop 方法产生,而该方法已经过时)。
14.5 同步
如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,这样一个情况通常称为竞争条件( race condition )。
14.5.1 竞争条件的一个例子
银行转账例子
14.5.2 竞争条件详解
一条名利是由几条指令组成的,执行它们的线程可以在任何一条指令点上被中断。
14.5.3 锁对象
有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。 synchronized 关键字自动提供了一个锁以及相关的“条件”,对于大多数需要显示锁的情况,这是很遍历的。 java.util.concurrent 框架为这些基础机制提供独立的类。
用 ReentrantLock 保护代码块的基本结构如下:
myLock.lock(); //a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock();//make sure the lock is unlocked even if an exception is three
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock 语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。
把解锁操作放在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
如果使用锁,就不能使用带资源的 try 语句。首先,解锁方法名不是 close 。不过,即使将它重命名,带资源的 try 语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,可能想使用多个线程共享的那个变量(而不是新变量)。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( hold count )来跟踪对 lock 方法的嵌套调用。线程在每一次调用lock都要调用 unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
通常,可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同对象。
要留心临界区中的代码,不要因为异常的抛出而跳出了临界区。如果在临界区代码结束之前抛出了异常, finally 子句将释放锁,但会使对象可能处于一种受损状态。
java.util.concurrent.locks.Lock 5.0
void lock()
获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。
void unlock()
释放这个锁。
java.util.concurrent.locks.ReentrantLock 5.0
ReentrantLock()
构建一个可以被用来保护临界区的可重入锁。
ReentrantLock(boolean fair)
构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是么这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。
听起来公平锁更合理一些,但是使用公平锁比使用常规锁要慢很多。只有当你确实了解自己要做什么并且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。即使使用公平锁,也无法确保线程调度器是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,那么就没有机会公平地处理这个锁了。
14.5.4 条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。由于历史的原因,条件对象经常被称为条件变量( conditional variable )。
一个锁对象可以有一个或多个相关的条件对象。可以用 newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它锁表达的条件的名字。如果条件不满足,调用 Condition.await() 。当前线程现在被阻塞了,并放弃了锁。
等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 singalAll 方法时为止。 singalAll() 调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继承执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足 -signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
通常,对 await 的调用应该在如下形式的循环体中:
while(!(ok to proceed))
condition.await();
至关重要的是最终需要某个其他线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock )现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
应该何时调用 signalAll 呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用 signalAll 。
注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
另一个方法 signal ,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用 signal ,那么系统就死锁了。
当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用 await 、 signalAll 或 signal 方法。
java.util.concurrent.locks.Lock 5.0
Condition newCondition()
返回一个与该锁相关的条件对象。
java.util.concurrent.locks.Condition 5.0
void await()
将该线程放到条件的等待集中。
void signalAll()
解除该条件的等待集中的所有线程的阻塞状态。
void signal()
从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
14.5.5 synchronized关键字
锁和条件的关键之处:
锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
锁可以管理试图进入被保护代码段的线程。
锁可以拥有一个或多个相关的条件对象。
每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
从 1.0 版开始, Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
内部对象锁只有一个相关条件。 wait 方法添加一个线程到等待集中, notifyAll/notify 方法解除等待线程的阻塞状态。换句话说,调用 wait 或 notifyAll 等价于
intrinsicCondition.await();
intrinsicCondition.signalAll();
wait 、 notifyAll 以及 notify 方法是 Object 类的 final 方法。 Condition 方法必须被命名为 await 、 signalAll 和 signal 以便它们不会与那些方法发生冲突。
将静态方法声明为 synchronized 也是合法的。如果调用这个方法,该方法获得相关的类对象的内部类。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
内部锁和条件存在一些局限。包括:
不能中断一个正在试图获得锁的线程。
试图获得锁时不能设定超时。
每个锁仅有单一的条件,可能是不够的。
在代码中应该使用哪一种? Lock 和 Conditon 对象还是同步方法?下面是一些建议:
最好既不使用 Lock/Condition 也不实用 synchronized 关键字。在许多情况下你可以使用 java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。
如果 synchronized 关键字适合你的程序,那么请尽量使用它。这样可以减少编写的代码数量,减少出错的几率。
如果特别需要 Lock/Condition 结构提供的特有特性时,才使用 Lock/Condition 。
java.lang.Object 1.0
void notifyAll()
解除那些在该对象上调用 wait 方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
void notify()
随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。改方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
void wait()
导致线程进入等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
void wait(long millis)
void wait(long millis,int nanos)
导致线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法抛出一个 IllegalMonitorStateException 异常。
参数:millis 毫秒数
nanos 纳秒数,<1 000 000
14.5.6 同步阻塞
每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized(obj) //this is the synchronized block
{
critical section
}
于是它获得 obj 的锁。
使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定( client-slide locking )。客户端锁定是非常脆弱的,通常不推荐使用。
14.5.7 监视器概念
锁和条件是线程同步的强大工具,但是,严格来讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器( monitor ),这一概念最早是由 Per Brinch Hansen 和 Tony Hoare 在 20 世纪 70 年代提出的。用 Java 的术语来讲,监视器具有如下特性:
监视器是只包含私有域的类。
每个监视器的对象有一个相关的锁。
使用该锁对所有的方法进行加锁。
该锁可以有任意多个相关条件。
Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监视器方法。通过调用 wait/notifyAll/notify 访问条件变量。
在下述的 3 个方面 Java 对象不同于监视器,从而使得线程的安全性下降。
域不要求必须是 private 。
方法不要求必须是 synchronized 。
内部锁对客户是可用的。
14.5.8 Volatile 域
有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大。
多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变。
如果你使用锁来保护可以被多个线程访问的代码,那么可以不考虑这种问题。编译器被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令。
volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
Volatile 变量不能提供原子性。例如,方法
private volatile boolean done;
public void flipDone(){done = !done;} //not atomic
不能确保翻转域中的值。
14.5.9 final 变量
除非使用域或 volatile 修饰符,否则无法从多个线程安全地读取一个域。还有一种情况可以安全地访问一个共享域,即这个域声明为 final 时。考虑以下声明:
final Map<String,Double> accounts = new HashMap();
其他线程会在构造函数完成构造之后才看到这个 accounts 变量。
如果不使用 final ,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是看到 null ,而不是新构造的 HashMap 。
对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。
14.5.10 原子性
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatic 。
java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如, AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet ,它们分别以原子方式将一个整数自增或自减。可以安全地使用 AtomicInteger 作为共享计数器而无须同步。
另外这个包中还包含 AtomicBoolean 、 AtomicLong 和 AtomicReference 以及 Boolean 值、整数、 long 值和引用的原子数组。应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。
14.5.11 死锁
有可能会因为每一个线程要等待条件而导致所有线程都被阻塞。这样的状态称为死锁( deadlock )。
Java 编程语言中没有任何东西可以避免或打破这种死锁现象,必须仔细设计程序,以确保不会出现死锁。
14.5.12 线程局部变量
有时可能要避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。
要为每个线程构造一个实例,可以使用以下代码:
public static final ThreadLocal< SimpleDateFormat > dateFormat = new ThreadLocal< SimpleDateFomrat >()
{
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyy-MM-dd");
}
}
要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new Date());
在一个给定线程中首次调用 get 时,会调用 initilaValue 方法。在此之后, get 方法会返回属于当前线程的那个实例。
在多个线程中生成随机数也存在类似的问题。 java.util.Random 类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。
可以使用 ThreadLocal 辅助类为各个线程提供一个单独的生成器,不过 Java SE 7 还另外提供一个便利类。只需要做一下调用:
int random = ThreadLocalRandom.current().nextInt(upperBound);
ThreadLocalRandom.current() 调用会返回特定于当前线程的 Random 类实例。
java.lang.ThreadLocal< T > 1.2
T get()
得到这个线程的当前值。如果是首次调用get,会调用 initialize 来得到这个值。
protected initialize()
应覆盖这个方法来提供一个初始值。默认情况下,这个方法返回 null 。
void set(T t)
为这个线程设置一个新值。
void remove()
删除对应这个线程的值。
java.util.concurrent.ThreadLocalRandom 7
static ThreadLocalRandom current()
返回特定于当前线程的 Random 类实例。
14.5.13 锁测试与超时
线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。
TimeUnit 是一个枚举类型,可以取的值包括 SECONDS 、 MILLISECONDS 、 MICROSECONDS 和 NANOSECONDS 。
lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么, lock 方法就无法终止。
然而,如果调用带有用超时参数的 tryLock ,那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。
也可以调用 lockInterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。
在等待一个条件时,也可以提供一个超时:
myCondition.await(100,TimeUnit.MILLISECONDS)
如果一个线程被另一个线程通过调用 signalAll 或 signal 激活,或者超时时限已达到,或者线程被中断,那么 await 方法将返回。
如果等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你希望出现这种情况时线程继续等待(可能不太合理),可以使用 awaitUniterruptibly 方法代替 await 。
java.util.concurrent.locks.Lock 5.0
boolean tryLock()
尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁,即使该锁有公平加锁策略,即便其他线程已经等待很久也是如此。
boolean tryLock(long time,TimeUnit unit)
尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true 。
void lockInterruptibly()
获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个 InterruptedException 异常。
java.util.concurrent.locks.Condition 5.0
boolean await(long time,TimeUnit unit)
进入该条件的等待集,直到线程从等待集中移出或等待了指定的时间之后才解除阻塞。。如果因为等待时间到了而返回就返回 false ,否则返回 true 。
void awaitUninterruptinly()
进入该条件的等待集,直到线程从等待集移出才解除阻塞。如果线程被中断,该方法不会抛出 InterruptedException 异常。
14.5.14 读/写锁
java.util.concurrent.locks包定义了两个锁类, ReentrantLock 类和 ReentrantReadWriteLock 类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是十分有用的。在这种情况下,允许对读者共享访问是合适的。当然,写者线程依然必须是互斥访问的。
使用读/写锁的必要步骤:
(1)构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
(2)抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
(3)对所有的获取方法加读锁:
public double getTotalBalance()
{
readLock.lock();
try{...}
finally{readLock.unlock();}
}
(4)对所有的修改方法加写锁:
public void transfer(...)
{
writeLock.lock();
try{...}
finally{writeLock.unlock();}
}
java.util.concurrent.locks.ReentrantReadWriteLock 5.0
Lock readLock()
得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。
Lock writeLock()
得到一个写锁,排斥所有其他的读操作和写操作。
14.5.15 为什么弃用stop和suspend方法
初始的Java版本定义了一个 stop 方法用来终止一个线程,以及一个 suspend 方法来阻塞一个线程直至另一个线程调用 resume 。 stop 和 suspend 方法有一些共同点:都试图控制一个给定线程的行为。
这两个方法已经弃用。 stop 方法天生就不安全,经验证明 suspend 方法会经常导致死锁。
stop 方法终止所有未结束的方法,包括 run 方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。
当线程要终止另一个线程时,无法知道什么时候调用 stop 方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。
一些作者声称 stop 方法被弃用是因为它会导致对象被一个已停止的线程永久锁定。但是,这一说法是错误的。从技术上讲,被停止的线程通过抛出 ThreadDeath 异常退出所有它所调用的同步方法。结果是,该线程释放它持有的内部对象锁。
与 stop 不同, suspend 不会破坏对象。但是,如果用 suspend 方法的线程试图获得同一个锁,那么,该锁在恢复之前是不可用的。如果调用 suspend 方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
如果想安全地挂起线程,引入一个变量 suspendRequested 并在 run 方法的某个安全的地方测试它,安全的地方是指该线程没有封锁其他线程需要的对象的地方。当该线程发现 suspendRequested 变量已经设置,将会保持等待状态直到它再次获得为止。
14.6 阻塞队列
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。
当试图向队列添加元素而队列已满,或是想从队列移除元素而队列为空的时候,阻塞队列( bolcking queue )导致线程阻塞。在协调多个线程之前的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动的平衡负载。如果第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个队列集赶上来。
阻塞队列方法
方法
正常动作
特殊情况下的动作
add
添加一个元素
如果队列满,则抛出IllegalStateException异常
element
返回队列的头元素
如果队列空,抛出NoSuchElementException异常
offer
添加一个元素并返回true
如果队列满,返回false
peek
返回队列的头元素
如果队列空,则返回null
poll
移出并返回队列的头元素
如果队列空,则返回null
put
添加一个元素
如果队列满,则阻塞
remove
移出并返回头元素
如果队列空,则抛出NoSuchElementException异常
take
移出并返回头元素
如果队列空,则阻塞
阻塞队列方法分为以下 3 类,这取决于当队列满或空时它们的响应方式。如果将队列当作线程管理工具来使用,将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列中移出元素时, add 、remove和element操作抛出异常。当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer、poll和peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
poll和peek方法返回空来指示失败。因此,向这些队列中插入null值是非法的。
还有带有超市的offer方法和poll方法的变体。例如,下面的调用:
boolean success = q.offer(x,100,TimeUnit.MILLISECONDS);
尝试在100毫秒的时间内在队列的尾部插入一个元素。如果成功返回true;否则,达到超时时,返回false。类似地,下面的调用:
Object head = q.poll(100,TimeUnit.MILLISEDS);
尝试用100毫秒的时间移除队列的头元素;如果成功返回头元素,否则,达到在超时时,返回null。
如果队列满,则put方法阻塞;如果队列空,则take方法阻塞。在不带超时参数时,offer和poll方法等效。
java.util.concurrent包提供了阻塞队列的几个变种。默认情况下,LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque是一个双端的版本。ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的,取元素的操作会阻塞。
DelayQueue包含是Delayed接口的对象:
interface Delayed extends Comparable< Delayed >
{
long getDelay(TimeUnit unit);
}
getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的情况下才能从DelayQueue移除。还必须实现compareTo方法。DelayQueue使用该方法对元素进行排序。
Java SE 7增加了一个TransferQueue接口,允许生产者线程等待,直到消费者准备就绪可以接收一个元素。如果生产者调用
q.transfer(item);
这个调用会阻塞,直到另一个线程将元素(item)删除。LinkedTransferQueue实现了这个接口。
java.util.concurrent.ArrayBlockingQueue< E > 5.0
ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity,boolean fair)
构造一个带有指定的容量和公平性的阻塞队列。该队列用循环数组实现。
java.util.concurrent.LinkedBlockingQueue< E > 5.0
java.uti..concurrent.LinkedBlockingDeque< E > 6.0
LinkedBlockingQueue()
LinkedBlockingDeque()
构造一个无上限的阻塞队列或双向队列,用链表实现。
LinkedBolckingQueue(int capacity)
LinkedBlockingDeque(int capacity)
根据指定容量构建一个有限的阻塞队列或双向队列,用链表实现。
java.util.concurrent.DelayQueue< E extends Delayed > 5.0
DelayQueue()
构造一个包含Delayed元素的无界的阻塞时间有限的阻塞队列。只有那些延迟已经超过时间的元素可以从队列中移出。
java.util.concurrent.Delayed 5.0
long getDelay(TimeUnit unit)
得到该对象的延迟,用给定的时间单位进行度量。
java.util.concurrent.PriorityBlockingQueue< E > 5.0
PriorityBlockingQueue()
PriorityBlockingQueue(int initialCapacity)
PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator)
构造一个无边界阻塞优先队列,用堆实现。
参数:initialCapacity 优先队列的初始容量。默认值是11。
comparator 用来对元素进行比较的比较器,如果没有指定,则元素必须实现Comparable接口。
java.util.concurrent.BlockingQueue< E > 5.0
void put(E element)
添加元素,在必要时阻塞。
E take()
移除并返回头元素,必要时阻塞。
boolean offer(E element,long time,TimeUnit unit)
添加给定的元素,如果成功返回true,如果必要时阻塞,直至元素已经被添加或超时。
E poll(long time,TimeUnit unit)
移除并返回头元素,必要时阻塞,直至元素可用或超时用完。失败时返回null。
java.util.concurrent.BolckingDeque< E > 6
void putFirst(E element)
void putLast(E element)
添加元素,必要时阻塞。
E takeFirst()
E takeLast()
移除并但会头元素或尾元素,必要时阻塞。
boolean offerFirst(E element,long time,TimeUnit unit)
boolean offerLast(E element,long time,TimeUnit unit)
添加给定的元素,成功时返回true,必要时阻塞直至元素被添加或超时。
E pollFirst(long time,TimeUnit unit)
E pollLast(long time,TimeUnit unit)
移动并返回头元素或尾元素,必要时阻塞,直至元素可用或超时。失败时返回null。
java.util.concurrent.TransferQueue< E > 7
void transfer(E element)
boolean tryTransfer(E element,long time,TimeUnit unit)
传输一个值,或者尝试在给定的超时时间内传输这个值,这个调用将阻塞,直到另一个线程将元素删除。第二个方法会在调用成功时返回true。
14.7 线程安全的集合
可以通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易写。
14.7.1 高效的映射表、集合和队列
java.util.concurrent包提供了映射表、有序集合队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。
集合返回弱一致性(weakly consisteut)的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会抛出Concurrent ModificationException异常。
与之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出一个ConcurrentModificationException异常。
并发地散列映射表,可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达16个写者线程同时执行。可以有更多的写者线程,但是,如果同一时间多于16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而,没有这种必要。
ConcurrentHashMap和ConcurrentSkipListMap类有相应的方法用于原子性的关联插入以及关联删除。putIfAbsent方法自动地添加新的关联,前提是原来没有这一关联。对于多线程访问的缓存来说这是很有用的,确保只有一个线程向缓存添加项:
cache.putIfAbsent(key,value);
相反的操作是删除(或许应该叫做removeIfPresent)。调用
cache.remove(key,value);
将原子性地删除键值对,如果它们在映像表中出现的话。最后,
cache.replace(key,oldValue,newValue);
原子性地用新值替换旧值,假定旧值与指定的键值关联。
java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
ConcurrentLinkedQueue< E >()
构造一个可以被多线程安全访问的无边界非阻塞的队列。
java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
ConcurrentSkipListSet< E >()
ConcurrentSkipListSet< E >(Comparator<? super E> comp)
构造一个可以被多线程安全访问的有序集。第一个构造器要求元素实现Comparable接口。
java.util.concurrent.ConcurrentHashMap< K,V > 5.0
java.util.concurrent.ConcurrentSkipListMap< K,V > 6
ConcurrentHashMap< K,V >()
ConcurrentHashMap< K,V >(int initialCapacity)
ConcurrentHashMap< K,V >(int initialCapacity,float loadFactor,int concurrencyLevel)
构造一个可以被多线程安全访问的散列映射表。
参数:initialCapacity 集合的初始容量。默认值为16。
loadFactor 控制调整:如果每一个桶的平均负载超过这个因子,表的大小会被重新调整。默认值是0.75。
concurrencyLevel 并发写者线程的估计数目。
ConcurrentSkipListMap< K,V >()
ConcurrentSkipListSet< K,V >(Comparator<? super K> comp)
构造一个可以被多线程安全访问的有序的映像表。第一个构造器要求键实现Comparable接口。
V putIfAbsent(K key,V value)
如果该键没有在映像表中出现,则将给定的值同给定的键关联起来,并返回null。否则返回与该键关联的现有值。
boolean remove(K hey,V value)
如果给定的键与给定的值关联,删除给定的键与值并返回真。否则,返回false。
boolean replace(K key,V oldValue,V newValue)
如果给定的键当前与oldvalue相关联,用它与newValue关联。否则,返回false。
第二部分 实验总结
实验1: 导入第13章示例程序,测试程序并进行代码注释。
测试程序1
l 在elipse IDE中调试运行教材585页程序13-1,结合程序运行结果理解程序;
l 将所生成的JAR文件移到另外一个不同的目录中,再运行该归档文件,以便确认程序是从JAR文件中,而不是从当前目录中读取的资源。
l 掌握创建JAR文件的方法;
ResourceTest package resource;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
/**
* @version 1.41 2015-06-12
* @author Cay Horstmann
*/
public class ResourceTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new ResourceTestFrame();
frame.setTitle("ResourceTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);//可视化
});
}
}
/**
* A frame that loads image and text resources.
*/
class ResourceTestFrame extends JFrame
{
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 300;
public ResourceTestFrame()
{
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
URL aboutURL = getClass().getResource("about.gif"); //确定目标文件,加载资源代码
Image img = new ImageIcon(aboutURL).getImage(); //设置图标
setIconImage(img);
JTextArea textArea = new JTextArea();
InputStream stream = getClass().getResourceAsStream("about.txt"); //读取文件
try (Scanner in = new Scanner(stream, "UTF-8"))
{
while (in.hasNext())
textArea.append(in.nextLine() + "\n");
}
add(textArea); //添加到文件区域
}
}
实验输出结果截图为:
测试程序2:
l 在elipse IDE中调试运行ThreadTest,结合程序运行结果理解程序;
l 掌握线程概念;
l 掌握用Thread的扩展类实现线程的方法;
l 利用Runnable接口改造程序,掌握用Runnable接口创建线程的方法。
class Lefthand extends Thread {
public void run()
{
for(int i=0;i<=5;i++)
{ System.out.println("You are Students!");
try{ sleep(500); }
catch(InterruptedException e)
{ System.out.println("Lefthand error.");}
}
}
}
class Righthand extends Thread {
public void run()
{
for(int i=0;i<=5;i++)
{ System.out.println("I am a Teacher!");
try{ sleep(300); }
catch(InterruptedException e)
{ System.out.println("Righthand error.");}
}
}
}
public class ThreadTest
{
static Lefthand left;
static Righthand right;
public static void main(String[] args)
{ left=new Lefthand();
right=new Righthand();
left.start();
right.start();
}
}
使用Runable接口改造后的程序如下:
class Lefthand implements Runnable {
public void run()
{
for(int i=0;i<=5;i++)
{ System.out.println("You are Students!");
try{ Thread.sleep(500);//休眠500毫秒
}
catch(InterruptedException e)
{ System.out.println("Lefthand error.");}
}
} }
class Righthand implements Runnable {
public void run()
{
for(int i=0;i<=5;i++)
{ System.out.println("I am a Teacher!");
try{ Thread.sleep(300);//休眠300毫秒
}
catch(InterruptedException e)
{ System.out.println("Righthand error.");}
}
} }
public class ThreadTest
{
public static void main(String[] args)
{ Righthand righthand = new Righthand();//新建一个righthand对象
Lefthand lefthand = new Lefthand();//新建一个lefthand对象
Thread right = new Thread(righthand);//start方法在Thread类中,新建Thread类
right.start();
Thread left=new Thread(lefthand);
left.start();
}
}
程序运行结果如下:
可以通过两种方法实现多线程,分别为:(1)利用Thread类的子类(2)用Runnable()接口实现线程
(1)——定义Thread类的子类并实现用户线程操作,即 run()方法的实现。
——在适当的时候启动线程。
(2)——首先设计一个实现Runnable接口的类;
——然后在类中根据需要重写run方法;
——再创建该类对象,以此对象为参数建立Thread 类的对象;
——调用Thread类对象的start方法启动线程,将 CPU执行权转交到run方法。
测试程序2:
l 在Elipse环境下调试教材625页程序14-1、14-2 、14-3,结合程序运行结果理解程序;
l 在Elipse环境下调试教材631页程序14-4,结合程序运行结果理解程序;
l 对比两个程序,理解线程的概念和用途;
l 掌握线程创建的两种技术。
程序如下:
14-1、14-2 、14-3:
import java.awt.*;
import java.util.*;
import javax.swing.*; /**
* The component that draws the balls.
* @version 1.34 2012-01-26
* @author Cay Horstmann
*/
public class BallComponent extends JPanel
{
private static final int DEFAULT_WIDTH = 450;
private static final int DEFAULT_HEIGHT = 350; private java.util.List<Ball> balls = new ArrayList<>(); /**
* Add a ball to the component.
* @param b the ball to add
*/
public void add(Ball b)
{
balls.add(b);
} public void paintComponent(Graphics g)
{
super.paintComponent(g); // 擦除背景
Graphics2D g2 = (Graphics2D) g;
for (Ball b : balls)
{
g2.fill(b.getShape());
}
} public Dimension getPreferredSize()
{
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); }
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*; /**
* Shows an animated bouncing ball.
* @version 1.34 2015-06-21
* @author Cay Horstmann
*/
public class Bounce
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
} /**
* The frame with ball component and buttons.
*/
class BounceFrame extends JFrame
{
private BallComponent comp;
public static final int STEPS = 1000;
public static final int DELAY = 3; /**
* Constructs the frame with the component for showing the bouncing ball and
* Start and Close buttons
*/
public BounceFrame()
{
setTitle("Bounce");
comp = new BallComponent();
add(comp, BorderLayout.CENTER);//使用边框布局管理器使其显示在窗口中心位置
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", event -> addBall());//在窗口添加两个按钮
addButton(buttonPanel, "Close", event -> System.exit(0));
add(buttonPanel, BorderLayout.SOUTH);//使用边框布局管理器使其显示在窗口下方位置
pack();
} /**
* Adds a button to a container.
* @param c the container
* @param title the button title
* @param listener the action listener for the button
*/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
} /**
* Adds a bouncing ball to the panel and makes it bounce 1,000 times.
*/
public void addBall()
{
try
{
Ball ball = new Ball();
comp.add(ball); for (int i = 1; i <= STEPS; i++)
{
ball.move(comp.getBounds());
comp.paint(comp.getGraphics());
Thread.sleep(DELAY);
}
}
catch (InterruptedException e)
{
}
}
}
import java.awt.geom.*; /**
* A ball that moves and bounces off the edges of a rectangle
* @version 1.33 2007-05-17
* @author Cay Horstmann
*/
public class Ball
{
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private double x = 0;
private double y = 0;
private double dx = 1;
private double dy = 1; /**
* 将球移动到下一个位置,如果球碰到其中一条边,则反向移动
*/
public void move(Rectangle2D bounds)
{
x += dx;
y += dy;
if (x < bounds.getMinX())
{
x = bounds.getMinX();
dx = -dx;
}
if (x + XSIZE >= bounds.getMaxX())
{
x = bounds.getMaxX() - XSIZE;
dx = -dx;
}
if (y < bounds.getMinY())
{
y = bounds.getMinY();
dy = -dy;
}
if (y + YSIZE >= bounds.getMaxY())
{
y = bounds.getMaxY() - YSIZE;
dy = -dy;
}
} /**
*获取球在当前位置的形状。
*/
public Ellipse2D getShape()
{
return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
}
}
14-4:
package bounceThread; import java.awt.*;
import java.awt.event.*; import javax.swing.*; /**
* Shows animated bouncing balls.
* @version 1.34 2015-06-21
* @author Cay Horstmann
*/
public class BounceThread
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
JFrame frame = new BounceFrame();
frame.setTitle("BounceThread");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
} /**
* The frame with panel and buttons.
*/
class BounceFrame extends JFrame
{
private BallComponent comp;
public static final int STEPS = 1000;
public static final int DELAY = 5; /**
* Constructs the frame with the component for showing the bouncing ball and
* Start and Close buttons
*/
public BounceFrame()
{
comp = new BallComponent();
add(comp, BorderLayout.CENTER);//使用边框布局管理器将它放在窗口的中心位置
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", event -> addBall());
addButton(buttonPanel, "Close", event -> System.exit(0));
add(buttonPanel, BorderLayout.SOUTH);//使用边框布局管理器将它放在窗口的中心位置
pack();
} /**
* Adds a button to a container.
* @param c the container
* @param title the button title
* @param listener the action listener for the button
*/
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
} /**
* Adds a bouncing ball to the canvas and starts a thread to make it bounce
*/
public void addBall()
{
Ball ball = new Ball();
comp.add(ball);
Runnable r = () -> {
try
{
for (int i = 1; i <= STEPS; i++)
{
ball.move(comp.getBounds());
comp.repaint();
Thread.sleep(DELAY);
}
}
catch (InterruptedException e)
{
}
};
Thread t = new Thread(r);
t.start();//调用Thread类中的start方法
}
}
package bounceThread; import java.awt.*;
import java.util.*;
import javax.swing.*; /**
* The component that draws the balls.
* @version 1.34 2012-01-26
* @author Cay Horstmann
*/
public class BallComponent extends JComponent
{
private static final int DEFAULT_WIDTH = 450;
private static final int DEFAULT_HEIGHT = 350; private java.util.List<Ball> balls = new ArrayList<>(); /**
* Add a ball to the panel.
* @param b the ball to add
*/
public void add(Ball b)
{
balls.add(b);
} public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D) g;
for (Ball b : balls)
{
g2.fill(b.getShape());
}
} public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); }
} package bounceThread; import java.awt.geom.*; /**
A ball that moves and bounces off the edges of a
rectangle
* @version 1.33 2007-05-17
* @author Cay Horstmann
*/
public class Ball
{
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private double x = 0;
private double y = 0;
private double dx = 1;
private double dy = 1; /**
Moves the ball to the next position, reversing direction
if it hits one of the edges
*/
public void move(Rectangle2D bounds)
{
x += dx;
y += dy;
if (x < bounds.getMinX())
{
x = bounds.getMinX();
dx = -dx;
}
if (x + XSIZE >= bounds.getMaxX())
{
x = bounds.getMaxX() - XSIZE;
dx = -dx;
}
if (y < bounds.getMinY())
{
y = bounds.getMinY();
dy = -dy;
}
if (y + YSIZE >= bounds.getMaxY())
{
y = bounds.getMaxY() - YSIZE;
dy = -dy;
}
} /**
Gets the shape of the ball at its current position.
*/
public Ellipse2D getShape()
{
return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
}
}
三:实验总结
这周主要学习了JAR文件和线程相关的知识,其中线程是程序中一个单一的顺序从控制流程。在学习过程中,了解了线程创建的两种技术:用Thread类的子类创建线程以及用Runnable()接口实现线程。学习用多线程提高实际运行效率,有效增加了cpu的利用时间,提高计算机工作的效率。对于结对编程,预想的非常好,但是在实际操作过程中,发现自己还存在着很大的问题。在实验课上老师和学长的帮助后,理解了线程中的优先级。课后自己还会再继续学习。
201871010132--张潇潇--《面向对象程序设计(java)》第十六周学习总结的更多相关文章
- 201571030332 扎西平措 《面向对象程序设计Java》第八周学习总结
<面向对象程序设计Java>第八周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https: ...
- 201771010118马昕璐《面向对象程序设计java》第八周学习总结
第一部分:理论知识学习部分 1.接口 在Java程序设计语言中,接口不是类,而是对类的一组需求描述,由常量和一组抽象方法组成.Java为了克服单继承的缺点,Java使用了接口,一个类可以实现一个或多个 ...
- 201771010134杨其菊《面向对象程序设计java》第八周学习总结
第八周学习总结 第一部分:理论知识 一.接口.lambda和内部类: Comparator与comparable接口: 1.comparable接口的方法是compareTo,只有一个参数:comp ...
- 201771010134杨其菊《面向对象程序设计java》第七周学习总结
第七周学习总结 第一部分:理论知识 1.继承是面向对象程序设计(Object Oriented Programming-OOP)中软件重用的关键技术.继承机制使用已经定义的类作为基础建立新的类定义,新 ...
- 201771010128 王玉兰《面象对象程序设计 (Java) 》第六周学习总结
---恢复内容开始--- 第一部分:基础知识总结: 1.继承 A:用已有类来构建新类的一种机制,当定义了一个新类继承一个类时,这个新类就继承了这个类的方法和域以适应新的情况: B:特点:具有层次结构. ...
- 201871010126 王亚涛 《面向对象程序设计 (Java)》第十七周学习总结
内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p/12 ...
- 201871010126 王亚涛《面向对象程序设计 JAVA》 第十三周学习总结
内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p/ ...
- 201777010217-金云馨《面向对象程序设计Java》第八周学习总结
项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...
- 马凯军201771010116《面向对象程序设计Java》第八周学习总结
一,理论知识学习部分 6.1.1 接口概念 两种含义:一,Java接口,Java语言中存在的结构,有特定的语法和结构:二,一个类所具有的方法的特征集合,是一种逻辑上的抽象.前者叫做“Java接口”,后 ...
- 周强201771010141《面向对象程序设计Java》第八周学习总结
一.理论知识学习部分 Java为了克服单继承的缺点,Java使用了接口,一个类可以实现一个或多个接口. 接口体中包含常量定义和方法定义,接口中只进行方法的声明,不提供方法的实现. 类似建立类的继承关系 ...
随机推荐
- 各版本mysql修改root密码
今天在安装mysql5.7.8的时候遇到一些问题,首当其冲便的是初始root密码的变更,特分享解决方法如下: 1.mysql5.7会生成一个初始化密码,而在之前的版本首次登陆不需要登录. shell& ...
- C++ class内的=重载,拷贝赋值函数copy op=,重载示例。必须是class内
#include <iostream> // overloading "operator = " inside class // = 是一元操作符.不写,编译器会提供 ...
- 360安全浏览器右击不显示审查元素 或按F12不弹出开发人员工具的原因和解决方法:设为极速模式
IE兼容模式 会显示 IE的开发人员工具 极速模式 才会显示谷歌的那种方式 IE调试模式不怎么习惯,如下图 正常调试模式如下图
- AGC037C Numbers on a Circle(神奇思路)
Atcoder 全是神仙题-- 先变成能不能从 \(b\) 到 \(a\).操作变成一个数减掉旁边两个数. 考虑里面最大的且不和 \(a\) 中相等的那个数.它两边的数此时都不能操作,否则就减到非正数 ...
- kubelet证书过期解决方法
昨天收到报警短信:集群中某node状态为notReady,由于是长期不用的,所以放到今天才有空处理,以下记录处理过程. 查看kubelet日志,发现不停的打印证书过期相关提示信息. 以下操作基于kub ...
- 在windows下使用VirtualEnv建立flask项目
1.系统中安装VirtualEnv 在安装完Python后,自带的有pip或easy_install工具,可进行VirtualEnv的安装 pip install virtualenv 2.构造项目, ...
- Eureka服务注册中心错误:com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect
报错信息 14:43:45.484 [main] INFO com.netflix.discovery.DiscoveryClient - Getting all instance registry ...
- poj-2803 Defining Moment
Defining Moment Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 1660 Accepted: 760 De ...
- Navicat Premium 12.0.22 安装与破解
一.安装 Navicat Premium 12.0.22的下载链接:https://pan.baidu.com/s/1swRY_fwIZfufdxDZj3hDyw 密码:09k8 安装步骤就是一路向下 ...
- RookeyFrame模块初始化
上一篇讲了下线上创建模块,这一次讲下线下创建的模块如何初始化,实体类的创建可参考Demo中的客户主数据模块 首先讲下model类创建中的约定: 1.所有数据模型继承BaseEntity 2.需要绑定枚 ...