当我们对代码就行反编译,会发现其实synchronized就是monitor
假如现在有一个线程过来了,要执行当前代码,会执行到synchronized (lock),lock是一个对象锁。首先会让这个lock对象和monitor进行关联,然后判断Owner是否为null,如果为null则让当前线程直接拥有对象锁。
并且Owner只能关联一个线程,如果此时线程2来了,就会到EntryList中等待,无论来了多少线程,只要Owner不为努力了,就都要到EntryList中等待,这些线程都会处于block状态。而当线程1执行完成,Owner为null了,就会唤醒EntryList中阻塞的这些线程,让这些线程争抢Owner的所有权
当一个线程调用了wait方法之后,就会处于等待状态,他会把这些线程放到WaitSet当中
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
首先我们可以看到obj的内存结构,当来了一个线程来执行method方法,在这个线程执行的时候,就会创建一个锁记录,叫做Lock Record,每个线程的栈帧都包含一个锁记录的结构.此时他内部就可以存储锁对象的mark word,首先会让Object reference去指向锁对象,就是obj。
当线程去执行锁的时候,会修改对象的mw,用cas的方式交换数据,如果发现了重入锁,则会再添加一个LR作为重入的计数,线程中包含几个LR就说明他重入了这个锁几次
当代码执行完成,也就是解锁的情况,这时候我们要判断当前的锁记录是否为null,如果是null则说明有重入,就会删掉这个LR,这个时候,第一个LR也要退出,他的锁记录可不为null,这个时候会再来一次cas操作。把这些值再交换回来,这时就代表这解锁成功了
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
在轻量级锁不同的是,偏向锁会记录某一个线程的id,并且把偏向锁的标志改为1,并且直接把当前线程的线程id写到Obj的MW当中
当我们执行m2,会再去添加一个锁记录,然后不会进行CAS操作,而是来看一些MW中的线程id是否是自己的,如果是自己的,则只是记录一下当前重入的次数。m3也是,只是会添加一条记录,而不是进行cas操作
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
共享内存:我们定义的成员变量,创建的对象或者是数组
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作
AbstractQueuedSynchronizer(AQS框架)
AtomicXXX类
假如线程A先执行完成
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
被native修饰,也就是说都是由系统提供的,是由c或c++实现的
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
① 保证线程间的可见性
② 禁止进行指令重排序
那为什么线程2可以读到线程1的值,而线程3不行呢
实际执行指令的时候JVM会对指令进行重排不一定按照写的顺序执行
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
下面操作会出现问题,并不能禁止指令重排序,以写操作为例,写屏障可以阻止上面代码越过屏障往下,但是不能阻止下面代码往上
写变量让volatile修饰的变量的在代码最后位置
读变量让volatile修饰的变量的在代码最开始位置
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
synchronized |
AQS |
关键字,c++ 语言实现 |
java 语言实现 |
悲观锁,自动释放锁 |
悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 |
锁竞争激烈的情况下,提供了多种解决方案 |
AQS常见的实现类
ReentrantLock 阻塞式锁
Semaphore 信号量
CountDownLatch 倒计时锁
在AQS内部,首先会有一个变量叫state,是由volatile修饰的,保证了多个线程的可见性。
当有一个线程想要获取这个锁,首先会讲state从无锁状态0改为1,这时候就说明线程持有了锁,此时又有一个新的线程来了,发现state是1,则会请求失败,于是会进入FIFO队列中等待。
这个队列其实是一个先进先出的双向队列,内部是一个双向链表,在AQS当中还有俩个属性。一个是head,一个是tail。
当线程0执行完成,会讲state的值改为0,并且会唤醒队列中的head元素,让他去持有锁,来完成一个简单的排队效果
假如同时来了两个线程,同时都要修改state,为例保证原子性,使用的CAS操作,然后将没抢到的线程添加到队列中
新的线程与队列中的线程共同来抢资源,是非公平锁
新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
公平与非公平一般由具体锁实现类实现,AQS本身没有这个概念。就拿ReentranLock来说,它内部的公平锁与非公平锁的区别在于获取锁是否严格遵循排队顺序:
如果锁被其他线程持有,那么再申请锁的其他线程会被挂起等待,加入到等待队列的末端,并遵循先入先出原则排队获取锁,这就是公平锁。
非公平锁则是让当前正在请求的线程优先插队第一个获取锁(不管等待队列是否有其他线程等待获取锁),如果获取到了直接返回,如果获取不到才加入到等待队列的末端。
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
可中断:ReentrantLock使得线程可以在被阻塞时响应中断,
可以设置超时时间:synchronized如果没有获取到锁只能进入阻塞状态等待,而ReentrantLock可以设置超时时间,如果没有获取到锁可以直接放弃
可以设置公平锁:synchronized只支持非公平锁,而ReentrantLock都支持
支持多个条件变量:synchronized有一个wait方法,可以让线程等待。ReentrantLock也有对应的方法,并且可以设置多个条件,让线程进入等待状态
都支持重入:支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
如果是无参的,则会创建非公平锁,如果有参,则会根据你传入的值判断应该是公平锁还是非公平锁
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock 的实现通常会提供更好的性能
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息
ConcurrentHashMap 是一种线程安全的高效Map集合
JDK1.7底层采用分段的数组+链表实现
JDK1.8 采用数组+链表/红黑二叉树。
Java并发编程三大特性