java多线程(二)锁对象

时间:2022-04-20 13:05:46

转载请注明出处: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条件对象,希望与大家一起学习一起进步,请大家继续关注我的博客,如果大家支持我的话,就顶我一下吧。