线程间协作

本文摘抄自《Java 多线程编程实战指南》核心篇 第五章小结

本章介绍了多线程编程中线程间常见的协作形式以及 Java 平台对这些协作形式所提出的支持。


  等待线程可以通过执行 Object.wait()/wait(long) 来实现等待。通知线程可以通过执行 Object.notify()/notifyAll() 来实现通知。等待线程/通知线程在执行 Object.wait()/wait(long)/Object.notify()/notifyAll() 时必须持有相应对象对应的内部锁。为了避免信号丢失问题以及欺骗性唤醒问题,等待线程将等待线程对保护条件的判断/Object.wait()/wait(long) 的调用必须放在相应对象所引导的临界区中的一个循环语句之中。

  使用 notify() 替代 notifyAll() 必须使以下两个条件同时得以满足:

  • 一次通知仅需要唤醒至多一个线程;
  • 相应对象上的所有等待线程都是同质等待线程。

  使用 notify() 替代 notifyAll() 可以减少等待/通知中产生的上下文切换。通知线程在执行完 Object.notify()/notifyAll() 后尽快释放相应对象的内部锁也有助于减少上下文切换。

  条件变量(Condition 接口)是 wait/notify 的替代品。Condition 接口的 API 与 wait/notify 类似:Condition.await()/awaitUtil(Date) 相当于 Object.wait()/wait(long);Condition.signal()/signalAll() 相当于 Object.notify()/notifyAll()。Condition.awaitUtil(Date) 解决了 Object.wait(long) 存在的问题——无法区分其返回是否是由等待超时而导致的。

  Condition 接口本身只是对解决过早唤醒问题提供了支持。要真正解决过早唤醒问题,我们需要通过应用代码维护保护条件与条件变量之间的对应关系,即使用不同保护条件的等待线程需要调用不同的条件变量的 await 方法来实现其等待,并使通知线程在更新了相关共享变量之后,仅调用与这些共享变量有关的保护条件所对应的条件变量的 signal/signalAll 方法来实现通知。

  CountDownLatch 能够用来实现一个线程等待其他线程执行的特定操作的结束。等待线程执行 CountDownLatch.await(),通知线程执行 CountDownLatch.countDown()。为避免等待线程永远处于暂停状态而无法被唤醒,CountDownLatch.countDown() 调用通常需要被放在 finally 块中。一个 CountDownLatch 实例只能实现一次等待/通知。对于同一个 CountDownLatch 实例 latch,latch.countDown() 的执行线程在执行该方法之前所执行的任何内存操作,对等待线程在 latch.await() 调用返回之后的代码是可见的且有序。

  CyclicBarrier 能够用于实现多个线程间的相互等待。CyclicBarrier.await() 既是等待方法又是通知方法。CyclicBarrier 实例的所有参与方除最后一个线程外都相当于等待线程,最后一个线程则相当于通知线程。与 CountDownLatch 不同的是,CyclicBarrier 实例是可以复用的——一个 CyclicBarrier 实例可以实现多次等待/通知。在使用 CountDownLatch 足以满足要求的情况下,我们应该避免使用 CyclicBarrier。CyclicBarrier 的典型应用场景包括:使迭代(Iterative)算法并发化,在测试代码中模拟高并发。

  在生产者——消费者模式中,生产者负责生产产品并通过传输通道将产品以线程安全的方式发布到消费者线程。消费者线程仅负责从传输通道中取出产品进行“消费”。产品既可以是数据,也可以是待处理的任务。BlockingQueue 的实现类 ArrayBlockingQueue/LinkedBlockingQueue 和 SynchronousQueue 等以及 Exchanger 类可作为传输通道。

  生产者与消费者所执行的处理,即产品的生产与“消费”是并发的。这使得我们能够平衡生产者/消费者处理能力的差异,即避免了一方处理过慢对另一方产生影响。另外,生产者——消费者模式使得一个线程(消费者线程)可以处理多个任务,提高了线程的利用率。

  使用无界队列作为传输通道时往往需要借助 Semaphore 控制生产者的生产速率。Semaphore 相当于能够对程序访问虚拟资源的并发程度进行控制的配额调度器。Semaphore.acquire() 用于申请配额,Semaphore.release() 用于返还配额,Semaphore.release() 调用总是放在 finally 块中。Semaphore.acquire() 和 Semaphore.release() 总是配对使用的,这点需要由应用代码来确保。Semaphore 对配额的调度既支持非公平策略(默认策略),也支持公平策略。

  PipedOutputStream/PipedInputStream 是 Java 标准库类中生产者——消费者模式的一个具体例子。PipedOutputStream/PipedInputStream 适合在单生产者——单消费者模式中使用,应避免在单线程程序中使用 PipedOutputStream/PipedInputStream。生产者线程发生异常而导致其无法继续提供新的数据时,生产者线程必须主动提前关闭相应的 PipedOutputStream 实例(调用 PipedOutputStream.close())。

  Exchanger 类也可作为传输通道,它对双缓冲技术提供了支持:生产者与消费者各自维护一个缓冲区,双方通过执行 Exchanger.exchange(V) 来交换各自持有的缓冲区。当消费者在“消费”一个已填充完毕的缓冲区时,生产者可以对待填充的缓冲区进行填充(生产产品),从而实现了产品的“消费”与生成的并发。Exchanger 类便于我们能够对产品的粒度进行优化。

  Java 线程中断机制相当于 Java 线程与线程间协作的一套协议框架:发起线程通过 Thread.interrupt() 调用给目标线程发送中断,这相当于将目标线程的线程中断标记置为 true;目标线程则通过 Thread.currentThread().isInterrupted()/Thread.interrupted() 来获取或者获取并重置线程中断的响应方式。给目标线程发送中断还能够产生唤醒目标线程的效果。目标线程可以通过对 InterruptedException 进行处理的方式或者直接通过判断线程中断标记并执行相应的处理逻辑的方式来响应中断。对 InterruptedException 进行处理的正确方式包括:不捕获InterruptedException/捕获 InterruptedException 后重新将该异常抛出,以及捕获 InterruptedException 并在捕获该异常后中断当前线程。

  需要主动停止线程的典型场景包括:服务或者系统关闭/错误处理以及用户取消任务。通用的线程优雅停止办法:发起线程更新目标线程的线程停止标记并给其发送中断,目标线程仅在当前无待处理任务且不会产生新的待处理任务情况下才能使 run 方法返回。Web 应用自身启动的工作者线程需要由应用自身在 Web 应用停止时主动停止。

本章知识结构图


闷热七月(2019)

七月尾上海梅雨季节过去了,这天一下子就闷热起来,温度直逼三十五

公司搬家

公司换了个办公地,原先两个办公地相聚一公里左右,现在联合广场那边的办公地搬到了新纪元国际广场这边,跟原先这边在同一楼,折腾啊,我搬过去才刚刚半年又搬了。

忙了一整天,衣服湿了干干了又湿,老板不愿花钱叫搬家公司的来只能我们自己动手,还有两个大金属屏风从一楼手动搬到十六楼,健身效果不错不错。。

新办公地这边工位比之前的大了点,环境好了点吧,不过最致命的是吃。原先在联合广场那边可以去旁边的设计院食堂吃,一顿饭也就吃个十块出头就有一荤两素,现在这边不行了,生活成本一下子上去了不少。每天还得想着要去哪里吃,骑车去设计院吃的话大夏天热得发毛,一身汗吃饭肯定不爽,难办啊。

今天中午跟着他们去了大食堂吃了顿,划不来,吃个二十几也就那样。

出差

又去潭溪山跑了一趟,一路曲折不说,回来还是凌晨。刚好那几天还上火,去那也只能吃粗粮,那几天身体难受的啊,回来赶紧喝了一顿稀饭补了一觉,身体瞬间焕发青春,难说神奇矣。

周末跟朋友吃了顿烤鸭火锅,看了场鬼卞的现场,回去倒头大睡。

估计过几天又要去昆明了,这项目要么不催不忙,要么一股脑儿一起来。

松江云廊屋盖也得我去一趟。

办居住证

来上海这么多年,终于去办居住证了,现在政策又改了,还要网签备案,真是麻烦到死。以后换个地方住还得再去重新备案,重新办理,折腾啊。

这制度也不合理,我们三人一起合租,其中一个原先办过居住证结果以为备案就不需要去了就没跟我们一起去网签备案。后来去街道办事处办理居住证更改信息时人家说你没备案不能更改办理,瞬间懵逼。之前还是居委会人口登记的那位阿姨跟他说不需要备案的,惨兮兮。这样的话,假如新来的租客要办居住证就必须得所有人一起再去网签备案咯?

我们两人办好了手续等六个月拿居住证,他就只能另想办法了,房东大爷也不会再专门来跑一趟,也七十好几了。

在外面漂终究抵不过各种烦扰。争取早日买房,告别沪漂一族!

水果

今年水果好贵啊,家里葡萄能吃了赶紧寄了一箱过来,都好久没吃水果了。

前几天京东众筹上又买了一箱黑布林十斤二十四,感觉挺便宜的就买了,后来去超市看了下也就这个价,不过送货速度挺快的,口感还不错!

真是不会照顾自己啊。


在实践中运用多线程

本文摘抄自《Java 多线程编程实战指南》核心篇 第四章小结

本章介绍了利用多线程实现并发计算的基本方法以及多线程编程实践中的注意事项及应对措施。


  挖掘出程序中的可并发点是实现多线程编程的目标——并发计算的前提。

  实现并发化的策略包括基于数据的分割策略和基于任务的分割策略。前者从程序处理的数据角度入手,将原始输入分解为若干规模更小的子输入,并将这些子输入指派给专门的工作者线程处理。其结果是产生若干同质的工作者线程。后者从程序的处理逻辑角度入手,将原始任务处理逻辑依照任务的资源消耗属性或者处理步骤分解为若干个子任务,并创建专门的工作者线程来执行这些子任务。其结果是产生多个相互协作的异质工作者线程。

  多线程编程实践中需要注意以下几下问题。

  • 考虑到多线程程序往往比相应的单线程程序要复杂,且未必比相应的单线程程序快,因此多线程编程的一个实施策略是考虑从单线程程序向多线程程序“进化”,而不是直接迈向“多线程”。

  • 线程数的合理设置。设置线程数的基本原则就是避免随意设置/使线程数可配置或者可以动态计算得来。设置合理的线程数需要考虑系统的资源状况(处理器数目/内存大小等)/线程所执行的任务的特性(CPU 密集型任务/I/O 密集型任务)/资源使用情况规划(CPU 使用率上限)以及程序运行过程中使用到的其他稀缺资源情况(如数据库连接/文件句柄数)等因素。

  • 多线程程序往往比相应的单线程程序产生更多的开销,且需要注意工作者线程的异常处理以及原始任务规模未知问题的应对。

本章知识结构图


Java 线程同步机制

本文摘抄自《Java 多线程编程实战指南》核心篇 第三章小结

本章介绍了 Java 平台提供的各种线程同步机制。


  Java 线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同,如下表所示。因此,我们需要根据实际情况选择一个功能适用且开销较小的同步机制。

Java 线程同步机制的功能与开销/问题

volatile CAS final static
原子性保障 具备 具备② 具备 不涉及 不涉及
可见性保障 具备 具备 不具备 不具备 具备③
有序性保障 具备 具备 不涉及 具备 具备④
上下文切换? 可能① 不会 不会 不会 可能⑤
备注 ①被争用的锁可能导致上下文切换 ②仅能够保障对 volatile 变量读/写操作本身的原子性 ③④仅在一个线程初次读取一个类的静态变量时起作用
⑤静态变量所属类的初始化可能导致上下文切换

  锁是 Java 平台中功能最强大的一种线程同步机制,同时其开销也最大,可能导致的问题也最多。被争用的锁会导致上下文切换,锁还可能导致死锁/锁死等线程活性故障。锁适用于存在多个线程对多个共享数据进行更新/check-then-act 操作或者 read-modify-write 操作这样的场景。

  锁的排他性以及 Java 虚拟机在临界区前后插入的内存屏障使得临界区中的操作具有原子性。由此,锁还保障了写线程在临界区中执行操作在读线程看来是有序的,即保障了有序性。Java 虚拟机在 MonitorExit 对应的机器码后插入的内存屏障则保障了可见性。锁能够保障线程安全的前提是访问同一组共享数据的多个线程必须同步在同一个锁之上,否则原子性/可见性和有序性均无法得以保障。在满足貌似串行语义的前提下,临界区内以及临界区外的操作可以在各自范围内重排序。临界区外的操作可能会被 JIT 编译器重排到临界区内,但是临界区内的操作不会被编译器/处理器重排到临界区之外。

  Java 中的所有锁都是可重入锁。内部锁(synchronized)仅支持非公平锁,因此它可能导致饥饿。而显式锁(ReentrantLock)既支持非公平锁又支持公平锁,显式锁可能导致锁泄露。内部锁和显式锁各有所长,各有所短。读写锁(ReadWriteLock)由于其内部实现的复杂性,仅适用于只读操作比更新操作要频繁得多且读线程持有锁的时间比较长的场景。读写锁(ReadWriteLock)中的读锁和写锁是一个锁实例所充当的两个角色,并不是两个独立的锁。

  线程转储中可以包含锁的相关信息——线程在等待哪些锁,这些锁又是被哪些线程持有的。

  volatile 相当于轻量级锁。在线程安全保障方面与锁相同的是,volatile 能够保障可见性/有序性;与锁不同的是 volatile 不具有排他性,也不会导致上下文切换。与锁类似,Java 虚拟机实现 volatile 对有序性和可见性的保障也是借助于内存屏障。从这个角度来看,volatile 变量写操作相当于释放锁,volatile 变量读操作相当于获得锁——Java 虚拟机通过在 volatile 变量写操作之前插入一个释放屏障,在 volatile 读操作之后插入一个获取屏障这种成对的释放屏障和获取屏障的使用实现了 volatile 对有序性的保障。类似地,Java 虚拟机在 volatile 变量写操作之后插入一个存储屏障,在 volatile 变量读操作之前插入一个加载屏障这种成对的存储屏障与加载屏障的使用实现了 volatile 对可见性的保障。

  在原子性方面,volatile 仅能够保障 long/double 型变量写操作的原子性。如果要保障对 volatile 变量的赋值操作的线程安全,那么赋值操作右边的表达式不能涉及任何共享变量(包括被赋值的变量本身)。volatile 关键字在可见性/有序性和原子性方面的保障并不会对其修饰的数组元素的读/写起作用。

  volatile 变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。读取一个 volatile 变量总是意味着(通过高速缓存进行的)读内存操作,而不是从寄存器中读取。因此,volatile 变量读操作的成本比读取普通变量要略高一些,但比在临界区中读取变量要低。

  volatile 的典型运用场景包括:一,使用 volatile 变量作为状态标志;二,使用 volatile 保障可见性;三,使用 volatile 变量替代锁;四,使用 volatile 实现简易版读写锁。

  CAS 使得我们可以在不借助锁的情况下保障 read-modify-write 操作/check-then-act 操作的原子性,但是它并不保障可见性。原子变量类相当于基于 CAS 实现的增强型 volatile 变量(保障 volatile 无法保障的那一部分操作的原子性)。常用的原子变量类包括 AtomicInteger/AtomicLong/AtomicBoolean 等。AtomicStampedReference 则可以用于规避 CAS 的 ABA 问题。

  static 关键字能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值(而不是默认值)。对于引用型静态变量,static 还确保了该变量引用的对象已经初始化完毕。但是,static 的这种可见性和有序性保障仅在一个线程初次读取静态变量的时候起作用。

  final 关键字在多线程环境下也有其特殊作用:当一个对象被发布到其他线程的时候,该对象的所有 final 字段(实例变量)都是初始化完毕的。而非 final 字段没有这种保障,即这些线程读取该对象的非 final 字段时所读取到的值可能仍然是相应字段的默认值。对于引用型 final 字段,final 关键字还进一步确保该字段所引用的对象已经初始化完毕。

  实现对象的安全发布,通常可以依照以下顺序选择适用且开销最小的线程同步机制。

  • 使用 static 关键字修饰引用该对象的变量。
  • 使用 final 关键字修饰引用该对象的变量。
  • 使用 volatile 关键字修饰引用该对象的变量。
  • 使用 AtomicReference 来引用该对象。
  • 对访问该对象的代码进行加锁。

  为避免将 this 代表的当前对象逸出到其他线程,我们应该避免在构造器中启动工作者线程。通常我们可以定义一个 init 方法,在该方法中启动工作者线程。在此基础上,定义一个工厂方法来创建(并返回)相应的实例,并在该方法中调用该实例的 init 方法。

本章知识结构图


多线程编程的目标与挑战

本文摘抄自《Java 多线程编程实战指南》核心篇 第二章小结

本章通过一些具体概念介绍了多线程编程的目标及其面临的挑战。

  • 单线程程序所进行的计算本质上是串行。多线程编程的目标是将原本串行的计算改为并发乃至并行。

  • 竞态(Race Condition)是指计算的正确性依赖于相对时间顺序(Relative Timing)或者线程的交错(Interleaving)。竞态表现为计算的结果时而正确时而错误,它并不意味着计算的结果一定是错误的,其往往伴随着读脏数据/丢失更新的问题。竞态是访问(读取/更新)同一组共享变量的多个线程所执行的操作相互交错(Interleave)而导致的干扰(读取脏数据)或者冲突(丢失更新)的结果。二维表分析法是分析和解释竞态的有效和常用工具。一个类能够导致竞态,那么它就不是线程安全的。线程安全意味着不存在竞态,但是不存在竞态却未必意味着线程安全。

  • 线程安全问题表现为原子性/可见性和有序性这三个方面。这几个方面既相互区别,又相互联系。原子性的保障能够消除竞态。可见性描述了一个线程对共享变量的更新对于另外一个线程而言是否可见,或者说什么情况下可见的问题。原子性和可见性一同得以保障了一个线程能够共享变量的相对新值,而不是一个“半成品”的值。有序性描述了一个处理器上运行的一个线程对共享变量所做的更新,在另外一个处理器上运行的其他线程看来,这些线程是以什么样的顺序观察到这些更新的问题。可见性是有序性的基础,而有序性又可能影响可见性。

  • 原子操作是“不可分割”的操作。所谓“不可分割”包括两层含义:其一,访问(读/写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束,要么尚未发生,即其他线程不会“看到”该操作执行了部分的中间效果;其二,访问同一组共享变量的原子操作是不能够被交错的,这通常意味着互斥(Mutual Exclusion),即对于访问同一组共享变量的多个原子操作,一个线程执行其中一个操作的时候其他线程无法访问这组共享变量中的任意一个变量。将 read-modify-write 操作和 check-then-act 转换为原子操作能够消除竞态。在 Java 语言中,对 long/double 型以外的任何变量的写操作都是原子的。volatile 关键字修饰的 long/double 型写操作也具有原子性。针对任何变量的读操作都是原子操作。

  • 可见性问题不是必然出现的,而一旦出现则可能导致灾难性后果。导致可见性问题的因素既有软件因素(JIT 编译器)也有硬件因素(处理器和内存等存储设备)。可见性的保障仅仅意味着一个线程能够读取到共享变量的相对新值,而不能保障更新对这个子线程可见,子线程执行期间对共享变量所做的更新对该线程的 join() 执行线程可见(从 join() 返回处开始才是可见的)。

  • 编译器/处理器/存储子系统(写缓冲器和高速缓存等)和运行时(JIT 编译器)都可能导致重排序。重排序是出于性能的需要并在满足“貌似串行语义”的前提下进行的,它可能导致线程安全问题。于可见性问题类似,重排序也不是必然出现的。有序性的保障是通过部分地从逻辑上禁止重排序实现的。可见性是有序性的基础,而有序性反过来又可能影响可见性。

  • 上下文切换可以被看作多线程编程的必然产物,一方面它使得充分利用极其有限的处理器资源成为可能;另一方面它也增加了系统的开销。因此,多线程编程未必比单线程的计算效率要高。程序运行过程中发生的上下文切换既有自发性上下文切换,也有非自发性上下文切换。Linux 内核提供的 perf 命令可以帮助我们测量程序运行过程中发生的上下文切换的次数和频率。

  • 多线程程序可能由于资源稀缺性或者程序自身的错误和缺陷而一直处于非 RUNNABLE 状态,或者即使是处于 RUNNABLE 状态,但是其要执行的任务一直无法进展,即产生了活性故障。

  • 非公平调度策略是我们多数情况下的首选资源调度策略。其优点是吞吐率较大;缺点是资源申请者申请资源所需的时间偏差可能较大,并可能导致饥饿现象。公平调度策略适合在资源的持有线程占用资源的时间相对长或资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用。其优点是线程申请资源所需的时间偏差较小,并且不会导致饥饿现象;其缺点是吞吐率较小。

本章知识结构图


焦躁六月(2019)

送走了愉快的五月,就来了令人焦躁不已的六月。

不知道是不是温度高了天气热了,总是在这样的季节使得焦躁浑身难受然后发生各种事情。


租房

在外漂没有自己的房子真的是很难受,换工作不在附近得搬,房东不续租说搬也就得搬。

住的地是三室户,这不马上就到期了,一室友不续租了跟女朋友住去结果导致空一间房,找不到人接盘,房东也就不跟我们续租了。也罢,老房子什么设施都是坏的,是得要对自己好一点了,搬吧,那就找房子吧。

找房也还算顺利,虽然是通过中介的,多付了好几千中介费,这也算是找了个新住处,无缝对接。这房子也是个三室,不过面积要稍微大点,环境也好一点,出门就是欧尚超市不远还有个生活广场。原来住的三小伙要搬去浦东刚好也要到期了,房子是真的贵,感觉还合适那就快刀斩乱麻,直接签了得。

过几天得收拾东西准备搬了,其它的倒是还好,就我那个大鱼缸有点费事,也是我要搬之所以头疼的事。说到还是个三室,那还是要准备拉一个人来的,刚好有个新同事过来,先住着,后面就再说吧。也不知道搬过去还能住多久,沪漂什么时候是个头。事情真多,头疼。

淄博

公司以前的一个项目突然就把我派过去考察了,山东淄博潭溪山景区。地方是真的挺偏的,下了高铁,打了个出租一直开了将近两小时,其中山路开了近一个小时,还是盘山上去的。据了解来这的都是自驾的多,不然交通确实是不方便。

在那边呆了差不多四天吧,他们吃粗粮的多。我就入乡随俗,跟着吃了几天的菜饼馒头,一天吃个两顿,还真管饱。

家族企业自建的景区,项目确实是多,玻璃桥建得也很奇,十五根索支持,还是值得去一趟的,就是地方偏了点。有一天中午我从那边跟着人群走山路下来,八百多米啊,走得后来腿都打颤了。现在小腿还酸着呢。在这边我就不吐槽公司。

回来路上开车师傅路边买了点油桃,才两块钱一斤,那边的水果确实是好吃点,跟气候环境关系很大吧。看看他们那边居民生活也挺自在的,景点工作人员早上七点集合一起去山上上班,平时没什么人没什么事就坐在那玩玩手机睡睡觉,等到下午五点就下班回去吃饭了。

回来的火车不知道因为什么事还晚点了半小时左右,我是中转去济南西再到上海的,还好给自己留的时间够长,不然又得要费一番劲了。回到上海晚上八点多,回到住的地方晚上十点多,喝了点稀饭吃了两个家里带的粽子,澡就第二天洗了,弄弄就睡了。

回来公司还有一堆事。

搬搬搬

回头再来说说搬家这个事。

其它东西倒还好,搬的地方也不是很远,还是就那个缸是真的不好弄。思来想去还是叫了辆依维柯,一趟省事,六七十倒也不贵。

周末大雨啊,必是一番雨淋淋。之前一直是直接用的投影,没用上显示器,这下那边房间虽然小但是有桌子,显示器也有地方可以放了,刚好家里有个闲置的显示器,这不得要叫我姐给我寄过来,顺丰21,我最喜欢的数字。家里电脑配置都是七八年前的了,看年底的时候要不要把这边的电脑给他们带回去,自己再重新配一台,要努力赚钱攒钱咯。

这么一搬,房租肯定是上去了,不过还好,还能接受吧。独自在外无人关怀,也不知道这样还要再过上几年啊。


最近发现一个很好听的声音,小C英乐,学英语又有动力了!


走近 Java 世界中的线程

本文摘抄自《Java 多线程编程实战指南》核心篇 第一章小结

本章介绍了线程、多线程编程这两个基本概念以及 Java 平台对线程的实现。


  • 进程是程序的运行实例,一个进程可以包含多个线程,这些线程共享其所在进程的资源。

  • 线程是进程中可独立执行的最小单位。Java 标准库类 java.lang.Thread 就是 Java 平台对线程的实现。特定线程总是在执行特定的任务,线程的 run 方法就是线程所要执行任务的处理逻辑的入口方法,该方法由 Java 虚拟机直接调用执行。Java 标准库接口 java.lang.Runnable 就是对任务的抽象,Thread 类就是 Runnable 接口的一个实现类

  • 应用程序负责线程的创建与启动,而线程调度器负责线程的调度和执行。Java 平台中有两种方式创建线程:创建 Thread 的子类和以 Runnable 接口实例为构造器参数直接通过 new 创建 Thread 实例。

  • 在 Java 平台中,任何一段代码总是执行在确定的代码中的。同一段代码可以被不同的线程执行。代码可以通过 Thread.currentThread() 调用来获取其当前执行线程。

  • 为每个线程设置一个简短而含义明确的名称属性有助于多线程程序的调试和问题定位。

  • 一个线程从其创建到运行结束的整个生命周期会经历若干状态。线程执行过程中调用一些对象的方法(如 Thread.sleep(long millis))或者执行特定的操作(如 I/O 操作)往往导致其状态的变更。线程转储是对线程进行监视的重要媒介。操作系统以及 JDK 都提供了一些工具(jvisualvm、jstack 和 Java Mission Control),可以用来获取线程转储。

  • Java 平台是一个多线程的平台,线程的身影在 Java 平台中无处不在。按照线程间的创建关系,我们可以将多个线程间的关系理解为一个层次关系。Java 并无相关 API 用于获取一个线程的父线程或子线程,父线程和子线程之间的生命周期并无必然联系。

  • 线程是多线程编程的基本单位。多线程编程一方面有助于提高系统的吞吐率、提高软件的响应性、充分利用多核处理器资源、最小化对系统资源的使用和简化程序的结构,另一方面面临线程安全问题、线程活性问题、上下文切换和可靠性等问题。因此,多线程编程绝不仅仅是使用多个线程进行编程那么简单,多线程编程有其自身需要解决的问题,而这正是后续章节的主要内容。

本章知识结构图


SpringBoot2 实现邮件发送功能

springboot2 实现邮件发送功能,QQ/Gmail/163/126..

个人博客:DoubleFJ の Blog

效果图如下:springboot 实现邮件发送

技术选型

  • Spring Boot 2.1.3.RELEASE (原本官网推荐 2.1.5.RELEASE,可是搭建途中发现部分注解未生效,故改之)
  • Thymeleaf (用作邮件模板)
  • JDK 1.8

简要讲解

依赖以及配置

这里还是用的 Spring Boot 来整合,自带了模块依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

随着 Spring Boot 热度越来越大,现在从事 Java 的要是不知道 Spring Boot 的存在那就真的很不应该了。本来只是为了取代繁琐的 EJB,一直发展到了如今无所不在的地步。

然后在配置文件中进行对应的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8887

spring:
mail:
host: smtp.163.com
username: ffj0721@163.com
password: xxxx # 授权码
protocol: smtp
properties.mail.smtp.auth: true
properties.mail.smtp.port: 994
properties.mail.display.sendmail: DoubleFJ
properties.mail.display.sendname: Spring Boot Email
properties.mail.smtp.starttls.enable: true
properties.mail.smtp.starttls.required: true
properties.mail.smtp.ssl.enable: true
default-encoding: utf-8
from: ffj0721@163.com

一切就是这样的简单清晰。不同的邮件个别配置数据不同,请自行查阅,这里只用 163 做测试。

配置了之后我们开始操刀敲代码,其实只需要调用 JavaMailSender 接口即可,传参实现,已经给我们封装好了。

常用邮件接口

这里是几个常用的邮件接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.springbootmail.service;
import javax.mail.MessagingException;

/**
* 常用邮件接口
*/
public interface IMailService {
/**
* 发送文本邮件
* @param to
* @param subject
* @param content
*/
public void sendSimpleMail(String to, String subject, String content);

public void sendSimpleMail(String to, String subject, String content, String... cc);

/**
* 发送HTML邮件
* @param to
* @param subject
* @param content
* @throws MessagingException
*/
public void sendHtmlMail(String to, String subject, String content) throws MessagingException;

public void sendHtmlMail(String to, String subject, String content, String... cc);

/**
* 发送带附件的邮件
* @param to
* @param subject
* @param content
* @param filePath
* @throws MessagingException
*/
public void sendAttachmentsMail(String to, String subject, String content, String filePath) throws MessagingException;

public void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc);

/**
* 发送正文中有静态资源的邮件
* @param to
* @param subject
* @param content
* @param rscPath
* @param rscId
* @throws MessagingException
*/
public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId) throws MessagingException;

public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc);

}

接口实现类注入 JavaMailSender

实现类分别实现上述接口 注入 JavaMailSender

1
2
3
4
5
6
7
8
9
10
11
/**
* 发送邮件实现类
*/
@Service
public class IMailServiceImpl implements IMailService {

@Autowired
private JavaMailSender mailSender;

@Value("${spring.mail.from}")
private String from;

其中 from 就是配置中我们配置的发送方,直接读取使用。

实现发送文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 发送文本邮件
*
* @param to
* 邮件接收方
* @param subject
* 邮件标题
* @param content
* 邮件内容
*/
@Override
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}

/**
*
* @param to
* 邮件接收方
* @param subject
* 邮件标题
* @param content
* 邮件内容
* @param cc
* 抄送方
*/
@Override
public void sendSimpleMail(String to, String subject, String content, String... cc) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setCc(cc);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}

如上所示,三个参数的,第一个是你要发邮件的对象,第二个是邮件标题,第三个是邮件发送的内容,第二个 cc 字符串数组就是需要抄送的对象。

实现发送 HTML 邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 发送 HTML 邮件
*
* @param to
* @param subject
* @param content
*/
@Override
public void sendHtmlMail(String to, String subject, String content) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

// true 表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

mailSender.send(message);
}

官网 MimeMessageHelper 使用介绍。

实现发送带附件邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 发送带附件的邮件
*
* @param to
* @param subject
* @param content
* @param filePath
*/
public void sendAttachmentsMail(String to, String subject, String content, String filePath) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

FileSystemResource file = new FileSystemResource(new File(filePath));
// 截取附件名
String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
helper.addAttachment(fileName, file);

mailSender.send(message);
}

这里附件名的截取要按照自己的实际需求来,有人喜欢用 \\,有人喜欢用 /,看具体情况具体分析了。

实现发送正文中有静态资源邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 发送正文中有静态资源(图片)的邮件
*
* @param to
* @param subject
* @param content
* @param rscPath
* @param rscId
*/
public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

FileSystemResource res = new FileSystemResource(new File(rscPath));
helper.addInline(rscId, res);

mailSender.send(message);
}

其中 rscId 是资源的唯一 id,rscPath 就是对应资源的路径。具体待会我们看 Controller 调用方法。

实现发送模板邮件

前面说了我们选择使用 Thymeleaf 作为邮件的模板,那就需要在 POM 文件中加入对应依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

然后在 templates 文件夹下新建 mailTemplate.html 页面,我的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>spring-boot-mail test</title>
<style>
body {
text-align: center;
margin-left: auto;
margin-right: auto;
}
#welcome {
text-align: center;
}
</style>
</head>
<body>
<div id="welcome">
<h3>Welcome To My Friend!</h3>

GitHub:
<a href="#" th:href="@{${github_url}}" target="_bank">
<strong>GitHub</strong>
</a>
<br />
<br />
个人博客:
<a href="#" th:href="@{${blog_url}}" target="_bank">
<strong>DoubleFJ の Blog</strong>
</a>
<br />
<br />
<img width="258px" height="258px"
src="https://raw.githubusercontent.com/Folgerjun/materials/master/blog/img/WC-GZH.jpg">
<br />微信公众号(诗词鉴赏)
</div>
</body>
</html>

这个模板自己 DIY 即可,不过对应填充参数不可弄错。例如我上面的是 github_urlblog_url

Thymeleaf 官网

调用实现

MailController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.example.springbootmail.controller;

import com.example.springbootmail.service.impl.IMailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@RestController
@RequestMapping("/Mail")
public class MailController {

private static final String SUCC_MAIL = "邮件发送成功!";
private static final String FAIL_MAIL = "邮件发送失败!";

// 图片路径
private static final String IMG_PATH = "C:/Users/zjj/Desktop/github/materials/blog/img/WC-GZH.jpg";
// 发送对象
private static final String MAIL_TO = "folgerjun@gmail.com";

@Autowired
private IMailServiceImpl mailService;
@Autowired
private TemplateEngine templateEngine;

@RequestMapping("/Email")
public String index(){
try {
mailService.sendSimpleMail(MAIL_TO,"这是一封普通的邮件","这是一封普通的SpringBoot测试邮件");
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/htmlEmail")
public String htmlEmail(){
try {
mailService.sendHtmlMail(MAIL_TO,"这是一HTML的邮件","<body>\n" +
"<div id=\"welcome\">\n" +
" <h3>Welcome To My Friend!</h3>\n" +
"\n" +
" GitHub:\n" +
" <a href=\"#\" th:href=\"@{${github_url}}\" target=\"_bank\">\n" +
" <strong>GitHub</strong>\n" +
" </a>\n" +
" <br />\n" +
" <br />\n" +
" 个人博客:\n" +
" <a href=\"#\" th:href=\"@{${blog_url}}\" target=\"_bank\">\n" +
" <strong>DoubleFJ の Blog</strong>\n" +
" </a>\n" +
" <br />\n" +
" <br />\n" +
" <img width=\"258px\" height=\"258px\"\n" +
" src=\"https://raw.githubusercontent.com/Folgerjun/materials/master/blog/img/WC-GZH.jpg\">\n" +
" <br />微信公众号(诗词鉴赏)\n" +
"</div>\n" +
"</body>");
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/attachmentsMail")
public String attachmentsMail(){
try {
mailService.sendAttachmentsMail(MAIL_TO, "这是一封带附件的邮件", "邮件中有附件,请注意查收!", IMG_PATH);
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/resourceMail")
public String resourceMail(){
try {
String rscId = "DoubleFJ";
String content = "<html><body>这是有图片的邮件<br/><img src=\'cid:" + rscId + "\' ></body></html>";
mailService.sendResourceMail(MAIL_TO, "这邮件中含有图片", content, IMG_PATH, rscId);

}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/templateMail")
public String templateMail(){
try {
Context context = new Context();
context.setVariable("github_url", "https://github.com/Folgerjun");
context.setVariable("blog_url", "http://putop.top/");
String emailContent = templateEngine.process("mailTemplate", context);

mailService.sendHtmlMail(MAIL_TO, "这是模板邮件", emailContent);
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}
}

程序运行后若访问 http://localhost:8887/Mail/Email 页面出现邮件发送成功!字样就说明邮件发送已经实现了。

相关链接


愉快五月(2019)

这个五月过得是真的快啊~

五一回家在家呆了三天,跟父母老姐和姐夫(已订婚)去了趟老家县城附近的自建的一个玻璃栈道景点。过年期间就在朋友圈看到刷屏了,这次趁着空一起去逛了逛。老家小县城本就四面环山,景色相当不错,也有几个国家 4A 级景点,什么仙华山、神丽峡、江南第一家啊……

我老爸恐高啊,我也有点。全程在桥上老爸搭着老妈的肩膀小心翼翼地走在玻璃桥的边缘,事后一直被老妈嘲笑,家里也是很少有这样的机会一家人一起出去玩玩,我很喜欢一家人在一起的感觉。

一定要多跟他们在一起!


五一回来上班一周,又出去耍了!公司旅游投票菲律宾和塞班岛,结果毫无疑问,塞班岛走起~大晚上的飞机四个半小时凌晨飞到那边,一下飞机热浪扑面而来,弄弄行李到达酒店,天就亮了,太快了吧。

进房间洗洗躺下,大太阳就刺进来了,朝东的房间,太丫的亮了。结果躺了起,起了躺,后来终于睡着了,本来是十点钟门口集合导游大巴带我们去景点逛,这一睡也太舒服了,空调打得低。微信群里无人反馈(睡死了都),随着同事破门而入,一阵慌乱中奔向大巴。

岛不大,逛逛一圈也用不了多久。

景色是真的不错,碧海蓝天白云。

(拍了不少帅比照,哈哈)

就是太阳毒了点,回来黑了不少,后背轻微脱皮。本想晒成古天乐的,差点晒了个熊猫眼,下次再晒。在海边一点腥味都没闻到,在这样的海边上住着才是真的享受啊。

不得不说,在老美的地盘花着美金消费就是高,资金不允许啊就没怎么玩多少项目,不过也是体验了拖伞和浮潜。大头都留在那边的赌场了……

逛了一圈那边的礼品店,乍一眼都是从中国的地摊上运过来的,本地也没什么特色,我也就没买东西,哈哈(反正买了也没人送)

玩着耍着,六天五晚的旅程就结束了,要回来上班咯。

再上两三个星期就端午了,再之后就只有到中秋国庆了,能回家还是要回家的啊。


刚从塞班岛回来就又去了临港出差,开了个房还没走进去就听到老大声的“老公老公,啊啊啊嗯……”,真特么刺激,真好。

晚上就在他们的欢声笑语中入睡。


生活都不易,且活且珍惜。

做最好的自己。


惊险四月(2019)

就在昨天,公司自己刚搭没多久的小服务器中了 ETH 勒索病毒,还好刚起步数据量不大,真可怕!

把环境又给装了一遍,轻车熟路了。

专业事情还是需要专业人士来做,网络安全也是一个相当大的方面,只要被不法分子盯上就很有可能是一笔不小的损失了。

虽然说现在的技术科技发展很快,但是安全方面却不是那么跟得上脚步。中午跟同事聊起来,有些公司还是用最初的刻盘来保存数据,银行系统还有很多是 XP,回到本初,最原始的即最安全也是不无道理,整得再花再虚,我最需要的还是安全实用。

从这些角度继续往下想,有时候我们会埋怨某些系统太过于古老,技术太过于落后,这也是前几年我看到一些古老项目的想法。技术的世界日新月异,新轮子不断出现,内涵包裹越来越深,过于追求抽象化。

当我们选择项目中要用的技术栈时,肯定会优先选择那些稳定的、有人长期维护的、社区较活跃的,这些现象也正说明了这个技术关注度高,较为成熟。不然找偏门技术栈的可能出现了 Bug,其创始人都不知道为什么会出现该怎么解决,那不就废了吗。现在开源社区大多都是拿来即用,也很少会有人去专门研究源码,这就是技术选型。

当你接手了一个老项目,你了解了其作用与每个地方的功能后,发现市面上完全有成熟的技术可以取代之,且换架构后项目会更加清晰易理解。这个时候你就可以向上申报或者自己闲暇时间尝试给它换一个壳(当然是因人而异,也有可能是费力不讨好之事,若对自身有益且有闲时,可一做)。当完成这个“手术”之后,往往自己会对这个项目有了一个更深的理解,很可能还会找到几处优化的地方。改造成功一个项目,看着它,就像是你赋予了它生命一般,后期需要改动或是加功能打补丁都会更得心应手。

最近在看《深入理解计算机系统》,本是一本基础教材。明白了“万变不离其宗”的道理之后,深知基础的重要性。别人问我:你们搞计算机的要记的东西不少,感觉很难啊。我说:只要你脑子不笨不呆,灵活会转会用心记就行。

调调几个 API 谁不会呢,我才不管你是怎么实现的,我只要拿到我想要的数据得到我想要的结果就行了,不就是一堆数据传过来传过去。现在框架轮子多得死,一拼一凑就是一个“项目”。

可不是嘛,平时要写的就是一大坨的业务逻辑处理代码,正常的有几个会拎不清呢?当然也是有的,所以这层就开始出现了能力的差别。越往细小的领域,这差距就会越大。

业务逻辑写个几年,天花板就到头了,若是脑子不活络无管理能力的,那就是真的“废”了。虽说饿不死,至少以后找个公司维护维护老项目都能养活自己。这就导致了以后没有了 竞争力,年级越大越无奈,这一阶段的人可以说是到处都是,市场完全不会缺。

前一段时间招了个实习生(上海理工研二),自动化专业想要来写代码(跨专业,基础确实是差),说实话已是一片红海,若是像我上面说的那样还要去那个阶段一起游泳,那我觉得真是有点不值了。


前几天智齿长出来了!右腮帮就像被人挥了一拳一样,感觉是真的不爽!本命年才会长智齿,真的是准啊,长大了长大了。

同事搬过来变成了新室友,好家伙现在天天晚上都要粥煮起来吃,不吃睡不着,这吃下去再不长肉就真不科学了,再加上每天下午去楼下全家买两个包子或是两个馒头。搞了张尊享会员卡,也开始了没事喝喝咖啡的日子了(送的不喝那我就是有病了),血压低,多喝喝。

哈哈,室友将他那辆二手永久车送我了!我自己去补了个胎,就花了二十换了辆自行车,怎么都不亏啊哈哈。刚好摩拜季卡到期了,共享单车又集体涨价,这辆三手车(可能不止三手了……)至少能骑个半年咯。

等夏天到了,早上戴着墨镜骑着车听听音乐去上班,然后一身汗,湿透。


有名气有底蕴的大学是真的不一样,看了附近同济、复旦的校区后,自己的母校是何等的简陋。。。以后还要多去其它学校看看,欣赏欣赏别人大学的风景。

生活在于折腾,前提是还能经得起折腾。年纪轻轻就不要想那么多,想要的想做的就尽量去满足自己,一旦念头消散可能就再无机会了。

至少还能有可以愿意去回想的东西,还有内心觉得美好的日子,还有至今无悔的事情。

也许现在的生活并不是那么尽人意,但若是有可以回去的机会我还是会选择那样过,还是想要经历那些经历过的事情,虽然也许无果但无悔、无愧,青春,知足。


我这个人啊,耳根子软,心也软又善,还重情义。你若对我一丝好,我便会含泪感动入睡。

不过这不代表一向如此,上述只是我大多状态。人啊,久了都会不懂自己。

最近在补权游,Valar Morghulis.