(JAVA高并发程序设计)第二章、java并行程序基础

简介: (JAVA高并发程序设计)第二章、java并行程序基础

一、线程的基本操作

进行Java并发设计的第一步,就是必须要了解Java中为线程操作所提供的一些API。比如,如何新建并且启动线程,如何终止线程、中断线程等。当然,因为并行操作要比串行操作复杂得多,于是,围绕着这些常用接口,可能有些比较隐晦的“坑”等着你去踩,而本节也会尽可能地将一些潜在问题描述清楚。
1.1、新建线程

新建线程很简单。只要使用new关键字创建一个线程对象,并且将它start()起来即可。
public class D1 {
    //新建线程
    public static void main(String[] args) {
        Thread a = new Thread();
        a.start();
    }
}

那么线程start()后,会干什么呢?这才是问题的关键。线程 Thread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。
这里要注意,下面的代码通过编译,也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

public class D1 {
    //新建线程
    public static void main(String[] args) {
        Thread a = new Thread();
        a.run();
    }
}

因此,在这里希望大家特别注意,调用start()方法和直接调用run()方法的区别。
在默认情况下,线程Thread 的run()方法什么都没有做,因此,这个线程一启动就马上结束了。如果你想让线程做点什么,就必须重写run()方法,把你的“任务”填进去。

public class D1 {
    //新建线程
    public static void main(String[] args) {
        Thread a = new Thread() {
            public void run() {
                System.out.println("hello world");
            }
        };
        a.start();
    }
}

上述代码使用匿名内部类,重写了run()方法,并要求线程在执行时打印“Hello, I am t1”的字样。如果没有特别的需要,都可以通过继承线程Thread,重写run()方法来自定义线程。但考虑到Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runnable接口是一个单方法接口,它只有一个run()方法:

public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

单纯的用接口来创建线程也是最常用的方法

public class Th1 implements Runnable {
    public void run() {
        System.out.println("Th1");
    }
}

1.2 终止线程
一般来说,线程执行完毕就会结束,无须手工关闭。但是,凡事都有例外。一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。比如,它们的执行体本身就是一个大大的无穷循环,用于提供某些服务。
那么如何正常地关闭一个线程呢?查阅JDK,你不难发现线程Thread提供了一个stopO方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。但如果你使用Eclipse之类的IDE 写代码,就会发现stop()方法是一个被标注为废弃的方法。也就是说,在将来,JDK可能就会移除该方法。
为什么stop)方法被废弃而不推荐使用呢?原因是 stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
为了让大家更好地理解本节内容,我先简单介绍一些有关数据不一致的概念。假设我们在数据库里维护着一张用户表,里面记录了用户ID和用户名。假设这里有两条记录:

在这里插入图片描述
如果我们用一个User对象去保存这些记录,我们总是希望这个对象要么保存记录1,要么保存记录2。如果这个User对象一半存着记录1,另外一半存着记录2,我想大部分人都会抓狂吧!如果现在真的由于程序问题,出现了这么一个怪异的对象u,u的ID是1,但是u的NAME是小王。那么,在这种情况下,数据就已经不一致了,说白了就是系统有错误了。这种情况是相当危险的,如果我们把一个不一致的数据直接写入了数据库,那么就会造成数据永久地被破坏和丢失,后果不堪设想。
也许有人会问,怎么可能呢?跑得好好的系统,怎么会出现这种问题呢?在单线程环境中,确实不会,但在并行程序中,如果考虑不周,就有可能出现类似的情况。不经思考地使用stop)方法就有可能导致这种问题。
Thread.stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章地读到了这个不一致的对象,悲剧也就此发生。
如果需要停止一个线程,那么应该怎么做呢?其实方法很简单,只需要由我们自行决定线程何时退出就可以了。仍然用本例说明,只需要将ChangeObjectThread 线程增加一个stopMe()方法即可。
1.3 线程中断
在Java 中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。在上一节中,我们已经详细讨论了stop()方法停止线程的坏处,并且使用了一套自有的机制完善线程退出的功能。在 JDK中是否有提供更强大的支持呢?答案是肯定的,那就是线程中断。
严格地讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到 stop()方法的老问题。
有三个方法与线程中断有关,这三个方法看起来很像,可能会引起混淆和误用,希望大家注意。
在这里插入图片描述
Thread.interrupt)方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。Thread.isInterrupted()方法也是实例方法,它判断当前线程是否被中断(通过检查中断标志位)。最后的静态方法Thread.interrupted()也可用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。
1.4 等待和通知
为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程:等待wait()方法和通知notify)方法。这两个方法并不是在Thread类中的,而是输出 Object类。这也意味着任何对象都可以调用这两个方法。
这两个方法的签名如下:
在这里插入图片描述
当在一个对象实例上调用wait)方法后,当前线程就会在这个对象上等待。这是什么意思呢?比如,在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,转为等待状态。等待到何时结束呢?线程A会一直等到其他线程调用了obj.notify()方法为止。这时,object对象俨然成了多个线程之间的有效通信手段。
那么wait()方法和 notify()方法究竟是如何工作的呢?图2.5展示了两者的工作过程。如果一个线程调用了object.wait()方法,那么它就会进入 object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当 object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。
在这里插入图片描述
除notify()方法外,Object对象还有一个类似的notifyAll()方法,它和 notify()方法的功能基本一致,不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。
这里还需要强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的synchronzied语句中,无论是wait()方法或者notify()方法都需要首先获得目标对象的一个监视器。图2.6显示了wait()方法和notify()方法的工作流程细节。其中T1和T2表示两个线程。T1在正确执行wait()方法前,必须获得object对象的监视器。而 wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。
线程T2在notify)方法调用前,也必须获得object对象的监视器。所幸,此时T1已经释放了这个监视器。因此,T2可以顺利获得object对象的监视器。接着,T2执行了notifyO方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得 object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。
在这里插入图片描述

1.5 挂起和继续执行
如果你阅读JDK有关Thread类的API文档,可能还会发现两个看起来非常有用的接口,即线程挂起( suspend)和继续执行(resume)。这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续指定。乍看之下,这对操作就像Thread.stop(方法一样好用。但如果你仔细阅读文档说明,会发现它们也早已被标注为废弃方法,并不推荐使用。
不推荐使用suspend)方法去挂起线程是因为suspend()方法在导致线程暂停的同时,并不会释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行(如图2.7所示)。直到对应的线程上进行了resume()方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()方法操作意外地在 suspend()方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是 Runnable,这也会严重影响我们对系统当前状态的判断。
在这里插入图片描述
未完待续。。。。

相关文章
|
1月前
|
Java 流计算
利用java8 的 CompletableFuture 优化 Flink 程序
本文探讨了Flink使用avatorscript脚本语言时遇到的性能瓶颈,并通过CompletableFuture优化代码,显著提升了Flink的QPS。文中详细介绍了avatorscript的使用方法,包括自定义函数、从Map中取值、使用Java工具类及AviatorScript函数等,帮助读者更好地理解和应用avatorscript。
利用java8 的 CompletableFuture 优化 Flink 程序
|
1月前
|
Java 测试技术 开发者
💡Java 零基础:彻底掌握 for 循环,打造高效程序设计
【10月更文挑战第15天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
112 63
|
20天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
1月前
|
Java Maven 数据安全/隐私保护
如何实现Java打包程序的加密代码混淆,避免被反编译?
【10月更文挑战第15天】如何实现Java打包程序的加密代码混淆,避免被反编译?
54 2
|
1月前
|
安全 Java Linux
java程序设置开机自启
java程序设置开机自启
105 1
|
1月前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
40 1
|
1月前
|
Java Python
如何通过Java程序调用python脚本
如何通过Java程序调用python脚本
31 0
|
1月前
|
Java
java的程序记录时间
java的程序记录时间
26 0
|
算法 Java
JAVA并发处理经验(四)并行模式与算法6:NIO网络编程
一、前言 首先我们必须了解NIO的一些基本概念 channel:是NIO中的一个通道,类似我们说的流。
892 0
|
9天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
下一篇
无影云桌面