多线程拨号Java版(探讨synchronized和Lock线程同步)

0.需求

需求是这样的:实现多线程拨号,从数据中的批量读取需要拨打的号码,然后多线程拨号。拨号就是打电话给某一个号码,然后播放一段录音,自动化并非人工拨号。为什么要多线程拨号,如果一个一个拨号的话,一个电话3分钟,一个小时就打20个电话,太慢了,如果这里用10个线程拨号的话,那么20个电话6分钟就能搞定了。本文主要是探讨如何实现多线程拨号。

首先来思考一下实现流程:

首先从数据库中拿到需要拨打的号码,放在一个List中,每个号码加上一个标识,来标记这个号码是否已拨打。多线程每个线程都遍历这个List然后拿到标记是未拨打的号码来拨号,拨打完成后更改标记为已拨打。

1 第一个版本

好,开始写代码:

(号码列表应该从数据库中读取,拨号线程的数量应该写在配置文件中方便用户根据不同配置的计算机来设置线程数,如何读取数据库和配置文件不是本文重点,这里就直接写在main方法里了)

//号码对象,加上一个状态标记

class Number

{

//号码

private StringphoneNumber;

//号码状态,用于标记是否已拨号

private NumberStatestate;

public Number(String phoneNumber, NumberState state) {

this.phoneNumber = phoneNumber;

this.state = state;

}

public String getPhoneNumber() {

returnphoneNumber;

}

public void setPhoneNumber(String phoneNumber) {

this.phoneNumber = phoneNumber;

}

public NumberState getState() {

returnstate;

}

public void setState(NumberState state) {

this.state = state;

}

}

//枚举类型用来标识号码的状态

enum NumberState

{

notDial, //未拨号

finish //已完成

}

class Phone {

// 要拨打的号码列表

private List<Number>numbers;

// 线程数量

private int threadCount = 0;

public Phone(List<Number> numbers,int threadCount) {

this.numbers = numbers;

this.threadCount = threadCount;

}

public void run() {

System.out.println("拨号程序启动...");

// 启动threadCount个线程来拨号

for (int i = 0; i <threadCount; i++) {

// new一个线程,参数dial是函数名,也就是线程启动执行dial函数

Thread thread = new ThreadPhone(numbers);

// 线程启动

thread.start();

}

}

class ThreadPhoneextends Thread {

private List<Number>numbers;

public ThreadPhone(List<Number> numbers) {

this.numbers = numbers;

}

public void run() {

// 遍历列表

for (Number number :numbers) {

// 状态未拨号

if (number.getState() == NumberState.notDial) {

System.out.println("[多线程]号码" + number.getPhoneNumber()

+ "开始拨号...");

// 通话过程,这里用sleep来模拟

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("[多线程]号码" + number.getPhoneNumber()

+ "拨号完成");

// 设置状态已完成

number.setState(NumberState.finish);

}

}

}

}

}

public class Test {

public static void main(String[] args) {

List<Number> numbers = new LinkedList<Number>();

// 列表中添加号码

Number number1 = new Number("11111", NumberState.notDial);

numbers.add(number1);

Number number2 = new Number("22222", NumberState.notDial);

numbers.add(number2);

Number number3 = new Number("33333", NumberState.notDial);

numbers.add(number3);

Number number4 = new Number("44444", NumberState.notDial);

numbers.add(number4);

Number number5 = new Number("55555", NumberState.notDial);

numbers.add(number5);

Number number6 = new Number("66666", NumberState.notDial);

numbers.add(number6);

Number number7 = new Number("77777", NumberState.notDial);

numbers.add(number7);

Number number8 = new Number("88888", NumberState.notDial);

numbers.add(number8);

Number number9 = new Number("99999", NumberState.notDial);

numbers.add(number9);

// 启动拨号程序

// 这里要传入两个参数,一个是号码列表,一个是线程数量(这里的线程数量应该是写在配置文件中可配置的,用户可以根据不同配置的计算机来配置线程数)

new Phone(numbers, 3).run();

}

}

到这里,也许你就会看出来一个问题了。一个线程正在拨未完成的号码尚未拨完,状态该没有来得及改成已完成,另一个线程遍历列表,发现这个未完成的号码,也开始拨号…这样就会出现多个线程同时拨打一个号码的情况。

多线程拨号Java版(探讨synchronized和Lock线程同步)

此时就需要用到线程同步了。我们要控制每个号码只能有一个线程在拨打,也就是在拨打之前先给这个号码加锁防止别的线程再次拨打。

2 synchronized线程同步

说到线程同步,第一反应是关键字synchronized关键字。简单解释一下synchronized,其语法是这样的:

synchronized (加锁的对象) {
 一段代码……
}

当一个线程对一个对象加锁以后,别的线程是不能再次加锁了,要想再次加锁,必须等待直到该线程解锁。大括号中的代码执行完成以后,会自动对其解锁。

本人在网上读过很多线程同步方面的文章,很多都有这么一种说法:这段代码在一个时刻内只可能被一个线程执行。实际这种说法是错误的。实际上lock并没有锁定这段代码,而是括号内的对象。也就是,这段代码可以被多个线程执行,但是这个对象不能被多个线程加锁,如果lock中换成不同的对象,这段代码是可以同时执行的。

针对本例,需要在拨打某个号码之前把一个Number对象加锁,结束修改状态之后,再解锁对象。这样就不会出现多个线程同时拨打一个号码的情况了。

修改上面的代码,在for循环中加锁Number对象:

// 遍历列表

for (Numbernumber :numbers) {

//加锁对象,别的线程就不能再次加锁并拨号

synchronized (number) {

// 状态未拨号

if (number.getState() == NumberState.notDial) {

System.out.println("[多线程]号码" + number.getPhoneNumber() + "开始拨号...");

// 通话过程,这里用sleep来模拟

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("[多线程]号码" + number.getPhoneNumber()

+ "拨号完成");

// 设置状态已完成

number.setState(NumberState.finish);

}

}

}

OK,再次运行…

多线程拨号Java版(探讨synchronized和Lock线程同步)

什么,居然变成一个一个拨打了,这和单线程没什么区别了。

先停下来想想为什么会出现这种现象。

在这里,每个线程都是从上往下遍历一个list,拿到号码,然后加锁,然后再拨号。如果一个线程对某个号码已经加锁并在拨号,另一个线程遍历到这里的这里,还会试图加锁,于是就等啊等,终于等到前一个线程解锁了,它才加上了锁,但是人家都拨号完成了…

到这里我们就想,一个线程能不能在对号码加锁之前,先判断一下这个号码有没有被别的线程加锁。如果被其他线程加锁了,说明这个号码正在拨号,自动向下遍历…

3 Lock

Lock接口的ReentrantLock实现类可以实现类似synchronized的功能对对象进行加锁和解锁。Lock相对synchronized更牛之处是它能判断一个对象是否已经被加锁了,返回true或false,即Lock的tryLock()方法。

使用Lock要注意一点,就是在使用完成后一定要释放锁。这一点不像synchronized那么方便(synchronized在程序块的大括号结束后自动释放锁)。释放锁的方法是unlock()。锁的释放代码要写在finally代码块中保证执行释放操作。

以下是比较规范的做法:

if (lock.tryLock()) {

try{

……

}catch(..){

……

}finally{

lock.unlock();

}

}

从上面可以看出来,lock不像synchronized那样可以指定锁定的对象,那么使用look后到底锁定哪个对象呢?Lock一般在定义为类的成员变量,调用tryLock锁定的就是这个类的对象,也就是this。

( 这里和C#的Monitor .TryEnter(obj)不同,C#可以在参数中指定加锁对象,细节可以参考 <多线程拨号C#版>http://blog.****.net/xiao__gui/article/details/8018054)

回到多线程拨号了问题上来,再次改一下上面的代码:

由于我们要加锁的对象是Number,首先修改一下Number类,加入一个ReentrantLock类成员变量并加上get方法:

class Number {

private Lock lock = new ReentrantLock();

public Lock getLock() {

returnlock;

}

……

}

修改对List的for循环:

for (Number number :numbers) {

//首先,要查看这个对象是否被lock(正在拨号),如果没有则给它加锁,防止其他线程再对它进行拨号操作

if (number.getLock().tryLock()) {

try {

// 状态未拨号

if (number.getState() == NumberState.notDial) {

System.out.println("[多线程]号码"

+ number.getPhoneNumber() + "开始拨号...");

// 通话过程,这里用sleep来模拟

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("[多线程]号码"

+ number.getPhoneNumber() + "拨号完成");

// 设置状态已完成

number.setState(NumberState.finish);

}

} catch (Exception e) {

e.printStackTrace();

} finally {

// 释放锁

number.getLock().unlock();

}

}

}

运行结果:

多线程拨号Java版(探讨synchronized和Lock线程同步)

至此,我们想要的多线程拨号功能已经实现。


作者:叉叉哥 转载请注明出处:http://blog.****.net/xiao__gui/article/details/8020142