Synchronized
synchronized 的 3 种用法:
指定加锁对象(代码块):对给定对象加锁,进入同步代码前要获得给定对象的锁。
void resource1() {
synchronized ("resource1") {
System.out.println("作用在同步块中");
}
}
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
synchronized void resource3() {
System.out.println("作用在实例方法上");
}
直接作用于静态方法:相当于对当前类加锁,进入同步代码块前要获得当前类的锁。
static synchronized void resource2() {
System.out.println("作用在静态方法上");
}
synchronized 在发生异常的时候会释放锁,这点需要注意一下。
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();
}
lock():用来获取锁,若锁已被其他线程获取,则需等待
Lock()方法必须主动去释放锁,并且在发生异常时也不会自动释放锁。
使用Lock必须在try…catch…块中进行:
- 释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生
- 获取锁的操作应该方法try的外面,防止误释放锁。
(如果说在获取锁时发生了异常,那么肯定也会走 finally 代码块,执行lock.unlock();去释放锁,可问题是我还没获取到锁啊!!!)
在 try-finally 外加锁的话,如果因为发生异常导致加锁失败,try-finally 块中的代码不会执行。
相反,如果在 try{ } 代码块中加锁失败,finally 中的代码无论如何都会执行,但是由于当前线程加锁失败并没有持有 lock 对象锁,所以程序会抛出异常。
链接:
https://blog.csdn.net/u013568373/article/details/98480603?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
unlock():解锁
tryLock():尝试获取锁,若获取不到,立刻返回false;这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit):在给定的时间里等待锁,超过时间则自动放弃,返回false;在等待期间内拿到了锁,返回true。
lockInterruptibly():
获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的;
而用synchronized修饰的话,synchronized 只有2种情况:1继续执行,2保持等待。
public void method(Lock lock) throws InterruptedException {
lock.lockInterruptibly();
try {
//执行代码
} catch (Exception e) {
// 异常处理
} finally {
lock.unlock();
}
}
一般将lockTry.lockInterruptibly();写在了try{}catch{}之外,原因同上。
Lock 的标准实现是重入锁 ReentrantLock, 和读写锁 ReadWriteLock。
ReentrantLock 重入锁
可重入性:
重入锁ReentrantLock,是支持重进入的锁,该锁能够支持一个线程对资源的重复加锁。
先来一个简单的例子:
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();
}
}
}
}
PS:
Synchronized 也支持重进入,但只支持隐式的重进入。
public synchronized void test1() {
value = value + 1;
test2();
}
public synchronized void test2() {
value = value + 1;
}
ReentrantLock与synchronized隐式获取和释放锁相比,它缺少了便捷性,但却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
Lock的使用:
Lock lock = new ReentrantLock();
lock.lock();
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
lock.unlock();
}
注意:
1.在finally块中释放锁,保证在获取到锁之后,最终能够被释放。
2.不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
中断响应
synchronized修饰的线程在等待锁,那么只有2种情况:1获取到锁继续执行,2保持等待;
lockInterruptibly 方法修饰的线程,可在获取锁的过程种响应线程中断,那么就会抛出异常。
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 顺利完成任务。
}
}
锁申请
trylock(),tryLock(long time, TimeUnit unit)在获取锁如果尝试失败或者超时,线程就放弃获取=锁,这点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();
}
}
公平锁和非公平锁
公平锁:不会产生饥饿现象。线程按照等待顺序得到资源
非公平锁:系统在选择锁的时候都是随机的,不会按照某种顺序,比如时间顺序。
synchronized 得到的锁是非公平锁,而ReentrantLock可以自由选择使用公平锁或者非公平锁,同时非公平锁 比 公平锁 更有效率,一般选择非公平锁。
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();
}
}
可以看到,公平锁的打印顺序是完全交替运行,而不公平锁的顺序完全是随机的。
重入锁相比 synchronized 有哪些优势:
- 可以在线程等待锁的时候中断线程,synchronized 是做不到的。
- 可以尝试获取锁,如果获取不到就放弃,或者设置一定的时间,这也是 synchroized 做不到的。
- 可以设置公平锁,synchronized 默认是非公平锁,无法实现公平锁。
重入锁的好搭档-----Condition
synchronized 通过 Object 类的 wait 方法和 notify 方法实现线程之间的通信;
重入锁 ReentrantLock 通过 Condition 接口中的await()、signal()方法进行通信。
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 类的冲突,使用 await 方法 对应 wait 方法,signal 方法对应 notify 方法。signalAll 方法对应 notifyAll 方法;
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();
}
}
ReadWriteLock:
线程不安全的原因来自于多线程对数据的修改,如果你不修改数据,根本不需要锁。我们完全可以将读写分离,提高性能,在读的时候不使用锁,在写的时候才加入锁。这就是 ReadWriteLock 的设计原理。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
读写锁的使用:
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();
}
}
}
两个循环:一个循环开启18个线程去读数据,一个循环开启两个线程去写。如果使用普通的重入锁,将耗时20秒,因为普通的重入锁在读的时候依然是串行的。而如果使用读写锁,只需要2秒,也就是写的时候是串行的。读的时候是并行的,极大的提高了性能。
注意:只要涉及到写都是串行的。比如读写操作,写写操作,都是串行的,只有读读操作是并行的。
ReadWriteLock遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
ReentrantReadWriteLock是ReadWriteLock的一个实现类:
-
公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
-
重进入:读锁和写锁都支持线程重进入。
-
锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。