写点什么

Java 并行程序基础

发布于: 2020 年 07 月 22 日
Java并行程序基础

一、进程和线程



在操作系统这门课程中,对进程的定义是这样的:



进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进行是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。



上面的定义很完整,对进程进行了全方面的定义,但是貌似进程是看不见摸不着的一个东西,实际上,我们可以通过查看计算机的进程管理器来查看应用程序的进程。



<img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g930n9rjanj317k0u0wr9.jpg" alt="image-20191119075437166" style="zoom: 50%;" />



上述进程列表中,展示了多个应用程序的进程,通常情况下,一个应用程序占用一个进程,系统资源的分配与调配也是基于进程的。其实可以理解为,一个进程就是一个应用程序。



那么线程和进程究竟是什么关系呢?简单说来,进程就是线程的“母亲”,是承载线程的基本单位,也是承载线程的容器。举个例子,一栋公司大楼里,许多员工都在各司其职,井然有序地工作着,每个员工就可以理解为一个活动线程,多个员工有时候会进行分组,每个组的员工共同协调合作完成一份工作,那么可以理解为线程分组,线程组内的线程共同合作完成工作,有时候,员工会排队等待领取下午茶,只有当前员工成功领取了下午茶之后才会走出队列,那么可以理解为线程访问临界区,多个线程等待临界区线程完成任务后离开临界区。那么进程就可以被理解为这栋公司大楼,它是承载公司正常运行(员工日常工作)的载体。



一个进程是由多个线程组合而成,那么可以这么说线程其实就是轻量级的进程,是程序执行的最小单位。现在的程序设计中,强调使用多线程,而不是多进程,那是因为线程间的切换与调度所消耗的成本远远低于进程所消耗的成本。



二、线程的生命周期



在Java的Thread类中有一个枚举类型State,State枚举内列举了线程的生命周期,代码如下:



public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}



从上面的代码注释可以了解到,线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态。



  • NEW状态:表示线程处于刚刚创建好的状态,线程还未执行,需要等待线程调用start()方法后才表示线程开始执行。一旦执行的线程,那么就不会再回到NEW状态了。

  • RUNNABLE状态:表示线程执行所需要的资源都准备好了,正处于执行状态。

  • BLOCKED状态:表示处于RUNNABLE状态进入了阻塞状态,进入阻塞状态的原因可能是因为当前线程优先级低于其他线程,暂时尚未获取锁,线程暂时执行,直到获取到请求的锁。

  • WAITING、TIMED_WAITING状态:都表示线程进入等待状态,两者的区别在于前者是一个没有期限的等待,而后者则是有限时间内的等待。两种等待状态的线程一旦再次执行,那么又会进入到RUNNABLE状态。

  • TERMINATED状态:表示线程执行完毕后的状态,一旦进入到TERMINATED状态的线程就不会再次回到RUNNABLE状态了。



三、线程的基本操作



3.1 开启线程



开启一个新的线程很简单,在这里暂时不讨论线程池的内容,开启新线程只需要使用new关键字创建一个线程对象,并将其start()起来即可。



Thread thread = new Thread();
thread.start();



调用线程对象的start()方法以后,会开启一个新的线程去执行线程对象的run()方法,这里需要注意的是,不能直接调用run()方法,否则就是在当前线程里直接执行了run()方法,而不是在新线程里执行,这就是start()和run()方法的区别。通常创建线程对象的时候会传入一个Runnable的实现类对象,Runnable接口只有一个run()方法需要去实现,那么调用线程的start()方法就会去开启新线程执行实现类对象的run()方法。



3.2 结束线程



通常来说,新建线程在在完成执行任务后会自动关闭,无需人工理会,但是在某些情况下,可能为了减少不必要的执行流程,会手动去关闭线程。从JDK源码来看,线程类Thread提供了一个停止线程的方法stop(),该方法可以停止线程的执行,使用起来十分方便,但是它已经被标注为“废弃”了,也就是说不推荐使用了。这是为什么呢?原因是因为stop()方法过于暴力,强行将执行中的线程停止,这样就有可能会造成内存中数据不一致的现象。



举个例子,比如新建线程的主要执行流程就是给user对象设置ID和名称,创建新线程后,设置完ID就强行停止了线程,那么内存中的user对象的两个属性中名称属性可能就是默认值,并没有成功设置,这样其他线程读取该数据就和预想的不一致了。



Thread类的stop()方法会强行停止线程,也就释放该线程持有的锁,释放锁后其他线程就有机会获取该锁,从而读取到该对象,那么读取到的数据就是不完整的数据。



3.3 中断线程



线程中断的解决方案要优于线程终止,线程中断不会像线程终止一样,会立马结束线程的后续执行流程,前者更像是得到了一个通知,通知他可以退出执行了,当线程接受这样的通知以后,会在一个合适的时机退出线程,并且不会造成脏数据问题。这个特点将线程中断和线程终止区别开来,是一个很重要的特点。



在Thread类中,有三个方法与线程中断息息相关:



public void interrupt()
public boolean isInterrupted()
public static boolean interrupted()



  • 第一个方法的作用是设置线程的中断标志位,也就是通知线程需要中断了。

  • 第二个方法的作用是通过检查中断标志位,来判断当前线程是否被中断。

  • 第三个方法是一个静态方法,也是用来检查线程的是否被中断,但是和第二个方法的区别就是会清除线程的中断标志位。



以上三个方法的方法体很简单,读者可以自行前往Thread类进行阅读。



下面的案例用来测试线程中断,代码如下:



public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("test");
Thread.yield();
}
});
thread.start();
Thread.sleep(2000);
thread.interrupt();
}



仔细观察代码,发现最后一行使用了线程中断方法,将thread线程进行中断,将代码运行起来之后发现,在控制台一直打印着“test”,完全没有停止下来的意思。分析原因得知,调用线程对象的interrupt()方法仅仅是设置了中断标志位,并不会去主动中断线程,那么上文中所说的通知线程中断后,会在合适的时机退出线程,那么何时是合适的时机呢?这就需要人工介入了。



public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted");
break;
}
System.out.println("test");
Thread.yield();
}
});
thread.start();
Thread.sleep(2000);
thread.interrupt();
}



上述代码在合适的位置加入了一个判断,判断当前线程的中断标志位是否被设置为中断,从而决定是否要中断线程,这就是所说的何时的时机。



这里需要注意一点的是,Thread.sleep()方法如果发生异常,那么当前线程设置的中断标志位会被清除掉,如果要捕获此类异常,那么需要重新设置中断标志位,也就是通知线程需要中断了。



3.4 等待和通知



为了适应线程间的协作能力,JDK的Object类提供了wait()和notify()方法,即等待方法和通知方法,等待方法指的是调用某个类对象的wait()方法后,当前线程进入到等待状态,等待直到其他线程内的同一对象调用了notify()方法为止,其他线程将通知当前线程继续执行后续流程。由于这两个方法位于Object类中,那么就代表任何对象都是可以调用这两个方法。



当某个线程调用了object对象的wait()方法,那么该线程就进入了等待object对象的队列中,由于多个线程执行到同一位置,都会进入到等待object对象的队列中,都在等待其他线程调用object对象的notify()方法,当调用了notify()方法后,并不是所有的等待线程都会继续执行后续流程,而是其中的某个线程收到通知后继续执行后续流程。notify()方法是从等待队列中随机选取一个线程去激活,并不是所有的线程都能收到通知。当然,Object类也提供了notifyAll()方法,那么它的作用就是通知所有的等待线程继续后续流程。



这里有个细节需要注意,那就是调用wait()和notify()首先都必须包含在synchronized语句中,因为它们的调用必须获取到目标对象的锁。下图展示了wait()方法和notify()方法的工作流程细节。



<img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g96gxpr4njj30qk0s8whi.jpg" alt="image-20191122073456465" style="zoom:50%;" />



wait()方法和sleep()方法都可以让线程等待,但是二者还是有却别的:



  • wait()方法使线程进入等待,但是可以重新被唤醒

  • wait()方法会释放目标对象的锁,而sleep()方法不会释放任何资源



3.5 挂起和继续



挂起(suspend)和继续(resume)是一对JDK提供的线程接口,挂起可以将当前线程暂停,直到对应线程执行了继续接口,否则将不会释放目标对象的资源,这一对方法已经被JDK标注为“废弃”,不再被推荐使用。



3.6 等待线程结束(join)和谦让(yeild)



当一个线程的输入可能非常依赖另外一个或者多个线程的输出,此时,这个线程就必须等待被依赖的线程执行完毕,才能继续执行。对于这种需求,JDK提供了join()方法来实现这个功能。JDK提供了两个join()方法,方法签名如下所示:



public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException



方法一表示目标线程调用后,那么当前线程就会一直等待,直到目标线程执行完毕。方法二设置了一个最大等待时间,如果超过这个最大等待时间,那么当前线程就不会等待目标线程执行完毕,就会继续执行后续的流程。



public class JoinThread extends Thread {
private volatile static int i = 0;
@Override
public void run() {
for (i = 0; i < 100000; i++) ;
}
public static void main(String[] args) throws InterruptedException {
JoinThread joinThread = new JoinThread();
joinThread.start();
joinThread.join();
System.out.println(i);
}
}



上述代码中,如果不使用join()方法,那么打印出来的i值为0或者很小很小的值,使用了join()方法后,那么始终会等待新线程的执行完毕后继续执行,此时打印出来的i值始终是100000。



线程谦让是指线程主动让出CPU,让出CPU后还会进入到资源争夺中,至于还有没有机会再争夺到资源,那就不一定了。JDK提供了yeild()方法来实现此功能,目的是为了让低优先级的线程尽量少占用过多资源,尽量让出资源给高优先级的线程。



了解更多干货,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)



发布于: 2020 年 07 月 22 日阅读数: 217
用户头像

还未添加个人签名 2018.09.30 加入

Java码农一枚。

评论

发布
暂无评论
Java并行程序基础