Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)
Posted 宋子浩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)相关的知识,希望对你有一定的参考价值。
文章目录:
1.从乐观锁和悲观锁开始说起
- 悲观锁:悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
悲观锁的实现方式:① synchronized关键字;
② Lock接口的实现类都是悲观锁。
适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
public synchronized void method()
//加锁之后的业务逻辑
Lock lock = new ReentrantLock();
public void method2()
lock.lock();
try
//加锁之后的业务逻辑
finally
lock.unlock();
- 乐观锁:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
乐观锁的实现方式:① 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
ABA问题:再CAS中想读取一个值A,想把值 A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
解决方法:Juc包提供了一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
② 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升。
AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.incrementAndGet();
2.synchronized的8锁案例
首先,我们可以看一下阿里巴巴Java开发手册中,关于锁的强制性要求。
2.1 第一种情况:两个线程锁的是同一个实例对象
这里我能使用 Lambda 表达式的原因是,Phone类中的这两个实例方法是无参、无返回值的,和Runnable中的run方法一致,所以直接方法引用是OK的。
两个线程锁的都是我 new 的同一个对象 phone,所以当第一个线程去发邮件的时候就拿到了 phone 对象这把锁,此时第二个线程就拿不到了,只能等待第一个线程执行完释放锁,它才可以去发短信。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public void sendEmail()
synchronized (this)
System.out.println("-----发送邮件");
public void sendSMS()
synchronized (this)
System.out.println("-----发送短信");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
new Thread(phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(phone::sendSMS, "b").start();
2.2 第二种情况:第一个线程的逻辑中添加sleep睡眠
和第一种情况不同的是:当第一个线程拿到 phone 对象锁之后,在发邮件的过程中,sleep睡眠了2秒。但是执行结果和第一种情况是一样的。
原因就是 sleep 方法并不会释放锁,只是让线程暂定一段时间,一段时间过后线程照常执行(不要interrupt打断。。。)。
某一个时刻内,只能有唯一的一个线程去访问这些针对于实例对象的synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public void sendEmail()
synchronized (this)
try
TimeUnit.SECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("-----发送邮件");
public void sendSMS()
synchronized (this)
System.out.println("-----发送短信");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
new Thread(phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(phone::sendSMS, "b").start();
2.3 第三种情况:第二个线程执行的是无锁方法
这种情况,虽然第一个线程拿到 phone 对象锁去发邮件了,中间睡了2秒不会释放锁对象。但是第二个线程的任务是 hello,这个方法并没有任何锁机制,它并不会和synchronized修饰的同步方法、代码块发生争抢,所以两个线程你干你的、我干我的。 由于线程a睡眠了,所以这里线程b就先执行完毕。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public void sendEmail()
synchronized (this)
try
TimeUnit.SECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("-----发送邮件");
public void sendSMS()
synchronized (this)
System.out.println("-----发送短信");
public void hello()
System.out.println("-----hello");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
new Thread(phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(phone::hello, "b").start();
2.4 第四种情况:两个线程锁的是两个不同的实例对象
两个线程锁的实例对象不同,线程a锁了phone,线程b锁了phone2,那么这里就算执行的是 synchronized 同步方法、代码块,也互不干扰,因为这是两把锁。
又因为线程a中间睡了2秒,所以线程b先执行完。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public void sendEmail()
synchronized (this)
try
TimeUnit.SECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("-----发送邮件");
public void sendSMS()
synchronized (this)
System.out.println("-----发送短信");
public void hello()
System.out.println("-----hello");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(phone2::sendSMS, "b").start();
2.5 第五种情况:两个线程锁的是同一个类对象
这里将上面的实例方法修改成了静态方法,一经修改,那么这两个方法就是类级别了,synchronized锁的是 Phone 这个类,那么无论开了多少个线程,当第一个线程获得类锁之后,其他的线程都无法再拿到这个类锁。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public static void sendEmail()
synchronized (Phone.class)
try
TimeUnit.SECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("-----发送邮件");
public static void sendSMS()
synchronized (Phone.class)
System.out.println("-----发送短信");
public void hello()
System.out.println("-----hello");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(Phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(Phone::sendSMS, "b").start();
2.6 第六种情况
和第五种情况的区别是:声明了两个Phone对象(phone、phone2),但是原理和第五种情况是一样的,因为这里是类锁。
2.7 第七种情况:一个线程锁实例对象,一个线程锁类对象
两个线程锁的对象不同,第一个线程锁的类对象,去发邮件,中间睡了2秒,执行稍慢;第二个线程锁的phone实例对象,去发短信;二者是互不干扰的,你干你的,我干我的。 由于线程a睡眠了,所以线程b先完成执行。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
class Phone
public static void sendEmail()
synchronized (Phone.class)
try
TimeUnit.SECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("-----发送邮件");
public void sendSMS()
synchronized (this)
System.out.println("-----发送短信");
public void hello()
System.out.println("-----hello");
public class Lock8
public static void main(String[] args)
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(Phone::sendEmail, "a").start();
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
new Thread(phone::sendSMS, "b").start();
2.8 第八种情况
和第七种情况的区别是:Phone对象声明了2个,但是和第七种情况的原理仍然一样,一个锁了类对象、一个锁了实例对象,二者不产生竞争条件。
3.字节码角度分析synchronized
先介绍两个东西:
文件反编译技巧
文件反编译
javap -c ***.class
文件反编译,-c表示对代码进行反汇编假如需要更多信息
javap -v ***.class
,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)
3.1 synchronized同步代码块
以下面这段代码为例,在IDEA中编译运行之后,会生成 .class 字节码文件,我们找到它所在的目录,cmd打开。
package com.juc.lock;
/**
*
*/
public class LockSyncDemo
Object object = new Object();
public void method1()
synchronized (object)
System.out.println("----- synchronized code block");
public static void main(String[] args)
synchronized同步代码块,实现使用的是 moniterenter 和 moniterexit 指令(moniterexit可能有两个,是因为程序要完完全全的确保你能够释放掉占有的锁对象,可能第一次 exit 没有释放掉,你的程序中有一些错误什么的,所以在后面还会有第二个 exit),底层实际上就是靠 这两个指令来确保锁的获取和释放。
那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
3.2 synchronized同步实例方法
package com.juc.lock;
/**
*
*/
public class LockSyncDemo
public synchronized void m2()
System.out.println("------hello synchronized m2");
public static void main(String[] args)
使用 javap -v LockSyncDemo.class,更详细的查看字节码文件。
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成无论是正常完成还是非正常完成)时释放monitor。
3.3 synchronized同步静态方法
package com.juc.lock;
/**
*
*/
public class LockSyncDemo
public static synchronized void m2()
System.out.println("------hello synchronized m2");
public static void main(String[] args)
所以它这里就是通过这两个东西去判断的,如果同时具备了 ACC_STATIC、ACC_SYNCHRONIZED,那就是类锁,只具备 ACC_SYNCHRONIZED,那就是对象锁。
3.4 synchronized锁的是什么?
管程:Monitor(监视器),也就是我们平时说的锁。监视器锁
信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
4.公平锁和非公平锁
非公平锁:默认是非公平锁。非公平锁可以插队,买卖票不均匀。
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)
公平锁:ReentrantLock lock = new ReentrantLock(true); 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着, 这是公平的。
为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
来看下面的代码案例。
package com.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 资源类,模拟3个售票员卖完50张票
*/
class Ticket
private int number = 50;
Lock lock = new ReentrantLock(); //默认就是非公平锁
public void sale()
lock.lock();
try
if(number > 0)
System.out.println(Thread.currentThread().getName() + "卖出第:\\t" + (number--) + "\\t 还剩下: " + number);
finally
lock.unlock();
public class SaleTicketDemo
public static void main(String[] args)
Ticket ticket = new Ticket();
new Thread(() ->
for (int i = 0; i <55; i++)
ticket.sale();
,"a").start();
new Thread(() ->
for (int i = 0; i <55; i++)
ticket.sale();
,"b").start();
new Thread(() ->
for (int i = 0; i <55; i++)
ticket.sale();
,"c").start();
下面的执行结果,没有截完整,最后全部都是线程b卖的票,这里就可以看到,严重的非公平锁了,压根就没有线程c的事。
将上述代码修改为公平锁是很简单的,在 ReentrantLock 的构造方法中传入一个布尔值 true就可以了。
至于原因,我在后面会为大家讲解,就是AQS!!!
Lock lock = new ReentrantLock(true); //公平锁
5.可重入锁
可重入锁又名递归锁:是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可:可以
重:再次
入:进入
锁:同步锁
进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入。自己可以获取自己的内部锁。
5.1 可重入锁之隐式锁synchronized
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。
5.1.1 针对同步代码块
package com.juc.lock;
/**
*
*/
public class ReEntryLockDemo
public static void main(String[] args)
final Object obj = new Object();
new Thread(() ->
synchronized (obj)
System.out.println(Thread.currentThread().getName() + " 外层调用....");
synchronized (obj)
System.out.println(Thread.currentThread().getName() + " 中层调用....");
synchronized (obj)
System.out.println(Thread.currentThread().getName() + " 内层调用....");
, "t1").start();
5.1.2 针对同步方法
package com.juc.lock;
/**
*
*/
public class ReEntryLockDemo
public synchronized void m1()
System.out.println(Thread.currentThread().getName() + " --- come in");
m2();
System.out.println(Thread.currentThread().getName() + " --- end");
public synchronized void m2()
System.out.println(Thread.currentThread().getName() + " --- come in");
m3();
public synchronized void m3()
System.out.println(Thread.currentThread().getName() + " --- come in");
public static void main(String[] args)
ReEntryLockDemo obj = new ReEntryLockDemo();
new Thread(obj::m1, "t1").start();
针对上面两个案例,为什么可以这样重入锁呢?
- 首次加锁:当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
- 重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
- 释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
5.2 可重入锁之显式锁Lock
package com.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
*/
public class ReEntryLockDemo
private static Lock lock = new ReentrantLock();
public static void main(String[] args)
new Thread(() ->
lock.lock();
try
System.out.println(Thread.currentThread().getName() + " --- 外层调用");
lock.lock();
try
System.out.println(Thread.currentThread().getName() + " --- 内层调用");
finally
lock.unlock();
finally
lock.unlock();
, "t1").start();
上面的案例中,切记:你lock了几次,对应的就要 unlock 几次,否则线程的获取锁、释放锁次数是无法对应的,程序就炸了。
6.死锁及排查
死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。 a跟b两个资源互相请求对方的资源。
死锁产生的原因:系统资源不足;进程运行推进的顺序不合适;资源分配不当。
package com.juc.lock;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class DeadLockDemo
public static void main(String[] args)
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() ->
synchronized (objectA)
System.out.println(Thread.currentThread().getName() + " --- 获取到了A锁,希望获取B锁");
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
synchronized (objectB)
System.out.println(Thread.currentThread().getName() + " --- 获取到了B锁");;
, "A").start();
new Thread(() ->
synchronized (objectB)
System.out.println(Thread.currentThread().getName() + " --- 获取到了B锁,希望获取A锁");
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
synchronized (objectA)
System.out.println(Thread.currentThread().getName() + " --- 获取到了A锁");
, "B").start();
并不是说我们的程序出现了上图中的小红方块,没有结束就一定是死锁,我也可以写一个 while(true) 死循环卡在这里。
所以我们需要对是否是死锁进行排查。
jps -l
查看当前进程运行状况
jstack 进程编号
查看该进程信息
或者我们可以 win + R,输入 jconsole。
7.总结
这篇文章是关于锁的,是入门级别的文章,后续还会有更深入、更底层的JUC,我会慢慢更新的。。。
以上是关于Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)的主要内容,如果未能解决你的问题,请参考以下文章
Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)
Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)
Java——聊聊JUC中的CompletableFuture