Java多线程编程核心技术-4-Lock对象的使用

基本用法

Java多线程可以使用synchronized关键字来实现线程间同步,不过JDK 1.5新增加的ReentrantLock类也能达到同样的效果,并且在扩展功能上更加强大,如具有嗅探锁定、多路分支通知等功能。

ReentrantLock类

可以直接new一个ReentranLock。然后再后面的方法中使用该对象的方法来进行同步处理。

例如:

1
private Lock lock = new ReentrantLock();

lock()方法

lock方法用于锁定当前的ReentrantLock对象。在此之后,所有的方法都是同步运行的了。与synchoronized代码块一样,其他调用该lock锁的代码都会被同步,等待该锁释放。

例如:

1
2
3
private Lock lock = new ReentrantLock();
lock.lock();
//后面的代码都会同步执行

tryLock()

public boolean tryLock()方法的作用是嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,程序继续执行后面的代码,而不是呈阻塞等待锁的状态。

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package service;

import java.util.concurrent.locks.ReentrantLock;

public class MyService {

public ReentrantLock lock = new ReentrantLock();

public void waitMethod() {
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "获得锁");
} else {
System.out.println(Thread.currentThread().getName() + "没有获得锁");
}
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;

import service.MyService;

public class Run {

public static void main(String[] args) throws InterruptedException {
final MyService service = new MyService();

Runnable runnableRef = new Runnable() {
@Override
public void run() {
service.waitMethod();
}
};

Thread threadA = new Thread(runnableRef);
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(runnableRef);
threadB.setName("B");
threadB.start();
}
}

结果:

1
2
A获得锁
B没有获得锁

注意这里为synchoronized不具有的方法,即尝试获取锁,如果没获得,就继续执行下去,而不是阻塞。

tryLock(long timeout, TimeUnit unit)

这里的方法与上面方法一样,但这里加了延迟时间。如果当前线程在指定的timeout内持有了锁,则返回值是true,超过时间则返回false。参数timeout代表当前线程抢锁的时间。

lockInterruptibly()

public void lockInterruptibly()方法的作用是当某个线程尝试获得锁并且阻塞在lockInterrup-tibly()方法时,该线程可以被中断。

unlock()方法

unlock方法会解除该锁,也就是说,后面的代码不再受该该锁的限制,可以异步执行了。

例如:

1
2
3
4
5
private Lock lock = new ReentrantLock();
lock.lock();
// 同步代码
lock.unlock();
// 异步代码

整体例子

服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyService {

private Lock lock = new ReentrantLock();

public void testMethod() {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
lock.unlock();
}

}

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package extthread;

import service.MyService;

public class MyThread extends Thread {

private MyService service;

public MyThread(MyService service) {
super();
this.service = service;
}

@Override
public void run() {
service.testMethod();
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package test;

import service.MyService;
import extthread.MyThread;

public class Run {

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();

}

}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ThreadName-Thread-01
ThreadName-Thread-02
ThreadName-Thread-O3
ThreadName-Thread-04
THreadName-Thread-05
ThreadName-Thread-41
ThreadName-Thread-42
ThreadName-Thread-43
ThreadName-Thread-44
ThreadNane-Thread-45
ThreadName-Thread-11
ThreadName-Thread-12
ThreadName-Thread-13
ThreadName=Thread-14
ThreadName-Thread-15
ThreadName=Thread-21
ThreaaName-Threaa-22
ThreadName-Thread-23
ThreadName-Thread-24
ThreadName-Thread-25
ThreadName-Tnread-31
ThreadName-Thread-32
ThreadName-Thread-33
ThreadName-Thread-34
ThreadName-Thread-35

从程序运行结果来看,只有当当前线程输出完毕之后将锁释放,其他线程才可以继续抢锁并输出,每个线程内输出的数据是有序的,从1到5,因为当前线程已经持有锁,具有互斥排他性,但线程之间输出的顺序是随机的,即谁抢到锁,谁输出。

condition对象

关键字synchronized与wait()、notify()/notifyAll()方法相结合可以实现wait/notify模式,ReentrantLock类也可以实现同样的功能,但需要借助于Condition对象。Condition类是JDK 5的技术,具有更好的灵活性,例如,可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

await()方法

await方法与synchonized的wait方法类似。调用该方法可以使线程进入到wait状态。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyService {

private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void await() {
try {
lock.lock();
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

这里的调用await方法机会调用对应Condition对象的await方法。使线程进入wait状态。直到该Condtition的signal/signalAll方法被调用,才会将其进行唤醒。

signal()方法

signal()与synchonized的notify方法类似。即唤醒等待队列中的其中一个线程。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService {

private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void signal() {
try {
lock.lock();
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
}

signalAll()方法

同样的,signalAll()方法类似于synchonized的notifyAll()方法。即唤醒所有的等待该Condition的线程。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService {

private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void signalAll() {
try {
lock.lock();
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signalAll();
} finally {
lock.unlock();
}
}
}

公平锁与非公平锁

公平锁:采用先到先得的策略,每次获取锁之前都会检查队列里面有没有排队等待的线程,没有才会尝试获取锁,如果有就将当前线程追加到队列中。

非公平锁:采用“有机会插队”的策略,一个线程获取锁之前要先去尝试获取锁而不是在队列中等待,如果获取锁成功,则说明线程虽然是后启动的,但先获得了锁,这就是“作弊插队”的效果。如果获取锁没有成功,那么才将自身追加到队列中进行等待。

而ReentrantLock默认是非公平锁,与synchoronized一致,也可以在构造函数中传入一个布尔参数来构造出公平锁。

即:

1
Lock lock = new ReentrantLock(true);

相关的方法

Lock-public int getHoldCount()

public int getHoldCount()方法的作用是查询“当前线程”保持此锁定的个数,即调用lock()方法的次数。

Lock-public final int getQueueLength()

public final int getQueueLength()方法的作用是返回正等待获取此锁的线程估计数,例如,这里有5个线程,其中1个线程长时间占有锁,那么调用getQueueLength()方法后,其返回值是4,说明有4个线程同时在等待锁的释放。

Lock-public int getWaitQueueLength(Condition condition)

public int getWaitQueueLength(Condition condition)方法的作用是返回等待与此锁相关的给定条件Condition的线程估计数。例如,这里有5个线程,每个线程都执行了同一个Condition对象的await()方法,则调用getWaitQueueLength(Condition condition)方法时,其返回的int值是5。

Lock-public final boolean hasQueuedThread(Thread thread)

public final boolean hasQueuedThread(Thread thread)方法的作用是查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中。

Lock-public final boolean hasQueued-Threads()

public final boolean hasQueuedThreads()方法的作用是查询是否有线程正在等待获取此锁,也就是等待队列中是否有等待的线程。

Lock-public boolean hasWaiters(Condition condition)

public boolean hasWaiters(Condition condition)方法的作用是查询是否有线程正在等待与此锁有关的condition条件,也就是是否有线程执行了condition对象中的await()方法而呈等待状态。而public int getWaitQueueLength(Condition condition)方法的作用是返回有多少个线程执行了condition对象中的await()方法而呈等待状态。

Lock-public final boolean isFair()

public final boolean isFair()方法的作用是判断是不是公平锁。

Lock-public boolean isHeldByCurrentThread()

public boolean isHeldByCurrentThread()方法的作用是查询当前线程是否保持此锁。

Lock-public boolean isLocked()

public boolean isLocked()方法的作用是查询此锁是否由任意线程保持,并没有释放。

Lock-public boolean await(long time,TimeUnit unit)-重要

public boolean await(long time,TimeUnit unit)方法的作用和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能。即在规定时间内没有线程对其进行唤醒,则自动进行唤醒。

Lock-public long awaitNanos(long nanosTimeout)

public long awaitNanos(long nanosTimeout)方法的作用和public final native void wait(long timeout)方法一样,都具有自动唤醒线程的功能,不过时间单位是纳秒(ns)。

Lock-public boolean awaitUntil(Date deadline)-重要

public boolean awaitUntil(Date deadline)方法的作用是在指定的Date结束等待

Lock-public void awaitUninterruptibly()

public void awaitUninterruptibly()方法的作用是实现线程在等待的过程中,不允许被中断

ReentrantReadWriteLock类

ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率是非常低下的,所以JDK提供了一种读写锁——ReentrantReadWriteLock类,使用它可以在进行读操作时不需要同步执行,提升运行速度,加快运行效率。

读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作相关的锁,也称排他锁。

读锁之间不互斥,读锁和写锁互斥,写锁与写锁互斥,因此只要出现写锁,就会出现互斥同步的效果。

读操作是指读取实例变量的值,写操作是指向实例变量写入值。

ReentrantLock类的缺点

与ReentrantReadWriteLock类相比,ReentrantLock类的主要缺点是使用ReentrantLock对象时,所有的操作都同步,哪怕只对实例变量进行读取操作,这样会耗费大量的时间,降低运行效率。

ReentrantReadWriteLock类的使用

读读共享

由于读并不会改变数据内容,所以相当于可异步操作。因此这个锁可以共享。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private String username = "abc";

public void testMethod1() {
try {
lock.readLock().lock(); // 获取读锁
System.out.println("begin " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
System.out.println("print service " + username);
Thread.sleep(4000);
System.out.println(" end " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
lock.readLock().unlock(); // 解除读锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

写写互斥

由于写会改变数据内容,因此需要进行同步处理,这里可以获得一个排他锁。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package service;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Service {

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

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();
}
}

}

结果:

1
2
获得写锁A1414899878671
获得写锁B1414899888671

读写互斥、写读互斥

实际上只要涉及到写操作,即修改数据,那么就必定需要排他锁。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

package service;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Service {

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();
}
}

}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;

import service.Service;
import extthread.ThreadA;
import extthread.ThreadB;

public class Run {
public static void main(String[] args) throws InterruptedException {

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();

}

}

结果:

1
2
获得读锁1414899977328
获得写锁1414899987328

总结

ReentrantLock与synchoronized的相同点

  • ReentrantLock与synchoronized都默认是非公平锁。
  • ReentrantLock与synchoronized都默认是可重入锁。
  • Object类中的wait()方法相当于Condition类中的await()方法。
  • Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。
  • Object类中的notify()方法相当于Condition类中的signal()方法。
  • Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

ReentrantLock与synchoronized的不同点

  • synchronized是关键字属于JVM层面,Lock是具体类是api层面的锁。

  • synchronized是重量级锁,重量级锁需要将线程从内核态和用户态来回切换。而ReentrantLock是轻量级锁。

  • synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。需要lock,unlock配合try finally完成。

  • Condition支持不响应中断,而Object不支持,也就是Object只要有中断就要响应。

  • Condition支持多个等待队列(new多个Condition对象),而Object只有一个等待队列,但两者都只要一个同步队列;即可以实现分组唤醒线程。

  • Condition支持截止时间设置,而Object是超时时间设置,支持截止时间设置,不用计算需要等多久。

ReentrantLock缺点

  • lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。

  • 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象,不方便调试。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :