线程管理

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

本章介绍了如何将线程管控起来以便高效/可靠地利用线程这种有限地资源。


  线程组是 Thread.UncaughtExceptionHandler 的一个实现类,它可以帮助我们检测线程的异常终止。多数情况下,我们可以忽略线程组这一概念以及线程组的存在。

  Thread.UncaughtExceptionHandler 接口使得我们能够侦测到线程运行过程中抛出的未捕获的异常,以便做出相应的补救措施,例如创建并启动相应的替代线程。一个线程在其抛出未捕获的异常而终止前,总有一个 UncaughtExceptionHandler 实例会被选中。被选中的 UncaughtExceptionHandler 实例的 uncaughtException 方法会被该线程在其终止前执行。UncaughtExceptionHandler 实例 > 线程所在线程组 > 默认 UncaughtExceptionHandler。

  线程工厂 ThreadFactory 能够封装线程的创建与配置的逻辑,这使得我们能够对线程的创建与配置进行统一的控制。

  利用条件变量我们能实现线程的暂挂与恢复,用于替代 Thread.suspend()/resume() 这两个废弃的方法。

  线程池是生产者——消费者模式的一个具体例子,它能够摊销线程的创建/启动与销毁的开销,并在一定程度上有利于减少线程调度的开销。线程池使得我们能够充分利用有限的线程资源。ThreadPoolExecutor 支持核心线程大小以及最大线程池大小这两种阈值来控制线程池中的工作者线程总数。ThreadPoolExecutor 支持对核心线程以外的空闲了指定时间的工作者线程进行清理,以减少不必要的资源消耗。RejectedExecutionHandler 接口使得我们能够对被线程池拒绝的任务进行重试以提高系统的可靠性。Future 接口使得我们可以获取提交给线程池执行的任务的处理结果/侦测任务处理异常以及取消任务的执行。当一个线程池实例不再被需要的时候,我们需要主动将其关闭以节约资源。ThreadPoolExecutor 提供了一组能够对线程池进行监控的方法,通过这些方法我们能够了解线程池的当前线程池大小/工作队列的情况等数据。同一个线程池只能用于执行相互独立的任务,彼此有依赖关系的任务需要提交给不同的线程池执行以避免死锁。我们可以通过线程工厂为线程池中的工作者线程关联 UncaughtExceptionHandler,但是这些 UncaughtExceptionHandler 只会对通过 ThreadPoolExecutor.execute 方法提交给线程池的任务起作用。

本章知识结构图


糊涂八月(2019)

稀里糊涂稀里糊涂啊

狂风暴雨 阳光烈焰 雾雨蒙蒙


昆明

不出意外,跟同事一起又跑了趟昆明。没想到的是呆了七天,干了七天的活,出去耍的时间都没有,惨兮兮。

呈贡大学城也确实偏了点,连市区都没得去,只能在附近随便吃点喝点。米粉确实是正宗还实惠,十三块钱吃得我啤酒肚了,可总不能天天搞米粉吃吧。

阴凉地小风吹得,舒服极了,站在太阳直射下却不一会就晒得通红,热得要炸。

从去的那天开始内火就开始旺啊,上次来也不见得体内火气这么大,还是天气太干燥了,不适应。

水果都是按公斤计价的,买了点桃子芒果,便宜倒也不是很便宜,总之比上海还是要便宜点的,在上海都不舍得买水果吃。

一直心心念念想要吃炸虫子,吃爆浆的那种,可是到走的那天也没能吃着,亏边上同事还是在昆明读过几年书的,真的一点不靠谱啊。后来去了家农家乐随便吃了点,大盘的菜,量大,当然相对昆明的物价来说也不能算便宜。

虽说附近是大学城,可真也不巧,刚好暑假。校园里也都没几个人,看到的大多是对对小情侣,不禁感叹:大学生活好啊!

地区会影响人的外貌习性,总的看来,个人还是较喜欢沿海的南方妹子,下次有机会去传说中的成都/长沙养养眼,见识见识。

唉,出差真的也是累得慌啊。

七夕

什么七不七的,那天半夜才在浦东下的飞机,热乎饭都没能吃上口。

台风利奇马

这个台风是真的厉害了,刚好周末登陆各种预警,那天真是狂风催嫩叶啊。刚登陆温岭的时候风速巨大,陆地后强度减弱,听新闻说也有不少人遭难,天灾。

已经造成浙江损失一百多亿了。

周五晚已经是暴雨连连了,想着周末也铁定出不了门了就在超市备了两天的伙食,自己随便开开火得了。周六确实是下了一天的雨,挂了一天的风,到了周日台风过去后雨过天晴,一切又像是都没发生过一样,只有地上的落叶能说明一切。

无锡

无锡太湖试验厅项目需要测个索力数据,没人啊,派我过去了呗。无锡太湖一日游,也就在边角眺望了一会。

跟同事晚上吃了个自助,喝了点小酒,本想着再去洗个脚按个摩,奈何吃饱喝足累得慌,还是回去早点休息吧。

做的事越来越杂,好像已经开始脱离了最初进来时的职责。

周末

现在有了个喜欢做饭的室友,周末比之前是好多了,对面就是超市买菜也方便。弄个一荤两素一汤,健康又实惠。不过这次周末有点奢侈,大荤大素的整了不少,室友还把“珍藏”的红酒给贡献了出来。说好的吃素,这整的,以后吃吃蔬菜就得了。

身上的湿气重啊,房间里放的除湿袋不到一星期就积满了水,也可能跟我那朝北的小房间有关系,不过最近感觉全身都难受,站着不动都能往外出水来着。看看找个时间需要去拔个罐去去湿,出门在外要对自己好点。

压抑

不知道为什么,最近情绪不太高总觉得很压抑,胸闷有时候喘不过气来。

很难受,周末就去找了同学一起聚一聚,也有好长时间没有聚过了,大家都多多少少有点压抑。

胡吃海玩了之后回来还是不太好,我这是怎么了?凌晨两点的杭州夜景也治愈不了我。

什么都没意思,好难受啊,何以解忧


线程的活性故障

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

本章介绍了常见的线程活性故障以及相应的规避措施。


  死锁会导致相关线程一直被暂停使得其任务无法进展。产生死锁的必要条件包括:资源互斥/资源不可抢夺/占用并等待资源以及循环等待资源。我们可以通过查看线程转储手工检测死锁,也可以利用 ThreadMXBean.findDeadlockedThreads() 方法进行死锁的自动检测。死锁的规避方法包括:粗锁法(使用一个粗粒度的锁代替多个锁)/锁排序法(相关线程使用全局统一的顺序申请锁)/使用 ReentrantLock.tryLock(long,TimeUnit)来申请锁/使用开放调用(在调用外部方法时不加锁)以及使用锁的替代品。使用内部锁或者使用 lock.lock() 申请的显式锁导致的死锁是无法恢复的;使用 lock.lockInterruptibly()申请的显式锁导致的死锁理论上是可恢复的,但实际可操作性不强——自动恢复的尝试可能是徒劳且有害的(导致活锁)。

  锁死是等待线程由于某种原因一直无法被唤醒而导致其任务无法进展的一种活性故障。信号丢失锁死是由于没有相应的通知线程来唤醒等待线程而使等待线程一直处于等待状态的一种活性故障。嵌套监视器锁死是嵌套锁导致通知线程无法获得其为唤醒等待线程所需的锁从而使其无法唤醒等待线程,最终使得通知线程与等待线程都一直处于等待状态的一种活性故障。嵌套监视器锁死可以通过查看线程转储进行检测。为规避嵌套监视器锁死,我们应该避免在嵌套锁的内层临界区内实现等待/通知。

  线程饥饿指线程一直无法获得其所需的资源而导致其任务一直无法进展的一种活性故障。把锁看成一种资源,那么死锁可被看作一种线程饥饿。饥饿可能演变成活锁。

  活锁是线程一直在做无用功而使其任务一直无法进展的一种活性故障。试图进行死锁故障恢复可能导致活锁。

本章知识结构图


Spring JPA Data with REST

逛 Spring 官网学习总结


@RepositoryRestResource 看到这个注解,之前一直没有用到过,所以想要自己试试效果,顺道做下总结。

不想要看我废话想要直接看官网的Accessing JPA Data with REST

不想要看我废话想看大佬的Spring Boot之@RepositoryRestResource注解入门使用教程


构建项目

用 Maven 构建项目,建一个 Person 实体类,再建一个 PersonRepository 接口,这个是关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello;

import java.util.List;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {

List<Person> findByLastName(@Param("name") String name);

}

官网是这么介绍的:

1
At runtime, Spring Data REST will create an implementation of this interface automatically. Then it will use the @RepositoryRestResource annotation to direct Spring MVC to create RESTful endpoints at /people.

我们翻译过来就是:

1
在运行时,Spring Data REST将自动创建此接口的实现。然后它将会在使用 @RepositoryRestResource 注解的 Spring MVC 在 /people 创建 RESTful 端点。

点进 PagingAndSortingRepository 接口看:

1
2
3
4
5
6
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort var1);

Page<T> findAll(Pageable var1);
}

看到该接口还是继承了平时常使用的 CrudRepository,这就说明那些基本的增删改查方法也都有。

为了方便,我还写了一个接口用来保存 Person 实体,为后续测试使用。

1
2
3
4
5
6
7
8
9
10
@Service
public class PersonService {

@Autowired
PersonRepository personRepository;

public void savePerson(Person person){
personRepository.save(person);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/person")
public class PersonController {

@Autowired
PersonService personService;

@PutMapping("/save")
public String savePerson(Person person){
personService.savePerson(person);
return "success";
}
}

很简单的一个映射。

到这为止,我们的项目就全构建好了。

嫌麻烦不想自己动手建项目的GitHub

测试功能

我这里为了方便,用的是 Postman 进行测试。

首先访问 http://localhost:8080,返回的是:

1
2
3
4
5
6
7
8
9
10
11
{
"_links": {
"people": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile"
}
}
}

再访问 http://localhost:8080/people,返回的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"_embedded": {
"people": []
},
"_links": {
"self": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 20,
"totalElements": 0,
"totalPages": 0,
"number": 0
}
}

因为里面没有数据,我们先塞几个数据进去,这时候就利用我们之前写的接口来操作。如下:

1
2
3
我们用 PUT 方式去请求 http://localhost:8080/person/save?firstName=aaa&lastName=bbb

返回 success 就说明操作成功,成功塞入。

同样的方式我们再多塞几个。

塞完值之后我们接着去访问 http://localhost:8080/people,这时候发现结果与之前的稍有不同了:

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
{
"_embedded": {
"people": [
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
},
{
"firstName": "aaa",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/2"
},
"person": {
"href": "http://localhost:8080/people/2"
}
}
},
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 20,
"totalElements": 3,
"totalPages": 1,
"number": 0
}
}

是的,我们刚塞的值都查出来了。

接着测试,访问 http://localhost:8080/people/1,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
}

是的,就是第一条数据被查出来了,同理 /2 /3 也是。

访问 http://localhost:8080/people/search,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
{
"_links": {
"findByLastName": {
"href": "http://localhost:8080/people/search/findByLastName{?name}",
"templated": true
},
"self": {
"href": "http://localhost:8080/people/search"
}
}
}

对的,findByLastName{?name} 就是我们在里面新增的那个接口,那就顺着这个意思咱们继续来。

访问 http://localhost:8080/people/search/findByLastName?name=folger,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"_embedded": {
"people": [
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people/search/findByLastName?name=folger"
}
}
}

与我们猜测无二。接口写的返回是集合,所以就返回了一个集合。

之前还看到了这么一条 link "href": "http://localhost:8080/people{?page,size,sort}"

有经验的程序员一眼就肯定知道这些个参数都是干什么用的,是啊,就是分页用的。

我们访问这个 url : http://localhost:8080/people?page=0&size=4&sort=lastName,返回的结果为:

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
{
"_embedded": {
"people": [
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
},
{
"firstName": "aaa",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/2"
},
"person": {
"href": "http://localhost:8080/people/2"
}
}
},
{
"firstName": "ccc",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/4"
},
"person": {
"href": "http://localhost:8080/people/4"
}
}
},
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people"
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 4,
"totalElements": 4,
"totalPages": 1,
"number": 0
}
}

从返回的数据我们可以看出: page 当然是页数,size 就是每一页的个数,sort 就是排序的字段,默认为 asc。

这一波测试下来发现这样封装之后之前需要一大堆代码才能实现的分页,这里只需要一个注解即可。妙啊妙不可言。

相关链接


保障线程安全的设计技术

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

本章从面向对象编程的角度出发讲解了实现线程安全的几种常用技术。这些技术的运用通常可以产生具有固有线程安全性的对象,即这些对象本身无须借助锁就可以保障线程安全,从而有利于提高系统的并发性。本章还介绍了同步集合和并发集合。


  Java 运行时空间可分为堆空间/非堆空间以及栈空间。栈空间是线程的私有空间,而堆空间和非堆空间都是线程的共享空间。堆空间用于存储对象以及类的实例变量,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。非堆空间用于存储类的静态变量以及其他元数据,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。栈空间用于存储线程所执行的方法的局部变量/返回值等私有数据,它是线程创建时分配的容量固定不可变的存储空间。

  无状态对象不包含任何实例变量以及可更新的变量,它具有固有的线程安全性。无状态对象的客户端代码在调用该对象的任何方法时都无须加锁,而无状态对象自身的方法实现可能仍然需要借助锁。仅包含静态方法的类并不能取代无状态对象。Servlet 类通常需要被设计为无状态对象。

  不可变对象也具有固有的线程安全性。严格意义上的不可变对象需要同时满足这几个条件:类本身采用 final 修饰,所有字段都是 final 字段,在对象初始化过程中 this 代表的当前对象没有逸出,引用了状态可变的对象的字段不能直接暴露给其他对象。如果需要将引用了状态可变的对象的字段暴露给其他对象,那么需要在返回该对象前进行防御性复制,或者返回一个不支持 remove() 的 Iterator 实例。使用不可变对象建模时,系统状态的变化是通过创建新的不可变对象实现的。这种方式可能有利于提高垃圾回收效率,但也可能由于系统状态频繁变更/无状态对象占用较多内存空间等因素增加了垃圾回收的负担。不可变对象的典型应用场景包括:被建模对象的状态变化不频繁/同时对一组相关的数据进行写操作,因此需要保证原子性/使用不可变对象作为安全可靠的 Map 键。当被建模对象的状态变更比较频繁时,不可变对象也不见得就不能使用。此时,我们需要综合考虑被建模对象的规模/代码目标运行环境的 Java 虚拟机堆内存容量/系统对吞吐率和响应性的要求这几个因素。

  线程特有对象也具有固有的线程安全性。ThreadLocal 是线程访问其线程特有对象的代理。ThreadLocal 也被称为线程局部变量,一个线程可以通过使用不同的线程局部变量来访问不同的线程特有对象实例。多个线程即使是使用同一个线程局部变量,其访问到的对象也是各自的线程特有对象。线程局部变量通常作为一个类的静态字段来使用。为避免线程局部变量的使用导致内存泄露和伪内存泄露,我们需要确保在线程特有对象不再被需要的时候将其“删除”(即调用 ThreadLocal.remove())。线程特有对象的典型应用场景包括:需要使用非线程安全对象,但又不希望因此而引入锁;使用线程安全对象,但希望避免其使用的锁的开销和相关问题;实现方法间的隐式参数传递;实现特定于线程的单例模式。

  装饰器模式也能够用于实现线程安全。在使用装饰器模式的情况下,实现同一组功能的对象有非线程安全版和线程安全版两种。这两种对象具有相同的接口,其中非线程安全版对象仅关注功能的实现,而外包装对象(线程安全版)主要关注线程安全的保障。外包装对象在功能方面则是通过委托给相应的非线程安全对象来实现的。Java 并发集合就是使用装饰器模式来保障线程安全的。使用装饰器模式实现线程安全的优点是它支持关注点分离,并有利于降低开发难度和提高代码的可测试性,也有利于提高使用的灵活性。其缺点是并发性不高,并可能导致遍历操作是非线程安全的。

  并发集合一般可用于替代同步集合。其内部实现往往借助于 CAS 操作或者细粒度锁。并发集合支持线程安全的遍历操作,即对集合的遍历操作与更新操作是可以由不同线程并发执行的。并发集合实现线程安全的遍历操作由两种方式:快照和准实时。前者无法在遍历过程中反映其他线程对被遍历集合所做的更新,而后者在遍历过程中可能反映其他线程对被遍历集合所做的更新。CopyOnWriteArrayList 相当于 ArrayList 的线程安全版,它适用于遍历操作远比更新操作频繁或者不希望在遍历的时候加锁的场景,在其他场景下我们仍然要考虑使用相应的同步集合。CopyOnWriteArraySet 相当于 HashSet 的线程安全版,内部实现是基于 CopyOnWriteArrayList 的。因此,CopyOnWriteArraySet 适用场景与 CopyOnWriteArrayList 类似。ConcurrentLinkedQueue 相当于 LinkedList 的线程安全版,与 BlockingQueue 的实现类相比,ConcurrentLinkedQueue 适用于更新操作和遍历操作并发的场景。BlockingQueue 的实现类更适合于多个线程并发更新同一队列的场景,如生产者——消费者模式中。ConcurrentSkipListMap 相当于 TreeMap 的线程安全版。ConcurrentSkipListSet 相当于 TreeSet 的线程安全版。

本章知识结构图


线程间协作

本文摘抄自《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 状态,但是其要执行的任务一直无法进展,即产生了活性故障。

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

本章知识结构图