转载请注明出处:http://blog.csdn.net/xingjiarong/article/details/47679007
在上一篇博客中,我们讨论了Race Condition现象以及它产生的原因,现在我们知道它是不好的一种现象了,那么我们有什么方法避免它呢。最直接有效的方式就是放弃多线程,直接改为使用单线程但操作数据,但是这是不优雅的,因为我们知道有时候,多线程有它自己的优势。在这里我们讨论两种其他的方法——锁对象和条件对象。
锁对象
java SE5.0之后为实现多线程的互斥引入了ReentrantLock类。ReentrantLock类一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantLock类有两种构造方法:
构造方法
一、不带公平参数的构造方法
private ReentrantLock lock = new ReentrantLock();
默认的是非公平锁,这种锁不会根据线程等待时间的长短来优先调度线程。
这样就构造了一个锁对象lock。
二、带公平参数的锁对象
private ReentrantLock lock = new ReentrantLock(true);
此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。
公平锁和非公平锁的区别:
与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。
使用方法
class X {
private final ReentrantLock lock = new ReentrantLock();
// 其他变量的定义
public void m() {
lock.lock(); // 当试图获得锁时,如果锁已经被别的线程占有,那么该线程会一直被阻塞,直到获得锁
try {
// 处理数据
} finally {
lock.unlock(); //释放锁
}
}
}
首先为大家介绍一下,ReentrantLock类的两个最常用的方法:
lock()
获得锁对象,如果该锁对象没有被其他线程占有,那么可以立刻获得锁,并执行接下来的处理;如果锁对象已经被其他对象占有,那么该线程就会被阻塞在请求锁的对象的操作上,直到其他的线程释放锁,该线程得到锁,才能继续的向下执行。
unlock()
释放锁,已经获得锁对象的线程在操作完数据后要释放锁,以便其他的线程重新获得锁来执行自己的操作,否则所有的试图获得锁的线程都不能继续向下执行。
使用方法简单明了,就是在执行各个线程都要操作相同数据的代码之前请求锁,在finally语句中释放锁,为什么要在finally中释放锁呢,这是因为如果try语句块中有语句发生异常,则会直接跳过try中所有的剩余代码包括unlock(),所以锁对象就不能得到释放,其他的线程也不能继续向下执行,导致程序不能继续执行,我们在finally中释放锁,这样就能保证一定可以将锁释放掉,不管获得锁的线程是不是正确的执行结束。
现在来使用这个方法修改一下我们上一篇博客中的代码:
import java.util.concurrent.locks.ReentrantLock;
class MyThread implements Runnable {
/** * 计算类型,1表示减法,其他的表示加法 */
private int type;
/** * 锁对象 */
private static ReentrantLock lock = new ReentrantLock();
public MyThread(int type) {
this.type = type;
}
public void run() {
if (type == 1)
for (int i = 0; i < 10000; i++) {
lock.lock();
Test.num--;
lock.unlock();
}
else
for (int i = 0; i < 10000; i++) {
lock.lock();
Test.num++;
lock.unlock();
}
}
}
public class Test {
public static int num = 1000000;
public static void main(String[] args) {
Thread a = new Thread(new MyThread(1));
Thread b = new Thread(new MyThread(2));
a.start();
b.start();
/* * 主线程等待子线程完成,然后再打印数值 */
try {
a.join();
b.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(num);
}
}
再多运行几次,是不是结果都是正确的呢。
现在,我们来解释一下,什么是可重入的锁。ReentrantLock类中有一个计数器,用来表示一个线程获取锁的数量,初始值为0,当一个线程获得锁时,该值被置为1,可重入的意思就是,已经获得锁的线程还可以继续调用同一个锁所保护的方法,也就是再一次获得锁,当再一次获得锁时,ReentrantLock中的计数器就加1,每释放一次锁,计数器就减1,当计数器减为0的时候,这个线程才是真正的释放了这个锁。
我们接着讨论另外一个非常重要的问题。ReentrantLock类是依赖于创建它的类的对象。什么意思呢,就是说如果两个线程同时访问同一个ReentrantLock对象的lock()方法保护的方法时,OK,这是没有问题的,锁对象会成功的保护数据操作不会出错。但是如果两个线程同时访问ReentrantLock类的不同对象的被lock()保护的方法,那么这两个线程是不会相互影响的,也就是说lock()方法这时不能保证数据的正确性。
我们来看一下上边那个代码的这一部分:
Thread a = new Thread(new MyThread(1));
Thread b = new Thread(new MyThread(2));
这里建立了两个对象a和b,所以他们每个类都有自己的ReentrantLock对象,这就是我们上边所说的ReentrantLock类的不同对象,这样如果两个线程分别操作a和b的数据,lock方法是不会有效的。
不信我们试试看这个代码:
import java.util.concurrent.locks.ReentrantLock;
class MyThread implements Runnable {
/** * 计算类型,1表示减法,其他的表示加法 */
private int type;
/** * 锁对象 */
private ReentrantLock lock = new ReentrantLock();
public MyThread(int type) {
this.type = type;
}
public void run() {
if (type == 1)
for (int i = 0; i < 10000; i++) {
lock.lock();
Test.num--;
lock.unlock();
}
else
for (int i = 0; i < 10000; i++) {
lock.lock();
Test.num++;
lock.unlock();
}
}
}
public class Test {
public static int num = 1000000;
public static void main(String[] args) {
Thread a = new Thread(new MyThread(1));
Thread b = new Thread(new MyThread(2));
a.start();
b.start();
/* * 主线程等待子线程完成,然后再打印数值 */
try {
a.join();
b.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(num);
}
}
注意这里的代码和上边的代码并不一样,唯一的区别就在于声明ReentrantLock对象时前边是否加了static,这里是没有static修饰,再运行几次,是不是结果是不正确的呢。
为什么会这样呢?因为如果被static修饰,那么两个线程就是共用的同一个lock对象,如果不被static修饰,那么每个线程就是使用的它自己的lock对象,所以不会static修饰就会出现错误。
在下一篇博客里,我会为大家介绍java条件对象,希望与大家一起学习一起进步,请大家继续关注我的博客,如果大家支持我的话,就顶我一下吧。