0%

Java多线程2: Lock、信号量、原子量与队列

<<Java高级软件工程师知识结构

Java多线程是Java基础的重要的一部分,支持多线程是Java的重要特性之一. 主要包括如下内容:
  1. Java多线程1: 线程生命周期和多线程基础
  2. Java多线程2: Lock、信号量、原子量与队列
  3. Java多线程3: volatile
  4. Java多线程4: 同步锁与Java线程同步方法比较
  5. Java多线程5: 线程池
  6. Java多线程6: Java阻塞队列与生产者消费者模式

更详细内容请参考博文 Java并发编程:Lock

Sun在Java5中, 对Java线程的类库做了大量的扩展

java.util.concurrent.locks 包下面, 里面有三个重要的接口ConditionLockReadWriteLock.

Condition

Condition 将 Object 监视器方法(waitnotifynotifyAll)分解成截然不同的对象, 以便通过将这些对象与任意 Lock 实现组合使用, 为每个对象提供多个等待 setwait-set).

Lock

Lock实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作.

ReadWriteLock

ReadWriteLock维护了一对相关的锁定, 一个用于只读操作, 另一个用于写入操作.

Lock 与 synchronized的区别

见文献 深入研究 Java Synchronize 和 Lock 的区别与用法

  1. 用法上的区别
    synchronized可以加在方法或代码块上,而Lock必须指定起始位置,一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()unlock()显式指出。所以一般会在finally块中写unlock()以防死锁。

    1. ReentrantLock非阻塞
    2. ReentrantLock CAS实现无锁
    3. 可以指定等待时间,可以中断
    4. 公平锁:按照申请顺序
    5. 可以指定条件
    6. await必须与while一起使用
    7. Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的
    8. synchronized在发生异常时,会自动释放线程占有的锁,不会导致死锁现象发生;
      而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  2. 性能上的区别
    synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。synchronized是悲观锁,线程获取到的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。多个线程竞争资源,CPU频繁切换效率变低。

    Lock使用的是乐观锁,乐观锁就是CAS,调用CPU的指令,效率比较高。是非阻塞算法。 每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。获得锁的一个方法是compareAndSetState. CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

  3. 用途上的区别
    高并发情况下,比较适合使用Lock,特别是在下面的情况下:

    • 某个线程在等待一个锁的控制权的这段时间需要中断
    • 需要分开处理一些wait-notifyReentrantLock里面的Condition应用,能够控制notify哪个线程
    • 具有公平锁功能,每个到来的线程都将排队等候

ReentrantLock

ReentrantLock是唯一实现了Lock接口的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建并发访问的账户
MyCount myCount = new MyCount("95599200901215522", 10000);
//创建一个锁对象
Lock lock = new ReentrantLock();
//创建一个线程池
ExecutorService pool = Executors.newCachedThreadPool();
//创建一些并发访问用户, 一个信用卡, 存的存, 取的取, 好热闹啊
User u1 = new User("张三", myCount, -4000, lock);
User u2 = new User("张三他爹", myCount, 6000, lock);
User u3 = new User("张三他弟", myCount, -8000, lock);
User u4 = new User("张三", myCount, 800, lock);
//在线程池中执行各个用户的操作
pool.execute(u1);
pool.execute(u2);
pool.execute(u3);
pool.execute(u4);
//关闭线程池
pool.shutdown();
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
/**
* 信用卡的用户
*/
class User implements Runnable {
private String name; //用户名
private MyCount myCount; //所要操作的账户
private int iocash; //操作的金额, 当然有正负之分了
private Lock myLock; //执行操作所需的锁对象
User(String name, MyCount myCount, int iocash, Lock myLock) {
this.name = name;
this.myCount = myCount;
this.iocash = iocash;
this.myLock = myLock;
}
public void run() {
//获取锁
myLock.lock();
//执行现金业务
System.out.println(name + "正在操作" + myCount + "账户, 金额为" + iocash + ", 当前金额为" + myCount.getCash());
myCount.setCash(myCount.getCash() + iocash);
System.out.println(name + "操作" + myCount + "账户成功, 金额为" + iocash + ", 当前金额为" + myCount.getCash());
//释放锁, 否则别的线程没有机会执行了
myLock.unlock();
}
}
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
/**
* 信用卡账户, 可随意透支
*/
class MyCount {
private String oid; //账号
private int cash; //账户余额
MyCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public int getCash() {
return cash;
}
public void setCash(int cash) {
this.cash = cash;
}
@Override
public String toString() {
return "MyCount{" +
"oid='" + oid + '\'' +
", cash=" + cash +
'}';
}
}

线程的中断

Java线程中的中断,提供了三个方法:

  • interrupt()
    通知线程中断, 但是线程不会立即中断, 只会将interrupt标志位设置为 true, 当在线程中获取到这个标志位时自行判断
  • interrupted()
    判断是否中断, 如果未中断, 立即设置为中断
  • isInterrupted()
    判断是否中断

这三个方法都不能使线程中断执行。而ReetrantLock提供了响应的方案。

ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。

比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,

  • 第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);Lock.lock()也不会响应中断操作
  • 第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
    在Thread类中使用lock.lockInterruptibly();锁定时可以响应中断操作,当调用Thread.interupt()方法,该lock会放弃锁定,抛出InterruptedException异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
volatile boolean isProcess = false;
ReentrantLock lock = new ReentrantLock();
Condition processReady = lock.newCondition();
thread: run() {
lock.lock();
isProcess = true;
try {
while(!isProcessReady) {
//isProcessReady 是另外一个线程的控制变量
processReady.await();
//释放了lock,在此等待signal
}catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
isProcess = false;
}
}
}
}
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
41
42
43
44
45
46
47
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly();
//注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
//插入数据
}
}finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}

读写锁 ReadWriteLock

为了提高性能, Java提供了读写锁, 在读的地方使用读锁, 在写的地方使用写锁, 灵活控制, 在一定程度上提高了程序的执行效率.
Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock, 也有具体的实现ReentrantReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建并发访问的账户
MyCount myCount = new MyCount("95599200901215522", 10000);
//创建一个锁对象
ReadWriteLock lock = new ReentrantReadWriteLock(false);
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建一些并发访问用户, 一个信用卡, 存的存, 取的取, 好热闹啊
User u1 = new User("张三", myCount, -4000, lock, false);
User u2 = new User("张三他爹", myCount, 6000, lock, false);
User u3 = new User("张三他弟", myCount, -8000, lock, false);
User u4 = new User("张三", myCount, 800, lock, false);
User u5 = new User("张三他爹", myCount, 0, lock, true);
//在线程池中执行各个用户的操作
pool.execute(u1);
pool.execute(u2);
pool.execute(u3);
pool.execute(u4);
pool.execute(u5);
//关闭线程池
pool.shutdown();
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 信用卡的用户
*/
class User implements Runnable {
private String name; //用户名
private MyCount myCount; //所要操作的账户
private int iocash; //操作的金额, 当然有正负之分了
private ReadWriteLock myLock; //执行操作所需的锁对象
private boolean ischeck; //是否查询
User(String name, MyCount myCount, int iocash, ReadWriteLock myLock, boolean ischeck) {
this.name = name;
this.myCount = myCount;
this.iocash = iocash;
this.myLock = myLock;
this.ischeck = ischeck;
}
public void run() {
if (ischeck) {
//获取读锁
myLock.readLock().lock();
System.out.println("读:" + name + "正在查询" + myCount + "账户, 当前金额为" + myCount.getCash());
//释放读锁
myLock.readLock().unlock();
} else {
//获取写锁
myLock.writeLock().lock();
//执行现金业务
System.out.println("写:" + name + "正在操作" + myCount + "账户, 金额为" + iocash + ", 当前金额为" + myCount.getCash());
myCount.setCash(myCount.getCash() + iocash);
System.out.println("写:" + name + "操作" + myCount + "账户成功, 金额为" + iocash + ", 当前金额为" + myCount.getCash());
//释放写锁
myLock.writeLock().unlock();
}
}
}

/**
* 信用卡账户, 可随意透支
*/
class MyCount {
private String oid; //账号
private int cash; //账户余额
MyCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public int getCash() {
return cash;
}
public void setCash(int cash) {
this.cash = cash;
}
@Override
public String toString() {
return "MyCount{" +
"oid='" + oid + '\'' +
", cash=" + cash +
'}';
}
}

条件变量 Condition

条件变量就是表示条件的一种变量. 但是必须说明, 这里的条件是没有实际含义的, 仅仅是个标记而已, 并且条件的含义往往通过代码来赋予其含义.

条件变量都实现了java.util.concurrent.locks.Condition接口, 条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的, 这样, 条件就和一个锁对象绑定起来了. 因此, Java中的条件变量只能和锁配合使用, 来控制并发程序访问竞争资源的安全.

在Java5中, 一个锁可以有多个条件, 每个条件上可以有多个线程等待, 通过调用await()方法, 可以让线程在该条件下等待. 当调用signalAll()方法, 又可以唤醒该条件下的等待的线程.

实例

有一个账户, 多个用户(线程)在同时操作这个账户, 有的存款有的取款, 存款随便存, 取款有限制, 不能透支, 任何试图透支的操作都将等待里面有足够存款才执行操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建并发访问的账户
MyCount myCount = new MyCount("95599200901215522", 10000);
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
Thread t1 = new SaveThread("张三", myCount, 2000);
Thread t2 = new SaveThread("李四", myCount, 3600);
Thread t3 = new DrawThread("王五", myCount, 2700);
Thread t4 = new SaveThread("老张", myCount, 600);
Thread t5 = new DrawThread("老牛", myCount, 1300);
Thread t6 = new DrawThread("胖子", myCount, 800);
//执行各个线程
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
//关闭线程池
pool.shutdown();
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 存款线程类
*/
class SaveThread extends Thread {
private String name; //操作人
private MyCount myCount; //账户
private int x; //存款金额
SaveThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() {
myCount.saving(x, name);
}
}

/**
* 取款线程类
*/
class DrawThread extends Thread {
private String name; //操作人
private MyCount myCount; //账户
private int x; //存款金额
DrawThread(String name, MyCount myCount, int x) {
this.name = name;
this.myCount = myCount;
this.x = x;
}
public void run() {
myCount.drawing(x, name);
}
}
/**
* 普通银行账户, 不可透支
*/
class MyCount {
private String oid; //账号
private int cash; //账户余额
private Lock lock = new ReentrantLock(); //账户锁
private Condition _save = lock.newCondition(); //存款条件
private Condition _draw = lock.newCondition(); //取款条件
MyCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
/**
* 存款
*
* @param x 操作金额
* @param name 操作人
*/
public void saving(int x, String name) {
lock.lock(); //获取锁
if (x > 0) {
cash += x; //存款
System.out.println(name + "存款" + x + ", 当前余额为" + cash);
}
_draw.signalAll(); //唤醒所有等待线程.
lock.unlock(); //释放锁
}
/**
* 取款
*
* @param x 操作金额
* @param name 操作人
*/
public void drawing(int x, String name) {
lock.lock(); //获取锁
try {
if (cash - x < 0) {
_draw.await(); //阻塞取款操作
} else {
cash -= x; //取款
System.out.println(name + "取款" + x + ", 当前余额为" + cash);
}
_save.signalAll(); //唤醒所有存款操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
}

CountDownLatch

参考来源:未引入样例

一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行

在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作。 这个时候就可以使用CountDownLatch。CountDownLatch最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。

主要方法

countDown

1
2
3
4
5
6
public void countDown()

递减锁存器的计数,如果计数到达零,则释放所有等待的线程。如果当前计数大于零,则将计数减少。
如果新的计数为零,出于线程调度目的,将重新启用所有的等待线程。

如果当前计数等于零,则不发生任何操作。

await

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
public boolean await(long timeout,
TimeUnit unit)
throws InterruptedException

使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
如果当前计数为零,则此方法立刻返回 true 值。
如果当前计数大于零,则出于线程调度目的,将禁用当前线程,
且在发生以下三种情况之一前,该线程将一直处于休眠状态:

由于调用 countDown() 方法,计数到达零;或者其他某个线程中断当前线程;
或者已超出指定的等待时间。

如果计数到达零,则该方法返回 true 值。

如果当前线程:

在进入此方法时已经设置了该线程的中断状态;或者在等待时被中断,
则抛出 InterruptedException,并且清除当前线程的已中断状态。
如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于零,则此方法根本不会等待。

参数:
timeout - 要等待的最长时间
unit - timeout 参数的时间单位。
返回:
如果计数到达零,则返回 true;如果在计数到达零之前超过了等待时间,则返回 false
抛出:
InterruptedException - 如果当前线程在等待时被中断

在控制访问量上的应用

信号量 Semaphore

Java的信号量实际上是一个功能完备的计数器, 对控制一定资源的消费与回收有着很重要的意义, 信号量常常用于多线程的代码中, 并能监控有多少数目的线程等待获取资源, 并且通过信号量可以得知可用资源的数目等等, 这里总是在强调“数目”二字, 但不能指出来有哪些在等待, 哪些资源可用.

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class Test {
public static void main(String[] args) {
MyPool myPool = new MyPool(20);
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
MyThread t1 = new MyThread("任务A", myPool, 3);
MyThread t2 = new MyThread("任务B", myPool, 12);
MyThread t3 = new MyThread("任务C", myPool, 7);
//在线程池中执行任务
threadPool.execute(t1);
threadPool.execute(t2);
threadPool.execute(t3);
//关闭池
threadPool.shutdown();
}
}

/**
* 一个池
*/
class MyPool {
private Semaphore sp; //池相关的信号量
/**
* 池的大小, 这个大小会传递给信号量
* @param size 池的大小
*/
MyPool(int size) {
this.sp = new Semaphore(size);
}
public Semaphore getSp() {
return sp;
}
public void setSp(Semaphore sp) {
this.sp = sp;
}
}

class MyThread extends Thread {
private String threadname; //线程的名称
private MyPool pool; //自定义池
private int x; //申请信号量的大小
MyThread(String threadname, MyPool pool, int x) {
this.threadname = threadname;
this.pool = pool;
this.x = x;
}
public void run() {
try {
//从此信号量获取给定数目的许可
pool.getSp().acquire(x);
//TODO 也许这里可以做更复杂的业务
System.out.println(threadname + "成功获取了" + x + "个许可!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放给定数目的许可, 将其返回到信号量.
pool.getSp().release(x);
System.out.println(threadname + "释放了" + x + "个许可!");
}
}
}

CyclicBarrier

原子量

所谓的原子量即操作变量的操作是“原子的”, 该操作不可再分, 因此是线程安全的.
为何要使用原子变量呢, 原因是多个线程对单个变量操作也会引起一些问题.
Java5之后, 专门提供了用来进行单变量多线程并发安全访问的工具包java.util.concurrent.atomic, 其中的类也很简单.

1
2
3
4
5
//原子量, 每个线程都可以自由操作
private static AtomicLong aLong = new AtomicLong(10000);
lock.lock();
System.out.println(name + "执行了" + x + ", 当前余额:" + aLong.addAndGet(x));
lock.unlock();

可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,
可重入性 实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,
此时线程不必重新去申请锁,而是可以直接执行方法method2。

看下面这段代码就明白了:

1
2
3
4
5
6
7
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}

两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,
而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,
因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,
可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()的用法时已经体现了Lock的可中断性。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

看一下这2个类的源代码就清楚了:

在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

1
ReentrantLock lock = new ReentrantLock(true);

如果参数为true表示为公平锁,为false为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

另外在ReentrantLock类中定义了很多方法,比如:

1
2
3
4
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁

在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。

减少锁带来的开销

使用Java API中的并发类库

可以采用java.util.concurrent等包下面的并发类,通常它们已经经过了充分的优化,能有效地支持高并发环境下的操作,并发类中大量采用了非阻塞算法,有些利用了CAS实现无锁。这里有一个小提示:使用并发哈希表时应优先采用ConcurrentHashMap而不是Hashtable,前者通过分解锁的方法使得效率更高。

用CAS代替锁

和第一点中提到的一样,CAS可以减小锁的开销,但是CAS本身是基于轮询的操作,实际使用中反而可能增加开销,这一点需要实验来测试。

减小锁的粒度

尽可能缩小锁定的范围,可以从两个方面入手。第一是只锁定所需的对象,少用synchronized(this)。第二是尽可能缩小锁定的方法块,缩小临界区大小,避免将不必要的操作也归入临界区。

拆分锁

将普通的对象锁、互斥锁按照场景拆分为读写锁或像ConcurrentHashMap一样拆分为若干把锁。

利用写时复制

对于读操作次数远远大于写操作的场景,可以在读操作时不加锁,写操作时利用写时复制来完成,但是内存占用会相应上升。

其次,系统中的线程数最好不是固定的,而是按CPU数来计算,这样当CPU增加时,相应的系统会自动增加线程提高并发率。
最后,如果对系统的CPU使用率还不满意,应当考虑分解一些单线程任务,改为多线程并发执行,以提高效率。
增加内存时,需要进行如下调整:
首先类似于线程数按CPU数计算,将Cache大小按内存大小计算,扩展内存后,Cache可以自动增长大小。
其次,可以分配更大的JVM堆内存给虚拟机,能减少OOM发生的几率。


【参考文献】: