I.はじめに
目の前で、「Javaの並行処理-同期の決意」我々は詳細に入るには、暗黙的なロック、ホールド属し同期、同期マルチスレッド同期キーワードの問題を解決し、暗黙的であること、すべてのロックを解除しなければならなかった、我々は介入する必要はありませんが、この記事では、我々はしたいです明示的ロックと解除を保持しているロックを手動たちによって書かれなければならないことを説明。
二、ロックインタフェース
2.1ロックインターフェイスの概要
ロックは、複数のスレッドで共有リソースへのアクセスを制御するためのツールです。典型的には、ロックは共有リソースへの排他的アクセスを提供する:1つのスレッドだけがロックを取得することができ、共有リソースへのすべてのアクセスは、最初にロックを取得するために必要とされます。しかし、いくつかのロックは、読み書きロックReadWriteLockなどの共有リソースへの同時アクセスを可能にすることができます。
ロックインターフェースが行く前に、Javaプログラムは、synchronizedキーワード、ロック機能によって実現されます。JDK1.5に収縮及び添加ロックインターフェースと関連する実装クラスの後にロック機能を実現します。
メカニズム文の同期方法と範囲は、それが簡単にロックプログラムを監視することができ、そしてロックを含む回避多くの一般的なプログラミングエラーを助け、時にはあなたは、ロックへのより柔軟なアプローチを必要としますが。例えば、データ構造をトラバースするためのいくつかのアルゴリズムは、「手動」または「チェーンロック」への同時アクセスを使用する必要があります。あなたは、ノードAを取得ロックし、次にノードBを取得し、A及び取得のCと、その後解放され、その後、BとDと得等を放出します。このシナリオでは、synchronizedキーワードはそう簡単に実装でき、そしてロックの多くのインタフェースを使用することは容易ではありません。ロックは高度なsynchronizedキーワードで、マスターロックは、学習やソースコードの契約、契約および同期プロセスとしてロック・インターフェースを使用してクラスの数に貢献しています。
実装クラスのインターフェイスロック:
- ReentrantLockの
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
2.2ロックの簡単な使用
Lock lock = new ReentrantLock();
lock.lock();
try{
//临界区......
}finally{
lock.unlock();
}
重要なゾーンに)(アンロックするために、現在のスレッドロック()メソッドを使用して、現在のスレッドがロックを解放するまでロックを保持できないことがクリティカルセクションに入ることができなくなり、他のスレッドに囲まれています。ロックオブジェクトfinallステートメントブロックを解放するだけでなく、使用されている場合、インタフェースの実装クラスにバインドされているロックのためにロックが取得された後、最終的にリリースされることを保証することです。注:ロックを取得中に例外が発生した場合に、スローされた例外も解放することができないロックにつながるので、それは、tryブロックの作成プロセスでロックを取得することが最善ではありません。
特性2.3ロックインターフェイスと共通のメソッド
ロックインタフェースは、synchronizedキーワードは、主な機能を持っていない提供しています。
プロパティ | 説明 |
---|---|
ロックを取得するためにノンブロッキングの試み | ロックがロックを保持している別のスレッドによって獲得されていないこの時間が正常に取得された場合、現在のスレッドは、ロックを取得しようとすると、 |
ロックへのアクセスを中断することができます | スレッドが応答を中断することができますロックするには、Get、取得したロックのスレッドが中断されたときにロックが解除されている間、割り込み例外は、スローされます |
タイムアウト獲得ロック | 指定された期限前にロックを獲得し、より多くの締め切り後よりも、まだリターンを得ることができません |
ロックインタフェースの基本的な方法:
メソッド名 | 説明 |
---|---|
空ロック() | ロックを取得します。ロックが利用できない場合は、現在のスレッドは、スレッドスケジューリングに関して無効になり、それがロックを取得するまで嘘が休止します。 |
空lockInterruptibly() | すぐに利用可能と返した場合、ロックを取得。ロックが使用できない場合、現在のスレッドがスレッドスケジューリング、および休止状態、及びロックを無効になります()メソッドは、現在のスレッドの割り込み(対応する割り込み)を取得することができるロックは異なっています。 |
条件newCondition() | バインディングをロック通知コンポーネントと現在のを待って、現在のスレッドが唯一のコンポーネント待ち()メソッドを呼び出す前にロックを取得取得し、呼び出しの後、現在のスレッドがロックを解除します。 |
ブールのtryLock() | あなただけが呼び出されたときにロックを取得することができます。可能な場合は、ロックを取得し、すぐにtrueを返しますされ、ロックが利用できない場合、このメソッドはfalseにすぐに戻ります。 |
ブールのtryLock(長い時間、TimeUnitでユニット) | 3つの状況について、現在のスレッドのリターンのロックを取得タイムアウト:; 2現在のスレッドがタイムアウト時間内に中断された。1.タイムアウト内のロックを獲得するために、現在のスレッド3のタイムアウトが終了し、falseを返します... |
空ロック解除() | ロックを解除してください。 |
三、Lock接口的实现类:ReentrantLock
3.1 ReentrantLock 使用
ReentrantLock 和 synchronized 关键字一样可以用来实现线程之间的同步互斥,但是在功能是比 synchronized 关键字更强大而且更灵活。
构造方法:
方法名称 | 描述 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock的实例。 |
ReentrantLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例 |
ReentrantLock 类常见方法(Lock 接口已有方法这里没加上):
方法名称 | 描述 |
---|---|
int getHoldCount() | 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。 |
protected Thread getOwner() | 返回当前拥有此锁的线程,如果不拥有,则返回 null |
protected Collection getQueuedThreads() | 返回包含可能正在等待获取此锁的线程的集合 |
int getQueueLength() | 返回等待获取此锁的线程数的估计。 |
protected Collection getWaitingThreads(Condition condition) | 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。 |
int getWaitQueueLength(Condition condition) | 返回与此锁相关联的给定条件等待的线程数的估计。 |
boolean hasQueuedThread(Thread thread) | 查询给定线程是否等待获取此锁。 |
boolean hasQueuedThreads() | 查询是否有线程正在等待获取此锁。 |
boolean hasWaiters(Condition condition) | 查询任何线程是否等待与此锁相关联的给定条件 |
boolean isFair() | 如果此锁的公平设置为true,则返回 true 。 |
boolean isHeldByCurrentThread() | 查询此锁是否由当前线程持有。 |
boolean isLocked() | 查询此锁是否由任何线程持有。 |
ReentrantLock 简单使用:
public class ReentrantLockTest {
public static void main(String[] args) {
MyService service = new MyService();
MyThread a1 = new MyThread(service);
MyThread a2 = new MyThread(service);
MyThread a3 = new MyThread(service);
MyThread a4 = new MyThread(service);
MyThread a5 = new MyThread(service);
a1.start();
a2.start();
a3.start();
a4.start();
a5.start();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
try {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
}
} finally {
lock.unlock();
}
}
}
static public class MyThread extends Thread {
private MyService service;
public MyThread(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
}
运行结果:
从运行结果可以看出,当一个线程运行完毕后才把锁释放,其他线程才能执行,其他线程的执行顺序是不确定的。
3.2 Condition 接口简介
我们通过之前的学习知道了:synchronized 关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
在使用 notify/notifyAll() 方法进行通知时,被通知的线程是有 JVM 选择的,使用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”,这个功能非常重要,而且是 Condition 接口默认提供的。
而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll() 方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll() 方法只会唤醒注册在该 Condition 实例中的所有等待线程。
Condition 接口的常见方法:
方法名称 | 描述 |
---|---|
void await() | 相当于Object类的wait方法 |
boolean await(long time, TimeUnit unit) | 相当于Object类的wait(long timeout)方法 |
signal() | 相当于Object类的notify方法 |
signalAll() | 相当于Object类的notifyAll方法 |
3.3 使用 Condition 实现等待/通知机制
public class UseSingleConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() throws InterruptedException {
lock.lock();
try {
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("这是condition.signal()方法之后的语句");
} finally {
lock.unlock();
}
}
}
static public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
}
运行结果:
在使用 wait/notify 实现等待通知机制的时候我们知道必须执行完 notify() 方法所在的 synchronized 代码块后才释放锁。在这里也差不多,必须执行完 signal 所在的 try 语句块之后才释放锁,condition.await() 后的语句才能被执行。注意: 必须在 condition.await() 方法调用之前调用 lock.lock() 代码获得同步监视器,不然会报错。
3.4 公平锁与非公平锁
Lock 锁分为:公平锁 和 非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的 FIFO 先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。
public class FairorNofairLock {
public static void main(String[] args) throws InterruptedException {
final Service service = new Service(true);//true为公平锁,false为非公平锁
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("★线程" + Thread.currentThread().getName() + "运行了");
service.serviceMethod();
}
};
Thread[] threadArray = new Thread[10];
for (int i = 0; i < 10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i = 0; i < 10; i++) {
threadArray[i].start();
}
}
static public class Service {
private ReentrantLock lock;
public Service(boolean isFair) {
super();
lock = new ReentrantLock(isFair);
}
public void serviceMethod() {
lock.lock();
try {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ "获得锁定");
} finally {
lock.unlock();
}
}
}
}
运行结果:
可以看到公平锁的运行结果是有序的。现在把Service的参数修改为false则为非公平锁:
final Service service = new Service(false);//true为公平锁,false为非公平锁
可以看到非公平锁的运行结果是无序的。
四、ReadWriteLock 接口的实现类:ReentrantReadWriteLock
4.1 简介
我们刚刚接触到的 ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock 接口的实现类 ReentrantReadWriteLock 读写锁就是为了解决这个问题。读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的)。在没有线程 Thread 进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread 可以同时进行读取操作,但是同一时刻只允许一个 Thread 进行写入操作。
4.2 ReentrantReadWriteLock的特性与常见方法
ReentrantReadWriteLock的特性:
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量上来看还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁也能够同时获取读锁 |
锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级称为读锁 |
ReentrantReadWriteLock常见方法:
构造方法:
方法名称 | 描述 |
---|---|
ReentrantReadWriteLock() | 创建一个 ReentrantReadWriteLock()的实例 |
ReentrantReadWriteLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantReadWriteLock的实例 |
常见方法: 和 ReentrantLock 类 类似这里就不列举了。
4.3 ReentrantReadWriteLock的使用
4.3.1 读读共享
两个线程同时运行 read 方法,你会发现两个线程可以同时或者说是几乎同时运行 lock() 方法后面的代码,输出的两句话显示的时间一样。这样提高了程序的运行效率。
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(10000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
4.3.2 写写互斥
把上面的代码的
lock.readLock().lock();
改为:
lock.writeLock().lock();
两个线程同时运行read方法,你会发现同一时间只允许一个线程执行 lock() 方法后面的代码。
4.3.3 读写互斥
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(10000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void write() {
try {
try {
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(10000);
} finally {
lock.writeLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
测试代码:
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
Thread.sleep(1000);
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
运行两个使用同一个 Service 对象实例的线程 a,b,线程 a 执行上面的 read 方法,线程 b 执行上面的 write 方法。你会发现同一时间只允许一个线程执行 lock() 方法后面的代码。记住:只要出现写操作的过程就是互斥的。
4.3.4 写读互斥
和读写互斥类似,这里不用代码演示了。记住:只要出现写操作的过程就是互斥的。
五、关于 synchronized 与 ReentrantLock
在JDK 1.6之后,虚拟机对于 synchronized 关键字进行整体优化后,在性能上 synchronized 与 ReentrantLock 已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是 synchronized 关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而 ReentrantLock 提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized 的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择 ReentrantLock。