Java手撸HttpClient

先将代码贴上,后面再整理下

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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.UUID;

public class HttpTest {

private static final String SPLIT = "\r\n";

private static final String RAW_BODY = """
豫章故郡,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。物华天宝,龙光射牛斗之墟;人杰地灵,徐孺下陈蕃之榻。雄州雾列,俊采星驰。台隍枕夷夏之交,宾主尽东南之美。都督阎公之雅望,棨戟遥临;宇文新州之懿范,襜帷暂驻。十旬休假,胜友如云;千里逢迎,高朋满座。腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。家君作宰,路出名区;童子何知,躬逢胜饯。
时维九月,序属三秋。潦水尽而寒潭清,烟光凝而暮山紫。俨骖騑于上路,访风景于崇阿;临帝子之长洲,得天人之旧馆。层峦耸翠,上出重霄;飞阁流丹,下临无地。鹤汀凫渚,穷岛屿之萦回;桂殿兰宫,即冈峦之体势。
披绣闼,俯雕甍,山原旷其盈视,川泽纡其骇瞩。闾阎扑地,钟鸣鼎食之家;舸舰弥津,青雀黄龙之舳。云销雨霁,彩彻区明。落霞与孤鹜齐飞,秋水共长天一色。渔舟唱晚,响穷彭蠡之滨;雁阵惊寒,声断衡阳之浦。
遥襟甫畅,逸兴遄飞。爽籁发而清风生,纤歌凝而白云遏。睢园绿竹,气凌彭泽之樽;邺水朱华,光照临川之笔。四美具,二难并。穷睇眄于中天,极娱游于暇日。天高地迥,觉宇宙之无穷;兴尽悲来,识盈虚之有数。望长安于日下,目吴会于云间。地势极而南溟深,天柱高而北辰远。关山难越,谁悲失路之人?萍水相逢,尽是他乡之客。怀帝阍而不见,奉宣室以何年?
嗟乎!时运不齐,命途多舛。冯唐易老,李广难封。屈贾谊于长沙,非无圣主;窜梁鸿于海曲,岂乏明时?所赖君子见机,达人知命。老当益壮,宁移白首之心?穷且益坚,不坠青云之志。酌贪泉而觉爽,处涸辙以犹欢。北海虽赊,扶摇可接;东隅已逝,桑榆非晚。孟尝高洁,空余报国之情;阮籍猖狂,岂效穷途之哭!
勃,三尺微命,一介书生。无路请缨,等终军之弱冠;有怀投笔,慕宗悫之长风。舍簪笏于百龄,奉晨昏于万里。非谢家之宝树,接孟氏之芳邻。他日趋庭,叨陪鲤对;今兹捧袂,喜托龙门。杨意不逢,抚凌云而自惜;钟期既遇,奏流水以何惭?
呜乎!胜地不常,盛筵难再;兰亭已矣,梓泽丘墟。临别赠言,幸承恩于伟饯;登高作赋,是所望于群公。敢竭鄙怀,恭疏短引;一言均赋,四韵俱成。请洒潘江,各倾陆海云尔:
滕王高阁临江渚,佩玉鸣鸾罢歌舞。
画栋朝飞南浦云,珠帘暮卷西山雨。
闲云潭影日悠悠,物换星移几度秋。
阁中帝子今何在?槛外长江空自流。\r
\r
""";

public static void main(String[] args) throws Exception {
for (int i = 0; i < 600; i++) {
new Thread(() -> {
try {
httpPostMultipartFormData(new File("/data/abc.png"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}).start();
}
System.out.println("线程启动成功");
}

private static void httpPostMultipartFormData(File file) throws Exception {
String host = "172.20.103.88";
String path = "/fuck/file2";
int port = 8080;
try (Socket client = newSocket(host, port);
OutputStream os = client.getOutputStream();
InputStream is = client.getInputStream()) {

// 分隔符,分隔符前面必须加上--,否则不会被识别
String boundary = UUID.randomUUID().toString().replace("-", "");

byte[] data = writeFile(file, boundary);

writeLine(os, "POST " + path + " HTTP/1.1");
writeLine(os, "host:" + host + ":8080");
writeLine(os, "Connection:keep-alive");
writeLine(os, "Content-Type:multipart/form-data;boundary=" + boundary);
writeLine(os, "Content-Length:" + data.length);
writeLine(os, null);

System.out.println("开始上传");
// int c = 0;
for (int i = 0; i < data.length; i++) {
if (i != 0 && i % 100 == 0) {
Thread.sleep(8000L);
// c++;
// if (c == 3) {
// client.close();
// }
System.out.println("睡一会儿..." + i);
os.flush();
}
os.write(data[i]);
}
os.flush();

printResponse(is);
}
}

private static byte[] writeFile(File file, String boundary) throws IOException {
String mimeType = Files.probeContentType(file.toPath());

ByteArrayOutputStream os = new ByteArrayOutputStream();
writeLine(os, "--" + boundary);
writeLine(os, "Content-Disposition: form-data; name=\"file\"; filename=\"abc.png\"");
writeLine(os, "Content-Type: " + mimeType);
writeLine(os, null);

try (InputStream is = new FileInputStream(file)) {
byte[] bytes = new byte[1024];
int len;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
}
writeLine(os, null);
writeLine(os, "--" + boundary + "--");
return os.toByteArray();
}

// private static class MultipartFile {
// private String header;
// private String contentType;
// private File file;
// private String boundary;
//
// public MultipartFile(File file) {
// this.file = file;
// this.header = buildHeader();
// }
//
// private String buildHeader() {
// this.header = new StringBuilder()
// .append("--" + boundary).append(SPLIT)
// .append("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"").append(SPLIT)
// .toString();
// }
// }

// private static void httpPostChunked(byte[] data, int segment) throws IOException {
// try (Socket client = newSocket("misc.test.com", 443);
// OutputStream os = client.getOutputStream();
// InputStream is = client.getInputStream()) {
//
// writeLine(os, "POST /dump.htm HTTP/1.1");
// writeLine(os, "host:misc.test.com");
// writeLine(os, "Connection:close");
// writeLine(os, "Transfer-Encoding: chunked");
// writeLine(os, null);
//
// int len = data.length / segment;
// for (int i = 0; i < segment; i++) {
// int size = len;
// if (i == segment - 1) {
// size = data.length - (i * len);
// }
// writeLine(os, Integer.toString(size, 16));
// os.write(data, len * i, size);
// writeLine(os, null);
// }
// writeLine(os, "0");
// writeLine(os, null);
// printResponse(is);
// }
// }

private static void writeLine(OutputStream os, String str) throws IOException {
byte[] data;
if (str != null) {
data = (str + SPLIT).getBytes(StandardCharsets.UTF_8);
} else {
data = SPLIT.getBytes(StandardCharsets.UTF_8);
}
os.write(data);
}

private static void httpPost(byte[] body) throws Exception {
String host = "nginx-test.test.cn";
// String path = "/test/file.htm";
// String path = "/fuck/file2";
String path = "/api/test/sleep.htm?ms=100";
int port = 8080;

try (Socket client = newSocket(host, port);
OutputStream os = client.getOutputStream();
InputStream is = client.getInputStream()) {
writeLine(os, "POST " + path + " HTTP/1.1");
writeLine(os, "host:" + host);
writeLine(os, "Connection:close");
writeLine(os, "Content-Type:text/plain;charset=utf-8");
writeLine(os, "Content-Length:" + body.length);
writeLine(os, null);
os.flush();

System.out.println("开始上传");
for (int i = 0; i < body.length; i++) {
if (i != 0 && i % 100 == 0) {
Thread.sleep(300L);
System.out.println("睡一会儿..." + i);
os.flush();
}
os.write(body[i]);
}
os.flush();
// os.write(body);

printResponse(is);
}
}


private static Socket newSocket(String host, int port) {
try {
if (port == 443) {
return SSLSocketFactory.getDefault().createSocket(host, port);
} else {
return new Socket(host, port);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void printResponse(InputStream is) throws IOException {
System.out.println("\n-----输出内容-----\n");
int length;
byte[] buffer = new byte[1024];
while ((length = is.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, length));
}
}
}

Java生成GBK所有字符,以及判断字符是否是GBK编码内字符

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
/**
* 参见文档: <a href="https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97%E5%86%85%E7%A0%81%E6%89%A9%E5%B1%95%E8%A7%84%E8%8C%83">汉字内码扩展规范</a>
* <p>
* GBK/1:GB2312非汉字符号: A1~A9, A1~FE
* GBK/2:GB2312汉字: B0~F7, A1~FE
* GBK/3:扩充汉字: 81~A0, 40~FE (7F除外)
* GBK/4:扩充汉字: AA~FE, 40~A0 (7F除外)
* GBK/5:扩充非汉字: A8~A9, 40~A0 (7F除外)
*
* @author Gao Youbo
* @since 2022-08-23 16:34
*/
public class GBK {
private static final Charset CHARSET = Charset.forName("GBK");

/**
* 获取所有GBK编码字符
*/
public static List<String> getGBK() {
List<String> words = new ArrayList<>();
byte[] bytes = new byte[2];

// GBK/1:GB2312非汉字符号
for (int b1 = 0xA1; b1 <= 0xA9; b1++) {
bytes[0] = (byte) b1;
for (int b2 = 0xA1; b2 <= 0xFE; b2++) {
bytes[1] = (byte) b2;
words.add(new String(bytes, CHARSET));
}
}

// GBK/2:GB2312汉字
for (int b1 = 0xB0; b1 <= 0xF7; b1++) {
bytes[0] = (byte) b1;
for (int b2 = 0xA1; b2 <= 0xFE; b2++) {
bytes[1] = (byte) b2;
words.add(new String(bytes, CHARSET));
}
}

// GBK/3:扩充汉字
for (int b1 = 0x81; b1 <= 0xA0; b1++) {
bytes[0] = (byte) b1;
for (int b2 = 0x40; b2 <= 0xFE; b2++) {
bytes[1] = (byte) b2;
if (b2 != 0x7F) {
words.add(new String(bytes, CHARSET));
}
}
}

// GBK/4:扩充汉字
for (int b1 = 0xAA; b1 <= 0xFE; b1++) {
bytes[0] = (byte) b1;
for (int b2 = 0x40; b2 <= 0xA0; b2++) {
bytes[1] = (byte) b2;
if (b2 != 0x7F) {
words.add(new String(bytes, CHARSET));
}
}
}

// GBK/5:扩充非汉字
for (int b1 = 0xA8; b1 <= 0xA9; b1++) {
bytes[0] = (byte) b1;
for (int b2 = 0x40; b2 <= 0xA0; b2++) {
bytes[1] = (byte) b2;
if (b2 != 0x7F) {
words.add(new String(bytes, CHARSET));
}
}
}

return words;
}

public static boolean isGBK(String str) {
boolean isGBK = false;
char[] chars = str.toCharArray();
for (char c : chars) {
byte[] bytes = String.valueOf(c).getBytes(CHARSET);
if (bytes.length == 2) { // GBK 编码为两个字节
int b1 = bytes[0] & 0xff;
int b2 = bytes[1] & 0xff;
if (b1 >= 0xA1 && b1 <= 0xA9 && b2 >= 0xA1 & b2 <= 0xFE) {
isGBK = true;
break;
}

if (b1 >= 0xB0 && b1 <= 0xF7 && b2 >= 0xA1 & b2 <= 0xFE) {
isGBK = true;
break;
}

if (b1 >= 0x81 && b1 <= 0xA0 && b2 >= 0x40 & b2 <= 0xFE && b2 != 0x7F) {
isGBK = true;
break;
}

if (b1 >= 0xAA && b1 <= 0xFE && b2 >= 0x40 & b2 <= 0xA0 && b2 != 0x7F) {
isGBK = true;
break;
}

if (b1 >= 0xA8 && b1 <= 0xA9 && b2 >= 0x40 & b2 <= 0xA0 && b2 != 0x7F) {
isGBK = true;
break;
}
}
}
return isGBK;
}
}

Java实现Base128编码

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
/**
* @author Gao Youbo
* @since 2022-08-19 10:28
*/
public class Base128 {
private static final char[] DEFAULT_CHAR_TABLE = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '-', '_',

'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π',
'ρ', 'ς', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ',
'ω', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ',
'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î',
'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö',
'÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ',
'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć',
};

private final char[] charTable;
private final HashMap<Character, Integer> indexTable;

public Base128() {
this(DEFAULT_CHAR_TABLE);
}

public Base128(char[] charTable) {
this.charTable = charTable;
this.indexTable = new HashMap<>(charTable.length);
for (int j = 0; j < charTable.length; j++) {
indexTable.put(charTable[j], j);
}
}

private String encodeToString(byte[] data) {
if (data == null || data.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
int tail = 0;
for (int i = 0; i < data.length; i++) {
int mov = (i % 7 + 1);
int curr = 0xFF & data[i];
int code = tail + (curr >> mov);
sb.append(charTable[code]);
tail = (0xFF & (curr << (8 - mov))) >> 1;
if (mov == 7) {
sb.append(charTable[tail]);
tail = 0;
}
}
sb.append(charTable[tail]);
return sb.toString();
}

private byte[] decode(String str) {
if (StringUtils.isBlank(str)) {
return new byte[]{};
}
int length = (int) Math.floor(str.length() * 0.875);
byte[] result = new byte[length];
int idx = 0;
int head = indexTable.get(str.charAt(0)) << 1;
for (int i = 1; i < str.length(); ) {
int mod = i % 8;
int code = indexTable.get(str.charAt(i++));
result[idx++] = (byte) (0xFF & (head + (code >> (7 - mod))));
if (mod == 7) {
head = 0xFF & (indexTable.get(str.charAt(i++)) << 1);
} else {
head = 0xFF & (code << (mod + 1));
}
}
return result;
}
}

下面提供一份更全的编码字节表,使用这个表可以实现Base256等

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
private static final char[] symbolTable = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '-', '_',

'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ',
'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π',
'ρ', 'ς', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ',
'ω', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ',
'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î',
'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö',
'÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ',
'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć',

'ć',
'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 'č', 'Ď', 'ď',
'Đ', 'đ', 'Ē', 'ē', 'Ĕ', 'ĕ', 'Ė', 'ė',
'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ',
'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ',
'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ', 'Į', 'į',
'İ', 'ı', 'IJ', 'ij', 'Ĵ', 'ĵ', 'Ķ', 'ķ',
'ĸ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ',
'ŀ', 'Ł', 'ł', 'Ń', 'ń', 'Ņ', 'ņ', 'Ň',
'ň', 'ʼn', 'Ŋ', 'ŋ', 'Ō', 'ō', 'Ŏ', 'ŏ',
'Ő', 'ő', 'Œ', 'œ', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ',
'Ř', 'ř', 'Ś', 'ś', 'Ŝ', 'ŝ', 'Ş', 'ş',
'Š', 'š', 'Ţ', 'ţ', 'Ť', 'ť', 'Ŧ', 'ŧ',
'Ũ', 'ũ', 'Ū', 'ū', 'Ŭ', 'ŭ', 'Ů', 'ů',
'Ű', 'ű', 'Ų', 'ų', 'Ŵ', 'ŵ', 'Ŷ', 'ŷ',
'Ÿ', 'Ź', 'ź', 'Ż', 'ż', 'Ž', 'ž', 'ſ',
'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƅ', 'ƅ', 'Ɔ', 'Ƈ',
'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'ƍ', 'Ǝ', 'Ə',
'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'Ɣ', 'ƕ', 'Ɩ', 'Ɨ',
'Ƙ', 'ƙ', 'ƚ', 'ƛ', 'Ɯ', 'Ɲ', 'ƞ', 'Ɵ',
'Ơ', 'ơ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'Ʀ', 'Ƨ',
'ƨ',

// '=', '*', '&', '^', '%', '$', '#', '@'
};

Ipv4地址段匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IpMatcher {
public static boolean matchIPV4(String ip, String cidr) {
String[] ips = ip.split("\\.");
if (ips.length != 4) {
return false;
}
int ipAddr = (Integer.parseInt(ips[0]) << 24)
| (Integer.parseInt(ips[1]) << 16)
| (Integer.parseInt(ips[2]) << 8) | Integer.parseInt(ips[3]);
int type = Integer.parseInt(cidr.replaceAll(".*/", ""));
int mask = 0xFFFFFFFF << (32 - type);
String cidrIp = cidr.replaceAll("/.*", "");
String[] cidrIps = cidrIp.split("\\.");
int cidrIpAddr = (Integer.parseInt(cidrIps[0]) << 24)
| (Integer.parseInt(cidrIps[1]) << 16)
| (Integer.parseInt(cidrIps[2]) << 8)
| Integer.parseInt(cidrIps[3]);
return (ipAddr & mask) == (cidrIpAddr & mask);
}

public static void main(String[] args) {
System.out.println(matchIPV4("172.16.0.1", "172.16.0.0/20"));
}
}

同时安装多个版本JDK,快速切换JDK版本

推荐一个工具:https://github.com/jenv/jenv

1. Getting Started

Follow the steps below to get a working jenv installation with knowledge of your java environment. Read all the code you execute carefully: a $ symbol at the beginning of a line should be omitted, since it’s meant to show you entering a command into your terminal and observing the response after the command.

1.1 Installing jenv

On OSX, the simpler way to install jEnv is using Homebrew

position-relative
1
brew install jenv

Alternatively, and on Linux, you can install it from source :

position-relative
1
2
3
4
5
6
7
git clone https://github.com/jenv/jenv.git ~/.jenv
# Shell: bash
echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(jenv init -)"' >> ~/.bash_profile
# Shell: zsh
echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(jenv init -)"' >> ~/.zshrc

Restart your shell by closing and reopening your terminal window or running exec $SHELL -l in the current session for the changes to take effect.

To verify jenv was installed, run jenv doctor. On a macOS machine, you’ll observe the following output:

position-relative
1
2
3
4
5
6
$ jenv doctor
[OK] No JAVA_HOME set
[ERROR] Java binary in path is not in the jenv shims.
[ERROR] Please check your path, or try using /path/to/java/home is not a valid path to java installation.
PATH : /Users/user/.jenv/libexec:/Users/user/.jenv/shims:/Users/user/.jenv/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
[OK] Jenv is correctly loaded

Observe that jenv is correctly loaded but Java is not yet installed.

To make sure JAVA_HOME is set, make sure to enable the export plugin:

position-relative
1
2
jenv enable-plugin export
exec $SHELL -l

Problem? Please visit the Trouble Shooting Wiki page.

Continue to the next section to install java.

Untested: While this fork has improved fish shell support, it has not been tested by this maintainer. To install jenv for Fish according to the contributor’s instructions:

position-relative
1
2
3
4
echo 'set PATH $HOME/.jenv/bin $PATH' >> ~/.config/fish/config.fish
echo 'status --is-interactive; and source (jenv init -|psub)' >> ~/.config/fish/config.fish
cp ~/.jenv/fish/jenv.fish ~/.config/fish/functions/jenv.fish
cp ~/.jenv/fish/export.fish ~/.config/fish/functions/export.fish

1.2 Adding Your Java Environment

Use jenv add to inform jenv where your Java environment is located. jenv does not, by itself, install Java.

For example, on macOS, use brew to install the latest Java (OpenJDK 11) followed by the appropriate jenv add PATH_TO_JVM_HOME command to recognize it.

position-relative
1
2
brew install --cask java
jenv add $(/usr/libexec/java_home)

With macOS OpenJDK 11.0.2 installed, for example, either of these commands will add /Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home as a valid JVM. Your JVM directory may vary!

Observe now that this version of Java is added to your java versions command:

position-relative
1
2
3
4
5
$ jenv versions
* system (set by /Users/user/.jenv/version)
11.0
11.0.2
openjdk64-11.0.2

By default, the latest version of Java is your system Java on macOS.

We’ll now set a jenv local VERSION local Java version for the current working directory. This will create a .java-version file we can check into Git for our projects, and jenv will load it correctly when a shell is started from this directory.

position-relative
1
2
3
4
$ jenv local 11.0.2
$ exec $SHELL -l
$ cat .java-version
11.0.2

Is JAVA_HOME set?

position-relative
1
2
$ echo ${JAVA_HOME}
/Users/bberman/.jenv/versions/11.0.2

Yes! Observe that JAVA_HOME is set to a valid shim directory. Unlike the main repository’s documentation we helpfully installed the export plugin, and we now have the most important jenv features covered.

If you executed this commands inside your $HOME directory, you can now delete .java-version:

position-relative
1
rm .java-version

1.3 Setting a Global Java Version

Use jenv global VERSION to set a global Java version:

position-relative
1
jenv global 11.0.2

When you next open a shell or terminal window, this version of Java will be the default.

On macOS, this sets JAVA_HOME for GUI applications on macOS using jenv macos-javahome. Integrates this tutorial to create a file that does not update dynamically depending on what local or shell version of Java is set, only global.

1.4 Setting a Shell Java Version

Use jenv shell VERSION to set the Java used in this particular shell session:

position-relative
1
jenv shell 11.0.2

2 Common Workflows

These common workflows demonstrate how to use jenv to solve common problems.

2.1 Using Two JVMs on macOS

Our goal is to have both the latest version of Java and JDK 8 installed at the same time. This is helpful for developing Android applications, whose build tools are sensitive to using an exact Java version.

We’ll resume where we left off with Java 11.0.2 installed. Let’s install Java 8 now:

position-relative
1
2
brew install --cask adoptopenjdk8
brew install --cask caskroom/versions/adoptopenjdk8

This will install the latest version of Java 8 to a special directory in macOS. Let’s see which directory that is:

position-relative
1
2
3
$ ls -1 /Library/Java/JavaVirtualMachines 
adoptopenjdk-8.jdk
openjdk-11.0.2.jdk

Observe the adoptopenjdk-8.jdk directory. Your exact version may vary. We cannot retrieve this using /usr/libexec/java_home, unfortunately. We’ll add the Java home directory using jenv so that it shows up in our jenv versions command:

position-relative
1
2
3
4
5
6
7
8
9
10
11
12
13
$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/
openjdk64-1.8.0.222 added
1.8.0.222 added
1.8 added
$ jenv versions
* system
1.8
1.8.0.222
openjdk64-1.8.0.222
11.0
11.0.2
openjdk64-11.0.2
oracle64-1.8.0.202-ea

Java程序员Go语言入门简介

为什么是Go语言

  • 类C的语法,这意味着Java、C#、JavaScript程序员能很快的上手
  • 有自己的垃圾回收机制
  • 跨平台、编译即可执行无需安装依赖环境
  • 支持反射

Go语言简介

Go 语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具Python等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。

数据类型

数据类型 说明
bool 布尔
string 字符串
int uint8,uint16,uint32,uint64,int8,int16,int32,int64
float float32,float64
byte byte

参考:https://www.runoob.com/go/go-data-types.html

基本语法

HelloWorld

在线运行示例:https://play.golang.org/p/-4RylAqUV36

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

var name string

func init() {
name = "world"
}

func main() {
fmt.Println("hello " + name)
}

我们来执行一下:

1
2
$ go run main.go # main.go 为刚刚创建的那个文件的名称
$ hello world

变量

变量声明

在线运行示例:https://play.golang.org/p/zPqCkRZgrgp

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

import (
"fmt"
)

func main() {
var name string // 声明
name = "gaoyoubo" // 赋值
fmt.Println(name)

var age int = 18 // 声明并赋值
fmt.Println(age)
}

类型推断

在线运行示例:https://play.golang.org/p/0My8veBvtJ8

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
name := "gaoyoubo"
fmt.Println(name)

age := 18
fmt.Println(age)
}

函数

  • 函数可以有多个返回值
  • 隐式的指定函数是private还是public,函数首字母大写的为public、小写的为private
  • 没有类似Java中的try cachethrow,Go语言是通过将error作为返回值来处理异常。
  • 不支持重载

下面我们通过一个示例来了解一下,在线运行示例:https://play.golang.org/p/PYy3ueuPFS6

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
package main

import (
"errors"
"fmt"
"strconv"
)

func main() {
log1()

log2("hello world")

ret1 := add1(1, 1)
fmt.Println("add1 result:" + strconv.Itoa(ret1))

ret2, err := Add2(0, 1)
if err == nil {
fmt.Println("Add2 result:" + strconv.Itoa(ret2))
} else {
fmt.Println("Add2 error", err)
}
}

// 私有、无入参、无返回值
func log1() {
fmt.Println("execute func log1")
}

// 私有、入参、无返回值
func log2(msg string) {
fmt.Println("execute func log2:" + msg)
}

// 私有、两个入参、一个返回值
func add1(count1, count2 int) int {
total := count1 + count2
fmt.Println("execute func add3, result=" + strconv.Itoa(total))
return total
}

// Public、两个入参、多个返回值
func Add2(count1, count2 int) (int, error) {
if count1 < 1 || count2 < 1 {
return 0, errors.New("数量不能小于1")
}
total := count1 + count2
return total, nil
}

该示例输出结果为:

1
2
3
4
5
execute func log1
execute func log2:hello world
execute func add3, result=2
add1 result:2
Add2 error 数量不能小于1

但函数有多个返回值的时候,有时你只关注其中一个返回值,这种情况下你可以将其他的返回值赋值给空白符:_,如下:

1
2
3
4
_, err := Add2(1, 2)
if err != nil {
fmt.Println(err)
}

空白符特殊在于实际上返回值并没有赋值,所以你可以随意将不同类型的值赋值给他,而不会由于类型不同而报错。

结构体

Go语言不是像Java那样的面向对象的语言,他没有对象和继承的概念。也没有class的概念。在Go语言中有个概念叫做结构体(struct),结构体和Java中的class比较类似。下面我们定义一个结构体:

1
2
3
4
5
type User struct {
Name string
Gender string
Age int
}

上面我们定义了一个结构体User,并为该结构体分别设置了三个公有属性:Name/Gender/Age,下面我们来创建一个User对象。

1
2
3
4
5
user := User{
Name: "hahaha",
Gender: "男",
Age: 18, // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
}

结构体的属性可以在结构体内直接声明,那么如何为结构体声明函数(即Java中的方法)呢,我们来看下下面的示例:在线运行示例:https://play.golang.org/p/01_cTu0RzdH

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 main

import "fmt"

type User struct {
Name string
Gender string
Age int
}

// 定义User的成员方法
func (u *User) addAge() {
u.Age = u.Age + 1
}

func main() {
user := User{
Name: "哈", // 名称
Gender: "男", // 性别
Age: 18, // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
}
user.addAge()
fmt.Println(user.Age)
}

指针类型和值类型

Java中值类型和引用类型都是定死的,int、double、float、long、byte、short、char、boolean为值类型,其他的都是引用类型,而Go语言中却不是这样。

在Go语言中:

  • &表示取地址,例如你有一个变量a那么&a就是变量a在内存中的地址,对于Golang指针也是有类型的,比如a是一个string那么&a是一个string的指针类型,在Go里面叫&string。
  • *表示取值,接上面的例子,假设你定义b := &a 如果你打印b,那么输出的是&a的内存地址,如果要取值,那么需要使用:*b

下面我们来看下例子,在线运行:https://play.golang.org/p/jxAKyVMjnoy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
a := "123"
b := &a

fmt.Println(a)
fmt.Println(b)
fmt.Println(*b)
}

输出结果为:
123
0x40c128
123

并发编程

Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。

Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

下面我们来看一个例子(在线演示:https://play.golang.org/p/U9U-qjuY0t1)

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
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个goroutine
go runing()
// 创建一个匿名的goroutine
go func() {
fmt.Println("喜特:" + time.Now().String())
}()

// 这里sleep一下是因为main方法如果执行完了,main该程序创建的所有goroutine都会退出
time.Sleep(5 * time.Second)
}

func runing() {
fmt.Println("法克:" + time.Now().String())
time.Sleep(3 * time.Second)
}

输出:
法克:2009-11-10 23:00:00 +0000 UTC m=+0.000000001
喜特:2009-11-10 23:00:00 +0000 UTC m=+0.000000001

执行结果说明fuck函数中的sleep三秒并没有影响喜特的输出。

如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下:(在线执行:https://play.golang.org/p/lqUBugLdU-I)

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
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个通道
channel := make(chan int64)

// 异步去生产
go producer(channel)

// 数据消费
consumer(channel)
}

// 生产者
func producer(channel chan<- int64) {
for {
// 将数据写入通道
channel <- time.Now().Unix()
// 睡1秒钟
time.Sleep(time.Second)
}
}

// 消费者
func consumer(channel <-chan int64) {
for {
timestamp := <-channel
fmt.Println(timestamp)
}
}

输出为如下:(每秒钟打印一次)
1257894000
1257894001
1257894002
1257894003

Java程序员觉得不好用的地方

  • 异常处理
  • 没有泛型
  • 不支持多态、重载
  • 不支持注解(但是他的struct中的属性支持tag

参考

半径范围内随机经纬度

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
private static final double EARTH_RADIUS = 6372.796924;

public static GpsInfo getRandomLocation(GpsInfo center, double distance) {
if (distance <= 0) distance = 50;
double lat, lon, brg;
distance = distance / 1000;
GpsInfo location = new GpsInfo();
double maxdist = distance;
maxdist = maxdist / EARTH_RADIUS;
double startlat = rad(center.getLat());
double startlon = rad(center.getLon());
double cosdif = Math.cos(maxdist) - 1;
double sinstartlat = Math.sin(startlat);
double cosstartlat = Math.cos(startlat);
double dist;
double rad360 = 2 * Math.PI;
dist = Math.acos((new Random().nextDouble() * cosdif + 1));
brg = rad360 * new Random().nextDouble();
lat = Math.asin(sinstartlat * Math.cos(dist) + cosstartlat * Math.sin(dist) * Math.cos(brg));
lon = deg(normalizeLongitude(startlon * 1 + Math.atan2(Math.sin(brg) * Math.sin(dist) * cosstartlat, Math.cos(dist) - sinstartlat * Math.sin(lat))));
lat = deg(lat);

location.setLat(padZeroRight(lat));
location.setLon(padZeroRight(lon));
return location;
}

static double rad(double d) {
return d * Math.PI / 180.0;
}

static double deg(double rd) {
return (rd * 180 / Math.PI);
}

static double normalizeLongitude(double lon) {
double n = Math.PI;
if (lon > n) {
lon = lon - 2 * n;
} else if (lon < -n) {
lon = lon + 2 * n;
}
return lon;
}

static double padZeroRight(double s) {
double sigDigits = 8;
s = Math.round(s * Math.pow(10, sigDigits)) / Math.pow(10, sigDigits);
return s;
}

Golang和Java构建工具调查

Github:https://github.com/blindpirate/report-of-build-tools-for-java-and-golang

A Survey on Build Tools of Golang and Java

Java

Conclusion

In January 2017, the usage of build tools in Github’s top 1000 Java repositories is as follows:

Tool Name Reference Count
Gradle 627
Maven 264
Ant 52
Npm 4
Bazel 3
Make 1

And the trending over the past 8 years is:

trending

Algorithm

  • Clone top 1000 Java repositories to local disk
  • Analyze the repositories by identity files:
Tool Name Identity Files
Gradle build.gradle
Maven pom.xml
Ant build.xml
Npm package.json
Bazel BUILD
Make Makefile/makefile

How

  • Make sure Git/Groovy 2.4+/JDK 1.7+ are installed.
  • Run groovy GithubTopRankCrawler.groovy -l java -d <path to store the 1000 repos> to clone all repositories locally.
  • Run groovy JavaBuildToolScanner.groovy -d <path to store the 1000 repos> to analyze these repos.

Golang

Conclusion

There are various package management tools for golang as listed here. But which one is the most popular?

The usage of package manage tools in Github’s top 1000 Go repositories is as follows:

Tool Name Url Reference Count (Feb 2017) Reference Count (Nov 2017)
Makefile Makefile 199 181
dep dep N/A 94
godep godep 119 90
govendor govendor 65 84
glide glide 64 77
gvt gvt 25 16
trash trash 7 13
submodule submodule 8 6
gpm/johnny-deps gpm johnny-deps 7 6
glock glock 5 4
gom gom 4 2
gopack gopack 3 2
gopm gopm 3 1
goop goop 1 1
gvend gvend 2 0

dep had a first release in May 2017, did not exist for first stats.

Technically, make is not a package management tool, here it is just for comparison.

Submodule refers to a set of tools which use git submodule to manage dependencies such as manul and Vendetta and so on.

Algorithm

  • Clone top 1000 Go repositories to local disk
  • Analyze the repositories by identity files:
Tool Name Identity Files
godep Godeps/Godeps.json
govendor vendor/vendor.json
gopm .gopmfile
gvt vendor/manifest
gvend vendor.yml
glide glide.yaml or glide.lock
trash vendor.conf
gom Gomfile
bunch bunchfile
goop Goopfile
goat .go.yaml
glock GLOCKFILE
gobs goproject.json
gopack gopack.config
nut Nut.toml
gpm/johnny-deps Godeps
Makefile makefile or Makefile
submodule .gitmodules

How

  • Make sure Git/Groovy 2.4+/JDK 1.7+ are installed.
  • Run groovy GithubTopRankCrawler.groovy -l go -d <path to store the 1000 repos> to clone all repositories locally. You can use -s to do the shallow clone and decrease disk usage.
  • Run groovy GoBuildToolScanner.groovy <path to store the 1000 repos> to analyze these repos.

Java学习资料

前几天突然有个姑娘加我的QQ(不知道哪儿来的我的QQ),让我参加他们免费的公开课,然后给我分享Java学习资料,我以为是会给我发几本书,就参加了,没想到是一个txt文件😂,内容如下:

Allen-架构师必备技能-分库分表应对数据量过大
链接:https://pan.baidu.com/s/1OF4RUHvRk98pBRdUiifH2g 密码:n4ev

Allen-互联网安全话题-使用https保障你的敏感数据不再裸奔
链接:https://pan.baidu.com/s/1qz23y-3ahaGua4YH02KTyw 密码:fgh0

Tony-多线程Future模式-写出支撑海量并发连接的服务端代码
链接:https://pan.baidu.com/s/1NwzNRxUB0_DPNQo2IW_Xhg 密码:0fpw

Tony-前后端分离架构分析与实现
链接:https://pan.baidu.com/s/1b7XnTibtqW26YCHfuAXkyA 密码:ah24

Tony-高并发系统架构之负载均衡全方位解析
链接:https://pan.baidu.com/s/1a87EH1Xe20O4XYZaNRo-hw 密码:p52e

Tony-学会举一反三-从Redis看RPC原理
链接:https://pan.baidu.com/s/1disSAbJo-01ESCu6_rTHYQ 密码:ih47

-Mike-分布式系统架构技能—zookeeper实现分布式锁
链接:https://pan.baidu.com/s/1adhFuoUsz1sMQTnWNGoKPA 密码:gjzh

Tony-数据库连接池原理源码分析
链接:https://pan.baidu.com/s/1uBiBBt-tJVSz_5t5p4jG3A 密码:jqo6

Allen-深入SpringMVC原理老司机带你手写自己的MVC框架
链接:https://pan.baidu.com/s/1rlhZCSqXaZpXWM_V5CA7EQ 密码:vysj

Tony-JVM类加载机制之JAVA热部署实战开发
链接:https://pan.baidu.com/s/1JSLGrG0k44um7weQcq5rvg 密码:twn3

Tony-实战高并发系统缓存雪崩场景重现及解决方案
链接:https://pan.baidu.com/s/1i8Q7sPNEcUIPYBuqFerRwQ 密码:lwgj

Mike-解密spring-boot-starter
链接:https://pan.baidu.com/s/12-1N3RTb68l3QfUOxSG1jQ 密码:sodb

Tony-细说springcloud微服务架构之客户端负载均衡
链接:https://pan.baidu.com/s/1VK3mMTkKYzRU4G9YXwlOLg 密码:achq

JavaCV分享

JavaCV是什么

JavaCV 是一款开源的视觉处理库,基于GPLv2协议,对各种常用计算机视觉库封装后的一组jar包,封装了OpenCV、ffmpeg、videoInput…等计算机视觉编程人员常用库的接口。

maven引用

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
<properties>
<javacpp.version>1.4.2</javacpp.version>
<!-- 这里要根据自己的平台选择不同的依赖 -->
<!--<javacpp.platform.dependencies>linux-x86_64</javacpp.platform.dependencies>-->
<javacpp.platform.dependencies>macosx-x86_64</javacpp.platform.dependencies>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacpp.version}</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
<classifier>${javacpp.platform.dependencies}</classifier>
</dependency>
</dependencies>

提取视频中的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 从视频中将每一帧的图片提取出来
*
* @param video
* @return
* @throws FrameGrabber.Exception
*/
public static List<BufferedImage> grab(File video) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video.getPath())) {
grabber.start();

List<BufferedImage> images = Lists.newArrayList();
Frame frame;
while ((frame = grabber.grabImage()) != null) {
images.add(Java2DFrameUtils.toBufferedImage(frame));
}
return images;
}
}

图片合成视频

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
private static class VideoRecorder implements Closeable {
private FFmpegFrameRecorder recorder;

public VideoRecorder(String output, int width, int height) throws FrameRecorder.Exception {
recorder = new FFmpegFrameRecorder(output, width, height);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(FPS);
recorder.setAudioBitrate(192000);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();
}

public void addFrame(BufferedImage image) throws FrameRecorder.Exception {
Frame frame = Java2DFrameUtils.toFrame(image);
recorder.record(frame, avutil.AV_PIX_FMT_ARGB);
}

public void addAudio(File audioFile) throws FrameGrabber.Exception, FrameRecorder.Exception {
if (audioFile == null || !audioFile.exists()) {
return;
}
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

@Override
public void close() throws IOException {
recorder.close();
}
}

Java图片处理工具类

这段代码是我四年前写的,当时的使用场景为使用tesseract做图片的预处理。功能包含图片二值化、移除杂色、横向切分、水平切分等。

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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
import java.awt.Color;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.util.ArrayList;
import java.util.List;

/**
* @author Gao Youbo
* @since 2014-05-29 14:34:13
*/
public class ImageUtils {

public static class SplitItem {

private int x;
private int w;
private int y;
private int h;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getW() {
return w;
}

public void setW(int w) {
this.w = w;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

public int getH() {
return h;
}

public void setH(int h) {
this.h = h;
}

}


/**
* 图片纵向切分(切分为列)
*
* @param image
* @param minWidth 每个汉字的最小宽度,如果汉字的最小宽度小于该参数,那么认为系统将一个汉字截断了
* @return
*/
public static List<BufferedImage> splitLengthwaysWithMinWidth(BufferedImage image, int minWidth) {
if (minWidth < 0) {
minWidth = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int width = image.getWidth();
int height = image.getHeight();
int startX = 0;
int endX = 0;
boolean start = false;
boolean end = false;
for (int x = 0; x < width; ++x) {
boolean blank = isXBlank(image, x);
if (!start) { //如果是白色
int space = spaceX(image, x);
x = x + space;
startX = x;
endX = x;
start = true;
}
if (start && !blank) {
endX = x;
}
int wordLength = endX - startX;
if (start && blank && wordLength > 0) {
// 汉字长度小于设定长度,那么认为这不是一个完成的汉字,而是将左右结构的汉字切分成了两份
if (wordLength < minWidth) {
int space = spaceX(image, x);
x = x + space;
} else {
end = true;
endX = x;
}
}
if (start && end && wordLength > 0) {
BufferedImage subImage = image.getSubimage(startX, 0, (endX - startX), height);
subImgs.add(subImage);
start = false;
end = false;
}
}
return subImgs;
}

/**
* x轴上的所有点是空白的(白色的)
*
* @param image
* @param x
* @return
*/
private static boolean isXBlank(BufferedImage image, int x) {
int height = image.getHeight();
for (int y = 0; y < height; y++) {
int rgb = image.getRGB(x, y);
if (isBlack(rgb)) {
return false;
}
}
return true;
}

/**
* 图片纵向切分(切分为列)
*
* @param image
* @param minGap 文字之间的最小间隙,如果间隙文字之间的间隙小于或等于该参数,那么认为该间隙为一个汉字上的正常间隙。主要处理左右结构的一些汉字,例如:”北、川、外...“
* @return
*/
public static List<BufferedImage> splitLengthways(BufferedImage image, int minGap) {
if (minGap < 0) {
minGap = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int width = image.getWidth();
int height = image.getHeight();
List<Integer> weightlist = new ArrayList<>();
for (int x = 0; x < width; ++x) {
int count = 0;
for (int y = 0; y < height; ++y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
}
if (minGap > 0) {
int space = spaceX(image, x);
if (space <= minGap) {
count = count + space;
}
}
weightlist.add(count);
}
List<SplitItem> splitItems = new ArrayList<>();
for (int i = 0; i < weightlist.size(); i++) {
int length = 0;
while (i < weightlist.size() && weightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int x = i - length;
int w = length;
int y = 0;
int h = height;
SplitItem item = new SplitItem();
item.setX(x);
item.setW(w);
item.setY(y);
item.setH(h);
splitItems.add(item);
}
}
for (SplitItem splitItem : splitItems) {
subImgs.add(image.getSubimage(splitItem.getX(), splitItem.getY(), splitItem.getW(), splitItem.getH()));
}
return subImgs;
}

/**
* X轴上两个字之间的间距
*
* @param image
* @param currentX 当前索引所在的x坐标
* @return
*/
private static int spaceX(BufferedImage image, int currentX) {
int w = image.getWidth();
int h = image.getHeight();
int spaceLength = 0;
for (int x = currentX; x < w; x++) {
boolean space = true;
for (int y = 0; y < h; y++) {
if (isBlack(image.getRGB(x, y))) { //有黑色的,表明非空白
space = false;
break;
}
}
if (space) {
spaceLength++;
} else {
return spaceLength;
}
}
return spaceLength;
}


/**
* y轴上两个字之间的间距
*
* @param image
* @param currentY 当前索引所在的y坐标
* @return
*/
private static int spaceY(BufferedImage image, int currentY) {
int w = image.getWidth();
int h = image.getHeight();
int spaceLength = 0;
for (int y = currentY; y < h; y++) {
boolean space = true;
for (int x = 0; x < w; x++) {
if (isBlack(image.getRGB(x, y))) { //有黑色的,表明非空白
space = false;
break;
}
}
if (space) {
spaceLength++;
} else {
return spaceLength;
}
}
return spaceLength;
}


/**
* 图片横向切分(切分为行)
*
* @param image
* @param minGap 两行之间的最小间隙,如果间隙小于或等于该参数,那么认为没有折行
* @return
*/
public static List<BufferedImage> splitCrosswise(BufferedImage image, int minGap) {
if (minGap < 0) {
minGap = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int w = image.getWidth();
int h = image.getHeight();
List<Integer> heightlist = new ArrayList<>();
for (int y = 0; y < h; y++) {
int count = 0;
for (int x = 0; x < w; x++) {
if (ImageUtils.isBlack(image.getRGB(x, y))) {
count++;
}
}
if (minGap > 0) {
int space = spaceY(image, y);
if (space <= minGap) {
count = count + space;
}
}
heightlist.add(count);
}
for (int i = 0; i < heightlist.size(); i++) {
int length = 0;
while (i < heightlist.size() && heightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int y = i - length;
int x = 0;
int height = length;
int width = w;
BufferedImage bufferedImage = image.getSubimage(x, y, width, height);
subImgs.add(bufferedImage);
}
}
return subImgs;
}

/**
* 图片横向切分(切分为行)
*
* @param image
* @return
*/
public static List<BufferedImage> splitCrosswise(BufferedImage image) {
List<BufferedImage> subImgs = new ArrayList<>();
int w = image.getWidth();
int h = image.getHeight();
List<Integer> heightlist = new ArrayList<>();
for (int y = 0; y < h; y++) {
int count = 0;
for (int x = 0; x < w; x++) {
if (ImageUtils.isBlack(image.getRGB(x, y))) {
count++;
}
}
heightlist.add(count);
}
for (int i = 0; i < heightlist.size(); i++) {
int length = 0;
while (i < heightlist.size() && heightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int y = i - length;
int x = 0;
int height = length;
int width = w;
BufferedImage bufferedImage = image.getSubimage(x, y, width, height);
subImgs.add(bufferedImage);
}
}
return subImgs;
}

/**
* 删除杂色(图片二值化)
* <p>
* 默认图片中字体颜色为黑色,如果非黑色像素全部替换为白色
*
* @param image
* @return
* @throws java.lang.InterruptedException
*/
public static final BufferedImage removeMotley(BufferedImage image) throws InterruptedException {
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
int grey = 100;
PixelGrabber pixelGrabber = new PixelGrabber(image.getSource(), 0, 0, width, height, pixels, 0, width);
pixelGrabber.grabPixels();
ColorModel cm = ColorModel.getRGBdefault();
for (int i = 0; i < width * height; i++) {
int red, green, blue;
int alpha = cm.getAlpha(pixels[i]);
if (cm.getRed(pixels[i]) > grey) {
red = 255;
} else {
red = 0;
}
if (cm.getGreen(pixels[i]) > grey) {
green = 255;
} else {
green = 0;
}
if (cm.getBlue(pixels[i]) > grey) {
blue = 255;
} else {
blue = 0;
}
pixels[i] = alpha << 24 | red << 16 | green << 8 | blue; //通过移位重新构成某一点像素的RGB值
}
//将数组中的象素产生一个图像
Image tempImg = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, pixels, 0, width));
image = new BufferedImage(tempImg.getWidth(null), tempImg.getHeight(null), BufferedImage.TYPE_INT_BGR);
image.createGraphics().drawImage(tempImg, 0, 0, null);
return image;
}

/**
* 清除空白部分
*
* @param image
* @return
*/
public static BufferedImage removeSpace(BufferedImage image) {
BufferedImage result = removeTBWhite(image);
return removeLRWhite(result);
}

/**
* 移除上下白色部分(top bottom)
*
* @param image
* @return
*/
public static BufferedImage removeTBWhite(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int start = 0;
int end = 0;
Label1:
for (int y = 0; y < height; ++y) {
int count = 0;
for (int x = 0; x < width; ++x) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
start = y;
break Label1;
}
}
}
Label2:
for (int y = height - 1; y >= 0; --y) {
int count = 0;
for (int x = 0; x < width; ++x) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
end = y;
break Label2;
}
}
}
return image.getSubimage(0, start, width, end - start + 1);
}

/**
* 移除左右白色部分(left right)
*
* @param image
* @return
*/
public static BufferedImage removeLRWhite(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int start = 0;
int end = 0;
Label1:
for (int x = 0; x < width; ++x) {
int count = 0;
for (int y = 0; y < height; ++y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
start = x;
break Label1;
}
}
}
Label2:
for (int x = width - 1; x >= 0; --x) {
int count = 0;
for (int y = height - 1; y >= 0; --y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
end = x;
break Label2;
}
}
}
return image.getSubimage(start, 0, end - start + 1, height);
}

/**
* 移除黑色部分
*
* @param img
* @return
*/
public static BufferedImage removeBlack(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
int start = 0;
int end = 0;
Label1:
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
if (isBlack(img.getRGB(x, y))) {
start = y;
break Label1;
}
}
}
Label2:
for (int y = height - 1; y >= 0; --y) {
for (int x = 0; x < width; ++x) {
if (isBlack(img.getRGB(x, y))) {
end = y;
break Label2;
}
}
}
return img.getSubimage(0, start, width, end - start + 1);
}

/**
* 是否是黑色
*
* @param colorInt
* @return
*/
public static boolean isBlack(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() <= 100;
}

/**
* 是否是白色
*
* @param colorInt
* @return
*/
public static boolean isWhite(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() > 100;
}
}

javacv使用笔记

使用过程中遇到的异常

异常:Could not initialize class org.bytedeco.javacpp.avutil

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.bytedeco.javacpp.avutil
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:274)
at org.bytedeco.javacpp.Loader.load(Loader.java:385)
at org.bytedeco.javacpp.Loader.load(Loader.java:353)
at org.bytedeco.javacpp.avformat$AVFormatContext.<clinit>(avformat.java:2249)
at org.bytedeco.javacv.FFmpegFrameGrabber.startUnsafe(FFmpegFrameGrabber.java:346)
at org.bytedeco.javacv.FFmpegFrameGrabber.start(FFmpegFrameGrabber.java:340)

解决办法:

1
mvn package exec:java -Dplatform.dependencies -Dexec.mainClass=Demo

警告:Warning: data is not aligned! This can lead to a speedloss

出现这个警告是因为ffmpeg要求视频的宽度必须是32的倍数,高度必须是2的倍数,按要求修改下宽高就好了。

使用示例

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
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.bytedeco.javacpp.avcodec;
import org.bytedeco.javacpp.opencv_core;
import org.bytedeco.javacpp.opencv_imgcodecs;
import org.bytedeco.javacv.*;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Collections;
import java.util.List;

/**
* @author Gao Youbo
* @since 2018-08-15 16:43
*/
public class OpenCVUtils {
public static void main(String[] args) throws Exception {
List<BufferedImage> images = grab(new File("/data/opencv/test.mp4"));
int i = 1;
for (BufferedImage image : images) {
ImageIO.write(image, "jpg", new File("/data/opencv/frame/" + i + ".jpg"));
i++;
}

// grabAudioFromVideo(new File("/data/opencv/test.mp4"), new File("/data/opencv/test.aac"));

List<File> files = Lists.newArrayList(FileUtils.listFiles(new File("/data/opencv/frame/"), new String[]{"jpg"}, false));
Collections.sort(files, (o1, o2) -> {
int i1 = NumberUtils.toInt(StringUtils.substringBefore(o1.getName(), "."));
int i2 = NumberUtils.toInt(StringUtils.substringBefore(o2.getName(), "."));
return Integer.compare(i1, i2);
});
record("/data/opencv/out.mp4", files, new File("/data/opencv/test.aac"), 544, 960);
}

/**
* 将多个图片文件合成视频
*
* @param output 输出文件
* @param images 序列帧图片
* @param audioFile 音频
* @param width 宽
* @param height 高
* @throws Exception
*/
public static void record(String output, List<File> images, File audioFile, int width, int height) throws Exception {
try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(output, width, height);
FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(30);
recorder.setAudioBitrate(192000);
recorder.setAudioQuality(0);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();

OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
for (File file : images) {
opencv_core.IplImage image = opencv_imgcodecs.cvLoadImage(file.getPath());
recorder.record(converter.convert(image));
opencv_core.cvReleaseImage(image);
}

grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.setTimestamp(frame.timestamp);
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

/**
* 从视频中将每一帧的图片提取出来
*
* @param video
* @return
* @throws FrameGrabber.Exception
*/
public static List<BufferedImage> grab(File video) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video.getPath())) {
grabber.start();

List<BufferedImage> images = Lists.newArrayList();
Frame frame;
while ((frame = grabber.grabImage()) != null) {
images.add(Java2DFrameUtils.toBufferedImage(frame));
}
return images;
}
}

/**
* 从视频中提取出音频
*
* @param video
* @param outputAudio
*/
public static void grabAudioFromVideo(File video, File outputAudio) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputAudio, 1)) {
grabber.start();
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
recorder.start();

Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.audioChannels == 1) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}
}

}

图片合成视频简单的封装

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
private static class VideoRecorder implements Closeable {
private FFmpegFrameRecorder recorder;

public VideoRecorder(String output, int width, int height) throws FrameRecorder.Exception {
recorder = new FFmpegFrameRecorder(output, width, height);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(FPS);
recorder.setAudioBitrate(192000);
recorder.setAudioQuality(0);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();
}

public void addFrame(BufferedImage image) throws FrameRecorder.Exception {
Frame frame = Java2DFrameUtils.toFrame(image);
recorder.record(frame, avutil.AV_PIX_FMT_ARGB);
}

public void addAudio(File audioFile) throws FrameGrabber.Exception, FrameRecorder.Exception {
if (audioFile == null || !audioFile.exists()) {
return;
}
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

@Override
public void close() throws IOException {
recorder.close();
}
}

解决maven打包时将不必要的包引入进来的问题

我在实际使用中只用到了ffmpeg,但是打包的时候却将flycapture、libdc1394、libfreenect、artoolkitplus、tesseract…等包都打进来了,这些都是我不需要的,下面贴出我的maven配置示例。

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
<properties>
<javacpp.version>1.4.2</javacpp.version>
<!-- 这里要根据自己的平台选择不同的依赖 -->
<!--<javacpp.platform.dependencies>linux-x86_64</javacpp.platform.dependencies>-->
<javacpp.platform.dependencies>macosx-x86_64</javacpp.platform.dependencies>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacpp.version}</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
<classifier>${javacpp.platform.dependencies}</classifier>
</dependency>
</dependencies>

DelayQueue使用

DelayQueue特性

  • 队列中的元素都必须实现Delayed,元素可以指定延迟消费时长。
  • 实现了BlockingQueue接口,所以他是一个阻塞队列。
  • 本质上是基于PriorityQueue实现的。

贴一段我在实际生产环境中使用到代码

队列管理

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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
* @author Gao Youbo
* @since 2018-07-26 19:53
*/
public class DelayQueueManager {
private static final Logger LOG = LoggerFactory.getLogger(DelayQueueManager.class);

private String name;
private ExecutorService executor;
private Thread monitorThread;
private DelayQueue<DelayTask<?>> delayQueue; // 延时队列

public DelayQueueManager(String name, int poolSize) {
this.name = name;
this.executor = Executors.newFixedThreadPool(poolSize);
this.delayQueue = new DelayQueue<>();
init();
}

/**
* 初始化
*/
private void init() {
monitorThread = new Thread(() -> {
execute();
}, "DelayQueueMonitor-" + name);
monitorThread.start();
}

private void execute() {
while (true) {
LOG.info("当前延时任务数量:" + delayQueue.size());
try {
// 从延时队列中获取任务
DelayTask<?> delayTask = delayQueue.take();
if (delayTask != null) {
Runnable task = delayTask.getTask();
if (task != null) {
// 提交到线程池执行task
executor.execute(task);
}
}
} catch (Exception e) {
LOG.error(null, e);
}
}
}

/**
* 添加任务
*
* @param id 任务编号
* @param task 任务
* @param time 延时时间
* @param unit 时间单位
*/
public void put(String id, Runnable task, long time, TimeUnit unit) {
long timeout = TimeUnit.MILLISECONDS.convert(time, unit);
long delayTimeMillis = System.currentTimeMillis() + timeout;
delayQueue.put(new DelayTask<>(id, delayTimeMillis, task));
}

/**
* 添加任务
*
* @param id 任务编号
* @param task 任务
* @param delayTimeMillis 延迟到什么时间点
*/
public void putAt(String id, Runnable task, long delayTimeMillis) {
delayQueue.put(new DelayTask<>(id, delayTimeMillis, task));
}

/**
* 根据任务编号删除任务
*
* @param id
* @return
*/
public boolean removeTaskById(String id) {
DelayTask task = new DelayTask(id, 0, null);
return delayQueue.remove(task);
}

/**
* 删除任务
*
* @param task
* @return
*/
public boolean removeTask(DelayTask task) {
return delayQueue.remove(task);
}
}

延迟任务对象

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

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

/**
* @author Gao Youbo
* @since 2018-07-26 19:54
*/
public class DelayTask<T extends Runnable> implements Delayed {
private final String id;
private final long delayTimeMillis; // 延迟到什么时间点执行
private final T task; // 任务

public DelayTask(String id, long delayTimeMillis, T task) {
this.id = id;
this.delayTimeMillis = delayTimeMillis;
this.task = task;
}

public T getTask() {
return task;
}

@Override
public int compareTo(Delayed o) {
DelayTask other = (DelayTask) o;
long diff = delayTimeMillis - other.delayTimeMillis;
if (diff > 0) {
return 1;
} else if (diff < 0) {
return -1;
} else {
return 0;
}
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.delayTimeMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DelayTask<?> delayTask = (DelayTask<?>) o;
return Objects.equals(id, delayTask.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

Guava Range使用方法

概念 表示范围 guava对应功能方法
(a..b) {x | a < x < b} open(C, C)
[a..b] {x | a <= x <= b} closed(C, C)
[a..b) {x | a <= x < b} closedOpen(C, C)
(a..b] {x | a < x <= b} openClosed(C, C)
(a..+∞) {x | x > a} greaterThan(C)
[a..+∞) {x | x >= a} atLeast(C)
(-∞..b) {x | x < b} lessThan(C)
(-∞..b] {x | x <= b} atMost(C)
(-∞..+∞) all values all()

命令行推送Jar包到nexus

1
mvn deploy:deploy-file -DgroupId=com.tencent -DartifactId=xinge -Dversion=1.1.8 -Dpackaging=jar -DrepositoryId=nexus -Dfile=/Users/gaoyoubo/xinge-push.jar -Durl=http://xxx.xxx.com:8081/nexus/content/repositories/thirdparty/ -DgeneratePom=false

并发队列-无界阻塞延迟队列DelayQueue原理探究

1
2
转载自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E9%98%9F%E5%88%97-%E6%97%A0%E7%95%8C%E9%98%BB%E5%A1%9E%E5%BB%B6%E8%BF%9F%E9%98%9F%E5%88%97delayqueue%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/
最近在开发中正好有类似场景。

前言

DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。

DelayQueue类图结构

如图DelayQueue中内部使用的是PriorityQueue存放数据,使用ReentrantLock实现线程同步,可知是阻塞队列。另外队列里面的元素要实现Delayed接口,一个是获取当前剩余时间的接口,一个是元素比较的接口,因为这个是有优先级的队列。

offer操作

插入元素到队列,主要插入元素要实现Delayed接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {(2
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}

首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加元素后,peek并不一定是当前添加的元素,如果(2)为true,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的线程,通知他们队列里面有元素了。

take操作

获取并移除队列首元素,如果队列没有过期元素则等待。

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
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//获取但不移除队首元素(1)
E first = q.peek();
if (first == null)
available.await();//(2)
else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0)//(3)
return q.poll();
else if (leader != null)//(4)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;//(5)
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)//(6)
available.signal();
lock.unlock();
}
}

第一次调用take时候由于队列空,所以调用(2)把当前线程放入available的条件队列等待,当执行offer并且添加的元素就是队首元素时候就会通知最先等待的线程激活,循环重新获取队首元素,这时候first假如不空,则调用getdelay方法看该元素海剩下多少时间就过期了,如果delay<=0则说明已经过期,则直接出队返回。否者看leader是否为null,不为null则说明是其他线程也在执行take则把该线程放入条件队列,否者是当前线程执行的take方法,则调用(5)await直到剩余过期时间到(这期间该线程会释放锁,所以其他线程可以offer添加元素,也可以take阻塞自己),剩余过期时间到后,该线程会重新竞争得到锁,重新进入循环。

(6)说明当前take返回了元素,如果当前队列还有元素则调用singal激活条件队列里面可能有的等待线程。leader那么为null,那么是第一次调用take获取过期元素的线程,第一次调用的线程调用设置等待时间的await方法等待数据过期,后面调用take的线程则调用await直到signal。

poll操作

获取并移除队头过期元素,否者返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
//如果队列为空,或者不为空但是队头元素没有过期则返回null
if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}

一个例子

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
class DelayedEle implements Delayed {

private final long delayTime; //延迟时间
private final long expire; //到期时间
private String data; //数据

public DelayedEle(long delay, String data) {
delayTime = delay;
this.data = data;
expire = System.currentTimeMillis() + delay;
}

/**
* 剩余时间=到期时间-当前时间
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}

/**
* 优先队列里面优先级规则
*/
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DelayedElement{");
sb.append("delay=").append(delayTime);
sb.append(", expire=").append(expire);
sb.append(", data='").append(data).append('\'');
sb.append('}');
return sb.toString();
}
}

public static void main(String[] args) {
DelayQueue<DelayedEle> delayQueue = new DelayQueue<DelayedEle>();

DelayedEle element1 = new DelayedEle(1000,"zlx");
DelayedEle element2 = new DelayedEle(1000,"gh");

delayQueue.offer(element1);
delayQueue.offer(element2);

element1 = delayQueue.take();
System.out.println(element1);
}

使用场景

TimerQueue的内部实现
ScheduledThreadPoolExecutor中DelayedWorkQueue是对其的优化使用

Java四舍五入

之前写的,总结成代码片段,留备后用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.math.BigDecimal;

/**
* @author Gao Youbo
* @since 2014-08-28 13:55:12
*/
public class NumberUtils {

/**
* 四舍五入取整数
*
* @param n
* @return
*/
public static int roundHalfUp(float n) {
return new BigDecimal(n).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
}


/**
* 四舍五入
*
* @param n 数字
* @param precision 精度(保留几位小数)
* @return
*/
public static float roundHalfUp(float n, int precision) {
return new BigDecimal(n).setScale(precision, BigDecimal.ROUND_HALF_UP).floatValue();
}

/**
* 4.11 -> 4.2
* <p/>
* 4.19 -> 4.2
*
* @param n 数字
* @param precision 精度(保留几位小数)
* @return
*/
public static float roundUp(float n, int precision) {
return new BigDecimal(n).setScale(precision, BigDecimal.ROUND_UP).floatValue();
}

}

支持生产阻塞的线程池

在各种并发编程模型中,生产者-消费者模式大概是最常用的了。在实际工作中,对于生产消费的速度,通常需要做一下权衡。通常来说,生产任务的速度要大于消费的速度。一个细节问题是,队列长度,以及如何匹配生产和消费的速度。
一个典型的生产者-消费者模型如下:
producer-consumer

在并发环境下利用J.U.C提供的Queue实现可以很方便地保证生产和消费过程中的线程安全。这里需要注意的是,Queue必须设置初始容量,防止生产者生产过快导致队列长度暴涨,最终触发OutOfMemory。

对于一般的生产快于消费的情况。当队列已满时,我们并不希望有任何任务被忽略或得不到执行,此时生产者可以等待片刻再提交任务,更好的做法是,把生产者阻塞在提交任务的方法上,待队列未满时继续提交任务,这样就没有浪费的空转时间了。阻塞这一点也很容易,BlockingQueue就是为此打造的,ArrayBlockingQueue和LinkedBlockingQueue在构造时都可以提供容量做限制,其中LinkedBlockingQueue是在实际操作队列时在每次拿到锁以后判断容量。

更进一步,当队列为空时,消费者拿不到任务,可以等一会儿再拿,更好的做法是,用BlockingQueue的take方法,阻塞等待,当有任务时便可以立即获得执行,建议调用take的带超时参数的重载方法,超时后线程退出。这样当生产者事实上已经停止生产时,不至于让消费者无限等待。

于是一个高效的支持阻塞的生产消费模型就实现了。

等一下,既然J.U.C已经帮我们实现了线程池,为什么还要采用这一套东西?直接用ExecutorService不是更方便?

我们来看一下ThreadPoolExecutor的基本结构:
ThreadPoolExecutor

可以看到,在ThreadPoolExecutor中,BlockingQueue和Consumer部分已经帮我们实现好了,并且直接采用线程池的实现还有很多优势,例如线程数的动态调整等。

但问题在于,即便你在构造ThreadPoolExecutor时手动指定了一个BlockingQueue作为队列实现,事实上当队列满时,execute方法并不会阻塞,原因在于ThreadPoolExecutor调用的是BlockingQueue非阻塞的offer方法:

1
2
3
4
5
6
7
8
9
10
11
12
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize &amp;gt;= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING &amp;amp;&amp;amp; workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}

这时候就需要做一些事情来达成一个结果:当生产者提交任务,而队列已满时,能够让生产者阻塞住,等待任务被消费。

关键在于,在并发环境下,队列满不能由生产者去判断,不能调用ThreadPoolExecutor.getQueue().size()来判断队列是否满。

线程池的实现中,当队列满时会调用构造时传入的RejectedExecutionHandler去拒绝任务的处理。默认的实现是AbortPolicy,直接抛出一个RejectedExecutionException。

几种拒绝策略在这里就不赘述了,这里和我们的需求比较接近的是CallerRunsPolicy,这种策略会在队列满时,让提交任务的线程去执行任务,相当于让生产者临时去干了消费者干的活儿,这样生产者虽然没有被阻塞,但提交任务也会被暂停。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a &amp;lt;tt&amp;gt;CallerRunsPolicy&amp;lt;/tt&amp;gt;.
*/
public CallerRunsPolicy() { }

/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}

但这种策略也有隐患,当生产者较少时,生产者消费任务的时间里,消费者可能已经把任务都消费完了,队列处于空状态,当生产者执行完任务后才能再继续生产任务,这个过程中可能导致消费者线程的饥饿。

参考类似的思路,最简单的做法,我们可以直接定义一个RejectedExecutionHandler,当队列满时改为调用BlockingQueue.put来实现生产者的阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
// should not be interrupted
}
}
}
};

这样,我们就无需再关心Queue和Consumer的逻辑,只要把精力集中在生产者和消费者线程的实现逻辑上,只管往线程池提交任务就行了。

相比最初的设计,这种方式的代码量能减少不少,而且能避免并发环境的很多问题。当然,你也可以采用另外的手段,例如在提交时采用信号量做入口限制等,但是如果仅仅是要让生产者阻塞,那就显得复杂了。

转载自:http://ifeve.com/blocking-threadpool-executor/

Java分隔、合并大文件

今天网百度网盘上上传文件提示单个文件大小超限,让我升级VIP。作为一个有逼格的程序猿怎么可能被这点小事难倒呢。

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
import com.google.common.collect.Lists;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;

/**
* @author Gao Youbo
* @since 2017-07-07 18:18
*/
public class Files {
public static void main(String[] args) {
int num = 10;

// 分割
cut(new File("/Users/gaoyoubo/360sync/数据迁移/document/a.zip"), num);

// 合并
List<File> files = Lists.newArrayList();
for (int i = 0; i < num; i++) {
files.add(new File("/Users/gaoyoubo/360sync/数据迁移/document/a-" + i + ".zip"));
}
File outFile = new File("/Users/gaoyoubo/360sync/数据迁移/document/b.zip");
merge(files, outFile);
}


/**
* 分文件
*
* @param sourceFile
* @param num 分隔文件数量
*/
public static void cut(File sourceFile, int num) {
long signMaxSize = sourceFile.length() / num + 1; // 单个文件最大长度
try (RandomAccessFile source = new RandomAccessFile(sourceFile, "r")) {
byte[] bytes = new byte[1024];
int len;
for (int i = 0; i < num; i++) {
File targetFile = new File(sourceFile.getParent(),
FilenameUtils.getBaseName(sourceFile.getName()) + "-" + i + "." + FilenameUtils.getExtension(sourceFile.getName()));
try (RandomAccessFile target = new RandomAccessFile(targetFile, "rw")) {
while ((len = source.read(bytes)) != -1) {//读到文件末尾时,len返回-1,结束循环
target.write(bytes, 0, len);
if (target.length() > signMaxSize) {
break;
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 合并文件
*/
public static void merge(List<File> files, File outFile) {
try (RandomAccessFile out = new RandomAccessFile(outFile, "rw")) {
for (File file : files) {
try (RandomAccessFile src = new RandomAccessFile(file, "r")) {
byte[] bytes = new byte[1024];//每次读取字节数
int len;
while ((len = src.read(bytes)) != -1) {
out.write(bytes, 0, len);//循环赋值
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

生产环境下JAVA进程高CPU占用故障排查

收藏一篇文章,这两天被驾校之家CPU占用过高的问题弄的寝食难安。马上用下面的方法监控一下。

参考文章:

    1. http://blog.csdn.net/blade2001/article/details/9065985

    2. http://blog.csdn.net/jiangguilong2000/article/details/17971247

问题描述:
生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高。

问题分析:
1,程序属于CPU密集型,和开发沟通过,排除此类情况。
2,程序代码有问题,出现死循环,可能性极大。

问题解决:
1,开发那边无法排查代码某个模块有问题,从日志上也无法分析得出。
2,记得原来通过strace跟踪的方法解决了一台PHP服务器CPU占用高的问题,但是通过这种方法无效,经过google搜索,发现可以通过下面的方法进行解决,那就尝试下吧。

解决过程:
1,根据top命令,发现PID为2633的Java进程占用CPU高达300%,出现故障。

2,找到该进程后,如何定位具体线程或代码呢,首先显示线程列表,并按照CPU占用高的线程排序:
[root@localhost logs]# ps -mp 2633 -o THREAD,tid,time | sort -rn

显示结果如下:
USER     %CPU PRI SCNT WCHAN  USER SYSTEM   TID     TIME
root     10.5  19    - -         -      -  3626 00:12:48
root     10.1  19    - -         -      -  3593 00:12:16

找到了耗时最高的线程3626,占用CPU时间有12分钟了!

将需要的线程ID转换为16进制格式:
[root@localhost logs]# printf “%x\n” 3626
e18

最后打印线程的堆栈信息:
[root@localhost logs]# jstack 2633 |grep e18 -A 30

将输出的信息发给开发部进行确认,这样就能找出有问题的代码。
通过最近几天的监控,CPU已经安静下来了。

FULL GC分析过程分享

转载-原文地址:http://www.taobaotest.com/blogs/2294

在性能测试过程中,FULL GC频繁是比较常见的问题,FULL GC 产生的原因有很多,这里主要针对meta压测过程中分析FULL GC问题的一些思路进行分享,供大家参考

1.如何发现是否发生FULL GC和FULL GC是否频繁

使用JDK自带的轻量级小工具jstat

     语法结构:

Usage: jstat -help|-options

             jstat -

 参数解释:

Options — 选项,我们一般使用 -gcutil 查看gc情况

vmid    — VM的进程号,即当前运行的java进程号

interval– 间隔时间,单位为秒或者毫秒

count   — 打印次数,如果缺省则打印无数次

比如 /opt/taobao/java/bin/jstat –gcutil pid 5000

 

输出结果:

        S0        S1         E          O          P        YGC      YGCT        FGC     FGCT        GCT

           0.00  90.63 100.00  58.82   3.51    183    2.059     0    0.000    2.059

    0.00  15.48   7.80  60.99   3.51    185    2.092     1    0.305    2.397

    0.00  15.48  18.10  47.90   3.51    185    2.092     2    0.348    2.440

 S0  — Heap上的 Survivor space 0 区已使用空间的百分比
 S1  — Heap上的 Survivor space 1 区已使用空间的百分比
 E   — Heap上的 Eden space 区已使用空间的百分比
 O   — Heap上的 Old space 区已使用空间的百分比
 P   — Perm space 区已使用空间的百分比
 YGC — 从应用程序启动到采样时发生 Young GC 的次数
 YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
 FGC — 从应用程序启动到采样时发生 Full GC 的次数
 FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
 GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)

    通过FGC我们可以发现系统是否发生FULL GC和FULL GC的频率

2.  FULL GC分析和问题定位

    a.     GC log收集和分析

(1)在JVM启动参数增加:”-verbose:gc -Xloggc:<file_name>  -XX:+PrintGCDetails -XX:+PrintGCDateStamps”

    PrintGCTimeStamp只能获得相对时间,建议使用PrintGCDateStamps获得full gc 发生的绝对时间

      (2)如果采用CMS GC,仔细分析jstat FGC输出和GC 日志会发现, CMS的每个并发GC周期则有两个stop-the-world阶段——initial mark与final re-mark使得CMS的每个并发GC周期总共会更新full GC计数器两次,initial mark与final re-mark各一次

    

    b.     Dump JVM 内存快照

/opt/taobao/java/bin/jmap -dump:format=b,file=dump.bin pid

这里有一个问题是什么时候进行dump?

一种方法是前面提到的用jstat工具观察,当OLD区到达比较高的比例如60%,一般会很快触发一次FULL GC,可以进行一次DUMP,在FULL GC发生以后再DUMP一次,这样比较就可以发现到底是哪些对象导致不停的FULL GC

另外一种方法是通过配置JVM参数

 -XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC分别用于指定在full GC之前与之后生成heap dump 

    c.     利用MAT((Memory Analyzer Tool)工具分析dump文件

关于MAT具体使用方法网上有很多介绍,这里不做详细展开,这里需要注意的是:

(1)   MAT缺省只分析reachable的对象,unreachable的对象(将被收集掉的对象)被忽略,而分析FULL GC频繁原因时unreachable object也应该同时被重点关注。如果要显示unreachable的对象细节必须用mat 1.1以上版本并且打开选项“keep unreachable object”

(2)   通常dump文件会好几个G,无法在windows上直接进行分析,我们可以先把dump文件在linux上进行分析,再把分析好的文件拷贝到windows上,在windows上用MAT打开分析文件。

下面是Meta2.0压测曾遇到的FULL GC频繁问题的分析结果,比较明显,DispatchRequest对象有4千多万个,一共超过2G,并最终导致OOM

83f48d2a9de38682ed93018f08211d9e_detail

用16G内存在Java Map中处理30亿对象

在一个下雨的夜晚,我在思考Java中内存管理的问题,以及Java集合对内存使用的效率情况。我做了一个简单的实验,测试在16G内存条件下,Java的Map可以插入多少对象。

这个试验的目的是为了得出集合的内部上限。所以,我决定使用很小的key和value。所有的测试,都是在64w位linux环境下进行的,操作系统是ubuntu12.04。JVM版本为Oracle Java 1.7.0_09-bo5 (HotSpot 23.5-b02)。在这个JVM中,压缩指针(compressed pointers(-XX:+UseCompressedOops))的选项是默认打开的。

首先是简单的针对java.util.TreeMap的测试。不停向其中插入数字,直到其抛出内存溢出异常。JVM的设置是-xmx15G

import java.util.*; 
Map m = new TreeMap();
for(long counter=0;;counter++){
  m.put(counter,"");
  if(counter%1000000==0) System.out.println(""+counter);
}

这个用例插入了1 7200 0000条数据。在接近结束的时候,由于高负荷的GC插入效率开始降低。第二次,我用HashMap代替TreeMap,这次插入了182 000 000条数据。

Java默认的集合并不是最高效利用内存的。所以,这回我们尝试最后化内存的测试。我选择了MapDB中的LongHashMap,其使用原始的long key并且对封装的内存占用进行了优化。JVM的设置仍然是-Xmx15G。

import org.mapdb.*
LongMap m = new LongHashMap();    
for(long counter=0;;counter++){
  m.put(counter,"");
  if(counter%1000000==0) System.out.println(""+counter);
}

这次,计数器停止在了276 000 000。同样,在插入接近结束的时候,速度开始减慢。看起来这是基于堆的结合的限制所在。垃圾回收带来了瓶颈 。

现在是时候祭出杀手锏了:-)。我们可以采用非基于堆的方式存储,这样GC就不会发现我们的数据。我来介绍一下MapDB,它提供了基于数据库引擎的并发同步的TreeMap和HashMap。它提供了多样化的存储方式,其中一个就是非堆内存的方式。(声明:我是MapDB的作者)。

现在,让我们再跑一下之前的用例,这次采用的是非堆的Map。首先是配置并打开数据库,它打开的直接基于内存存储并且关闭事物的模式。接下来的代码是在这个db中创建一个新的map。

import org.mapdb.*

DB db = DBMaker
   .newDirectMemoryDB()
   .transactionDisable()
   .make();

Map m = db.getTreeMap("test");
for(long counter=0;;counter++){
  m.put(counter,"");
  if(counter%1000000==0) System.out.println(""+counter);
}

这是非堆的Map,所以我们需要不同的JVM配置: -XX:MaxDirectMemorySize=15G -Xmx128M。这次测试在达到980 000 000条记录的时候出现内存溢出。

但是,MapDB还可以优化。之前样例的问题在于记录的破碎分散,b-tree的节点每次插入都要调整它的容量。变通的方案是,将b-tree的节点在其插入前短暂的缓存起来。这使得记录的分散降到最低。所以,我们来改变一下DB的配置:

DB db = DBMaker
     .newDirectMemoryDB()
     .transactionDisable()
     .asyncFlushDelay(100)
     .make();

Map m = db.getTreeMap("test");

这次记录数达到了 1 738 000 000。速度也是达到了惊人的31分钟完成了17亿数据的插入。

MapDB还能继续优化。我们把b-tree的节点容量从32提升到120并且打开透明(OneCoder理解为对用户不可见的)压缩:

DB db = DBMaker
            .newDirectMemoryDB()
            .transactionDisable()
            .asyncFlushDelay(100)
            .compressionEnable()
            .make();

   Map m = db.createTreeMap("test",120, false, null, null, null);

这个用例在大约3 315 000 000条记录时出现内存溢出。由于压缩,他的速度 有所降低,不过还是在几个小时内完成。我还可以进行一些优化(自定义序列化等等) ,使得数据量达到大约40亿。

也许你好奇所有这些记录是怎么存储的。答案就是,delta-key压缩。当然,向B-Tree插入已经排好序的递增key是最佳的使用场景,并且MapDB也对此进行了一些小小的 优化。最差的情形就是key是随机的.

后续更新:很多朋友对压缩有一些困惑。在这些用例中,Delta-key 压缩默认都是启用的。在下面的用例中,我又额外开启了zlib方式的压缩:

DB db = DBMaker
            .newDirectMemoryDB()
            .transactionDisable()
            .asyncFlushDelay(100)
            .make();

    Map m = db.getTreeMap("test");

    Random r = new Random();
    for(long counter=0;;counter++){
        m.put(r.nextLong(),"");
        if(counter%1000000==0) System.out.println(""+counter);
    }

即使在随机序列情况下,MapDB也可以存储652 000 000条记录,大概4倍于基于堆的集合。

这个简单的试验没有太多的目的。这仅仅是我对MapDB的一种优化。也许,更多的惊喜在于插入效率确实不错并且MapDB可以抗衡基于内存的集合。

原文地址:http://kotek.net/blog/3G_map

Java主线程等待子线程完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import static java.lang.Thread.sleep;

/**
*
* @author Gao Youbo
* @since 2014-05-16 10:20:08
*/
public class Test {

public static void main(String[] args) {
SubThread thread = new SubThread();
thread.start(); //子线程开始
mainThreadWorking();//主线程干活
System.out.println("main:等待子线程完成");
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有线程都完成!");
}

private static void mainThreadWorking() {
System.out.println("main:主线程开始干活...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main:主线程干完了!");
}
}

class SubThread extends Thread {

@Override
public void run() {
try {
System.out.println("sub:子线程开始干活...");
sleep(6000L); //子线程花6秒钟时间干活
System.out.println("sub:子线程干完了!");
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}

}