ReentrantLock锁的一种误用分析
在网上看到一个兄弟写的关于ReentrantLock的文章,介绍得挺详细的,但是有个例子却讲得有点问题。看下面的代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockPractice3 {
static ReentrantLock lock = new ReentrantLock();
private static String[] threadArr = {"A","B","C"};
public static void main(String[] args){
ReentrantLockPractice3 pc = new ReentrantLockPractice3();
pc.startDemo();
}
void startDemo(){
for(int i = 0;i<10;i++){
for(String name : threadArr){
TestThread t = new TestThread(name);
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TestThread extends Thread{
//自定义线程名字
TestThread(String str){
super(str);
}
public void run(){
try {
lock.lockInterruptibly();
System.out.println( Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
三个线程,线程名分别为A、B、C,设计程序使得三个线程循环打印“ABC”10次后终止。如:ABCABCABCABCABCABCABCABCABCABC
也许作者对ReentrantLock比较了解,但这个例子却是和ReentrantLock没有什么关系,也就是说,ReentrantLock锁在这里没有起到应有的作用。把ReentrantLock锁去掉,依然能够有序地输出。
public class ReentrantLockPractice3a {
private static String[] threadArr = {"A","B","C"};
public static void main(String[] args){
ReentrantLockPractice3a pc = new ReentrantLockPractice3a();
pc.startDemo();
}
void startDemo(){
for(int i = 0;i<10;i++){
for(String name : threadArr){
TestThread t = new TestThread(name);
t.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TestThread extends Thread{
//自定义线程名字
TestThread(String str){
super(str);
}
public void run(){
try {
System.out.print(Thread.currentThread().getName());
} catch(Exception e){
e.printStackTrace();
}
}
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
其实真正起让输出有序的是这几行代码:
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
线程每次start后,延时100ms,这让线程有足够的时间进行排队,也就是说,正是因为这个延迟,让原本乱序执行的线程,变得有序起来,即便将延时修改为1ms,也能取得同样的执行效果。因此,ReentrantLock锁在此没有起到应有的作用。
作者举的这个例子,从目的上讲,是要用到多线程快速处理,并且要让输出的线程有序,实际上的效果是线程都是顺序执行的,也就是说,与使用单线程没有两样,反而增加了创建线程的开销。
实际上,用到锁,必然涉及到资源的争用,如果不涉及到资源的争用,用锁就没有意义了。为了解释ReentrantLock,可以创建一个资源争用的例子,让锁真正发挥作用:当然也一定会用到多线程,多线程的作用是提高处理效率,其乱序执行是允许的,我们真正要关心的是处理好要顺序执行的变量。看下面的例子:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockPractice3b {
public int counter = 0;
static ReentrantLock lock = new ReentrantLock();
private static String[] threadArr = {"A","B","C"};
public static void main(String[] args){
ReentrantLockPractice3b pc = new ReentrantLockPractice3b();
pc.startDemo();
}
void startDemo(){
for(int i = 0;i<10;i++){
for(String name : threadArr){
TestThread t = new TestThread(name, this);
t.start();
}
}
}
class TestThread extends Thread{
ReentrantLockPractice3b rl2;
//自定义线程名字
TestThread(String str, ReentrantLockPractice3b rl2){
super(str);
this.rl2 = rl2;
}
public void run(){
try {
lock.lockInterruptibly();
System.out.println("counter:" + rl2.counter++);
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println( Thread.currentThread().getName() );
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
}
在这个例子中,我们构造了一个会引起资源争用的变量(counter),我们的目的是保证其有序访问。看下面的输出结果:
counter:0
A
counter:1
C
counter:2
C
counter:3
A
counter:4
A
counter:5
B
counter:6
B
counter:7
B
counter:8
C
counter:9
A
counter:10
A
counter:11
C
counter:12
A
counter:13
B
counter:14
B
counter:15
C
counter:16
A
counter:17
C
counter:18
A
counter:19
B
counter:20
C
counter:21
B
counter:22
B
counter:23
C
counter:24
C
counter:25
A
counter:26
B
counter:27
C
counter:28
A
counter:29
B
可以看到,通过ReentrantLock,保证了counter的有序,但是线程的启动并不是按设想的“ABCABC...”的方式执行的,而是无序的,这正是虚拟机高效调度的体现。如果限定了线程的依次执行,那不就等同于单线程了吗?
可以看看,去掉ReentrantLock后,结果会变成什么样子:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockPractice3b {
public int counter = 0;
static ReentrantLock lock = new ReentrantLock();
private static String[] threadArr = {"A","B","C"};
public static void main(String[] args){
ReentrantLockPractice3b pc = new ReentrantLockPractice3b();
pc.startDemo();
}
void startDemo(){
for(int i = 0;i<10;i++){
for(String name : threadArr){
TestThread t = new TestThread(name, this);
t.start();
}
}
}
class TestThread extends Thread{
ReentrantLockPractice3b rl2;
//自定义线程名字
TestThread(String str, ReentrantLockPractice3b rl2){
super(str);
this.rl2 = rl2;
}
public void run(){
try {
//lock.lockInterruptibly();
System.out.println("counter:" + rl2.counter++);
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println( Thread.currentThread().getName() );
} /*catch (InterruptedException e) {
e.printStackTrace();
} */finally{
//lock.unlock();
}
}
}
}
输出结果:
counter:0
counter:3
A
counter:4
B
counter:2
B
counter:6
C
counter:7
A
counter:1
C
counter:5
B
A
counter:8
counter:9
C
A
counter:11
counter:10
B
counter:13
C
counter:14
B
counter:16
A
counter:12
B
counter:17
A
counter:19
C
counter:15
A
counter:22
C
counter:24
C
counter:25
counter:21
C
counter:20
counter:18
B
counter:27
B
B
A
counter:26
B
C
counter:23
A
counter:28
A
counter:29
C
可以看到,counter的值是混乱的,没有按照预设的目标进行计数。ReentrantLock锁发挥了作用。
其实,这里的lock.lockInterruptibly();用lock.lock();更合适一些,因为这里不涉及到锁的中断。
注意:
使用ReentrantLock锁,一定要在finally代码块中进行解锁,否则,会跟同步造成的死锁一样,出现锁一直等待的情形。