1.什么是死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
死锁产生的四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁的几个常见方法:
1.避免一个线程同时获得多个锁
2.避免一个线程在锁内占多个资源,尽量保证一个锁只占用一个资源
3.尝试使用定时锁,使用lock.tryLock(timeout)来替代内部锁的机制
4.对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会出现解锁失败的场景
2.java的加锁方式
java的加锁方式有两种,第一种就是大家所熟知的synchronized关键字,另一种是用Lock接口的实现类。
synchronized关键字用法简单粗暴,没有太多的限制。另外,在 Java 早期版本中,synchronized
属于 重量级锁,效率低下。Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
自Java 5之后,才在java.util.concurrent.locks包下有了另外一种方式来实现锁,那就是Lock。Lock有三个重要的实现类ReentrantLock、ReadLock、WriteLock ,它们分别对应了“可重入锁”、“读锁”和“写锁”。和synchronized关键字相比,lock的实现类更能满足实际的场景,因为它能细化锁的粒度,能让你根据业务去调整使用什么样的锁。而synchronized关键字就属于那种一招鲜,吃遍天的感觉。
3.悲观锁和乐观锁
锁在宏观的角度可以分为两种,一种是悲观锁,另一种是乐观锁。悲观锁和乐观锁在java中没有对应的具体的实现类,因为它主要是一种思想,一种策略。比如在java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。而乐观锁主要的实现方式是CAS,在java中java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。
3.1悲观锁
悲观锁总是会假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。所以悲观锁适用于多写的场景,因为在这个场景下,线程竞争非常激烈,如果采用乐观锁的方式解决,不同线程采用CAS不断自旋重试,反而降低了系统的性能。
3.2乐观锁
乐观锁, 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。所以乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。悲观锁和乐观锁应用场景不同,我们不能一味的觉得哪个锁更好,它们都各有优缺点,需要根据系统的特点来进行斟酌。
乐观锁的实现方式有两种:1.版本号机制;2.CAS算法
3.2.1版本号机制
版本号机制比较好理解,我们在实际开发的时候在数据库表添加一个字段version。在我们每次需要进行操作之前先读取版本号,然后再执行业务。业务执行完毕以后先比较版本号是否相同。如果相同,提交业务同时版本号加1,否则不做操作或者更新重试!
原文https://zhuanlan.zhihu.com/p/50820372
3.2.2CAS算法
CAS操作包括三个操作数–内存位置V,预期原值A,新值B。
分为三个步骤:
- 读取内存中的值
- 将读取的值和预期的值进行比较
- 如果比较的结果符合预期,则写入新值;如果不符合,则什么都不做
我们简单理解一下上述的过程,这和版本号机制其实非常类似。我们在对一个值修改之前,先判断它有没有发生变化,就是在我们修改之前它会不会被其他线程所修改,如果没有,那么我们执行修改操作,否则就不断的自旋(就是不断的重试)
CAS的保证原子性分两种情况:
1.单核cpu情况下:CAS是一种系统原语,原语由若干条指令组成的,用于完成一定功能的一个过程。 原语的执行必须是连续的,在执行过程中不允许被中断。然后同一时刻只允许一个线程在更新,同时必须更新完成才能让下一个线程操作。更新完毕之后,下一个线程的A和V不同,所以更新失败。
2.多核cpu情况下:利用volatile的一致性,volatile的写操作是安全的,因为他在写入的时候lock会锁住cpu总线导致其他cpu不能访问内存(现在多用缓存一致性协议,即处理器嗅探总线上传播的数据来判断自己缓存的值是否过期),所以当cpu2火速修改了变量的值时,这就让该变量在所有cpu上缓存的值都失效了,cpu1在进行写操作时,发现自己缓存的值已经失效了,那么CAS操作失败。所以即使在多cpu多线程下,CAS机制也能保证线程安全。
所以乐观锁的底层我个人感觉还是有一些悲观锁的思想在里面。
注:参考https://blog.csdn.net/qq_41030039/article/details/101705360
乐观锁虽然不想悲观锁那样直接锁住资源,灵活性较好,但是也有一些缺点:
1.ABA问题:使用CAS算法实现乐观锁时会存在这个问题,简单来说就是内存的V值被两个线程A和B读取到,值都为0,其中线程A将值改为1提交到内存中.然后线程C又将值A改回0,线程B提交发现值还是0,没有变化,所以它也成功提交.解决ABA问题可以使用版本号机制.JDK 1.5 以后的 AtomicStampedReference 类
就提供了此种能力.
2.循环时间开销大:自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销.
3.只能保证一个共享变量的原子操作:从 JDK 1.5开始,提供了AtomicReference类
来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类
把多个共享变量合并成一个共享变量来操作.
4.synchronized
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行.
4.1synchronized的用法
4.1.1修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
public class SychronizedDemo01 implements Runnable {
static int i=100;
//加上了synchronized可以保证线程同步,结果是3100,否则结果可能小于3100
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j <1000 ; j++) {
increase();
}
}
public static void main(String[] args)throws Exception {
SychronizedDemo01 sychronizedDemo01=new SychronizedDemo01();
Thread thread1=new Thread(sychronizedDemo01);
Thread thread2=new Thread(sychronizedDemo01);
Thread thread3=new Thread(sychronizedDemo01);
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println(SychronizedDemo01.i);
}
}
4.1.2修饰静态方法
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
public class SychronizedDemo02 implements Runnable {
static int i = 100;
//加上了synchronized可以保证线程同步,结果是3100,否则结果可能小于3100
public static synchronized void increase() {
i++;
}
//不同线程访问这个方法不会互斥
public synchronized void increaseNoStatic() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increase();
}
}
public static void main(String[] args) throws Exception {
SychronizedDemo02 sychronizedDemo01 = new SychronizedDemo02();
Thread thread1 = new Thread(sychronizedDemo01);
Thread thread2 = new Thread(sychronizedDemo01);
Thread thread3 = new Thread(sychronizedDemo01);
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println(SychronizedDemo02.i);
}
}
4.1.3修饰代码块
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了
public class SychronizedDemo03 implements Runnable {
static SychronizedDemo03 instance = new SychronizedDemo03();
static int i = 100;
@Override
public void run() {
synchronized(instance){
for(int j=0;j<1000;j++){
i++;
}
}
}
public static void main(String[] args) throws Exception {
SychronizedDemo03 sychronizedDemo01 = new SychronizedDemo03();
Thread thread1 = new Thread(sychronizedDemo01);
Thread thread2 = new Thread(sychronizedDemo01);
Thread thread3 = new Thread(sychronizedDemo01);
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println(SychronizedDemo03.i);
}
}
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁。synchronized
关键字加到实例方法上是给对象实例上锁
来一个单例模式
public class Siglenton {
private volatile static Siglenton intance;
private Siglenton() {
}
public static Siglenton getIntance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (intance == null) {
//类对象加锁
synchronized (Siglenton.class) {
//如果第一个线程进来,实例化了一个对象,那么intance不为空
//第二个线程进来如果不做校验,又会重新实例化
if (intance == null) {
intance = new Siglenton();
}
}
}
return intance;
}
}
4.2synchronized的底层原理
首先使用通过 JDK 自带的 javap 命令查看 SychronizedDemo01 类的相关字节码信息.首先切换到类的对应目录执行 javac SychronizedDemo01.java 命令生成编译后的 .class 文件 然后执行 javap -c -s -v -l SychronizedDemo01.class
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。 不过两者的本质都是对对象监视器 monitor 的获取.
JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现
4.3synchronized的优化
在执行java代码时,synchronized会根据程序的运行特性从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁.
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高.
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争.但是并不是每次都会升级为轻量级锁.线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级.
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。
4.4sychronized和volatile的区别(待补充)
5.lock接口
JDK1.5之后,并发包提供了Lock接口以及相关的实现来实现锁的功能.
public interface Lock {
//用来获取锁,如果锁已被其他线程获取,则进行等待
void lock();
//中断优先于获取锁
void lockInterruptibly() throws InterruptedException;
//有返回值,成功获取到锁返回true,否则false,tryLock(long time, TimeUnit unit)方法和
//tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内
//如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁
void unlock();
//获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用
//后,当前线程将释放锁
Condition newCondition();
}
5.1可重入锁ReentrantLock
5.1.1代码展示
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。如果你需要不可重入锁,只能自己去实现了。
public class ReentrantLockForIncrease {
//初始化ReentrantLock
public static ReentrantLock reentrantLock = new ReentrantLock();
static int count = 0;
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
//加锁
reentrantLock.lock();
try {
int n = 10000;
while (n > 0) {
count++;
n--;
}
}catch (Exception e){
e.printStackTrace();
}finally {
//执行完毕后释放锁
reentrantLock.unlock();
}
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
Thread t4 = new Thread(r);
Thread t5 = new Thread(r);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
try {
//等待足够长的时间 确保上述线程均执行完毕
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
5.2.2公平锁非公平锁代码演示
如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁.对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁.
public class ThreadTest {
//设置锁为公平锁
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
try {
System.out.println("获得锁的线程:" + id);
}finally {
lock.unlock();
}
}
}
}
}
公平锁结果,我们可以看到不同线程轮流获得了锁
我们将变量改成 static Lock lock = new ReentrantLock(false);
非公平锁结果,线程会重复获取锁
在日常开发中,我们大部分情况下采用非公平锁即可,当然为了防止某些线程一直获取不到锁,也可以采用公平锁!
5.2.3可中断锁代码演示
可中断锁,字面意思是“可以响应中断的锁”。 这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。 在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以从Lock接口的方法得出。
public class ReentrantLockDemo01 {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock1, lock2));
//该线程先获取锁2,再获取锁1
Thread thread2 = new Thread(new ThreadDemo(lock2, lock1));
thread1.start();
thread2.start();
//是第一个线程中断
thread1.interrupt();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
//更好的触发死锁
TimeUnit.MILLISECONDS.sleep(10);
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName() + "正常结束!");
}
}
}
}
运行结果
我们中断了第一个线程,所以第二个线程正常结束,如果我们将thread1.interrupt();改成thread2.interrupt();那么结果又会有一些变化,第一个线程正常结束,注意Thread的名字是从0开始,类似于数组下标
5.2Java的读写锁
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁).读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1).
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
public class Foo {
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
// 读锁,允许同时N个线程进行读操作,不存在竞争
public void read() {
r.lock();
try {
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName() + " 正在读...");
} catch (Exception e) {
e.printStackTrace();
} finally {
r.unlock();
}
}
//写锁,同时允许一个线程写,明显能看到互斥等待
public void wirte() {
w.lock();
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 正在写...");
} catch (Exception e) {
e.printStackTrace();
} finally {
w.unlock();
}
}
public static void main(String[] args) {
//定义共享数据源
Foo source = new Foo();
//开启2个写线程:能明显看到t1,t2写线程之间的互斥等待
Thread t1 = new Thread(new LockWriteTask(source));
t1.setName("write-Thread-1");
Thread t2 = new Thread(new LockWriteTask(source));
t2.setName("write-Thread-2");
t1.start();
t2.start();
//开启5个读线程:读锁,允许同时N个线程进行操作,可以看到读打印操作同时秒出
Thread rt1 = new Thread(new LockReadTask(source));
rt1.setName("read-Thread-1");
Thread rt2 = new Thread(new LockReadTask(source));
rt2.setName("read-Thread-2");
Thread rt3 = new Thread(new LockReadTask(source));
rt3.setName("read-Thread-3");
Thread rt4 = new Thread(new LockReadTask(source));
rt4.setName("read-Thread-4");
rt1.start();
rt2.start();
rt3.start();
rt4.start();
}
}
public class LockReadTask implements Runnable{
private Foo source;
public LockReadTask(Foo source){
this.source = source;
}
@Override
public void run() {
source.read();
}
}
public class LockWriteTask implements Runnable{
private Foo source;
public LockWriteTask(Foo source){
this.source = source;
}
@Override
public void run() {
source.wirte();
}
}
5.4Lock和Synchronized的区别
- 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁
- synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活
- synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断
- synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁
- synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程
- synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢
6.总结
- 锁可以分为悲观锁和乐观锁,它们是两种不同的策略,无论是synchronized还是lock的实现类都是悲观锁,java的原子类底层是使用的乐观锁思想
- JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的(递归)
- synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁
- synchronized就是不可中断锁,而Lock的实现类都是可中断锁
- 读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁),这是悲观锁思想
参考:
https://zhuanlan.zhihu.com/p/126085068
https://www.cnblogs.com/aishangJava/p/10026664.html