0%

建立如下的表格:

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
SELECT * FROM tdb_goods;
+----------+----------------------------------------------------------+---------------+------------+-------------+---------+------------+
| goods_id | goods_name | goods_cate | brand_name | goods_price | is_show | is_saleoff |
+----------+-------------------------------------------------------------+---------------+------------+-------------+---------+------------+
| 1 | R510VC 15.6英寸笔记本 | 笔记本 | 华硕 | 3399.000 | 1 | 0 |
| 2 | Y400N 14.0英寸笔记本电脑 | 笔记本 | 联想 | 4899.000 | 1 | 0 |
| 3 | G150TH 15.6英寸游戏本 | 游戏本 | 雷神 | 8499.000 | 1 | 0 |
| 4 | X550CC 15.6英寸笔记本 | 笔记本 | 华硕 | 2799.000 | 1 | 0 |
| 5 | X240(20ALA0EYCD) 12.5英寸超极本 | 超级本 | 联想 | 4999.000 | 1 | 0 |
| 6 | U330P 13.3英寸超极本 | 超级本 | 联想 | 4299.000 | 1 | 0 |
| 7 | SVP13226SCB 13.3英寸触控超极本 | 超级本 | 索尼 | 7999.000 | 1 | 0 |
| 8 | iPad mini MD531CH/A 7.9英寸平板电脑 | 平板电脑 | 苹果 | 1998.000 | 1 | 0 |
| 9 | iPad Air MD788CH/A 9.7英寸平板电脑 (16G WiFi版) | 平板电脑 | 苹果 | 3388.000 | 1 | 0 |
| 10 | iPad mini ME279CH/A 配备 Retina 显示屏 7.9英寸平板电脑 (16G WiFi版) | 平板电脑 | 苹果 | 2788.000 | 1 | 0 |
| 11 | IdeaCentre C340 20英寸一体电脑 | 台式机 | 联想 | 3499.000 | 1 | 0 |
| 12 | Vostro 3800-R1206 台式电脑 | 台式机 | 戴尔 | 2899.000 | 1 | 0 |
| 13 | iMac ME086CH/A 21.5英寸一体电脑 | 台式机 | 苹果 | 9188.000 | 1 | 0 |
| 14 | AT7-7414LP 台式电脑 (i5-3450四核 4G 500G 2G独显 DVD 键鼠 Linux ) | 台式机 | 宏碁 | 3699.000 | 1 | 0 |
| 15 | Z220SFF F4F06PA工作站 | 服务器/工作站 | 惠普 | 4288.000 | 1 | 0 |
| 16 | PowerEdge T110 II服务器 | 服务器/工作站 | 戴尔 | 5388.000 | 1 | 0 |
| 17 | Mac Pro MD878CH/A 专业级台式电脑 | 服务器/工作站 | 苹果 | 28888.000 | 1 | 0 |
| 18 | HMZ-T3W 头戴显示设备 | 笔记本配件 | 索尼 | 6999.000 | 1 | 0 |
| 19 | 商务双肩背包 | 笔记本配件 | 索尼 | 99.000 | 1 | 0 |
| 20 | X3250 M4机架式服务器 2583i14 | 服务器/工作站 | IBM | 6888.000 | 1 | 0 |
| 21 | 玄龙精英版 笔记本散热器 | 笔记本配件 | 九州风神 | 0.000 | 1 | 0 |
| 22 | HMZ-T3W 头戴显示设备 | 笔记本配件 | 索尼 | 6999.000 | 1 | 0 |
| 23 | 商务双肩背包 | 笔记本配件 | 索尼 | 99.000 | 1 | 0 |
+----------+-----------------------------------------------+---------------+------------+-------------+---------+------------+

计算所有商品的平均价格

1
SELECT AVG(goods_price) AS "average price" FROM tdb_goods;

评价价格保留两位小数

1
SELECT ROUND(AVG(goods_price),2) AS "average price" FROM tdb_goods;

计算高于平均价格的商品

1
SELECT goods_id,goods_name,goods_price FROM tdb_goods WHERE goods_price>=5635.36;

使用子查询查询价格高于平均价格的商品信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT goods_id,goods_name,goods_price FROM tdb_goods WHERE goods_price>=(
SELECT AVG(goods_price) FROM tdb_goods
);
+----------+----------------------------------+-------------+
| goods_id | goods_name | goods_price |
+----------+----------------------------------+-------------+
| 3 | G150TH 15.6英寸游戏本 | 8499.000 |
| 7 | SVP13226SCB 13.3英寸触控超极本 | 7999.000 |
| 13 | iMac ME086CH/A 21.5英寸一体电脑 | 9188.000 |
| 17 | Mac Pro MD878CH/A 专业级台式电脑 | 28888.000 |
| 18 | HMZ-T3W 头戴显示设备 | 6999.000 |
| 20 | X3250 M4机架式服务器 2583i14 | 6888.000 |
| 22 | HMZ-T3W 头戴显示设备 | 6999.000 |
+----------+----------------------------------+-------------+
7 rows in set (0.00 sec)

使用子查询 计算高于平均价格的所有商品的评价价格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT AVG(goods_price) FROM tdb_goods WHERE goods_price>=(SELECT AVG(goods_price) FROM tdb_goods);
+------------------+
| AVG(goods_price) |
+------------------+
| 10780.0000000 |
+------------------+

使用avg函数时, 只有一条记录, 其他字段的信息只显示第一条.
SELECT goods_id,goods_name,goods_price,AVG(goods_price) FROM tdb_goods WHERE goods_price>=(SELECT AVG(goods_price) FROM tdb_goods);
+----------+-----------------------+-------------+------------------+
| goods_id | goods_name | goods_price | AVG(goods_price) |
+----------+-----------------------+-------------+------------------+
| 3 | G150TH 15.6英寸游戏本 | 8499.000 | 10780.0000000 |
+----------+-----------------------+-------------+------------------+

利用子查询查询比”超极本”贵的商品信息(这里的”贵”就体现于多条记录比较时的处理)

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
select goods_price from tdb_goods where goods_cate='超级本';
+-------------+
| goods_price |
+-------------+
| 4999.000 |
| 4299.000 |
| 7999.000 |
+-------------+

SELECT goods_id,goods_name,goods_price FROM tdb_goods
WHERE goods_price >ANY (
select goods_price from tdb_goods where goods_cate='超级本'
);
+----------+----------------------------------+-------------+
| goods_id | goods_name | goods_price |
+----------+----------------------------------+-------------+
| 2 | Y400N 14.0英寸笔记本电脑 | 4899.000 |
| 3 | G150TH 15.6英寸游戏本 | 8499.000 |
| 5 | X240(20ALA0EYCD) 12.5英寸超极本 | 4999.000 |
| 7 | SVP13226SCB 13.3英寸触控超极本 | 7999.000 |
| 13 | iMac ME086CH/A 21.5英寸一体电脑 | 9188.000 |
| 16 | PowerEdge T110 II服务器 | 5388.000 |
| 17 | Mac Pro MD878CH/A 专业级台式电脑 | 28888.000 |
| 18 | HMZ-T3W 头戴显示设备 | 6999.000 |
| 20 | X3250 M4机架式服务器 2583i14 | 6888.000 |
| 22 | HMZ-T3W 头戴显示设备 | 6999.000 |
+----------+----------------------------------+-------------+

SELECT goods_id,goods_name,goods_price FROM tdb_goods WHERE goods_price > ALL (
select goods_price from tdb_goods where goods_cate='超级本'
);
+----------+----------------------------------+-------------+
| goods_id | goods_name | goods_price |
+----------+----------------------------------+-------------+
| 3 | G150TH 15.6英寸游戏本 | 8499.000 |
| 13 | iMac ME086CH/A 21.5英寸一体电脑 | 9188.000 |
| 17 | Mac Pro MD878CH/A 专业级台式电脑 | 28888.000 |
+----------+----------------------------------+-------------+

SELECT goods_id,goods_name,goods_price FROM tdb_goods WHERE goods_price = ANY (
select goods_price from tdb_goods where goods_cate='超级本'
);
+----------+---------------------------------+-------------+
| goods_id | goods_name | goods_price |
+----------+---------------------------------+-------------+
| 5 | X240(20ALA0EYCD) 12.5英寸超极本 | 4999.000 |
| 6 | U330P 13.3英寸超极本 | 4299.000 |
| 7 | SVP13226SCB 13.3英寸触控超极本 | 7999.000 |
+----------+---------------------------------+-------------+

多表更新:
参照分类表更新商品表

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
mysql> CREATE TABLE IF NOT EXISTS tdb_goods_cates(
-> cate_id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
-> cate_name VARCHAR(40) NOT NULL
-> );

SHOW COLUMNS FROM tdb_goods_cates;
+-----------+----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+----------------------+------+-----+---------+----------------+
| cate_id | smallint(5) unsigned | NO | PRI | NULL | auto_increment |
| cate_name | varchar(40) | NO | | NULL | |
+-----------+----------------------+------+-----+---------+----------------+

insert into tdb_goods_cates(cate_name) select goods_cate from tdb_goods
group by goods_cate;
Query OK, 7 rows affected (0.07 sec)


SELECT * FROM tdb_goods_cates;
+---------+---------------+
| cate_id | cate_name |
+---------+---------------+
| 1 | 台式机 |
| 2 | 游戏本 |
| 3 | 平板电脑 |
| 4 | 笔记本 |
| 5 | 笔记本配件 |
| 6 | 超级本 |
| 7 | 服务器/工作站 |
+---------+---------------+

UPDATE tdb_goods
INNER JOIN tdb_goods_cates
ON goods_cate = cate_name
SET goods_cate=cate_id;
Query OK, 23 rows affected (0.08 sec)

SELECT * FROM tdb_goods\G;
*************************** 1. row ***************************
goods_id: 1
goods_name: R510VC 15.6英寸笔记本
goods_cate: 4
brand_name: 华硕
goods_price: 3399.000
is_show: 1
is_saleoff: 0
*************************** 2. row ***************************
goods_id: 2
goods_name: Y400N 14.0英寸笔记本电脑
goods_cate: 4
brand_name: 联想
goods_price: 4899.000
is_show: 1
is_saleoff: 0
*************************** 3. row ***************************
goods_id: 3
goods_name: G150TH 15.6英寸游戏本
goods_cate: 2
brand_name: 雷神
goods_price: 8499.000
is_show: 1
is_saleoff: 0
...

CREATE TABLE goods_brands(
id TINYINT AUTO_INCREMENT PRIMARY KEY,
brand_name VARCHAR(20)
)
SELECT brand_name FROM tdb_goods GROUP BY brand_name;

SELECT * FROM goods_brands;
+----+------------+
| id | brand_name |
+----+------------+
| 1 | 联想 |
| 2 | 雷神 |
| 3 | 索尼 |
| 4 | IBM |
| 5 | 苹果 |
| 6 | 戴尔 |
| 7 | 宏碁 |
| 8 | 惠普 |
| 9 | 华硕 |
| 10 | 九州风神 |
+----+------------+

update tdb_goods INNER JOIN goods_brands
-> on tdb_goods.brand_name=goods_brands.brand_name
-> set tdb_goods.brand_name=goods_brands.id;
Query OK, 23 rows affected (0.09 sec)


SELECT * FROM tdb_goods\G;
*************************** 1. row ***************************
goods_id: 1
goods_name: R510VC 15.6英寸笔记本
goods_cate: 4
brand_name: 9
goods_price: 3399.000
is_show: 1
is_saleoff: 0
*************************** 2. row ***************************
goods_id: 2
goods_name: Y400N 14.0英寸笔记本电脑
goods_cate: 4
brand_name: 1
goods_price: 4899.000
is_show: 1
is_saleoff: 0
.....

mysql> DESC tdb_goods;
+-------------+------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------------+------+-----+---------+----------------+
| goods_id | smallint(5) unsigned | NO | PRI | NULL | auto_increment |
| goods_name | varchar(150) | NO | | NULL | |
| goods_cate | varchar(40) | NO | | NULL | |
| brand_name | varchar(40) | NO | | NULL | |
| goods_price | decimal(15,3) unsigned | NO | | 0.000 | |
| is_show | tinyint(1) | NO | | 1 | |
| is_saleoff | tinyint(1) | NO | | 0 | |
+-------------+------------------------+------+-----+---------+----------------+


mysql> ALTER TABLE tdb_goods
-> CHANGE goods_cate cate_id SMALLINT UNSIGNED NOT NULL,
-> CHANGE brand_name brand_id SMALLINT UNSIGNED NOT NULL;
Query OK, 23 rows affected (0.17 sec)

mysql> DESC tdb_goods;
+-------------+------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------------+------+-----+---------+----------------+
| goods_id | smallint(5) unsigned | NO | PRI | NULL | auto_increment |
| goods_name | varchar(150) | NO | | NULL | |
| cate_id | smallint(5) unsigned | NO | | NULL | |
| brand_id | smallint(5) unsigned | NO | | NULL | |
| goods_price | decimal(15,3) unsigned | NO | | 0.000 | |
| is_show | tinyint(1) | NO | | 1 | |
| is_saleoff | tinyint(1) | NO | | 0 | |
+-------------+------------------------+------+-----+---------+----------------+


修改类名
desc tdb_goods_brands;
+------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+----------------+
| id | tinyint(4) | NO | PRI | NULL | auto_increment |
| brand_name | varchar(20) | YES | | NULL | |
+------------+-------------+------+-----+---------+----------------+

alter table tdb_goods_brands change id brand_id tinyint(4) not null ;
Query OK, 13 rows affected (0.20 sec)

desc tdb_goods_brands;
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| brand_id | tinyint(4) | NO | PRI | NULL | |
| brand_name | varchar(20) | YES | | NULL | |
+------------+-------------+------+-----+---------+-------+

删除商品名称重复记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
查找名称相同的记录
select * from tdb_goods as t1 group by t1.goods_name having count(t1.goods_name)>=2;
+----------+-----------------------+---------+----------+-------------+---------+------------+
| goods_id | goods_name | cate_id | brand_id | goods_price | is_show | is_saleoff |
+----------+-----------------------+---------+----------+-------------+---------+------------+
| 18 | HMZ-T3W 头戴显示设备 | 5 | 3 | 6999.000 | 1 | 0 |
| 19 | 商务双肩背包 | 5 | 3 | 99.000 | 1 | 0 |
+----------+-----------------------+---------+----------+-------------+---------+------------+

错误的写法:
delete tt1 from tdb_goods as tt1 left join
(select * from tdb_goods as t1 group by t1.goods_name having count(t1.goods_name)>=2) as tt2
on tt1.goods_name=tt2.goods_name and tt1.goods_id>tt2.goods_id;
Query OK, 24 rows affected (0.07 sec)

正确的写法:
delete tt1 from tdb_goods as tt1 left join (
select * from tdb_goods as t1 group by t1.goods_name
having count(t1.goods_name)>=2) as tt2
on tt1.goods_name=tt2.goods_name
where tt1.goods_id>tt2.goods_id;
Query OK, 0 rows affected (0.00 sec)

Delete 中包含 left join

  1. 左侧的表是待删除的表
  2. 不能关联成功的要过滤掉
1
2
3
delete from tbl_pps_nc_group_member
WHERE user_id = #fromUserId#
and group_id in (SELECT group_id FROM tbl_pps_nc_group where user_id = #myUserId# );

1
2
3
select * FROM tbl_pps_nc_group_member nm
LEFT JOIN tbl_pps_nc_group ng ON nm.user_id = '' AND nm.group_id=ng.group_id AND ng.user_id = ''
WHERE ng.group_id is NOT null

在好友关系中获取对方的信息

对于好友关系来说,是彼此的关系,可以不分先后,如果要区分邀请和被邀请,就必须考虑。
假设存在两个表格:
第一个表格是用户信息表t_user_info,用于记录用户的信息。
第二个表格是好友关系表t_friend, 用于记录用户的好友关系。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE t_user_info (
user_id INT PRIMARY KEY auto_increment,
`name` VARCHAR(10),
phone VARCHAR(11)
);

CREATE TABLE t_friend (
friend_ref_id INT PRIMARY KEY auto_increment,
from_user_id int,
to_user_id int,
`status` TINYINT(1)
);

如果要获取对方的信息,邀请者获取得到的是被邀请者的信息,而被邀请者获取得到的是邀请者的信息。
状态值status代表 0:待验证,1:已添加,2:已拒绝,4:待对方验证,5: 对方已添加,6:对方已拒绝

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
u.`name` AS userName,
u.phone AS userPhone,
u.user_id AS userId,
CASE WHEN f.from_user_id = '1018' THEN f.`status` + 4 ELSE f.`status` END AS `status`
FROM
t_friend f
LEFT JOIN t_user_info u
ON (u.user_id = f.from_user_id AND f.to_user_id = '1018' )
OR (u.user_id = f.to_user_id AND f.from_user_id = '1018' )
WHERE
u.user_id IS NOT NULL

[总结]

  1. left join中的on语句, 是在被关联表满足on条件才会关联,否则显示为null, 因此通过判断右侧是否为空,可以判断是否满足条件
  2. 关联条件是另一个技巧, 取对方的id条件

Explain 解析查询

MYSQL explain 详解

<<Java高级软件工程师知识结构

Java多线程是Java基础的重要的一部分,支持多线程是Java的重要特性之一. 主要包括如下内容:
  1. Java多线程1: 线程生命周期和多线程基础
  2. Java多线程2: Lock、信号量、原子量与队列
  3. Java多线程3: volatile
  4. Java多线程4: 同步锁与Java线程同步方法比较
  5. Java多线程5: 线程池
  6. Java多线程6: Java阻塞队列与生产者消费者模式

volatile用处

在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量. 为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才将私有拷贝与共享内存中的原始值进行比较,在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致,

volatile 指示JVM这个变量不稳定,每次使用它都到主存中进行读取,一般说来,多任务环境下,各任务间共享的变量都应该加volatile修饰符,

volatile 修饰的成员变量在每次被线程访问时, 都强迫从共享内存中重读该成员变量的值, 而且, 当成员变量发生变化时, 强迫线程将变化值回写到共享内存, 这样在任何时刻, 两个不同的线程总是看到某个成员变量的同一个值,

使用同步解决变量不一致问题

内存可见性:
在一个任意对象上执行同步语句,目的是为了让该线程在进入和离开同步代码块时,将该线程中的所有变量的私有拷贝与共享内存中的原始值进行比较,从而发现没有用volatile标记的变量所发生的变化

加锁(synchronized同步)的功能不仅仅局限于互斥行为, 同时还存在另外一个重要的方面:内存可见性, 我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态, 而且还希望确保当一个线程修改了对象状态后, 其他线程能够看到该变化, 而线程的同步恰恰也能够实现这一点, 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果, 为了确保所有的线程都能看到共享变量的最新值, 可以在所有执行读操作或写操作的线程上加上同一把锁, 下图示例了同步的可见性保证,

当线程A执行某个同步代码块时, 线程B随后进入由同一个锁保护的同步代码块, 这种情况下可以保证, 当锁被释放前, A看到的所有变量值(锁释放前 , A看到的变量包括y和x)在B获得同一个锁后同样可以由B看到, 换句话说, 当线程B执行由锁保护的同步代码块时, 可以看到线程A之前在同一个锁保护的同步代码块中的所有操作结果, 如果在线程A unlock M之后, 线程B才进入lock M, 那么线程B都可以看到线程A unlock M之前的操作, 可以得到i=1,j=1, 如果在线程B unlock M之后, 线程A才进入lock M, 那么线程B就不一定能看到线程A中的操作, 因此j的值就不一定是1

volatile同步机制

volatile 是一种稍弱的同步机制,在访问 volatile 变量时不会执行加锁操作,也就不会执行线程阻塞,因此 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制,

在两个或者更多的线程需要访问的成员变量上使用volatile, 当要访问的变量已在synchronized代码块中, 或者为常量时, 没必要使用volatile, 由于使用volatile屏蔽掉了JVM中必要的代码优化, 所以在效率上比较低, 因此一定在必要时才使用此关键字

实际现象

对于非volatile修饰的变量, 尽管jvm的优化, 会导致变量的可见性问题, 但这种可见性的问题也只是在短时间内高并发的情况下发生, CPU执行时会很快刷新Cache, 一般的情况下很难出现, 而且出现这种问题是不可预测的, 与jvm、机器配置环境等都有关

正确使用Volatile变量

当且仅当满足以下所有条件时,才应该使用volatile变量:

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
  2. 该变量没有包含在具有其他变量的不变式中

关于正确使用Volatile请参考: Java 理论与实践: 正确使用 Volatile 变量


【参考文献】:

  1. 【Java并发编程】之五:volatile变量修饰符—意料之外的问题(含代码)
  2. 【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)
  3. 《深入Java虚拟机——JVM高级特性与最佳实践》
  4. Java 理论与实践: 正确使用 Volatile 变量

读入图片(本地图片和网络图片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
InputStream readImageFromPath(String filePath)
throws IOException {
if (filePath.startsWith("http")) {
URL url = null;
HttpURLConnection conn;
try {
url = new URL(filePath);
conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.connect();
InputStream inputStream = conn.getInputStream();
return inputStream;
} catch (MalformedURLException e) {
e.printStackTrace();
}
} else {
return new FileInputStream(new File(filePath));
}
return null;
}

// BufferedImage bi = ImageIO.read(readImageFromPath(filePath));

压缩图像

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
/**
* 图片缩放
*
* @param filePath
* 图片路径
* @param height
* 高度
* @param width
* 宽度
* @param bb
* 比例不对时是否需要补白
*/
BufferedImage resize2(String filePath, int height, int width, boolean bb) {
try {
double ratio = 0; // 缩放比例
BufferedImage bi = ImageIO.read(readImageFromPath(filePath));
Image itemp = bi.getScaledInstance(width, height,
Image.SCALE_SMOOTH);
// 计算比例
if ((bi.getHeight() > height) || (bi.getWidth() > width)) {
if (bi.getHeight() > bi.getWidth()) {
ratio = (new Integer(height)).doubleValue()
/ bi.getHeight();
} else {
ratio = (new Integer(width)).doubleValue() / bi.getWidth();
}
AffineTransformOp op = new AffineTransformOp(
AffineTransform.getScaleInstance(ratio, ratio), null);
itemp = op.filter(bi, null);
}
if (bb) {
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.white);
g.fillRect(0, 0, width, height);
if (width == itemp.getWidth(null))
g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2,
itemp.getWidth(null), itemp.getHeight(null),
Color.white, null);
else
g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0,
itemp.getWidth(null), itemp.getHeight(null),
Color.white, null);
g.dispose();
itemp = image;
}
return (BufferedImage) itemp;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

生成微信头像

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
class ImageElement {
int left;
int top;
BufferedImage bufferedImage;
}

/**
* 最多行列数
*/
private int MAX_GRIDS = 3;
/**
* 行间距
*/
private int spacing = 5;
/**
* 边距
*/
private int border = 10;

/**
* 图片的目标大小
*/
private int width = 200;

private ImageCombinationUtil() {
}

/**
* 生成组合头像
*
* @param paths
* 用户图像
* @throws IOException
*/
public void getCombinationOfhead(List<String> paths) throws IOException {
int pathSize = paths.size();
if (pathSize == 0) {
return;
}
final int maxImageCount = MAX_GRIDS * MAX_GRIDS;
int imageCount = pathSize > maxImageCount ? maxImageCount : pathSize;
int grids = (int) Math.ceil(Math.sqrt(imageCount));
grids = grids > MAX_GRIDS ? MAX_GRIDS : grids;
int hrow = imageCount / grids;
int rows = hrow + (imageCount - hrow * grids > 0 ? 1 : 0);
int columnCountOfLastRow = imageCount - (rows - 1) * grids;
int columns = imageCount / rows;
columns = imageCount - columns * rows > 0 ? columns + 1 : columns;
System.out.println("imageCount :" + pathSize + "\t grids :" + grids
+ "\trows:" + rows + "\t columns:" + columns
+ "\t columnsOfLastRow:" + columnCountOfLastRow);

int[] matrix = new int[rows];
Arrays.fill(matrix, grids);
matrix[0] = columnCountOfLastRow;

// 计算每个图片的尺寸
int imageMeasure = (width - 2 * border - (grids - 1)
* spacing)
/ grids;

List<ImageElement> imageElements = new ArrayList<ImageElement>();
for (int i = 0; i < imageCount; i++) {
ImageElement imageMatrix = new ImageElement();
imageMatrix.bufferedImage = ImageCombinationUtil.resize2(paths.get(i),
imageMeasure, imageMeasure, true);
imageElements.add(imageMatrix);
}

// 计算坐标
int top = border;
top = rows < grids ? top + (spacing + imageMeasure) * (grids - rows)
/ 2 : top;
int imageIndex = 0;
for (int row = 0; row < matrix.length; row++) {
int column = 0;
int left = border;
left = matrix[row] < grids ? left + (spacing + imageMeasure)
* (grids - matrix[row])/2 : left;
while (column < matrix[row]) {
imageElements.get(imageIndex).left = left + column
* (imageMeasure + spacing);
imageElements.get(imageIndex).top = top + row
* (imageMeasure + spacing);
imageIndex++;
column++;
}
}
// 创建画板
BufferedImage outImage1 = new BufferedImage(width,
width, BufferedImage.TYPE_INT_RGB);
// 生成画布
Graphics g = outImage1.getGraphics();
Graphics2D g2d = (Graphics2D) g;
// 设置背景色
g2d.setBackground(new Color(231, 231, 231));
// 通过使用当前绘图表面的背景色进行填充来清除指定的矩形。
g2d.clearRect(0, 0, width, width);
// 绘制
for (ImageElement imageElement : imageElements) {
g2d.drawImage(imageElement.bufferedImage, imageElement.left,
imageElement.top, null);
// 需要改变颜色的话在这里绘上颜色。可能会用到AlphaComposite类
}

String outPath = "d:\\" + System.currentTimeMillis() + ".jpg";
String format = "JPG";
ImageIO.write(outImage1, format, new File(outPath));
}

效果

图像转化为InputStream

方便直接从内存上传到服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
InputStream image2InputStream(BufferedImage bufferedImage) {
InputStream inputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageOutputStream imageOutputStream = null;
try {
imageOutputStream = ImageIO.createImageOutputStream(outputStream);
ImageIO.write(bufferedImage, "JPG", imageOutputStream);
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}

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
String cn2Spell(String chinese) {
if (chinese == null || chinese.equals("")) {
return "";
} else {
StringBuffer pybf = new StringBuffer();
char[] arr = chinese.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < arr.length; i++) {
if (arr[i] > 128) {
try {
String[] str = PinyinHelper.toHanyuPinyinStringArray(
arr[i], defaultFormat);
if (str == null || str.length == 0) {
break;
}
pybf.append(PinyinHelper.toHanyuPinyinStringArray(
arr[i], defaultFormat)[0]);
} catch (BadHanyuPinyinOutputFormatCombination e) {
break;
}
} else {
pybf.append(arr[i]);
}
}
return pybf.toString();
}
}

1. 声明一个数组

1
2
3
String[] aArray = new String[5];  
String[] bArray = {"a","b","c", "d", "e"};
String[] cArray = new String[]{"a","b","c","d","e"};

2. 输出一个数组

1
2
3
4
5
6
7
int[] intArray = { 1, 2, 3, 4, 5 };  
String intArrayString = Arrays.toString(intArray);
// print directly will print reference value
System.out.println(intArray);
// [I@7150bd4d
System.out.println(intArrayString);
// [1, 2, 3, 4, 5]

3. 从一个数组创建数组列表

1
2
3
4
String[] stringArray = { "a", "b", "c", "d", "e" };  
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(stringArray));
System.out.println(arrayList);
// [a, b, c, d, e]

4. 检查一个数组是否包含某个值

1
2
3
4
String[] stringArray = { "a", "b", "c", "d", "e" };  
boolean b = Arrays.asList(stringArray).contains("a");
System.out.println(b);
// true

5. 连接两个数组

1
2
3
4
int[] intArray = { 1, 2, 3, 4, 5 };  
int[] intArray2 = { 6, 7, 8, 9, 10 };
// Apache Commons Lang library
int[] combinedIntArray = ArrayUtils.addAll(intArray, intArray2);

6. 声明一个内联数组(Array inline)

1
method(new String[]{"a", "b", "c", "d", "e"});  

7. 把提供的数组元素放入一个字符串

1
2
3
4
5
// containing the provided list of elements  
// Apache common lang
String j = StringUtils.join(new String[] { "a", "b", "c" }, ", ");
System.out.println(j);
// a, b, c

8. 将一个数组列表转换为数组

1
2
3
4
5
6
String[] stringArray = { "a", "b", "c", "d", "e" };  
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(stringArray));
String[] stringArr = new String[arrayList.size()];
arrayList.toArray(stringArr);
for (String s : stringArr)
System.out.println(s);

9. 将一个数组转换为集(set)

1
2
3
Set<String> set = new HashSet<String>(Arrays.asList(stringArray));  
System.out.println(set);
//[d, e, b, c, a]

10. 逆向一个数组

1
2
3
4
int[] intArray = { 1, 2, 3, 4, 5 };  
ArrayUtils.reverse(intArray);
System.out.println(Arrays.toString(intArray));
//[5, 4, 3, 2, 1]

11. 移除数组中的元素

1
2
3
int[] intArray = { 1, 2, 3, 4, 5 };  
int[] removed = ArrayUtils.removeElement(intArray, 3);//create a new array
System.out.println(Arrays.toString(removed));

12. 将整数转换为字节数组

1
2
3
4
byte[] bytes = ByteBuffer.allocate(4).putInt(8).array();  
for (byte t : bytes) {
System.out.format("0x%x ", t);
}

<Java高级软件工程师知识结构

Java多线程是Java基础的重要的一部分,支持多线程是Java的重要特性之一. 主要包括如下内容:

  1. Java多线程1: 线程生命周期和多线程基础
  2. Java多线程2: Lock、信号量、原子量与队列
  3. Java多线程3: volatile
  4. Java多线程4: 同步锁与Java线程同步方法比较
  5. Java多线程5: 线程池
  6. Java多线程6: Java阻塞队列与生产者消费者模式

阻塞队列与阻塞栈

Java定义了阻塞队列的接口java.util.concurrent.BlockingQueue,
阻塞队列的概念是, 一个指定长度的队列, 如果队列满了, 添加新元素的操作会被阻塞等待, 直到有空位为止. 同样, 当队列为空时候, 请求队列元素的操作同样会阻塞等待, 直到有可用元素为止.
阻塞队列还有更多实现类, 用来满足各种复杂的需求:ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue , 具体的API差别也很小.

对于阻塞栈, 与阻塞队列相似. 不同点在于栈是“后入先出”的结构, 每次操作的是栈顶, 而队列是“先进先出”的结构, 每次操作的是队列头.
Java为阻塞栈定义了接口:java.util.concurrent.BlockingDeque

1、核心方法

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
public interface BlockingQueue<E> extends Queue<E> {

//将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
boolean add(E e);

//将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
boolean offer(E e);

//将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
void put(E e) throws InterruptedException;

//将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;

//从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
E take() throws InterruptedException;

//在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;

//获取队列中剩余的空间。
int remainingCapacity();

//从队列中移除指定的值。
boolean remove(Object o);

//判断队列中是否拥有该值。
public boolean contains(Object o);

//将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c);

//指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c, int maxElements);
}

2、阻塞队列的成员

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出

插入方法 add(e) offer(e) put(e) offer(e,time,unit)

移除方法 remove() poll() take() poll(time,unit)

检查方法 element() peek() 不可用 不可用

3、成员详解

ArrayBlockingQueue

   基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。  

  ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

LinkedBlockingQueue

   基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。  

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

DelayQueue

   DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。  

使用场景:
DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

PriorityBlockingQueue

   基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

SynchronousQueue

   一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。  

  声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
  如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
  但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

4、秒杀demo

秒杀对列

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
package com.hsshy.beam.queue.jvm;


import com.hsshy.beam.queue.entity.SuccessKilled;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
* 秒杀队列(固定长度为100)
* @author 科帮网 By https://blog.52itstyle.com
* 创建时间 2018年5月10日
*/
public class SeckillQueue {
//队列大小
static final int QUEUE_MAX_SIZE = 100;
/** 用于多线程间下单的队列 */
static BlockingQueue<SuccessKilled> blockingQueue = new LinkedBlockingQueue<SuccessKilled>(QUEUE_MAX_SIZE);

/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private SeckillQueue(){};
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder{
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static SeckillQueue queue = new SeckillQueue();
}
//单例队列
public static SeckillQueue getMailQueue(){
return SingletonHolder.queue;
}
/**
* 生产入队
* @param kill
* @throws InterruptedException
* add(e) 队列未满时,返回true;队列满则抛出IllegalStateException(“Queue full”)异常——AbstractQueue
* put(e) 队列未满时,直接插入没有返回值;队列满时会阻塞等待,一直等到队列未满时再插入。
* offer(e) 队列未满时,返回true;队列满时返回false。非阻塞立即返回。
* offer(e, time, unit) 设定等待的时间,如果在指定时间内还不能往队列中插入数据则返回false,插入成功返回true。
*/
public Boolean produce(SuccessKilled kill) throws InterruptedException {
return blockingQueue.offer(kill);
}
/**
* 消费出队
* poll() 获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null
* take() 与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒
*/
public SuccessKilled consume() throws InterruptedException {
return blockingQueue.take();
}
// 获取队列大小
public int size() {
return blockingQueue.size();
}
}

springboot启动时执行类

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
package com.hsshy.beam.queue.jvm;

import com.hsshy.beam.queue.entity.SuccessKilled;
import com.hsshy.beam.queue.service.ISeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
* 消费秒杀队列
* 创建者 科帮网
* 创建时间 2018年4月3日
*/
@Component
public class TaskRunner implements ApplicationRunner {

@Autowired
private ISeckillService seckillService;

@Override
public void run(ApplicationArguments var) throws Exception{
while(true){
//进程内队列
SuccessKilled kill = SeckillQueue.getMailQueue().consume();
if(kill!=null){
seckillService.startSeckil(kill.getId(), kill.getUserId());
}
}
}
}

controller层

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
@ApiOperation(value="秒杀柒(进程内队列)",nickname="科帮网")
@PostMapping("/startQueue")
public R startQueue(long seckillId){
seckillService.deleteSeckill(seckillId);
final long killId = seckillId;
LOGGER.info("开始秒杀柒(正常)");
for(int i=0;i<1000;i++){
final long userId = i;
Runnable task = new Runnable() {
@Override
public void run() {
SuccessKilled kill = new SuccessKilled();
kill.setId(killId);
kill.setUserId(userId);
try {
Boolean flag = SeckillQueue.getMailQueue().produce(kill);
if(flag){
LOGGER.info("用户:{}{}",kill.getUserId(),"秒杀成功");
}else{
LOGGER.info("用户:{}{}",userId,"秒杀失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
LOGGER.info("用户:{}{}",userId,"秒杀失败");
}
}
};
executor.execute(task);
}
try {
Thread.sleep(10000);
Long seckillCount = seckillService.getSeckillCount(seckillId);
LOGGER.info("一共秒杀出{}件商品",seckillCount);
} catch (InterruptedException e) {
e.printStackTrace();
}
return R.ok();
}

5、延时队列demo

消息体

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
package com.hsshy.beam.queue.delay;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
* 消息体定义 实现Delayed接口就是实现两个方法即compareTo 和 getDelay最重要的就是getDelay方法,这个方法用来判断是否到期……
*
*/
public class Message implements Delayed {
private int id;
private String body; // 消息内容
private long excuteTime;// 延迟时长,这个是必须的属性因为要按照这个判断延时时长。

public int getId() {
return id;
}

public String getBody() {
return body;
}

public long getExcuteTime() {
return excuteTime;
}

public Message(int id, String body, long delayTime) {
this.id = id;
this.body = body;
this.excuteTime = TimeUnit.NANOSECONDS.convert(delayTime, TimeUnit.MILLISECONDS) + System.nanoTime();
}

// 自定义实现比较方法返回 1 0 -1三个参数
@Override
public int compareTo(Delayed delayed) {
Message msg = (Message) delayed;
return Integer.valueOf(this.id) > Integer.valueOf(msg.id) ? 1
: (Integer.valueOf(this.id) < Integer.valueOf(msg.id) ? -1 : 0);
}

// 延迟任务是否到时就是按照这个方法判断如果返回的是负数则说明到期否则还没到期
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.excuteTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
}

消费者

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
package com.hsshy.beam.queue.delay;

import java.util.concurrent.DelayQueue;

public class Consumer implements Runnable {
// 延时队列 ,消费者从其中获取消息进行消费
private DelayQueue<Message> queue;

public Consumer(DelayQueue<Message> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
try {
Message take = queue.take();
System.out.println("消费消息id:" + take.getId() + " 消息体:" + take.getBody());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.hsshy.beam.queue.delay;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DelayQueueTest {
public static void main(String[] args) {
// 创建延时队列
DelayQueue<Message> queue = new DelayQueue<Message>();
// 添加延时消息,m1 延时3s
Message m1 = new Message(1, "world", 3000);
// 添加延时消息,m2 延时10s
Message m2 = new Message(2, "hello", 10000);
//将延时消息放到延时队列中
queue.offer(m2);
queue.offer(m1);
// 启动消费线程 消费添加到延时队列中的消息,前提是任务到了延期时间
ExecutorService exec = Executors.newFixedThreadPool(1);
exec.execute(new Consumer(queue));
exec.shutdown();
}
}

【参考文献】:

https://www.cnblogs.com/KingIceMou/p/8075343.html
http://www.cnblogs.com/WangHaiMing/p/8798709.html
https://gitee.com/52itstyle/spring-boot-seckill

BlockingQueue(阻塞队列)详解

ArrayBlockingQueue源码解析

Java中的阻塞队列

集群

一致哈希

构建一个存储数据集群的关键是有一个有效的数据存储和复制机制。我希望通过一个行之有效的方法来说明建造一个数据集群,在这个过程中你可以随意添加或移除一个 Redis 节点,同时保证你的数据仍然存在,而不会消失。这个方法称为一致哈希

应用

  • String:缓存、限流、计数器、分布式锁、分布式 Session
  • Hash:存储用户信息、用户主页访问量、组合查询
  • List:微博关注人时间轴列表、简单队列
  • Set:赞、踩、标签、好友关系
  • Zset:排行榜

IO 多路复用

I/O 多路复用技术,是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作。

在 Redis 中一个字符串最大的容量为 512MB

对于 Hash 结构存储,由于 Hash 结构会在单个 Hash 元素在不足一定数量时进行压缩存储,所以可以大量节约内存。这一点在 String 结构里是不存在的。

数据一致性

真正意义上来讲数据库的数据和缓存的数据是不可能一致的,数据分为最终一致和强一致两类。如果业务中对数据的要求必须强一致那么就不能使用缓存。缓存能做的只能保证数据的最终一致性。

我们能做的只能是尽可能地保证数据的一致性。不管是先删库再删缓存还是先删缓存再删库,都可能出现数据不一致的情况,因为读和写操作是并发的,

Redis 的过期和内存淘汰

Redis 存储数据时我们可以设置他的过期时间。

Redis 过期删除采用的是定期删除,默认是每 100ms 检测一次,遇到过期的 Key 则进行删除,这里的检测并不是顺序检测,而是随机检测。

那这样会不会有漏网之鱼?显然 Redis 也考虑到了这一点,当我们去读/写一个已经过期的 Key 时,会触发 Redis 的惰性删除策略,直接回干掉过期的 Key。

内存淘汰是指用户存储的一部分 Key 是可以被 Redis 自动的删除,从而会出现从缓存中查不到数据的情况。加入我们的服务器内存为 2G、但是随着业务的发展缓存的数据已经超过 2G 了。

但是这并不影响我们程序的运行,因为操作系统的可见内存并不受物理内存的限制。物理内存不够用没关系,计算机会从硬盘中划出一片空间来作为虚拟内存。这就是 Redis 设计两种应用场景的初衷:缓存、持久存储。

缓存击穿

解决方案:

1、后台设置定时任务,主动地去更新缓存数据。这种方案容易理解,但是当 Key 比较分散的时候,操作起来还是比较复杂的。

2、分级缓存。比如设置两层缓存保护层,1 级缓存失效时间短,2 级缓存失效时间长。有请求过来优先从 1 级缓存中去查找,如果在 1 级缓存中没有找到相应数据,则对该线程进行加锁,这个线程再从数据库中取到数据,更新至 1 级和 2 级缓存。其他线程则直接从 2 级线程中获取。

3、提供一个拦截机制,内部维护一系列合法的 Key 值。当请求的 Key 不合法时,直接返回。

缓存雪崩

如何避免雪崩:

1、给缓存加上一定区间内的随机生效时间,不同的 Key 设置不同的失效时间,避免同一时间集体失效。

2、和缓存击穿解决方案类似,做二级缓存,原始缓存失效时从拷贝缓存中读取数据。

3、利用加锁或者队列方式避免过多请求同时对服务器进行读写操作。

性能

Redis 的性能极高,读的速度是 110000 次/s,写的速度是 81000 次/s,支持事务,支持备份,丰富的数据类型。

任何事情都是两面性,Redis 也是有缺点的:

1、由于是内存数据库,所以单台机器存储的数据量是有限的,需要开发者提前预估,需要及时删除不需要的数据。

2、当修改 Redis 的数据之后需要将持久化到硬盘的数据重新加入到内容中,时间比较久,这个时候 Redis 是无法正常运行的。

运维

生产环境禁用命令

keys

1
2
# 禁止使用Keys正则匹配
keys * wxdb(此处省略)cf8*

1、redis 是单线程的,其所有操作都是原子的,不会因并发产生数据异常;

2、使用高耗时的 Redis 命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为 O(N)的 KEYS 命令,严格禁止在生产环境中使用);

  • 运维人员进行 keys *操作,该操作比较耗时,又因为 redis 是单线程的,所以 redis 被锁住;
  • 此时 QPS 比较高,又来了几万个对 redis 的读写请求,因为 redis 被锁住,所以全部 Hang 在那;
  • 因为太多线程 Hang 在那,CPU 严重飙升,造成 redis 所在的服务器宕机;
  • 所有的线程在 redis 那取不到数据,一瞬间全去数据库取数据,数据库就宕机了;

flushdb flushall config

1
2
3
flushdb 清空当前数据库的所有的key
flushall 清空整个redis服务器的所有的key
config 客户端连接后可配置服务器

禁用命令

1
2
3
4
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command KEYS ""

对于 FLUSHALL 命令,需要设置配置文件中 appendonly no,否则服务器是无法启动。

时间复杂度高于 O(N)的命令: hgetall、lrange、smembers、zrange、sinter 等,它们并非不能使用,但这些命令的时间复杂度都为 O(N),使用这些命令需要明确 N 的值,否则也会出现缓存宕机。

Redis2.8 版本以后有了一个新命令 scan,可以用来分批次扫描 redis 记录,这样肯定会导致整个查询消耗的总时间变大,但不会影响 redis 服务卡顿,影响服务使用。

分层架构设计,有一条准则:站点层、服务层要做到无数据无状态,这样才能任意的加节点水平扩展,数据和状态尽量存储到后端的数据存储服务,例如数据库服务或者缓存服务。

Redis or Memcached

value 是哈希,列表,集合,有序集合这类复杂的数据结构时,会选择 redis,因为 mc 无法满足这些需求,最典型的场景,用户订单列表,用户消息,帖子评论列表等。

Redis 支持持久化

千万不要把 redis 当作数据库用:

(1)redis 的定期快照不能保证数据不丢失

(2)redis 的 AOF 会降低效率,并且不能支持太大的数据量

缓存场景,开启固化功能,有什么利弊?

如果只是缓存场景,数据存放在数据库,缓存在 redis,此时如果开启固化功能:

优点是,redis 挂了再重启,内存里能够快速恢复热数据,不会瞬时将压力压到数据库上,没有一个 cache 预热的过程。

缺点是,在 redis 挂了的过程中,如果数据库中有数据的修改,可能导致 redis 重启后,数据库与 redis 的数据不一致。

redis 天然支持集群功能,可以实现主动复制,读写分离。

redis 官方也提供了 sentinel 集群管理工具,能够实现主从服务监控,故障自动转移,这一切,对于客户端都是透明的,无需程序改动,也无需人工介入。

memcache 的 value 存储,最大为 1M,如果存储的 value 很大,只能使用 redis

什么时候倾向于 memcache?

纯 KV,数据量非常大,并发量非常大的业务,使用 memcache 或许更适合。

内存分配

memcache 使用预分配内存池的方式管理内存,能够省去内存分配时间。Redis 则是临时申请空间,可能导致碎片。mc 会更快一些。

虚拟内存使用

memcache 把所有的数据存储在物理内存里。redis 有自己的 VM 机制,理论上能够存储比物理内存更多的数据,当数据超量时,会引发 swap,把冷数据刷到磁盘上。数据量大时,mc 会更快一些。

网络模型

memcache 使用非阻塞 IO 复用模型,redis 也是使用非阻塞 IO 复用模型。但由于 redis 还提供一些非 KV 存储之外的排序,聚合功能,在执行这些功能时,复杂的 CPU 计算,会阻塞整个 IO 调度。

由于 redis 提供的功能较多,mc 会更快一些。

线程模型

memcache 使用多线程,主线程监听,worker 子线程接受请求,执行读写,这个过程中,可能存在锁冲突。redis 使用单线程,虽无锁冲突,但难以利用多核的特性提升整体吞吐量。

从这一点上,mc 会快一些。

画外音:理论上,mc 只支持 kv,而 redis 支持了这么多功能,mc 性能应该高非常多非常多,但实际并非如此,真的可能和代码质量有关。

水平扩展的支持

不管是 mc 和 redis,服务端集群没有天然支持水平扩展,需要在客户端进行分片,这其实对调用方并不友好。如果能服务端集群能够支持水平扩展,会更完美一些。

<< Java高级软件工程师知识结构

  1. 掌握InputStream、OutputStream、Reader、Writer的继承体系。
  2. 掌握字节流(FileInputStream、DataInputStream、BufferedInputStream、FileOutputSteam、DataOutputStream、BufferedOutputStream)和 字符流(BufferedReader、InputStreamReader、FileReader、BufferedWriter、OutputStreamWriter、PrintWriter、FileWriter),并熟练运用。
  3. 掌握NIO实现原理及使用方法。

Java IO包括:

本文介绍的是Java IO的基础部分, 也叫作BIO.

Java IO 继承关系

java.io包中包含了一系列的Java IO相关的类和接口:

  • File 文件类
  • FileDescriptor 文件描述类
  • InputStream 字节流输入抽象类
  • OutputStream 字节流输出抽象类
  • Reader 字符流输入抽象类
  • Writer 字符流输出抽象类
  • RandomAccessFile随机访问文件类 不是InputStream或者OutputStream继承层次的一部分,除了实现了DataInput和DataOutput接口(DataInputStream和DataOutputStream也实现这两个接口)外, 他和这两个继承层次没任何关系。甚至不使用InputStream和OutputStream类中的任何功能。完全是独立的类,RandomAccessFile拥有和别的IO类型本质完全不同的行为,可以在一个文件中向前和向后移动,因此重新编写了所有的方法。任何情况下都是相互独立的,直接从Object类派生而来。
    • Console类,该类提供了用于读取密码的方法,可以禁止控制台回显并返回char数组,这两个特性对保证安全有作用,平时用的不多,了解就行。
    • StreamTokenizer 类,这个类非常有用,它可以把输入流解析为标记(token), StreamTokenizer 并非派生自InputStream或者OutputStream,而是归类于io库中,因为StreamTokenizer只处理InputStream对象。

流,是编程语言的IO类库常用的概念, 所谓流是指任何有能力产出数据的数据源和任何有能力接收数据的接收端对象。”流”屏蔽了实际的IO设备中处理数据的细节。
Java中的流,根据处理数据类型的不同分为:字符流和字节流; 根据数据流向不同分为:输入流和输出流。可以通过类名的结尾来判断。

字节流

java中的压缩都是面向字节的 (java.util.zip)。

InputStream

字节输入流都是继承于InputStream并且以InputStream结尾,InputStream表示从不同的数据源产生输入的类,这些数据源包括字节数组、String对象(StringBufferInputStream已经不再使用,需要转换为字节数组来实现)、文件、“管道”、其他种类的流组成的序列及其他数据源。每种数据源都有相应的 InputStream 子类,FilterInputStream也属于一种 InputStream, 为装饰器类提供基类,其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起。装饰器模式

  1. PipedInputStream 作为多线程中的数据源,将其与FilterInputStream对象相连以提供有用接口
  2. FilterInputStream 类定义了一些方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void mark(int readlimit)
    在输入流中的当前位置上作标记。
    reset 方法的后续调用将此流重新定位在最后标记的位置上,
    以便后续读取操作重新读取相同的字节。
    readlimit 参数告知此输入流在标记位置无效之前允许读取的字节数。
    public void reset() throws IOException
    将此流重新定位到对此输入流最后调用 mark 方法时的位置。
    此方法只执行 in.reset()。
    在需要提前读取一小部分数据以查看流中有什么的情况下,可以使用流的标记。
    通过调用通用解析器常常最容易做到这一点。如果流属于通过解析处理的类型,那么解析起来就很容易。
    如果流不属于那种类型,那么解析器应该在解析失败时抛出一个异常。
    如果这发生在 readlimit 个字节内,那么它允许外部代码重置流,并尝试另一种解析器。
  3. 除了DataInputStream以外的FilterInputStream类在内部修改了InputStream的行为方式,是否缓冲,是否记录读过的行,以及是否把单一字符推回输入流等。

OutputStream

接收端对象包括字节数组(但不是String)、文件或者管道。
字节输出流都是继承于OutputStream并以OutputStream结尾

字节输入流和输出流的对应关系

字符流

字节流仅支持8位字节流,不能很好的处理16位的Unicode字符,ReaderWriter提供了兼容Unicode与面向字符的I/O功能,可以方便的实现国际化。

Reader 字符输入流

读取字符序列的类都是继承于Reader并且以Reader结尾

Writer 字符输出流

写入字符流的类都是继承于Writer并且以Writer结尾

  1. FilterWriter形同虚设: BufferedWriter并不是FilterWriter的子类,尽管FilterWriter是抽象类。没有任何子类。
  2. CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据
  3. PrintWriter 和 PrintStream 极其类似,功能和使用也非常相似。
  4. OutputStreamWriter 是OutputStream 到Writer 转换的桥梁

字符输入流与字符输出流的对应关系

字节流与字符流的对应关系

字节流与字符流的区别

把一片二进制数据数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。

在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。

底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。字符向字节转换时,要注意编码的问题,因为字符串转成字节数组,其实是转成该字符的某种编码的字节形式,读取也是反之的道理。

字节流

FileInputStream 文件输入流

构造器只能接受File对象 File路径字符串或者 FileDescriptor对象

1
2
3
4
5
6
7
int available()
返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取(或跳过)的估计剩余字节数。
FileDescriptor getFD()
返回表示到文件系统中实际文件的连接的 FileDescriptor 对象,
该文件系统正被此 FileInputStream 使用。
long skip(long n)
从输入流中跳过并丢弃 n 个字节的数据。

FileOutputSteam 文件输出流

文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。与字符流中的FileWriter对应.

BufferedInputStream 缓冲输入流

支持一次性读入多个字节

BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。
在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。
在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。

实现了FilterInputStream的mark(int readLine)方法

mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。

构造器只能接受 InputStream

1
2
int  available()
返回可以从此输入流读取(或跳过)、且不受此输入流接下来的方法调用阻塞的估计字节数。

BufferedOutputStream 缓冲输出流

该类实现缓冲的字节输出流。通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。

BufferedOutputStream重写了OutputStream中的write方法,

1
2
3
4
public void write(byte[] b,
int off,
int len)
throws IOException

OutputStream写入多个字节时会逐个字节的调用单字节写入方法

1
2
public void write(int b)
throws IOException

而 BufferedOutputStream 中的可以一次性将这些字节写入输出流, 因此效率比较高.

DataInputStream 数据输入流

DataInputStream几乎可以任何形式输出,而readLine()是唯一成不了首选的原因。

数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流将数据读入。

使用DataInputStream写字符串并且能够通过DataInputStream恢复的唯一的方法是使用UTF-8编码

DataOutputStream 数据输出流

不同的数据类型(基本数据类型和String对象)的读取方法都以read开头,如readByte()readFloat()

1
2
3
4
5
6
7
8
9
10
11
12
13
DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("abc.txt")));
outputStream.writeDouble(3.14159265358979);
outputStream.writeUTF("仅仅是测试");
outputStream.writeDouble(3.58979);
outputStream.writeUTF("不仅仅是测试");
outputStream.close();

DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream("abc.txt")));
System.out.println(inputStream.readDouble());
System.out.println(inputStream.readUTF());
System.out.println(inputStream.readDouble());
System.out.println(inputStream.readUTF());
inputStream.close();

字符流

FileReader 文件输入字符流

用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。
要自己指定这些值,可以先在 FileInputStream 上构造一个 InputStreamReader。
FileReader 用于读取字符流。要读取原始字节流,请考虑使用 FileInputStream。

FileWriter 文件输出字符流

用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。
要自己指定这些值,可以先在 FileOutputStream 上构造一个 OutputStreamWriter。

文件是否可用或是否可以被创建取决于底层平台。特别是某些平台一次只允许一个 FileWriter(或其他文件写入对象)打开文件进行写入。
在这种情况下,如果所涉及的文件已经打开,则此类中的构造方法将失败。

FileWriter 用于写入字符流。要写入原始字节流,请考虑使用 FileOutputStream。

BufferedReader 缓冲字符输入流

字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。
因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。例如,

1
2
BufferedReader in
= new BufferedReader(new FileReader("foo.in"));

将缓冲指定文件的输入。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。

通过用合适的 BufferedReader 替代每个 DataInputStream,可以对将 DataInputStream 用于文字输入的程序进行本地化。

主要的方法:

1
2
3
4
String 	readLine()
读取一个文本行。
boolean ready()
判断此流是否已准备好被读取。

BufferedWriter 缓冲字符输出流

将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
可以指定缓冲区的大小,或者接受默认的大小。在大多数情况下,默认值就足够大了。
该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator 定义。
并非所有平台都使用新行符 (‘\n’) 来终止各行。因此调用此方法来终止每个输出行要优于直接写入新行符。

通常 Writer 将其输出立即发送到底层字符或字节流。
除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如 FileWriters 和 OutputStreamWriters)。
例如,

1
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

将缓冲 PrintWriter 对文件的输出。如果没有缓冲,则每次调用 print() 方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。

PrintWriter 打印输出字符流

向文本输出流打印对象的格式化表示形式,支持写入各种格式的字符流。但不能写入字节流。

如果启用了自动刷新(autoFlush),则只有在调用 println、printf 或 format 的其中一个方法时才可能完成此操作,而不是每当正好输出换行符时才完成。这些方法使用平台自有的行分隔符概念,而不是换行符。

为了更容易的过渡到使用PrintWriter,他提供了一个既能接受Writer对象又能接收OutputStream对象的构造器。
甚至提供了可以直接接收文件的构造器。

例如,基本文件输出

1
2
PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter("abc.text")));
PrintWriter printWriter = new PrintWriter("abc.text"); //快捷方式

构造器有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PrintWriter(File file)
使用指定文件创建不具有自动行刷新的新 PrintWriter。
PrintWriter(File file, String csn)
创建具有指定文件和字符集且不带自动刷行新的新 PrintWriter。
PrintWriter(OutputStream out)
根据现有的 OutputStream 创建不带自动行刷新的新 PrintWriter。
PrintWriter(OutputStream out, boolean autoFlush)
通过现有的 OutputStream 创建新的 PrintWriter。
PrintWriter(String fileName)
创建具有指定文件名称且不带自动行刷新的新 PrintWriter。
PrintWriter(String fileName, String csn)
创建具有指定文件名称和字符集且不带自动行刷新的新 PrintWriter。
PrintWriter(Writer out)
创建不带自动行刷新的新 PrintWriter。
PrintWriter(Writer out, boolean autoFlush)
创建新 PrintWriter。

构造PrintWriter的方式主要有: File或者文件名、OutputStream以及Writer.

主要的方法:

append方法将指定字符、字符序列和指定字符序列的子序列添加到此 writer。

1
2
3
PrintWriter 	append(char c)
PrintWriter append(CharSequence csq)
PrintWriter append(CharSequence csq, int start, int end)
1
void 	println() 通过写入行分隔符字符串终止当前行。

PrintStream 存在两个重要的方法:printprintln。对它们了重载,以便可打印出各种不同的类型。
print可以打印的类型有 boolean char char[] double float int long ObjectString.
println的参数有boolean char char[] double float int long ObjectString.

printfformatwrite可以打印更多的格式

1
2
3
4
5
6
7
8
9
10
11
12
PrintWriter 	printf(Locale l, String format, Object... args)
PrintWriter printf(String format, Object... args)
PrintWriter format(Locale l, String format, Object... args)
使用指定格式字符串和参数将一个格式化字符串写入此 writer 中。
PrintWriter format(String format, Object... args)
使用指定格式字符串和参数将一个格式化字符串写入此 writer 中。
void write(char[] buf)
void write(char[] buf, int off, int len) 写入字符数组,字符数组的某一部分。
void write(int c)
void write(String s)
void write(String s, int off, int len)
写入单个字符、字符串及字符串的一部分。

其他方法

1
2
3
4
5
6
boolean 	checkError()
如果流没有关闭,则刷新流且检查其错误状态。
protected void setError()
指示已发生错误。
protected void clearError()
清除此流的错误状态。

InputStreamReader 输入流转换器

有时需要将字节流转换为字符流,为了实现这个目的,要用到适配器(adapter)类: InputStreamReaderInputStream转换为ReaderOutputStreamWriterOutputStream转换为Writer

OutputStreamWriter 输出流转换器

其他

管道流

  1. PipedInputStream
  2. PipedOutputStream
  3. PipedReader
  4. PipedWriter
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
/**
* 验证管道流
* */

/**
* 消息发送类
* */
class Send implements Runnable{
private PipedOutputStream out=null;
public Send() {
out = new PipedOutputStream();
}
public PipedOutputStream getOut(){
return this.out;
}
public void run(){
String message = "hello , Rollen";
try{
out.write(message.getBytes());
}catch (Exception e) {
e.printStackTrace();
}try{
out.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 接受消息类
* */
class Recive implements Runnable{
private PipedInputStream input=null;
public Recive(){
this.input=new PipedInputStream();
}
public PipedInputStream getInput(){
return this.input;
}
public void run(){
byte[] b=new byte[1000];
int len=0;
try{
len=this.input.read(b);
}catch (Exception e) {
e.printStackTrace();
}try{
input.close();
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("接受的内容为 "+(new String(b,0,len)));
}
}
/**
* 测试类
* */
class hello{
public static void main(String[] args) throws IOException {
Send send=new Send();
Recive recive=new Recive();
try{
//管道连接
send.getOut().connect(recive.getInput());
}catch (Exception e) {
e.printStackTrace();
}
new Thread(send).start();
new Thread(recive).start();
}
}

SequenceInputStream

SequenceInputStream可以认为是一个工具类,将两个或者多个输入流当成一个输入流依次读取。完全可以从IO 包中去除,还完全不影响IO 包的结构,却让其更“纯洁”――纯洁的Decorator 模式。
【案例】将两个文本文件合并为另外一个文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 将两个文本文件合并为另外一个文本文件
* */

File file1 = newFile("d:" + File.separator + "hello1.txt");
File file2 = newFile("d:" + File.separator + "hello2.txt");
File file3 = newFile("d:" + File.separator + "hello.txt");
InputStream input1 =new FileInputStream(file1);
InputStream input2 =new FileInputStream(file2);
OutputStream output =new FileOutputStream(file3);
// 合并流
SequenceInputStream sis = new SequenceInputStream(input1, input2);
int temp = 0;
while((temp =sis.read()) != -1){
output.write(temp);
}
input1.close();
input2.close();
output.close();
sis.close();

RandomAccessFile

支持搜寻方法,并且只适用于文件。

getFilePointer() 用于查找当前所处的文件位置,seek() 用于在文件内移至新的位置, length()用于判断文件的最大尺寸. 构造器支持在第二个参数中指示随机读(r)还是读写(rw).但是不支持只写.
在JDK1.4后, RandomAccessFile的大多数功能由NIO存储映射文件所取代.

断点下载

注意:

  1. 使用 FileWriterBufferedOutputStream 或者 BufferedWriter 写文件时,一定要清理缓冲区并且关闭输出流,否则会造成写入的内容丢失
  2. 使用字符流装饰字节流时,一定要都关闭掉
  1. 如何判断读到文件结尾?
  • 字符流: 每一个Reader都包含一个ready()方法,该方法用于判断IO是否准备完毕,但是不能是否读到文件的结尾。判断是否读到文件结尾要通过判断读入的内容是否为空判断。

File类

getPath getAbsolutePath和getCanonicalPath

  • public String getPath() 获取为文件赋值时的路径名
  • public String getAbsolutePath() 获取绝对路径名(包含...等字符)。
  • public String getCanonicalPath()throws IOException 获取规范路径名(经过计算后的精简路径名)

getCanonicalPath()

1
2
File file = new File("D:\\Text.txt");
System.out.println(file.getCanonicalPath());

Windows系统下
(1),确定D盘下没有Text.txt这个文件,直接执行这段代码,得到的结果是:
D:\Text.txt
注意这里试大写的Text.txt
(2)在D盘下建立一个文件,名叫text.txt,再次执行代码,得到结果
D:\text.txt

尝试在linux下执行上边的步骤,两次打印的结果是相同的,因为linux是大小写敏感的系统。

遍历目录下的文件

  • String[] list()
    返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。
  • String[] list(FilenameFilter filter)
    返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。
  • File[] listFiles()
    返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。
  • File[] listFiles(FileFilter filter)
    返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
  • File[] listFiles(FilenameFilter filter)
    返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。

上面的遍历方法中存在两个通过FilenameFilter和FileFilter过滤文件的方法。
这是策略模式的典型应用。
String[] list(FilenameFilter filter)为例
list提供了基本的功能,而且按照FilenameFilter的形式提供策略,以便完善list在提供服务时所需的算法。因为list接受FilenameFilter对象作为参数, 任何实现了FilenameFilter接口的对象都可以传递给list方法,用以选择list方法的行为方式。

1
2
3
public interface FilenameFilter{
boolean accept(File dir, String name);
}
1
2
3
4
5
6
7
File srcDir=new File("~/testDir");
File[] files = srcDir.listFiles(
new FilenameFilter(){
public boolean accept(File dir, String name) {
return name.endsWith(".java");
}
});

设置指定权限

迭代创建目录

public boolean mkdirs() 创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。注意,此操作失败时也可能已经成功地创建了一部分必需的父目录。

文件系统根

public static File[] listRoots()

样例模板

关闭

无论使用任何方式打开文件或者流,必须准确的关闭, 标准的形式是在try-catch语句块的finally中关闭.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String fileName = "abc.txt";
BufferedReader bufferedReader = null;
try {
Reader reader = new FileReader(fileName);
bufferedReader = new BufferedReader(reader);
StringBuffer sb = new StringBuffer();
String s;
while ((s = bufferedReader.readLine()) != null) {
sb.append(s + "\n");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

读取文件

直接通过FileInputStream, 由于没有使用缓冲BufferedInputStream包装, 每次读取一组字节其实是在一个字节一个字节的从系统中读取.

一次性读入字节数组

1
2
3
4
5
6
File f=new File(fileName);
InputStream in=new FileInputStream(f);
byte[] b=new byte[1024];
int len = in.read(b);
in.close();
String content = new String(b,0,len);

或者

1
2
3
4
5
6
File f=new File(fileName);
InputStream in=new FileInputStream(f);
byte[] b=new byte[f.length()];
in.read(b);
in.close();
String content = new String(b);

逐个字节读

FileInputStream的底层原理决定了和上面的读法是一样的
读取字节流时, 读到文件的末尾的时候会返回-1, 以此作为判断读取结束的依据.

1
2
3
4
5
6
7
8
9
10
File f=new File(fileName);
InputStream in=new FileInputStream(f);
byte[] b=new byte[1024];
int count =0;
int temp=0;
while((temp=in.read())!=(-1)){
b[count++]=(byte)temp;
}
in.close();
System.out.println(new String(b));

加缓冲

使用BufferedInputStream包装输入流

1
2
3
4
5
6
7
File f=new File(fileName);
InputStream in=new FileInputStream(f);
BufferedInputStream bf = new BufferedInputStream(in);
byte[] b=new byte[1024];
int len = bf.read(b);
in.close();
String content = new String(b,0,len);

字符流读取

不加缓冲

1
2
3
4
5
// 直接通过字符流读文件 file可为File对象或者文件名String
FileReader fileReader = new FileReader(file);
char[] content = new char[file.length()]; // 调用该方法file必须为File对象
fileReader.read(content);
System.out.println(new String(content));

加缓冲

1
2
3
4
5
6
7
8
9
10
// 创建FileInputStream
FileInputStream fileInputStream = new FileInputStream(file);
// 将字节流转换为字符流
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
// 给字符流加缓冲
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = null;
while ((s = bufferedReader.readLine()) != null) {
System.out.println(s);
}
1
2
3
4
5
6
7
8
// 直接通过字符流读文件 file可为File对象或者文件名String
FileReader fileReader = new FileReader(file);
// 给字符流加缓冲
BufferedReader bufferedReader = new BufferedReader(fileReader);
String s;
while ((s = bufferedReader.readLine()) != null) {
System.out.println(s);
}

写文件

字节流写文件

1
2
3
4
5
OutputStream out =new FileOutputStream(file);
String str="Hello World";
byte[] b=str.getBytes();
out.write(b);
out.close();

或者

1
2
3
4
5
6
7
OutputStream out =new FileOutputStream(file);
String str="Hello World!!";
byte[] b=str.getBytes();
for (int i = 0; i < b.length; i++) {
out.write(b[i]);
}
out.close();

加缓冲

1
2
3
OutputStream outputStream = new FileOutputSteam(file);
BufferedOutputStream bf = new BufferedOutputStream(outputStream);
bf.write(str.getBytes());

利用字节流写文件

不加缓冲, 底层实现是逐个字符添加到系统文件中

1
2
FileWriter fileWriter = new FileWriter(file);
bufferedWriter.write("Hellp world!");

加缓冲

1
2
3
4
5
6
7
// file为File对象 文件名字符串 FileDescriptor对象
FileOutputStream fileOutputStream = new FileOutputStream(file);
// 字节流转换为字节流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
// 加缓冲
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
bufferedWriter.write("hello world!");

或者

1
2
3
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("Hello world!");

格式化字符输出

1
2
PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter("abc.text")));
PrintWriter printWriter = new PrintWriter("abc.text"); //快捷方式, 本身就已经添加了缓冲

内存中的字符串读取

从内存逐个字节读入字符串

1
2
3
4
5
6
7
8
9
10
11
12
String s = "这是需要读入的字符串";
StringReader stringReader = new StringReader(s);
int c = 1;
try {
while ((c = stringReader.read()) != -1) {
System.out.println((char) c);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
stringReader.close();
}

输出结果

1
2
3
4
5
6
7
8
9
10










格式化内存输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String s = "这是需要输出的字符串!";
ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes());
DataInputStream in = new DataInputStream(inputStream);
try {
while (in.available() != 0) {
System.out.println((char) in.readByte());
}
} catch (IOException e) {
System.err.println("End of Stream");
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}

DataInputStream.available()方法检测还有多少可供读取的字符

文件拷贝

这是采用Java IO实现的版本, 采用Java NIO可以大大提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
File file1=new File(args[0]);
File file2=new File(args[1]);
InputStream input=new FileInputStream(file1);
OutputStream output=new FileOutputStream(file2);
if((input!=null)&&(output!=null)){
int temp=0;
while((temp=input.read())!=(-1)){
output.write(temp);
}
}
input.close();
output.close();

RandomAccessFile

1
2
3
4
5
6
7
8
9
10
String fileName="D:"+File.separator+"hello.txt";
File f=new File(fileName);
RandomAccessFile demo=new RandomAccessFile(f,"rw");
demo.writeBytes("asdsad");
demo.writeInt(12);
demo.writeBoolean(true);
demo.writeChar('A');
demo.writeFloat(1.21f);
demo.writeDouble(12.123);
demo.close();

与字节数组结合

//TODO 与字节数组

标准IO

三个标准IO分别是 System.out System.err System.in, System.outSystem.err已经包装成PrintStream对象, 而System.io是没有被包装的未经过加工的InputStream, 因此在读取System.io之前必须对其进行包装.

例子: 一行一行读取系统输入

1
2
3
4
5
6
InputStreamReader in = new InputStreamReader(System.in);
BufferedReader stdin = new BufferedReader(in);
String s;
while((s = stdin.readLine())!=null && s.length!=0){
System.out.println(s);
}

字符输出到标准输出

1
2
PrintWriter out = new PrintWriter(System.out,true);
out.println("hello , world!");

标准IO重定向

1
2
3
System.setIn(in);
System.setOut(out);
System.setErr(out);

//TODO 如何重定向到java.swing.JTextArea

Java IO 异常与错误

进程控制

所谓的进程控制, 是当Java 调用系统指令启动系统进程时, 利用Java IO输出运行过程中的输出和错位信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String command = "CMD /C ping www.baidu.com";
try {
Process process = new ProcessBuilder(command.split(" ")).start();
InputStream inputStream = process.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader results = new BufferedReader(inputStreamReader);
String s;
while ((s = results.readLine()) != null) {
System.out.println(s);
}
BufferedReader errors = new BufferedReader(new InputStreamReader(process.getErrorStream()));
while ((s = errors.readLine()) != null) {
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}

[参考文献]:

  1. Think in Java

<< Java高级软件工程师知识结构

  1. 掌握InputStream、OutputStream、Reader、Writer的继承体系.
  2. 掌握字节流(FileInputStream、DataInputStream、BufferedInputStream、FileOutputSteam、DataOutputStream、BufferedOutputStream)和 字符流(BufferedReader、InputStreamReader、FileReader、BufferedWriter、OutputStreamWriter、PrintWriter、FileWriter), 并熟练运用.
  3. 掌握NIO实现原理及使用方法.

Java IO包括:

IO模式

IO模式可以分为:

  • blocking IO 阻塞IO
  • nonblocking IO 非阻塞IO
  • IO multiplexing IO多路复用
  • asynchronous IO 异步IO

所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

对于一个network IO (这里我们以read举例), 它会涉及到两个系统对象,
一个是调用这个IO的process (or thread), 另一个就是系统内核(kernel).
当一个read操作发生时, 它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

这些IO Model的区别就是在两个阶段上各有不同的情况.

阻塞IO(blocking IO)

b

当用户进程调用了recvfrom这个系统调用, kernel就开始了IO的第一个阶段:准备数据. 对于network io来说, 很多时候数据在一开始还没有到达(比如, 还没有收到一个完整的UDP包), 这个时候kernel就要等待足够的数据到来. 而在用户进程这边, 整个进程会被阻塞. 当kernel一直等到数据准备好了, 它就会将数据从kernel中拷贝到用户内存, 然后kernel返回结果, 用户进程才解除block的状态, 重新运行起来.

所以, blocking IO的特点就是在IO执行的两个阶段都被block了.

非阻塞IO(nonblocking IO)

n

当用户进程发出read操作时, 如果kernel中的数据还没有准备好, 那么它并不会block用户进程, 而是立刻返回一个error. 从用户进程角度讲 , 它发起一个read操作后, 并不需要等待, 而是马上就得到了一个结果. 用户进程判断结果是一个error时, 它就知道数据还没有准备好, 于是它可以再次发送read操作. 一旦kernel中的数据准备好了, 并且又再次收到了用户进程的system call, 那么它马上就将数据拷贝到了用户内存, 然后返回.

所以, 用户进程其实是需要不断的主动询问kernel数据好了没有.

IO多路复用(IO multiplexing)

m

IO multiplexing就是select/epoll, 也称为event driven IO. select/epoll 的好处就在于单个process就可以同时处理多个网络连接的IO. 它的基本原理就是select/epoll 这个function会不断的轮询所负责的所有socket, 当某个socket有数据到达了, 就通知用户进程. 当用户进程调用了select, 那么整个进程会被block, 而同时, kernel会“监视”所有select负责的socket, 当任何一个socket中的数据准备好了, select就会返回. 这个时候用户进程再调用read操作, 将数据从kernel拷贝到用户进程.

这个图和blocking IO的图其实并没有太大的不同, 事实上, 还更差一些. 因为这里需要使用两个system call (select 和 recvfrom), 而blocking IO只调用了一个system call (recvfrom). 但是, 用select的优势在于它可以同时处理多个connection. (多说一句. 所以, 如果处理的连接数不是很高的话, 使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好, 可能延迟还更大. select/epoll的优势并不是对于单个连接能处理得更快, 而是在于能处理更多的连接. )

IO multiplexing Model中, 实际中, 对于每一个socket, 一般都设置成为non-blocking, 但是, 如上图所示, 整个用户的process其实是一直被block的. 只不过process是被select这个函数block, 而不是被socket IO给block.

异步IO(asynchronous IO)

a
用户进程发起read操作之后, 立刻就可以开始去做其它的事. 而另一方面, 从kernel的角度, 当它收到一个asynchronous read之后, 首先它会立刻返回, 所以不会对用户进程产生任何block. 然后, kernel会等待数据准备完成, 然后将数据拷贝到用户内存, 当这一切都完成之后, kernel会给用户进程发送一个signal, 告诉它read操作完成了.

四种模式比较

1
2

在Java中,以socket.read()为例子:

  1. 传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
  2. 对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
  3. 最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

换句话说,
BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

blocking vs non-blocking

调用blocking IO会一直block住对应的进程直到操作完成, 而non-blocking IO 在kernel还准备数据的情况下会立刻返回.

synchronous IO vs asynchronous IO

Stevens给出的定义(其实是POSIX的定义)是这样子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not cause the requesting process to be blocked;

synchronous IO做”IO operation”的时候会将process阻塞. 按照这个定义, 之前所述的blocking IO, non-blocking IO, IO multiplexing都属于synchronous IO. 有人可能会说, non-blocking IO并没有被block啊. 这里有个非常“狡猾”的地方, 定义中所指的”IO operation”是指真实的IO操作, 就是例子中的recvfrom这个system call. non-blocking IO在执行recvfrom这个system call的时候, 如果kernel的数据没有准备好, 这时候不会block进程. 但是, 当kernel中数据准备好的时候, recvfrom会将数据从kernel拷贝到用户内存中, 这个时候进程是被block了, 在这段时间内, 进程是被block的. 而asynchronous IO则不一样, 当进程发起IO 操作之后, 就直接返回再也不理睬了, 直到kernel发送一个信号, 告诉进程说IO完成. 在这整个过程中, 进程完全没有被block.

non-blocking IO vs asynchronous IO

在non-blocking IO中, 虽然进程大部分时间都不会被block, 但是它仍然要求进程去主动的check, 并且当数据准备完成以后, 也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存. 而asynchronous IO则完全不同. 它就像是用户进程将整个IO操作交给了他人(kernel)完成, 然后他人做完后发信号通知. 在此期间, 用户进程不需要去检查IO操作的状态, 也不需要主动的去拷贝数据.

最后, 再举几个不是很恰当的例子来说明这四个IO Model
有A, B, C, D四个人在钓鱼:
A用的是最老式的鱼竿, 所以呢, 得一直守着, 等到鱼上钩了再拉杆;
B的鱼竿有个功能, 能够显示是否有鱼上钩, 所以呢, B就和旁边的MM聊天, 隔会再看看有没有鱼上钩, 有的话就迅速拉杆;
C用的鱼竿和B差不多, 但他想了一个好办法, 就是同时放好几根鱼竿, 然后守在旁边, 一旦有显示说鱼上钩了, 它就将对应的鱼竿拉起来;
D是个有钱人, 干脆雇了一个人帮他钓鱼, 一旦那个人把鱼钓上来了, 就给D发个短信.

IO设计模式

FIXME reactor和proactor模式详细

Reactor

Doug Lea《scalable IO in Java》
中文解读
another

首先来看看Reactor模式, Reactor模式应用于同步I/O的场景. 我们以读操作为例来看看Reactor中的具体步骤:
读取操作:

  1. 应用程序注册读就绪事件和相关联的事件处理器
  2. 事件分离器等待事件的发生
  3. 当发生读就绪事件的时候, 事件分离器调用第一步注册的事件处理器
  4. 事件处理器首先执行实际的读取操作, 然后根据读取到的内容进行进一步的处理

Proactor

读取操作:

  1. 应用程序初始化一个异步读取操作, 然后注册相应的事件处理器, 此时事件处理器不关注读取就绪事件, 而是关注读取完成事件, 这是区别于Reactor的关键.
  2. 事件分离器等待读取操作完成事件
  3. 在事件分离器等待读取操作完成的时候, 操作系统调用内核线程完成读取操作, 并将读取的内容放入用户传递过来的缓存区中. 这也是区别于Reactor的一点, Proactor中, 应用程序需要传递缓存区.
  4. 事件分离器捕获到读取完成事件后, 激活应用程序注册的事件处理器, 事件处理器直接从缓存区读取数据, 而不需要进行实际的读取操作.

Proactor中写入操作和读取操作, 只不过感兴趣的事件是写入完成事件.

从上面可以看出, Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的, Reactor中需要应用程序自己读取或者写入数据, 而Proactor模式中, 应用程序不需要进行实际的读写过程, 它只需要从缓存区读取或者写入即可, 操作系统会读取缓存区或者写入缓存区到真正的IO设备

IO设计模式比较

主动和被动

Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程, 做什么都要先放到监听事件集
合中等待handler可用时再进行操作;Proactor直接调用异步读写操作, 调用完后立刻返回;
实现
Reactor实现了一个被动的事件分离和分发模型, 服务等待请求事件的到来, 再通过不受间断的同步处
理事件, 从而做出反应;
Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行, 从而提高吞吐
量;并可执行耗时长的任务(各个任务间互不影响)

优点

Reactor实现相对简单, 对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待, 并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的, 可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,
Proactor性能更高, 能够处理耗时长的并发场景;

缺点

Reactor处理耗时长的操作会造成事件分发的阻塞, 影响到后续事件的处理;
Proactor实现逻辑复杂;依赖操作系统对异步的支持, 目前实现了纯异步操作的操作系统少, 实现优秀
的如windows IOCP, 但由于其windows系统用于服务器的局限性, 目前应用范围较小;而Unix/Linux
系统对纯异步的支持有限, 应用事件驱动的主流还是通过select/epoll来实现;

使用场景

  • BIO方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 并发局限于应用中, JDK1.4以前的唯一选择, 但程序直观简单易理解.

  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构, 比如聊天服务器, 并发局限于应用中, 编程比较复杂, JDK1.4开始支持.

  • AIO方式使用于连接数目多且连接比较长(重操作)的架构, 比如相册服务器, 充分调用OS参与并发操作, 编程比较复杂, JDK7开始支持.


[参考文献]:

  1. Think in Java

转载自 探索 ConcurrentHashMap 高并发性的实现机制

ConcurrentHashMap 是 Java concurrent 包的重要成员。本文将结合 Java 内存模型,来分析 ConcurrentHashMap 的 JDK 源代码。通过本文,读者将了解到 ConcurrentHashMap 高并发性的具体实现机制。这对于我们在实际应用中更加高效的使用它是很有帮助的。

[TOC]

简介

ConcurrentHashMap 是 util.concurrent 包的重要成员。本文将结合 Java 内存模型,分析 JDK 源代码,探索 ConcurrentHashMap 高并发的具体实现机制。

由于 ConcurrentHashMap 的源代码实现依赖于 Java 内存模型,所以阅读本文需要读者了解 Java 内存模型。同时,ConcurrentHashMap 的源代码会涉及到散列算法和链表数据结构,所以,读者需要对散列算法和基于链表的数据结构有所了解。

Java 内存模型

由于 ConcurrentHashMap 是建立在 Java 内存模型基础上的,为了更好的理解 ConcurrentHashMap,让我们首先来了解一下 Java 的内存模型。

Java 语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及何时可以确保它们对线程是可见的。下面我们将分别介绍 Java 内存模型的重排序,内存可见性和 happens-before 关系。

重排序

内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码 – 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

重排序类型包括:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
  • 处理器可以乱序或者并行的执行指令。
  • 缓存会改变写入提交到主内存的变量的次序。

内存可见性

由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。

在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:

  • 执行线程 A 的处理器把变量 V 缓存到寄存器中。
  • 执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。
  • 执行线程 B 的处理器的缓存中有变量 V 的旧值。

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。

下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C

ConcurrentHashMap 的结构分析

为了更好的理解 ConcurrentHashMap 高并发的具体实现,让我们先探索它的结构模型。

ConcurrentHashMap 类中包含两个静态内部类 HashEntrySegmentHashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

HashEntry 类

HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

清单 1.HashEntry 类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
static final class HashEntry<K,V> { 
final K key; // 声明 key 为 final 型
final int hash; // 声明 hash 值为 final 型
volatile V value; // 声明 value 为 volatile 型
final HashEntry<K,V> next; // 声明 next 为 final 型

HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}

在 ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图:

图 1. 插入三个节点后桶的结构示意图:

注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

避免热点域

ConcurrentHashMap中,每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的个数。这样当需要更新计数器时,不用锁定整个 ConcurrentHashMap

Segment 类

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

清单 2.Segment 类的定义

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
static final class Segment<K,V> extends ReentrantLock implements Serializable { 
/**
* 在本 segment 范围内,包含的 HashEntry 元素的个数
* 该变量被声明为 volatile 型
*/
transient volatile int count;

/**
* table 被更新的次数
*/
transient int modCount;

/**
* 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列
*/
transient int threshold;

/**
* table 是由 HashEntry 对象组成的数组
* 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表
* table 数组的数组成员代表散列映射表的一个桶
* 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
* 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16
*/
transient volatile HashEntry<K,V>[] table;

/**
* 装载因子
*/
final float loadFactor;

Segment(int initialCapacity, float lf) {
loadFactor = lf;
setTable(HashEntry.<K,V>newArray(initialCapacity));
}

/**
* 设置 table 引用到这个新生成的 HashEntry 数组
* 只能在持有锁或构造函数中调用本方法
*/
void setTable(HashEntry<K,V>[] newTable) {
// 计算临界阀值为新数组的长度与装载因子的乘积
threshold = (int)(newTable.length * loadFactor);
table = newTable;
}

/**
* 根据 key 的散列值,找到 table 中对应的那个桶(table 数组的某个数组成员)
*/
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
// 把散列值与 table 数组长度减 1 的值相“与”,
// 得到散列值对应的 table 数组的下标
// 然后返回 table 数组中此下标对应的 HashEntry 元素
return tab[hash & (tab.length - 1)];
}
}

下图是依次插入 ABC 三个 HashEntry 节点后,Segment 的结构示意图。

图 2. 插入三个节点后 Segment 的结构示意图:

ConcurrentHashMap 类

ConcurrentHashMap 在默认并发级别会创建包含 16 个 Segment 对象的数组。每个 Segment 的成员对象 table 包含若干个散列表的桶。每个桶是由 HashEntry 链接起来的一个链表。如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16。

清单 3.ConcurrentHashMap 类的定义

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
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
implements ConcurrentMap<K, V>, Serializable {

/**
* 散列映射表的默认初始容量为 16,即初始默认为 16 个桶
* 在构造函数中没有指定这个参数时,使用本参数
*/
static final int DEFAULT_INITIAL_CAPACITY= 16;

/**
* 散列映射表的默认装载因子为 0.75,该值是 table 中包含的 HashEntry 元素的个数与
* table 数组长度的比值
* 当 table 中包含的 HashEntry 元素的个数超过了 table 数组的长度与装载因子的乘积时,
* 将触发 再散列
* 在构造函数中没有指定这个参数时,使用本参数
*/
static final float DEFAULT_LOAD_FACTOR= 0.75f;

/**
* 散列表的默认并发级别为 16。该值表示当前更新线程的估计数
* 在构造函数中没有指定这个参数时,使用本参数
*/
static final int DEFAULT_CONCURRENCY_LEVEL= 16;

/**
* segments 的掩码值
* key 的散列码的高位用来选择具体的 segment
*/
final int segmentMask;

/**
* 偏移量
*/
final int segmentShift;

/**
* 由 Segment 对象组成的数组
*/
final Segment<K,V>[] segments;

/**
* 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。
*/
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if(!(loadFactor > 0) || initialCapacity < 0 ||
concurrencyLevel <= 0)
throw new IllegalArgumentException();

if(concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;

// 寻找最佳匹配参数(不小于给定参数的最接近的 2 次幂)
int sshift = 0;
int ssize = 1;
while(ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift; // 偏移量值
segmentMask = ssize - 1; // 掩码值
this.segments = Segment.newArray(ssize); // 创建数组

if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if(c * ssize < initialCapacity)
++c;
int cap = 1;
while(cap < c)
cap <<= 1;

// 依次遍历每个数组元素
for(int i = 0; i < this.segments.length; ++i)
// 初始化每个数组元素引用的 Segment 对象
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}

/**
* 创建一个带有默认初始容量 (16)、默认加载因子 (0.75) 和 默认并发级别 (16)
* 的空散列映射表。
*/
public ConcurrentHashMap() {
// 使用三个默认参数,调用上面重载的构造函数来创建空散列映射表
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

}

下面是 ConcurrentHashMap 的结构示意图。

图 3.ConcurrentHashMap 的结构示意图:

用分离锁实现多个线程间的并发写操作

在 ConcurrentHashMap 中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作才需要加锁。下面以 put 操作为例说明对 ConcurrentHashMap 做结构性修改的过程。

首先,根据 key 计算出对应的 hash 值:

清单 4.Put 方法的实现

1
2
3
4
5
6
7
public V put(K key, V value) { 
if (value == null) //ConcurrentHashMap 中不允许用 null 作为映射值
throw new NullPointerException();
int hash = hash(key.hashCode()); // 计算键对应的散列码
// 根据散列码找到对应的 Segment
return segmentFor(hash).put(key, hash, value, false);
}

然后,根据 hash 值找到对应的Segment 对象:

清单 5.根据 hash 值找到对应的 Segment

1
2
3
4
5
6
7
8
9
10
/** 
* 使用 key 的散列码来得到 segments 数组中对应的 Segment
*/
final Segment<K,V> segmentFor(int hash) {
// 将散列值右移 segmentShift 个位,并在高位填充 0
// 然后把得到的值与 segmentMask 相“与”
// 从而得到 hash 值对应的 segments 数组的下标值
// 最后根据下标值返回散列码对应的 Segment 对象
return segments[(hash >>> segmentShift) & segmentMask];
}

最后,在这个 Segment 中执行具体的 put 操作:

清单 6.在 Segment 中执行具体的 put 操作

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
V put(K key, int hash, V value, boolean onlyIfAbsent) { 
lock(); // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap
try {
int c = count;

if (c++ > threshold) // 如果超过再散列的阈值
rehash(); // 执行再散列,table 数组的长度将扩充一倍

HashEntry<K,V>[] tab = table;
// 把散列码值与 table 数组的长度减 1 的值相“与”
// 得到该散列码对应的 table 数组的下标值
int index = hash & (tab.length - 1);
// 找到散列码对应的具体的那个桶
HashEntry<K,V> first = tab[index];

HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;

V oldValue;
if (e != null) { // 如果键 / 值对以经存在
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; // 设置 value 值
}
else { // 键 / 值对不存在
oldValue = null;
++modCount; // 要添加新节点到链表中,所以 modCont 要加 1
// 创建新节点,并添加到链表的头部
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // 写 count 变量
}
return oldValue;
} finally {
unlock(); // 解锁
}
}

注意:
这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntryvalue 域的值为 null,此时需要加锁后重新读取该值)。

相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

用 HashEntery 对象的不变性来降低读操作对加锁的需求

在代码清单“HashEntry 类的定义”中我们可以看到,HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。

同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,**当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。**这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。

下面我们分别来分析线程写入的两种情形:对散列表做非结构性修改的操作和对散列表做结构性修改的操作。

非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程“看到”。

对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。

结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。

clear 操作只是把 ConcurrentHashMap 中所有的桶“置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。

从上面的代码清单“在 Segment 中执行具体的 put 操作”中,我们可以看出:put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。

下面来分析 remove 操作,先让我们来看看 remove 操作的源代码实现。

清单 7.remove 操作

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
V remove(Object key, int hash, Object value) { 
lock(); // 加锁
try{
int c = count - 1;
HashEntry<K,V>[] tab = table;
// 根据散列码找到 table 的下标值
int index = hash & (tab.length - 1);
// 找到散列码对应的那个桶
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while(e != null&& (e.hash != hash || !key.equals(e.key)))
e = e.next;

V oldValue = null;
if(e != null) {
V v = e.value;
if(value == null|| value.equals(v)) { // 找到要删除的节点
oldValue = v;
++modCount;
// 所有处于待删除节点之后的节点原样保留在链表中
// 所有处于待删除节点之前的节点被克隆到新链表中
HashEntry<K,V> newFirst = e.next;// 待删节点的后继结点
for(HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
// 把桶链接到新的头结点
// 新的头结点是原链表中,删除节点之前的那个节点
tab[index] = newFirst;
count = c; // 写 count 变量
}
}
return oldValue;
} finally{
unlock(); // 解锁
}
}

和 get 操作一样,首先根据散列码找到具体的链表;然后遍历这个链表找到要删除的节点;最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明 remove 操作。假设写线程执行 remove 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。

图 4. 执行删除之前的原链表:

图 5. 执行删除之后的新链表

从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了

在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。

综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。

用 Volatile 变量协调读写线程间的内存可见性

由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。

下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。

图 6. 协调读 - 写线程间的内存可见性的示意图:

假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。

根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。

根据 Volatile 变量法则,B happens-before C。

根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。

虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能“看到”这些修改。

ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。

清单 8.Count 变量的声明

1
transient volatile int count;

所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法:

清单 9.get 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
V get(Object key, int hash) { 
if(count != 0) { // 首先读 count 变量
HashEntry<K,V> e = getFirst(hash);
while(e != null) {
if(e.hash == hash && key.equals(e.key)) {
V v = e.value;
if(v != null)
return v;
// 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}

在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。

根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程“看到”。

这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。

ConcurrentHashMap 实现高并发的总结

基于通常情形而优化

在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。

总结

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  1. 减小请求 同一个锁的 频率。
  2. 减少持有锁的 时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  1. 用分离锁实现多个线程间的更深层次的共享访问。
  2. 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  3. 通过对同一个 Volatile 变量的写/读访问,协调不同线程间读/写操作的内存可见性。

使用分离锁,减小了请求 同一个锁 的频率。

通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读/写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。

通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。

Java 8中的优化

CAS版本的HashMap


jdk 1.8 取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作,在某些方面提升了性能。并且追随 1.8 版本的 HashMap 底层实现,使用数组+链表+红黑树进行数据存储。本篇主要介绍 1.8 版本的 ConcurrentHashMap 的具体实现,有关其之前版本的实现情况,这里推荐几篇文章:

谈谈ConcurrentHashMap1.7和1.8的不同实现
ConcurrentHashMap在jdk1.8中的改进
ConcurrentHashMap原理分析(1.7与1.8)

参考文献