以前一直搞不清楚,锁和同步的关系,经常打糊乱叫说“要实现同步,就要给对象的方法加一个锁”,现在想想,在面试时,听到我这翻言论的那些大神们,该是如何的无语啊,那么同步和锁到底是什么关系呢?
其实,可以这样理解,每个java对象都有一个内置的锁,本来就存在的,当程序运行到同步代码块时,就获得了该对象的锁(假如这里指的是非静态的同步块),当该线程获得了该对象的锁以后,其他线程就不能再获得该对象的锁了,其他线程必须等待,直到该线程执行完同步代码块,对于使用synchronized修饰的代码块,当代码执行完以后会自动释放锁,而如果使用的是ReadWriteLock,就必须我们手动的释放。注意一个对象只有一个锁,这就意味着,当一个线程获得了该对象的锁后,其他任何线程访问在该对象下建立的同步代码块或者函数都会被阻塞。
除了synchronized以外在java中还有一中锁的机制lock,在描述lock前,我们先来阐释下这样一个问题,就是使用synchronized实现资源的安全访问,必然会导致访问效率的降低,而在jdk1.5后JDK提出了的lock可以再一定程度上减少使用同步带来的效率低的问题。
在java中对于共享资源,在被多个线程同时访问的时候,为了保证资源的完整性,在进行一个事务操作的时候需要将资源进行锁定,保持其原子性,从而保证在进行事务操作的时候资源只被一个线程访问,从而保证资源的安全和唯一性,在实现这种需求上使用lock比使用synchronized更加的灵活,我们来简单使用以下lock吧:
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JavaThreadTest {
private static MyLockTest myMyLockTest = new MyLockTest();
public static void main(String[] args) throws IOException {
// 创建三个线程
Thread testThreadOne = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(1.0);
}
}, "test1");
Thread testThreadTwo = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(2.0);
}
}, "test2");
Thread testThreadThree = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(3.0);
}
}, "test3");
testThreadOne.start();
testThreadTwo.start();
testThreadThree.start();
}
private static class MyLockTest {
Lock myLock = new ReentrantLock();
double simpleValue = 0d;
private void addValue(double upValue) {
// 获得对象的锁
myLock.lock();
// 打印当前线程的id
System.out.println("current thread id----------->" + Thread.currentThread().getName());
this.simpleValue += upValue;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("simpleValue----------->" + simpleValue);
myLock.unlock();
}
}
}
可以看到多次的运行结果都是:
current thread id----------->test1
simpleValue----------->1.0
current thread id----------->test2
simpleValue----------->3.0
current thread id----------->test3
simpleValue----------->6.0
以上的运行结果表明,由于使用了lock三个线程是实现了同步的,我们看到这个线程是顺序执行的,在上面的例子中,我们看到我们获得锁以及释放锁都需要显示的调用获得锁,和显示的释放锁。
myLock.lock();
myLock.unlock();
必须是成对出现,如果在调用了lock后不调用unlock方法,那么该对象的锁将不会释放,也就是说其他对象是永远获得不了锁的,这样就会导致其他线程用户不能访问该对象。
2.ReadWriteLock相对其他的lock,ReadWriteLock具有更加广泛的灵活性,下面我们就用一个例子来体现一下吧
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Filename: JavaThreadTest.java Copyright: Copyright (c)2010 Company: Founder Mobile Media Technology(Beijing)
* Co.,Ltd.g
*
* @version: 1.0
* @since: JDK 1.6.0_21 Create at: 2014-8-15 下午1:53:29 Description: Modification History: Date Author Version
* Description ------------------------------------------------------------------ 2014-8-15 王涛 1.0 1.0 Version
*/
public class JavaThreadTest {
private static MyLockTest myMyLockTest = new MyLockTest();
public static void main(String[] args) throws IOException {
// 创建5个线程
// 写的线程
Thread testThreadOne = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(1.0);
}
}, "test1");
// 写的线程
Thread testThreadTwo = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(2.0);
}
}, "test2");
// 读的线程
Thread testThreadThree = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.getValue();
}
}, "test3");
// 读的线程
Thread testThreadFour = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.getValue();
}
}, "test4");
// 写的线程
Thread testThreadFive = new Thread(new Runnable() {
@Override
public void run() {
myMyLockTest.addValue(2.0);
}
}, "test5");
testThreadOne.start();
testThreadTwo.start();
testThreadThree.start();
testThreadFour.start();
testThreadFive.start();
}
private static class MyLockTest {
ReadWriteLock myLock = new ReentrantReadWriteLock();
double simpleValue = 0d;
/**
*
* @Title: addValue
* @Description: 增加值
* @date 2014-8-15
* @version 1.0
*/
private void addValue(double upValue) {
// 获得对象的写锁
Lock writeLock = myLock.writeLock();
writeLock.lock();
// 打印当前线程的id
System.out.println("current thread id----------->" + Thread.currentThread().getName());
this.simpleValue += upValue;
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("simpleValue----------->" + simpleValue);
writeLock.unlock();
}
/**
*
* @Title: getValue
* @Description: 获得值
* @date 2014-8-15
* @version 1.0
*/
private double getValue() {
// 获得读的锁
Lock readLock = myLock.readLock();
readLock.lock();
System.out.println("current thread id----------->" + Thread.currentThread().getName());
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// }
try {
System.out.println("get value----------->" + simpleValue);
return simpleValue;
} finally {
readLock.unlock();
}
}
}
}
可能你的运行结果会出现很多种情况,如下:
current thread id----------->test1
simpleValue----------->1.0
current thread id----------->test2
simpleValue----------->3.0
current thread id----------->test5
simpleValue----------->5.0
current thread id----------->test3
current thread id----------->test4
get value----------->5.0
get value----------->5.0
或者
current thread id----------->test1
simpleValue----------->1.0
current thread id----------->test5
simpleValue----------->3.0
current thread id----------->test2
simpleValue----------->5.0
current thread id----------->test3
get value----------->5.0
current thread id----------->test4
get value----------->5.0
或者
current thread id----------->test1
simpleValue----------->1.0
current thread id----------->test2
simpleValue----------->3.0
current thread id----------->test3
get value----------->3.0
current thread id----------->test4
get value----------->3.0
current thread id----------->test5
simpleValue----------->5.0
但是,在这个例子中我想说明的是以下一些东西,可能上面的例子并能很好的体现出要说明的东西,
(1)ReentrantLock和ReentrantReadWriteLock的区别是什么呢?
ReentrantLock 是一种和syncronized功能基本一样的锁的机制,也就是说它是一种比较保守的锁机制,在这个机制中,一次只能有一个线程持有锁,也就是在1.5之前我们一直使用的独占式的锁的机制,我前面也说过锁的机制必然会导致性能的下降,吞吐量会变低,但是在实际应用场景中我们可能会遇到这样的场景,某些资源虽然存在读写排斥的情况,但是在大多数的情况下,我们都希望能够进行同时读,写的情况是比较少的,共享资源被大量读取操作,而只有少量的写操作(修改数据),这时我们就希望能够最大的降低锁带来的吞吐量的下降,ReentrantReadWriteLock的提出解决了我们的问题,ReentrantReadWriteLock实现了ReadWriteLock接口,并没有实现Lock接口,是其内部类ReadLock和WriteLock实现了Lock的接口
(2)ReentrantReadWriteLock读写锁的类型
1.写的时候,对所有其他的读写操作都是排斥的,也就是说一个线程获得了写锁,那么这时候它是独占锁的;
2.读的时候,对所有的写操作是排斥的,也就是一个线程获得了读的锁,那么其他线程如果是需要修改共享资源,必须等待;
3.读的时候,对所有的读的操作是不排斥的,也就是说多个线程可以同时获取共享资源的值;
(3)在API中还对锁进行一些其他的描述
1.与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁的理想候选者。但是,如果数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性增强,也是微不足道的。更进一步地说,如果读取操作所用时间太短,则读-写锁实现(它本身就比互斥锁复杂)的开销将成为主要的执行成本,在许多读-写锁实现仍然通过一小段代码将所有线程序列化时更是如此。最终,只有通过分析和测量,才能确定应用程序是否适合使用读-写锁。
2.在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
3.是否重新进入锁方面,WriteLock内部可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则是不可能的(我通过程序测试发现,程序会永远处于等待状态)。 在写入线程保持的所有写入锁都已经释放后,才允许重入读取锁 使用它们
4. 我们也可以降级锁,允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);
rwl.readLock().unlock();
}
}
(4)获取顺序
此类不会将读取者优先或写入者优先强加给锁访问的排序。但是,它确实支持可选的公平 策略。
非公平模式(默认)
当非公平地(默认)构造时,未指定进入读写锁的顺序,受到 reentrancy 约束的限制。连续竞争的非公平锁可能无限期地推迟一个或多个 reader 或 writer 线程,但吞吐量通常要高于公平锁。
公平模式
当公平地构造线程时,线程利用一个近似到达顺序的策略来争夺进入。当释放当前保持的锁时,可以为等待时间最长的单个 writer 线程分配写入锁,如果有一组等待时间大于所有正在等待的 writer 线程 的 reader 线程,将为该组分配写入锁。如果保持写入锁,或者有一个等待的 writer 线程,则试图获得公平读取锁(非重入地)的线程将会阻塞。直到当前最旧的等待 writer 线程已获得并释放了写入锁之后,该线程才会获得读取锁。当然,如果等待 writer 放弃其等待,而保留一个或更多 reader 线程为队列中带有写入锁自由的时间最长的 waiter,则将为那些 reader 分配读取锁。试图获得公平写入锁的(非重入地)的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)。(注意,非阻塞 ReentrantReadWriteLock.ReadLock.tryLock()
和 ReentrantReadWriteLock.WriteLock.tryLock()
方法不会遵守此公平设置,并将获得锁(如果可能),不考虑等待线程)
(5)写入锁提供了一个 Condition
实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition()
提供的 Condition
实现对ReentrantLock 所做的行为相同。当然,此
Condition
只能用于写入锁。读取锁不支持 Condition
,readLock().newCondition()
会抛出 UnsupportedOperationException,关于
http://www.cjsdn.net/Doc/JDK60/java/util/concurrent/locks/Condition.htmlCondition的说明请参考: