前言:
提莫在家办公的第二天!
构建简单的 GUI 线程和数据线程工作模式
GUI 初始化和数据初始化
为了为我们所有的 test 构建一个上下文的 context,首先我们来写几个类,来简单模拟下我们安卓中,GUI 线程和数据线程是如何来显示按钮和数据的:
- ModelAndView 我们简单的编写一个可见的按钮,并给出了几个主要的属性,其中包含外观的数据和按钮本身要绑定的数据:
/**
* 一个 GUI 控件,缩略版本
*/
@Data
class ModelAndView {
/**
* 长度
*/
private Integer length;
/**
* 宽度
*/
private Integer width;
/**
* X 位置
*/
private Integer xPos;
/**
* Y 位置
*/
private Integer yPos;
/**
* 控件上绑定数据
*/
private Map<String, Object> data;
}
- 初始化 GUI 线程 在这个线程中,我们主要模拟按钮的外观初始化过程:
/**
* 初始化 GUI 线程
*/
class GuiInitThread implements Runnable {
ModelAndView modelAndView;
public GuiInitThread(ModelAndView modelAndView) {
this.modelAndView = modelAndView;
}
@Override
public void run() {
modelAndView.setLength(1);
modelAndView.setWidth(1);
modelAndView.setXPos(0);
modelAndView.setYPos(0);
}
}
- 数据初始化线程 在这个线程中,我们主要是进行绑定数据的初始化。因为一般来说,在我们实际使用中,这个数据的初始化时间是比较长的,为了跟展示的初始化相互影响,一般绑定数据的初始化都会放在额外的线程来做。 在类里面,我们主要是对绑定数据进行赋值。一般来说,这个线程所承担的工作大部分是对远端接口进行请求,获取数据,然后处理数据,绑定回控件,整个过程受网络影响,数据大小影响等等。
/**
* 数据初始化线程
*/
class DataThread implements Runnable {
ModelAndView modelAndView;
public DataThread(ModelAndView modelAndView) {
this.modelAndView = modelAndView;
}
@Override
public void run() {
Map<String, Object> data = new HashMap<>();
try {
Thread.sleep(1000); //徒增耗时
} catch (InterruptedException e) {
//先这样
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
data.put(String.valueOf(i), i);
}
modelAndView.setData(data);
}
}
主要的类我们编写完了,接下来来简单写个 test case:
/**
* init model and view test
*
* @throws Exception
*/
@Test
public void initModelAndView() throws Exception {
ModelAndView modelAndView = new ModelAndView();
Thread guiInitThread = new Thread(new GuiInitThread(modelAndView));
Thread dataThread = new Thread(new DataThread(modelAndView));
guiInitThread.start();
dataThread.start();
guiInitThread.join();
dataThread.join();
System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看初始完成之后的情况
}
以上就是第一个 demo,做这个 demo 的目的主要是自己当时初学多线程时候,由于当时还没接触过客户端的开发,对多线程的学习完全是从方法学起的,而不是在这样一个环境下,这就造成了不知道什么时候该多线程,这样的不在 context 环境下的对多线程的学习,其实是无用的。所以之后,我们尽量都会先构建一个环境,然后再环境下我们面对什么样子的问题,对于这个问题我们去学习。
PS:少理论,多硬核代码,主要还是对照例子体会。
涉及方法解析
- start
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
调用 start 马上会执行我们在 run 里面写的代码。
- sleep
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
使得当前线程睡眠一个毫秒数,但是当前线程不会放弃它的监视器,即不会释放锁(monitor 的事情后面再说);
- join
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
等待当前线程死掉,就是 run 方法跑完了。这里实际调用的是 join(0),再让我们看看 join(0)是啥:
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
当传入参数是 0 的时候,不计较时间,就一直等着就完事儿了。但是当大于 0 的时候,会有个循环,循环里面调用的是我们的 Object 的 wait 方法,所以实际上,调用 join 方法是会释放锁的。
GUI 不断刷新,当重新绑定数据时候,停止刷新过程
构造移动过程
我们通过调整 xPos 和 yPos 来改变这个小按钮的位置,来改变按钮的位置,从视觉上产生一个按钮在移动的感觉。之后我们重新绑定数据,同时希望重新绑定数据开始时候,小按钮位置不再改变。
下面我们来添加一个对象移动的方法:
/**
* 对象移动
*/
class MoveThread extends Thread {
ModelAndView modelAndView;
public Boolean isStop; //停止标记位置
public MoveThread(ModelAndView modelAndView) {
this.modelAndView = modelAndView;
this.isStop = false;
}
@Override
public void run() {
//注意:此处为正确停止线程方式
while (!this.isInterrupted() && !isStop) {
modelAndView.setXPos(modelAndView.getXPos() + 1);
modelAndView.setYPos(modelAndView.getYPos() + 1);
}
}
}
通过这个新线程,就能让我们的小按钮一直沿直线移动。
接着写我们的 test case:
/**
* 一直移动,但当重新绑定数据时候,停止移动
*/
@Test
public void moveInterruptedByBindingData() throws Exception {
ModelAndView modelAndView = new ModelAndView();
//初始化 gui
Thread guiInitThread = new Thread(new GuiInitThread(modelAndView));
guiInitThread.start();
//开始移动
MoveThread moveThread = new MoveThread(modelAndView);
moveThread.start();
//开始数据绑定
Thread dataThread = new Thread(new DataThread(modelAndView));
dataThread.setDaemon(true);
dataThread.start();
//把移动过程停止
moveThread.interrupt(); //moveThread.isStop = true;
Thread.sleep(100);
System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看完成之后的情况
}
观察打印结果,会发现由于数据绑定开始,所需要的时间较长,所以将移动线程中断之后,又过了一段时间,绑定数据的线程还没开始准备数据。
涉及方法解析
- 正确的停止线程 让我们再次重新看类MoveThread,它内部定义了 isStop 方法:
public Boolean isStop; //停止标记位置 通过对 run 方法执行条件的观察,可以发现,当遇到外部中断或者手动标记 stop 都会使 run 方法停止,这种方法不会抛出异常,或者像之前的 stop 方法一样,出现不会立即停止的情况。
- isInterrupted
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
该方法只会测试下线程是被中断,而不会影响中断标记位置,可以用作判断使用。
- interrupt
/**
* Interrupts this thread.
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
*
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p>
*
* <p> Interrupting a thread that is not alive need not have any effect.
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
中断线程,清除标记为,简单粗暴,没了;
- setDaemon
/**
* Marks this thread as either a {@linkplain #isDaemon daemon} thread
* or a user thread. The Java Virtual Machine exits when the only
* threads running are all daemon threads.
*
* <p> This method must be invoked before the thread is started.
*
* @param on
* if {@code true}, marks this thread as a daemon thread
*
* @throws IllegalThreadStateException
* if this thread is {@linkplain #isAlive alive}
*
* @throws SecurityException
* if {@link #checkAccess} determines that the current
* thread cannot modify this thread
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
标记线程为后台线程,注意 start 前面设置,之后再设置就没用了。
二人对话
交替对话
- hi hello
- how are u i'm fine,thank u
- and u? i'm ok!
下面我们来 imagine 一个初中背诵并默写全文的一个英语场景,这也能是你学习这么多年英语别的都忘了,就记得这段对话的一个场景。
(PS:我的建议是先自己写一个交替对话这样的两个线程,完成之后再往下看)
先上代码,然后我们来分析下这个:
String[] dialogs = {"hi", "hello", "how are u", "i'm fine,thank u", "and u?", "i'm ok!"};
Boolean isChineseSpeak = true;
final Object monitor = new Object();
Integer index = 0;
class ChinesePersonThread implements Runnable {
@Override
public void run() {
while (index < 5) {
synchronized (monitor) {
while (!isChineseSpeak) {
try {
//当条件不满足时候,在这里等待条件对方完成的通知
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isChineseSpeak = false;
System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]);
index++;
}
}
}
}
class ForeignPersonThread implements Runnable {
@Override
public void run() {
while (index < 5) {
synchronized (monitor) {
if (!isChineseSpeak) {
System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]);
index++;
isChineseSpeak = true;
//执行完成之后通知等待线程
monitor.notifyAll();
}
}
}
}
}
@Test
public void test() throws Exception {
Thread chineseThread = new Thread(new ChinesePersonThread());
Thread foreignThread = new Thread(new ForeignPersonThread());
chineseThread.start();
foreignThread.start();
Thread.sleep(1000);
}
涉及方法解析
- wait
/**
* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* In other words, this method behaves exactly as if it simply
* performs the call {@code wait(0)}.
* <p>
* The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* either through a call to the {@code notify} method or the
* {@code notifyAll} method. The thread then waits until it can
* re-obtain ownership of the monitor and resumes execution.
* <p>
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
* @throws InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final void wait() throws InterruptedException {
wait(0);
}
调用此方法时候,必须获得对象的锁,然后直到其他线程通过 notify/notifyAll 或中断,它才能继续执行。ps,wait 方法会释放锁(emm,大家都这么写,其实翻译过来是监视器,一个意思)。 同样,notify 这种通知方法,使用前也需要获取对象锁,然后通知一个在该对象上等待的线程。
- 等待通知模式
-
对象 1 在获得锁的基础上,当条件不达到,就循环等待;
-
对象 2 在获得锁的基础上,执行完成之后,通知等待对象。
这个例子主要是为了写线程交互中的等待通知模式,其实你可以看完之后,自己再写写其他实现方式。
测试锁的释放情况
测试 wait / notify 释放锁情况
final Object lock = new Object();
boolean waiting = true;
class WaitThread extends Thread {
@Override
public void run() {
synchronized (lock) {
System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock));
while (waiting) {
try {
System.out.println("begin wait ...." + "current time : " + System.currentTimeMillis());
lock.wait();
System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class NotifyThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println("current time : " + System.currentTimeMillis() + " ; notify thread hold lock : " + Thread.holdsLock(lock));
if (waiting) {
waiting = false;
lock.notify();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("current time : " + System.currentTimeMillis() + " ; notify thread hold lock : " + Thread.holdsLock(lock));
}
}
}
@Test
public void testWaitNotifyLock() throws Exception {
new WaitThread().start();
new Thread(new NotifyThread()).start();
Thread.sleep(3000);
}
输出:
current time : 1574852090614 ; wait thread hold lock : true
begin wait ....current time : 1574852090614
current time : 1574852090627 ; notify thread hold lock : true
current time : 1574852090729 ; notify thread hold lock : true
current time : 1574852090729 ; wait thread hold lock : true
从时间上来看, wait 线程先获得锁,之后进入等待过程,调用 wait 方法; 此时 wait 线程还没执行完,这时 notify 线程获取了锁,并执行完成,说明在 wait 之后,notify 线程获取到了 lock ,说明 wait 方法调用之后,锁被释放掉了, notify 线程才能获取到锁。当 notify 线程执行完成之后, wait 线程又重新获得了锁,继续执行。
测试 sleep 方法获取释放锁情况
@Test
public void testSleepLock() throws Exception {
Runnable r1 = () -> {
synchronized (sleepLock) {
System.out.println("r1 begin current time : " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("r1 end current time : " + System.currentTimeMillis());
}
};
Runnable r2 = () -> {
//让r1先获取到锁
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sleepLock) {
System.out.println("r2 current time : " + System.currentTimeMillis());
}
};
new Thread(r1).start();
new Thread(r2).start();
Thread.sleep(3000);
}
输出:
r1 begin current time : 1574855304815
r1 end current time : 1574855305819
r2 current time : 1574855305819
我们让存在 sleep 的线程 r1 先获取到锁,然后r1进入一个长时间的 sleep ,可以看到在这个时间内,r2 并没有获取到锁,而是 r1 执行完之后,r2 才获取到锁。
测试 yield 方法获取释放锁情况
在上面的基础上,我们已经证明了 sleep 不会释放线程拥有的锁,然后我们改改上面例子,测试下 yield 方法会不会释放锁:
@Test
public void testYieldLock() throws Exception {
Runnable r1 = () -> {
synchronized (sleepLock) {
System.out.println("r1 begin current time : " + System.currentTimeMillis());
Thread.yield();
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("r1 end current time : " + System.currentTimeMillis());
}
};
Runnable r2 = () -> {
//让r1先获取到锁
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sleepLock) {
System.out.println("r2 current time : " + System.currentTimeMillis());
}
};
new Thread(r1).start();
new Thread(r2).start();
Thread.sleep(2000);
}
输出:
r1 begin current time : 1574855591635
r1 end current time : 1574855592437
r2 current time : 1574855592437
可以看到 r1 获取锁之后,就一直占用,直到同步块结束。
完结福利
在即将到来的金三银四跳槽面试季,希望大家好好加油。
现在整理好了 1000 道多家公司 java 面试题 400 多页 pdf 文档,都已经分专题整理好了。
还有几百页的Java核心知识点PDF。
欢迎大家关注公众号领取,回复:PDF即可,顺便向大家讨个赞,点个关注!嘿嘿
我是提莫
一个节操泛滥,一身凛然正气,刚正不阿的Java程序员