Java 1.8 多线程之“锁”
jdk中提供的主要锁机制:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
- StampedLock
1.synchronized
这个老干部相信了解过多线程的小伙伴都知道他。他能锁对象、锁方法、锁代码段,放在不同位置可以锁定不同作用域下的代码段。再此不多说可自行百度。
2.ReentrantLock
重入锁为什么叫重入锁? 因为他有一个特性如果当前线程已经获得到了锁,那么接下来执行的锁定代码段中在遇到这个锁对象那么会直接获得锁并在自身记录当前线程获得了几次锁。只有当内部代码执行完释放锁,外部也执行完释放了锁之后相对于这个线程来说才会真正的失去锁。
public class Test3 {
public static void main(String[] args) {
User user = new User();
new Thread(() -> {
user.printUser();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.printUser();
}).start();
new Thread(() -> {
user.updateUser(1 + "");
}).start();
}
static class User {
ReentrantLock reentrantLock = new ReentrantLock();
private String name = "init";
private String loginName = "init";
private String password = "init";
/**
* 修改用户信息
*/
public void updateUser(String value) {
try {
// 第一次获得锁
reentrantLock.lock();
System.out.println("1 HoldCount:" + reentrantLock.getHoldCount());
name = value;
// 模仿一些耗时操作
for (int i = 0; i < 10000; i++) {
String ss = new String("1");
ss.indexOf('1');
Math.random();
}
loginName = name + "loginName";
// 第二次获得锁,重入锁
reentrantLock.lock();
System.out.println("2 HoldCount:" + reentrantLock.getHoldCount());
// 模仿一些耗时操作
for (int i = 0; i < 99999999; i++) {
String ss = new String("1");
ss.indexOf('1');
Math.random();
}
// 第三次获得锁,重入锁
reentrantLock.lock();
System.out.println("3 HoldCount:" + reentrantLock.getHoldCount());
password = name + "password";
// 释放最近获得的一次锁
reentrantLock.unlock();
System.out.println("UnHoldCount:" + reentrantLock.getHoldCount());
} catch (Exception e) {
e.printStackTrace();
} finally {
/**
* 必须在finally 检测是否所有锁都已经释放,否则可能导致死锁
*/
int count = reentrantLock.getHoldCount();
if (count > 0) {
for (int i = 0; i < count; i++) {
reentrantLock.unlock();
System.out.println("UnHoldCount:" + reentrantLock.getHoldCount());
}
}
}
}
/**
* 模仿消费信息的方法
*/
public void printUser() {
try {
reentrantLock.lock();
System.out.println("name: " + name + " loginName: " +
loginName + " password: " + password);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
}
以上是重入锁的一些小测试,大家可以试试如果将所有锁代码全部去掉。这段代码数据会脏读。 updateUser中获得了多次锁是为了演示重入锁的特性。
可以看出printUser先获得了锁打印了初始化的字段信息,然后updateUser获得了锁并依次取得了三次锁。代码中忘记了2次锁的释放在finally模块中得以释放所有锁(必须在finally中检测释放所有锁,因为代码即使没有忘记写释放锁也有可能代码段异常没有执行到锁释放代码,造成死锁)。
重入锁还有很多特性例如是否公平竞争、尝试获取锁、Condition等诸多特性可自行百度进行进一步学习。
3.ReentrantReadWriteLock
读写锁重入锁可能大家没听说过但是读写锁相信都听说过这个神奇的名号。重入锁的缺点是什么?
public void printUser() {
try {
reentrantLock.lock();
System.out.println("name: " + name +
" loginName: " + loginName + " password: " + password);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
对没错多线程并发读取时不会发生脏读问题,但是重入锁依旧是控制每个线程依次的获得锁在读取信息,大大的降低了执行效率。这个时候读写锁就发挥它的作用了。
public class Test {
public static void main(String[] args) {
User user = new User();
/**
* 模拟并发线程读取用信息
*/
new Thread(() -> {
while (true) {
user.printUser();
}
}).start();
new Thread(() -> {
while (true) {
user.printUser();
}
}).start();
/**
* 模拟并发修改信息
*/
new Thread(() -> {
user.updateUser(1 + "");
}).start();
}
static class User {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String name = "init";
private String loginName = "init";
private String password = "init";
/**
* 修改用户信息
*/
public void updateUser(String value) {
/**
* 再此处获得了写锁那么读取所将无法被任何人获取。
*/
try {
readWriteLock.writeLock().lock();
name = value;
// 模仿一些耗时操作
for (int i = 0; i < 10000; i++) {
String ss = new String("1");
ss.indexOf('1');
Math.random();
}
loginName = name + "loginName";
// 模仿一些耗时操作
for (int i = 0; i < 10000; i++) {
String ss = new String("1");
ss.indexOf('1');
Math.random();
}
password = name + "password";
readWriteLock.writeLock().unlock();
} catch (Exception e) {
e.printStackTrace();
} finally {
/**
* 必须在finally 代码段里检测锁的释放没有释放必须再次释放,防止死锁。
*/
int holdCount = readWriteLock.writeLock().getHoldCount();
if (holdCount > 0) {
for (int i = 0; i < holdCount; i++) {
readWriteLock.writeLock().unlock();
}
}
}
}
/**
* 模仿消费信息的方法
*/
public void printUser() {
/**
* 在没有人获得写锁之前,任何并发线程都可自由读取用户信息。锁相当于不存在
*/
readWriteLock.readLock().lock();
System.out.println("name: " + name + " loginName: " + loginName
+ " password: " + password + "thread Name:"
+ Thread.currentThread().getName());
readWriteLock.readLock().unlock();
}
}
}
可以看到在写线程没有得到写锁之前所有线程都可以并发读取用户信息,在写线程获得了锁之后开始消耗长时间写入用户信息。但是这个时候并没有发生脏读,好似读取就此停止了。是因为读写锁发挥了作用。
4.StampedLock
StampedLock是jdk1.8提供的新的锁机制。
以下摘自 https://my.oschina.net/benhaile/blog/264383
ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。
然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。
StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!
static class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); // 获得一个乐观读锁
double currentX = x, currentY = y; // 将两个字段读入本地局部变量
if (!sl.validate(stamp)) { // 如果没有人获得了读锁或者写锁那么返回True
stamp = sl.readLock(); // 如果有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { // 循环,检查当前状态是否符合
long ws = sl.tryConvertToWriteLock(stamp); // 将读锁转为写锁
if (ws != 0L) { // 这是确认转为写锁是否成功
stamp = ws; // 如果成功 替换票据
x = newX; // 进行状态改变
y = newY; // 进行状态改变
break;
} else { // 如果不能成功转换为写锁
sl.unlockRead(stamp); // 我们显式释放读锁
stamp = sl.writeLock(); // 显式直接进行写锁 然后再通过循环再试
}
}
} finally {
sl.unlock(stamp); // 释放读锁或写锁
}
}
}
以上是JDK提供的主要锁机制。
5.Condition
并发包还提供了条件控制,替代传统的synchronized wait notify。
public class Test {
public static void main(String[] args) {
User user = new User();
new Thread(() -> {
while (true) {
user.fillData();
}
}).start();
new Thread(() -> {
while (true) {
user.getData();
}
}).start();
}
static class User {
ReentrantLock reentrantLock = new ReentrantLock();
Condition w = reentrantLock.newCondition();
Condition r = reentrantLock.newCondition();
int windex = 0;
int rindex = 21;
int[] store = new int[20];
public void fillData() {
try {
reentrantLock.lock();
// 一直向其中填充数据
while (true) {
if (windex >= store.length) {
System.out.println("fill end");
rindex = 0;
// 唤醒所有读取线程
r.signalAll();
// 释放锁,让其他线程获取锁
w.await();
}
store[windex] = windex;
windex++;
System.err.print("pi:" + windex);
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void getData() {
try {
reentrantLock.lock();
// 一直向其中填充数据
while (true) {
rindex++;
if (rindex >= store.length) {
System.out.println();
System.out.println("get end");
windex = 0;
// 唤醒所有写入线程
w.signalAll();
// 释放锁,让其他线程获取锁
r.await();
}
System.out.print(store[rindex]);
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
}
从结果可以看到,生产线程和消费线程通过Condition协调可以有序的进行生产和消费信息,我们还发现下了区别于“synchronized wait notify”,一个锁可以创建多个条件。即一个锁拥有多个条件可以控制更多线程。