标签:word 自动 在线 string 重入 运行时 tag 那是 line
今天我们继续学习并发。在之前我们学习了 JMM 的知识,知道了在并发编程中,为了保证线程的安全性,需要保证线程的原子性,可见性,有序性。其中,synchronized 高频出现,因为他既保证了原子性,也保证了可见性和有序性。为什么,因为 synchronized 是锁。通过锁,可以让原本并行的任务变成串行。然而如你所见,这也导致了严重的性能受损。因此,不到万不得已,不要使用锁,特别是吞吐量要求特别高的 WEB 服务器。如果锁住,性能将呈几何级下降。
但我们仍然需要锁,在某些操作共享变量的时刻,仍然需要锁来保证数据的准确性。而Java 世界有 3 把锁,今天我们主要说说这 3 把锁的用法。
synchronized 可以说是我们学习并发的时候第一个学习的关键字,该关键字粗鲁有效,通常是初级程序员最爱使用的,也因此会经常导致一些性能损失和死锁问题。
下面是 synchronized 的 3 个用法:
void resource1() {
synchronized ("resource1") {
System.out.println("作用在同步块中");
}
}
synchronized void resource3() {
System.out.println("作用在实例方法上");
}
static synchronized void resource2() {
System.out.println("作用在静态方法上");
}
整理以下这个关键字的用法:
synchronized 在发生异常的时候会释放锁,这点需要注意一下。
synchronized 修饰的代码在生产字节码的时候会有 monitorenter 和 monitorexit 指令,而这两个指令在底层调用了虚拟机8大指令中其中两个指令-----lock 和 unlock。
synchronized 虽然万能,但是还是有很多局限性,比如使用它经常会发生死锁,且无法处理,所以 Java 在 1.5版本的时候,加入了另一个锁 Lock 接口。我们看看该接口下的有什么。
JDK 在 1.5 版本新增了java.util.concurrent 包,有并发大师 Doug Lea 编写,其中代码鬼斧神工。值得我们好好学习,包括今天说的 Lock。
Lock 接口
/**
* @since 1.5
* @author Doug Lea
*/
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
void lock(); 获得锁
void lockInterruptibly() ;
boolean tryLock(); 尝试获取锁,如果获取不到,立刻返回false。
boolean tryLock(long time, TimeUnit unit) 在
void unlock(); 在给定的时间里等待锁,超过时间则自动放弃
Condition newCondition(); 获取一个重入锁的好搭档,搭配重入锁使用
上面说了Lock的机构抽象方法,那么 Lock 的实现是什么呢?标准实现了 ReentrantLock, ReadWriteLock。也就是我们今天讲的重入锁和读写锁。我们先讲重入锁。
先来一个简单的例子:
package cn.think.in.java.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockText implements Runnable {
/**
* Re - entrant - Lock
* 重入锁,表示在单个线程内,这个锁可以反复进入,也就是说,一个线程可以连续两次获得同一把锁。
* 如果你不允许重入,将导致死锁。注意,lock 和 unlock 次数一定要相同,如果不同,就会导致死锁和监视器异常。
*
* synchronized 只有2种情况:1继续执行,2保持等待。
*/
static Lock lock = new ReentrantLock();
static int i;
public static void main(String[] args) throws InterruptedException {
LockText lockText = new LockText();
Thread t1 = new Thread(lockText);
Thread t2 = new Thread(lockText);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
lock.lock();
try {
i++;
} finally {
// 因为lock 如果发生了异常,是不会释放锁的,所以必须在 finally 块中释放锁
// synchronized 发生异常会主动释放锁
lock.unlock();
}
}
}
}
在上面的代码中,我们使用了try 块中保护了临界资源 i 的操作。可以看到, 重入锁不管是开启锁还是释放锁都是显示的,其中需要注意的一点是,重入锁运行时如果发生了异常,不会像 synchronized 释放锁,因此需要在 finally 中释放锁。否则将产生死锁。
什么是重入锁?锁就是锁呗,为什么叫重入锁?之所以这么叫,那是因为这种锁是可以反复进入的(一个线程),大家看看下面的代码:
lock.lock();
lock.lock();
tyr{
i++;
} finally{
lock.unlock();
lock.unlock();
}
在这种情况下,一个线程连续两次获得两把锁,这是允许的。如果不允许这么操作,那么同一个线程咋i第二次获得锁是,将会和自己产生死锁。当然,需要注意的是,如果你多次获得了锁,那么也要相同的释放多次,如果释放锁的次数多了,就会得到一个 IllegalMonitorStateException 异常,反之,如果释放锁的次数少了,那么相当于这个线程还没有释放锁,其他线程也就无法进入临界区。
重入锁能够实现 synchronized 的所有功能,而且功能更为强大,我们看看有哪些功能。
中断响应
对于 synchronized 来说,如果一个线程在等待锁,那么结果只有2种,要么他获得这把锁继续运行,要么他就保持等待。没有第三种可能,那如果我有一个需求:需要线程在等待的时候中断线程,synchronizded 是做不到的。而重入锁可以做到,就是 lockInterruptibly 方法,该方法可以获取锁,并且在获取锁的过程种支持线程中断,也就是说,如果调用了线程中断方法,那么就会抛出异常。相对于 lock 方法,是不是更为强大?还是写个例子吧:
package cn.think.in.java.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock(重入锁)
*
* Condition(条件)
*
* ReadWriteLock(读写锁)
*/
public class IntLock implements Runnable {
/**
* 默认是不公平的锁,设置为 true 为公平锁
*
* 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程;
* 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢)
* 还要注意的是,未定时的 tryLock 方法并没有使用公平设置
*
* 不公平:此锁将无法保证任何特定访问顺序
*
* 拾遗:1 该类的序列化与内置锁的行为方式相同:一个反序列化的锁处于解除锁定状态,不管它被序列化时的状态是怎样的。
* 2.此锁最多支持同一个线程发起的 2147483648 个递归锁。试图超过此限制会导致由锁方法抛出的 Error。
*/
static ReentrantLock lock1 = new ReentrantLock(true);
static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,方便制造死锁
* @param lock
*/
public IntLock(int lock) {
this.lock = lock;
}
/**
* lockInterruptibly 方法: 获得锁,但优先响应中断
* tryLock 尝试获得锁,不等待
* tryLock(long time , TimeUnit unit) 尝试获得锁,等待给定的时间
*/
@Override
public void run() {
try {
if (lock == 1) {
// 如果当前线程未被中断,则获取锁。
lock1.lockInterruptibly();// 即在等待锁的过程中,可以响应中断。
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 试图获取 lock 2 的锁
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 该线程在企图获取 lock1 的时候,会死锁,但被调用了 thread.interrupt 方法,导致中断。中断会放弃锁。
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
// 查询当前线程是否保持此锁。
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + ": 线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
/**
* 这部分代码主要是针对 lockInterruptibly 方法,该方法在线程发生死锁的时候可以中断线程。让线程放弃锁。
* 而 synchronized 是没有这个功能的, 他要么获得锁继续执行,要么继续等待锁。
*/
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
// 中断其中一个线程(只有线程在等待锁的过程中才有效)
// 如果线程已经拿到了锁,中断是不起任何作用的。
// 注意:这点 synchronized 是不能实现此功能的,synchronized 在等待过程中无法中断
t2.interrupt();
// t2 线程中断,抛出异常,并放开锁。没有完成任务
// t1 顺利完成任务。
}
}
在上面的代码种,我们分别启动两个线程,制造了一个死锁,如果是 synchronized 是无法解除这个死锁的,这个时候重入锁的威力就出来了,我们调用线程的 interrupt 方法,中断线程,我们说,这个方法在线程 sleep,join ,wait 的时候,都会导致异常,这里也一羊,由于我们使用的 lock 的 lockInterruptibly 方法,该方法就像我们刚说的那样,在等待锁的时候,如果线程被中断了,就会出现异常,同时调用了 finally 种的 unlock 方法,注意,我们在 finally 中用 isHeldByCurrentThread 判断当前线程是否持有此锁,这是一种预防措施,放置线程没有持有此锁,导致出现 monitorState 异常。
锁申请
除了等待通知之外,避免死锁还有另一种方法,就是超时等待,如果超过这个时间,线程就放弃获取这把锁,这点 ,synchronized 也是不支持的。那么,如何使用呢?
package cn.think.in.java.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLock implements Runnable {
static ReentrantLock lock = new ReentrantLock(false);
@Override
public void run() {
try {
// 最多等待5秒,超过5秒返回false,若获得锁,则返回true
if (lock.tryLock(5, TimeUnit.SECONDS)) {
// 锁住 6 秒,让下一个线程无法获取锁
System.out.println("锁住 6 秒,让下一个线程无法获取锁");
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
上面的代码中,我们设置锁的等待时间是5秒,但是在同步块中,我们设置了6秒暂停,锁外面的线程等待了5面发现还是不能获取锁,就会放弃。走 else 逻辑,结束执行,注意,这里,我们在 finally 块中依然做了判断,如果不做判断,就会出现 IllegalMonitorStateException 异常。
当然了,tryLock 方法也可以不带时间参数,如果获取不到锁,立刻返回false,否则返回 true。该方法也是应对死锁的一个好办法。我们还是写个例子:
package cn.think.in.java.lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLock implements Runnable {
static ReentrantLock lock1 = new ReentrantLock();
static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
// 线程1
if (lock == 1) {
while (true) {
// 获取1的锁
if (lock1.tryLock()) {
try {
// 尝试获取2的锁
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + " : My Job done");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
// 线程2
while (true) {
// 获取2的锁
if (lock2.tryLock()) {
try {
// 尝试获取1的锁
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ": My Job done");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
/**
* 这段代码如果使用 synchronized 肯定会引起死锁,但是由于使用 tryLock,他会不断的尝试, 当第一次失败了,他会放弃,然后执行完毕,并释放外层的锁,这个时候就是
* 另一个线程抢锁的好时机。
* @param args
*/
public static void main(String[] args) {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
这段代码如果使用 synchronized 肯定会引起死锁,但是由于使用 tryLock,他会不断的尝试, 当第一次失败了,他会放弃,然后执行完毕,并释放外层的锁,这个时候就是另一个线程抢锁的好时机。
公平锁和非公平锁
大多数情况下,为了效率,锁都是不公平的。系统在选择锁的时候都是随机的,不会按照某种顺序,比如时间顺序,公平锁的一大特点:他不会产生饥饿现象。只要你排队 ,最终还是可以得到资源的。如果我们使用 synchronized ,得到的锁就是不公平的。因此,这也是重入锁比 synchronized 强大的一个优势。我们同样写个例子:
package cn.think.in.java.lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLock implements Runnable {
// 公平锁和非公平锁的结果完全不同
/*
* 10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
10 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
9 获得锁
======================下面是公平锁,上面是非公平锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得锁
9 获得锁
10 获得
*
* */
static ReentrantLock unFairLock = new ReentrantLock(false);
static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getId() + " 获得锁");
} finally {
fairLock.unlock();
}
}
}
/**
* 默认是不公平的锁,设置为 true 为公平锁
*
* 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程;
* 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢)
* 还要注意的是,未定时的 tryLock 方法并没有使用公平设置
*
* 不公平:此锁将无法保证任何特定访问顺序,但是效率很高
*
*/
public static void main(String[] args) {
FairLock fairLock = new FairLock();
Thread t1 = new Thread(fairLock, "cxs - t1");
Thread t2 = new Thread(fairLock, "cxs - t2");
t1.start();
t2.start();
}
}
重入锁的构造函数有一个 boolean 参数,ture 表示公平,false 表示不公平,默认是不公平的,公平锁会降低性能。代码中由运行结果,可以看到,公平锁的打印顺序是完全交替运行,而不公平锁的顺序完全是随机的。注意:如果没有特殊需求,请不要使用公平锁,会大大降低吞吐量。
到这里,我们总结一下重入锁相比 synchronized 有哪些优势:
当然,大家会说, synchronized 可以通过 Object 的 wait 方法和 notify 方法实现线程之间的通信,重入锁可以做到吗?楼主告诉大家,当然可以了! JDK 中的阻塞队列就是用重入锁加 他的搭档 condition 实现的。
重入锁的好搭档-----Condition
还记的刚开始说 Lock 接口有一个newCondition 方法吗,该方法就是获取 Condition 的。该 Condition 绑定了该锁。Condition 有哪些方法呢?我们看看:
public interface Condition {
void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
void awaitUninterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
看着是不是特别属性,Condition 为了不和 Object 类的 wait 方法冲突,使用 await 方法,而 signal 方法对应的就是 notify 方法。signalAll 方法对应的就是 notifyAll 方法。其中还有一些时间限制的 await 方法,和 Object 的 wait 方法的作用相同。注意,其中有一个 awaitUninterruptibly 方法,该方法从名字可以看出,并不会响应线程的中断,而 Object 的 wait 方法是会响应的。而 awaitUntil 方法就是等待到一个给定的绝对时间。除非调用了 signal 或者中断了。如何使用呢?来一段代码吧:
package cn.think.in.java.lock.condition;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 重入锁的好搭档
*
* await 使当前线程等待,同时释放当前锁,当其他线程中使用 signal 或者 signalAll 方法时,线程会重新获得锁并继续执行。
* 或者当线程被中断时,也能跳出等待,这和 Object.wait 方法很相似。
* awaitUninterruptibly() 方法与 await 方法基本相同,但是它并不会在等待过程中响应中断。
* singal() 该方法用于唤醒一个在等待中的线程,相对的 singalAll 方法会唤醒所有在等待的线程,这和 Object.notify 方法很类似。
*/
public class ConditionTest implements Runnable {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
// 该线程会释放 lock 的锁,也就是说,一个线程想调用 condition 的方法,必须先获取 lock 的锁。
// 否则就会像 object 的 wait 方法一样,监视器异常
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionTest t = new ConditionTest();
Thread t1 = new Thread(t);
t1.start();
Thread.sleep(1000);
// 通知 t1 继续执行
// main 线程必须获取 lock 的锁,才能调用 condition 的方法。否则就是监视器异常,这点和 object 的 wait 方法是一样的。
lock.lock(); // IllegalMonitorStateException
// 从 condition 的等待队列中,唤醒一个线程。
condition.signal();
lock.unlock();
}
}
可以说,condition 的使用方式和 Object 类的 wait 方法的使用方式很相似,无论在哪一个线程中调用 await 或者 signal 方法,都必须获取对应的锁,否则会出现 IllegalMonitorStateException 异常。
到这里,我们可以说, Condition 的实现比 Object 的 wait 和 notify 还是强一点,其中就包括了等待到指定的绝对时间,并且还有一个不受线程中断影响的 awaitUninterruptibly 方法。因此,我们说,只要允许,请使用重入锁,尽量不要使用无脑的 synchronized 。虽然在 JDK 1.6 后, synchronized 被优化了,但仍然建议使用 重入锁。
伟大的 Doug Lea 不仅仅创造了 重入锁,还创造了 读写锁。什么是读写锁呢?我们知道,线程不安全的原因来自于多线程对数据的修改,如果你不修改数据,根本不需要锁。我们完全可以将读写分离,提高性能,在读的时候不使用锁,在写的时候才加入锁。这就是 ReadWriteLock 的设计原理。
那么,如何使用呢?
package cn.think.in.java.lock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
static Lock lock = new ReentrantLock();
static ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
static Lock readLock = reentrantReadWriteLock.readLock();
static Lock writeLock = reentrantReadWriteLock.writeLock();
int value;
public Object handleRead(Lock lock) throws InterruptedException {
try {
lock.lock();
// 模拟读操作,读操作的耗时越多,读写锁的优势就越明显
Thread.sleep(1000);
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000); // 模拟写操作
value = index;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
demo.handleRead(readLock);
// demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
try {
demo.handleWrite(writeLock, new Random().nextInt());
// demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
/**
* 使用读写锁,这段程序只需要2秒左右
* 使用普通的锁,这段程序需要20秒左右。
*/
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
}
}
使用 ReentrantReadWriteLock 的 readLock()方法可以返回读锁,writeLock 可以返回写锁,我们使用普通的的重入锁和读写锁进行测试,怎么测试呢?
两个循环:一个循环开启18个线程去读数据,一个循环开启两个线程去写。如果使用普通的重入锁,将耗时20秒,因为普通的重入锁在读的时候依然是串行的。而如果使用读写锁,只需要2秒,也就是写的时候是串行的。读的时候是并行的,极大的提高了性能。
注意:只要涉及到写都是串行的。比如读写操作,写写操作,都是串行的,只有读读操作是并行的。
读写锁 ReadWriteLock 接口只有 2个方法:
Lock readLock(); 返回一个读锁 Lock writeLock(); 返回一个写锁
他的标准实现类是 ReentrantReadWriteLock 类,该类和普通重入锁一样,也能实现公平锁,中断响应,锁申请等特性。因为他们返回的读锁或者写锁都实现了 Lock 接口。
到这里,我们已经将 Java 世界的三把锁的使用弄清楚了,从分析的过程中我们知道了,JDK 1.5 的重入锁完全可以代替关键字 synchronized ,能实现很多 synchronized 没有的功能。比如中断响应,锁申请,公平锁等,而重入锁的搭档 Condition 也比 Object 的wait 和notify 强大,比如有设置绝对时间的等待,还有忽略线程中断的 await 方法,这些都是 synchronized 无法实现的。还有优化读性能的 读写锁,在读的时候完全是并行的,在某些场景下,比如读很多,写很少,性能将是几何级别的提升。
所以,以后,能不用 synchronzed 就不要用,用的不好就会导致死锁。
本站文转载/出处:http://thinkinjava.cn/article/36
标签:word 自动 在线 string 重入 运行时 tag 那是 line
原文地址:https://www.cnblogs.com/xianshiwang/p/9064497.html