您的位置:首页 > 编程语言 > Java开发

Java多线程中 synchronized和Lock的区别

2015-04-08 21:28 204 查看
在上一节中,
我们已经了解了Java多线程编程中常用的关键字synchronized,以及与之相关的对象锁机制。这一节中,让我们一起来认识JDK 5中新引入的并发框架中的锁机制。

我想很多购买了《Java程序员面试宝典》之类图书的朋友一定对下面这个面试题感到非常熟悉:

问:请对比synchronized与java.util.concurrent.locks.Lock的异同。

答案:主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

恩,让我们先鄙视一下应试教育。

言归正传,我们先来看一个多线程程序。它使用多个线程对一个Student对象进行访问,改变其中的变量值。我们首先用传统的synchronized 机制来实现它:

public class ThreadDemo implements Runnable {

class Student {

private int age = 0;

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

}

Student student = new Student();

int count = 0;

public static void main(String[] args) {

ThreadDemo td = new ThreadDemo();

Thread t1 = new Thread(td, "a");

Thread t2 = new Thread(td, "b");

Thread t3 = new Thread(td, "c");

t1.start();

t2.start();

t3.start();

}

public void run() {

accessStudent();

}

public void accessStudent() {

String currentThreadName = Thread.currentThread().getName();

System.out.println(currentThreadName + " is running!");

synchronized (this) {//(1)使用同一个ThreadDemo对象作为同步锁

System.out.println(currentThreadName + " got lock1@Step1!");

try {

count++;

Thread.sleep(5000);

} catch (Exception e) {

e.printStackTrace();

} finally {

System.out.println(currentThreadName + " first Reading count:" + count);

}

}



System.out.println(currentThreadName + " release lock1@Step1!");

synchronized (this) {//(2)使用同一个ThreadDemo对象作为同步锁

System.out.println(currentThreadName + " got lock2@Step2!");

try {

Random random = new Random();

int age = random.nextInt(100);

System.out.println("thread " + currentThreadName + " set age to:" + age);

this.student.setAge(age);

System.out.println("thread " + currentThreadName + " first read age is:" + this.student.getAge());

Thread.sleep(5000);

} catch (Exception ex) {

ex.printStackTrace();

} finally{

System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());

}

}

System.out.println(currentThreadName + " release lock2@Step2!");

}

}
转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

运行结果:

a is running!

a got lock1@Step1!

b is running!

c is running!

a first Reading count:1

a release lock1@Step1!

a got lock2@Step2!

thread a set age to:76

thread a first read age is:76

thread a second read age is:76

a release lock2@Step2!

c got lock1@Step1!

c first Reading count:2

c release lock1@Step1!

c got lock2@Step2!

thread c set age to:35

thread c first read age is:35

thread c second read age is:35

c release lock2@Step2!

b got lock1@Step1!

b first Reading count:3

b release lock1@Step1!

b got lock2@Step2!

thread b set age to:91

thread b first read age is:91

thread b second read age is:91

b release lock2@Step2!

成功生成(总时间:30 秒)

显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁,所以JVM优先使刚刚释放该锁的线程重新获得该锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是30秒。

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这两个对象锁应该选择那些被所有线程所共享的对象。

那么好。我们把第二个同步块中的对象锁改为student(此处略去代码,读者自己修改),程序运行结果为:

a is running!

a got lock1@Step1!

b is running!

c is running!

a first Reading count:1

a release lock1@Step1!

a got lock2@Step2!

thread a set age to:73

thread a first read age is:73

c got lock1@Step1!

thread a second read age is:73

a release lock2@Step2!

c first Reading count:2

c release lock1@Step1!

c got lock2@Step2!

thread c set age to:15

thread c first read age is:15

b got lock1@Step1!

thread c second read age is:15

c release lock2@Step2!

b first Reading count:3

b release lock1@Step1!

b got lock2@Step2!

thread b set age to:19

thread b first read age is:19

thread b second read age is:19

b release lock2@Step2!

成功生成(总时间:21 秒)

从修改后的运行结果来看,显然,由于同步块的对象锁不同了,三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟的时间是两个线程同时工作的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线程执行第二个同步块的动作。相比较第一个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/2n,其中n为线程数。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个synchronized块,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m-1)n+1)/mn那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而这种方案至少具备数学上的可行性理论支持。)

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

可见,使用不同的对象锁,在不同的同步块中完成任务,可以使性能大大提升。

很多人看到这不禁要问:这和新的Lock框架有什么关系?

别着急。我们这就来看一看。

synchronized块的确不错,但是他有一些功能性的限制:

1.它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。

2.synchronized块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些更适合使用非块结构锁定的情况。

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

JDK官方文档中提到:

ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和getHoldCount() 方法来检查此情况是否发生。 ”

简单来说,ReentrantLock有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁。

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

ReentrantLock 类(重入锁)实现了 Lock ,它拥有与 synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

我们把上面的例程改造一下:

public class ThreadDemo implements Runnable {

class Student {

private int age = 0;

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

}

Student student = new Student();

int count = 0;

ReentrantLock lock1 = new ReentrantLock(false);

ReentrantLock lock2 = new ReentrantLock(false
);

public static void main(String[] args) {

ThreadDemo td = new ThreadDemo();

for (int i = 1; i <= 3; i++) {

Thread t = new Thread(td, i + "");

t.start();

}

}

public void run() {

accessStudent();

}

public void accessStudent() {

String currentThreadName = Thread.currentThread().getName();

System.out.println(currentThreadName + " is running!");

lock1.lock();//使用重入锁

System.out.println(currentThreadName + " got lock1@Step1!");

try {

count++;

Thread.sleep(5000);

} catch (Exception e) {

e.printStackTrace();

} finally {

System.out.println(currentThreadName + " first Reading count:" + count);

lock1.unlock();

System.out.println(currentThreadName + " release lock1@Step1!");

}

lock2.lock();//使用另外一个不同的重入锁

System.out.println(currentThreadName + " got lock2@Step2!");

try {

Random random = new Random();

int age = random.nextInt(100);

System.out.println("thread " + currentThreadName + " set age to:" + age);

this.student.setAge(age);

System.out.println("thread " + currentThreadName + " first read age is:" + this.student.getAge());

Thread.sleep(5000);

} catch (Exception ex) {

ex.printStackTrace();

} finally {

System.out.println("thread " + currentThreadName + " second read age is:" + this.student.getAge());

lock2.unlock();

System.out.println(currentThreadName + " release lock2@Step2!");

}

}

}

从上面这个程序我们看到:

对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。并且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的。

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

这说明两点问题:

1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码人员自行掌握。

2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。

3.使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而在finally中使用unlock方法。

转载注明出处:http://x-spirit.javaeye.com/、http://www.blogjava.net/zhangwei217245/

细心的读者又发现了:

在我们的例程中,创建ReentrantLock实例的时候,我们的构造函数里面传递的参数是false。那么如果传递true又回是什么结果呢?这里面又有什么奥秘呢?

请看本节的续 ———— Fair or Unfair? It is aquestion...

Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我们拿Java线程(二)中的一个例子简单的实现一下和sychronized一样的效果,代码如下:

[java]
view plaincopyprint?





public class LockTest {
public static void main(String[] args) {
final Outputter1 output = new Outputter1();
new Thread() {
public void run() {
output.output("zhangsan");
};
}.start();
new Thread() {
public void run() {
output.output("lisi");
};
}.start();
}
}
class Outputter1 {
private Lock lock = new ReentrantLock();// 锁对象
public void output(String name) {
// TODO 线程输出方法
lock.lock();// 得到锁
try {
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 释放锁
}
}
}

这样就实现了和sychronized一样的同步效果,需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。
如果说这就是Lock,那么它不能成为同步问题更完美的处理方式,下面要介绍的是读写锁(ReadWriteLock),我们会有一种需求,在对数据进行读写的时候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的,但是读和读是不需要互斥的,这样读和读不互斥性能更高些,来看一下不考虑互斥情况的代码原型:

[java]
view plaincopyprint?





public class ReadWriteLockTest {
public static void main(String[] args) {
final Data data = new Data();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
}).start();
}
}
}
class Data {
private int data;// 共享数据
public void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
}

部分输出结果:

[java]
view plaincopyprint?





Thread-1准备写入数据
Thread-3准备读取数据
Thread-2准备写入数据
Thread-0准备写入数据
Thread-4准备读取数据
Thread-5准备读取数据
Thread-2写入12
Thread-4读取12
Thread-5读取5
Thread-1写入12

我们要实现写入和写入互斥,读取和写入互斥,读取和读取互斥,在set和get方法加入sychronized修饰符:

[java]
view plaincopyprint?





public synchronized void set(int data) {...}
public synchronized void get() {...}

部分输出结果:

[java]
view plaincopyprint?





Thread-0准备写入数据
Thread-0写入9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9

我们发现,虽然写入和写入互斥了,读取和写入也互斥了,但是读取和读取之间也互斥了,不能并发执行,效率较低,用读写锁实现代码如下:

[java]
view plaincopyprint?





class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}

部分输出结果:

[java]
view plaincopyprint?





Thread-4准备读取数据
Thread-3准备读取数据
Thread-5准备读取数据
Thread-5读取18
Thread-4读取18
Thread-3读取18
Thread-2准备写入数据
Thread-2写入6
Thread-2准备写入数据
Thread-2写入10
Thread-1准备写入数据
Thread-1写入22
Thread-5准备读取数据

从结果可以看出实现了我们的需求,这只是锁的基本用法,锁的机制还需要继续深入学习。

本文来自:高爽|Coder,原文地址:/article/1391639.html,转载请注明。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: