Java的无锁编程和锁优化

释放双眼,带上耳机,听听看~!

转载自《四火的唠叨》

Peterson 算法(Dekker算法的演化),这个算法设计得很巧妙,理解的核心就是搞清楚三个标志位是怎样控制两个方法对临界区的访问的:


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
1volatile
2         int
3         flag1 =  
4        0
5        ;  
6        //主观因素:flag1表示方法1自身是否要求进入临界区    
7      
8
9      
10
11
12        volatile
13         int
14         flag2 =  
15        0
16        ;  
17        //主观因素:flag2表示方法2自身是否要求进入临界区    
18      
19
20      
21
22
23        volatile
24         int
25         turn =  
26        1
27        ;  
28        //客观因素:turn取1和2分别表示当前临界区针对方法1还是方法2开放   
29      
30
31      
32
33
34           
35      
36
37      
38
39
40        void
41         fun1(){    
42      
43
44      
45
46
47          
48        flag1 =  
49        1
50        ;    
51      
52
53      
54
55
56          
57        turn =  
58        2
59        ;    
60      
61
62      
63
64
65          
66        while
67        ( flag2==
68        1
69         && turn==
70        2
71         ){}  
72        //只有在方法2自身要求进入临界区且临界区针对方法2开放时,方法1才会阻塞    
73      
74
75      
76
77
78          
79        //Critical Section    
80      
81
82      
83
84
85          
86        ...  
87        //临界区内    
88      
89
90      
91
92
93          
94        flag1 =  
95        0
96        ;    
97      
98
99      
100
101
102        }    
103      
104
105      
106
107
108           
109      
110
111      
112
113
114        void
115         fun2(){    
116      
117
118      
119
120
121          
122        flag2 =  
123        1
124        ;    
125      
126
127      
128
129
130          
131        turn =  
132        1
133        ;    
134      
135
136      
137
138
139          
140        while
141        ( flag1==
142        1
143         && turn==
144        1
145         ){}  
146        //只有在方法1自身要求进入临界区且临界区针对方法1开放时,方法1才会阻塞    
147      
148
149      
150
151
152          
153        //Critical Section    
154      
155
156      
157
158
159          
160        ...  
161        //临界区内    
162      
163
164      
165
166
167          
168        flag2 =  
169        0
170        ;    
171      
172
173      
174
175
176        }
177

ConcurrentHashMap,设计巧妙,用桶粒度的锁,避免了put和get中对整个map的锁定,尤其在get中,只对一个HashEntry做锁定操作,性能提升是显而易见的。

 
细节参见 http://www.iteye.com/topic/344876 ,有详细的讨论;而在https://www.ibm.com/developerworks/java/library/j-jtp08223/ ,这里有关于Java内存模型结合ConcurrentHashMap的分析。

HashMap不是线程安全的,错误的使用并发状况下可能出现CPU100%的状况:


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
1/**   
2      
3
4      
5
6
7        * Transfers all entries from current table to newTable.   
8      
9
10      
11
12
13        */  
14      
15
16      
17
18
19        void
20         transfer(Entry[] newTable) {    
21      
22
23      
24
25
26            
27        Entry[] src = table;    
28      
29
30      
31
32
33            
34        int
35         newCapacity = newTable.length;    
36      
37
38      
39
40
41            
42        for
43         (
44        int
45         j =  
46        0
47        ; j < src.length; j++) {    
48      
49
50      
51
52
53                
54        Entry e = src[j];    
55      
56
57      
58
59
60                
61        if
62         (e !=  
63        null
64        ) {    
65      
66
67      
68
69
70                    
71        src[j] =  
72        null
73        ;    
74      
75
76      
77
78
79                    
80        do
81         {    
82      
83
84      
85
86
87                        
88        Entry next = e.next;    
89      
90
91      
92
93
94                        
95        int
96         i = indexFor(e.hash, newCapacity);    
97      
98
99      
100
101
102                        
103        e.next = newTable[i];    
104      
105
106      
107
108
109                        
110        newTable[i] = e;    
111      
112
113      
114
115
116                        
117        e = next;    
118      
119
120      
121
122
123                    
124        }  
125        while
126         (e !=  
127        null
128        );    
129      
130
131      
132
133
134                
135        }    
136      
137
138      
139
140
141            
142        }    
143      
144
145      
146
147
148        }
149

问题最终就出现在HashMap中transfer方法的这个while循环上,这个方法在HashMap扩容时调用,详细分析见:

http://blog.sina.com.cn/s/blog_56146a210100owft.html

在Lock-Free世界里,最简单也最普遍的一个通用原语是CAS(Compare and Swap)操作。支持并发的现代的处理器都提供了这个原语的硬件实现。CAS原语负责比较某个内存地址处的内容与一个期望值,如果比较成功则将该内存地址处的内容替换为一个新值。这整个操作是原子的。

这里有对无锁并发编程的介绍:http://www.cnblogs.com/lucifer1982/archive/2008/04/16/1154727.html

如果站在语言层面之上,仅从设计的层面看,可以免锁的思路至少包括:

1、单线程来主导行为,多线程池化操作避开状态变量。

比如在一个WEB应用中,每一个Action都可以给相应的用户线程分配一个实例,线程之间互不干扰;但是到了业务逻辑Service内,避开Service状态变量的使用,减少了开发人员对并发编程的关注。

2、函数式编码。

函数式编码是最天然的和最高效的免锁方式,如果你对函数式编码还不了解,请参看这篇文章。

3、资源局部复制、异步处理。

总所周知对资源的争夺是造成锁的一个重要原因,在很多情况下,资源只能有一份,但是对使用资源的每个线程来说,都可以看到属于它自己的一份(这一份并非是真正的资源,很可能只是一个缓冲区,每个线程使用它自己的一个缓冲区,到一定程度时将缓冲区的数据处理到唯一资源中,这就减少了需要加锁对线程的影响),无需考虑并发地去使用。

4、不变对象。

不妨参考ConcurrentHashMap的实现,其中的节点是不变的。对象不变性是保证线程安全的重要方式之一。

Java的锁操作和锁优化

锁自旋

线程要进入阻塞状态,肯定需要调用操作系统的函数来完成从用户态进入内核态的过程,这一步通常是性能低下的。

那么在遇到锁的争用时,或许等待线程可以不那么着急进入阻塞状态,而是等一等,看看锁是不是马上就释放了,这就是锁自旋。锁自旋在多处理器上有重要价值。

当然锁自旋也带来了一些问题,比如如何判断自旋周期,如何确定自旋锁的个数,如何处理线程优先级差异等。

锁偏向

锁偏向是JDK1.6引入的,主要为了解决在没有竞争情况下锁的性能问题。

锁都是可重入的,在已经获得锁的情况下,该线程可以多次锁住该对象,但是每次执行这样的操作会因为CAS(CPU的Compare-And-Swap指令)操作而造成一些开销,为了减少这种开销,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

锁消除

(JDK1.6)锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

锁膨胀

(JDK1.6)和数据库中的锁升级有些相似,多个或多次调用粒度太小的锁,进行加锁解锁的消耗,反而还不如一次大粒度的锁调用来得高效,因此JVM可将锁的范围优化到更大的区域。

轻量级锁

(JDK1.6)轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。轻量级锁在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的指向和状态。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

———————————————————————————————————-

2012年11月16日:

今天看到一个C#实现的读共享写互斥的代码:


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
1class
2         ReaderAndWriter
3      
4
5      
6
7
8        {
9      
10
11      
12
13
14               
15        private
16         static
17         Mutex mut =  
18        new
19         Mutex();
20        //用于保护读者数量的互斥信号量
21      
22
23      
24
25
26                
27        private
28         static
29         Mutex rw =  
30        new
31         Mutex();
32        //保证读者写者互斥的信号量
33      
34
35      
36
37
38          
39      
40
41      
42
43
44                
45        static
46         int
47         count = 0;
48        //读者数量
49      
50
51      
52
53
54                         
55      
56
57      
58
59
60                
61        private
62         static
63         void
64         Reader()
65      
66
67      
68
69
70                
71        {
72      
73
74      
75
76
77                    
78        mut.WaitOne();
79      
80
81      
82
83
84                    
85        if
86         (count == 0)
87      
88
89      
90
91
92                    
93        {
94      
95
96      
97
98
99                        
100        rw.WaitOne();
101      
102
103      
104
105
106                    
107        }
108      
109
110      
111
112
113                    
114        count++;
115      
116
117      
118
119
120                    
121        mut.ReleaseMutex();
122      
123
124      
125
126
127                     
128      
129
130      
131
132
133                    
134        Thread.Sleep(
135        new
136         Random().Next(2000));
137        //读取耗时1S
138      
139
140      
141
142
143                    
144        Console.WriteLine(
145        "读取完毕"
146        );
147      
148
149      
150
151
152                     
153      
154
155      
156
157
158                    
159        mut.WaitOne();
160      
161
162      
163
164
165                    
166        count--;
167      
168
169      
170
171
172                    
173        mut.ReleaseMutex();
174      
175
176      
177
178
179                    
180        if
181         (count == 0)
182      
183
184      
185
186
187                    
188        {
189      
190
191      
192
193
194                        
195        rw.ReleaseMutex();
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        private
223         static
224         void
225         writer()
226      
227
228      
229
230
231                
232        {
233      
234
235      
236
237
238                    
239        rw.WaitOne();
240      
241
242      
243
244
245                    
246        Console.WriteLine(
247        "写入数据"
248        );
249      
250
251      
252
253
254                    
255        rw.ReleaseMutex();
256      
257
258      
259
260
261             
262      
263
264      
265
266
267                
268        }
269      
270
271      
272
273
274        }
275

这种方式是写线程的逻辑比较简单的一种实现,只管获取到rw锁(用于表示互斥锁),而读线程在每次进行读区域前,通过mut锁的机制保证至少有一个互斥锁被读线程所占据(这样写线程就没法并行操作了),然后再真正开始读的时候放开mut锁,让其它读线程可以并发地进行读操作。其中的count用于表示当前读者的个数,当count降为0的时候,进入的读线程必须去获取一个写锁,以保证有读者的时候,写锁一定被某个读者占有。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块(2) Hmac

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索