接下来我们来看看多线程锁的理解
悲观锁:适合写操作多的场景,先加锁可以保证写操作时数据正确,效率低,会造成阻塞;
乐观锁:适合读操作多的场景,使得读操作性能提升,采用版本号,和cas(比较并交换)来实现;
高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁;
尽可能使得加锁的代码块工作量小,避免在锁代码块中调用RPC方法;
synchronized:同步锁,悲观锁,独占锁,只要在一个方法加了这个synchronized,那么整个类中其他的加synchronized的方法都会被锁住,因为他是独占的啊,不能让别人抢我的资源,千万不要认为他锁的是这个方法,同一时间内,只能有一个加锁的方法被访问
类锁就相当于你家里的大门锁,而对象锁就是你屋子那把小锁;
在方面上面了加了静态同步锁,那么就是类锁;
在方面上了加了普通的同步锁,那么就是对象锁;
2个同步锁方法,谁先调用?
public class Producer {
public synchronized void aa(){
System.out.println("aa:"+Thread.currentThread().getName());
}
public synchronized void bb(){
System.out.println("bb:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
new Thread(()->{
producer.aa();
}).start();
new Thread(()->{
producer.bb();
}).start();
System.out.println("main结束了");
}
}
我们可以看到aa先执行,bb在执行
我们给aa加3秒的延迟,在来看看谁先执行
可以看到还是aa先执行,然后再是bb,为什么呢?
因为aa和bb他们拿到的是同一把对象锁,也就是producer这个对象
我们可以知道加了同步锁的方法,在当前类中所有被加同步锁的方法,只能有一个被访问到
所以aa先执行,bb在执行
这次我们把aa和没有加锁的cc方法比较,看看谁先调用
public class Producer {
public synchronized void aa(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("aa:"+Thread.currentThread().getName());
}
public synchronized void bb(){
System.out.println("bb:"+Thread.currentThread().getName());
}
public void cc(){
System.out.println("cc:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
new Thread(()->{
producer.aa();
}).start();
new Thread(()->{
producer.cc();
}).start();
System.out.println("main结束了");
}
}
可以看到没有加锁的cc先调用,然后再是aa,为什么呢?
因为cc没有加锁,不会和aa的锁抢占资源,所以cc先执行
我们在看一下2个不同的对象,分别调用aa和bb方法
可以看到,bb先执行,aa在执行,为什么呢?
因为aa和bb的对象锁不是同一把,所以不存在互相争抢资源的现象,所以bb先执行
接下来我们在看下,有2个静态同步锁方法,谁先打印
public class Producer {
public static synchronized void aa(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("aa:"+Thread.currentThread().getName());
}
public static synchronized void bb(){
System.out.println("bb:"+Thread.currentThread().getName());
}
public void cc(){
System.out.println("cc:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
new Thread(()->{
producer.aa();
}).start();
new Thread(()->{
producer.bb();
}).start();
System.out.println("main结束了");
}
}
可以看到还是先打印了aa,然后再打印bb
我们在看下2个不同的对象分别调用aa和bb,谁先打印
可以看到还是aa先打印,然后再是bb,为什么呢?
在静态同步锁方法中,不管你是几个对象,他拿的都是同一把锁,并且静态方法的调用
不需要对象,静态方法,不管你new了多少个对象,他拿到的类模版都是同一个
所以aa先打印,然后再是bb
接下来我们在来看下静态同步锁方法和普通同步锁方法谁先调用
public class Producer {
public static synchronized void aa(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("aa:"+Thread.currentThread().getName());
}
public synchronized void bb(){
System.out.println("bb:"+Thread.currentThread().getName());
}
public void cc(){
System.out.println("cc:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
// Producer producer2=new Producer();
new Thread(()->{
producer.aa();
}).start();
new Thread(()->{
producer.bb();
}).start();
System.out.println("main结束了");
}
}
可以看到是普通同步锁方法先调用
可以看到bb先执行,然后才是aa,为什么呢?
因为bb是对象锁,而aa他是类锁,静态方法不管你new了多少个对象,他拿到的类模版都是同一个,aa和bb他们不是同一把锁,不存在互相争抢资源的情况,所以bb先执行
我们上面说过,能用对象锁,就不要使用类锁
我们在来看下分别使用2个不同的对象调用1个静态同步锁方法,1个普通同步锁方法
可以看到还是bb先执行,为啥呢?
因为aa是类锁,不管你new了多少个对象,拿到的类模版都是同一个,
所以aa和bb不存在竞争资源的问题,不是同一把锁;
bb是对象锁,所以bb先执行,所以能用对象锁,就不要使用类锁
接下来我们通过字节码指令的方式来看下同步锁
public class Producer {
public Object object=new Object();
public void aa(){
synchronized (object){
System.out.println("同步代码块");
}
}
public static void main(String[] args) throws Exception {
}
}
找到class文件的目录
在cmd输入javap -c Producer.class 反编译
可以看到同步锁上面monitorenter, 下面monitorexit
就是监视器,和监视器退出;
使用了synchronized 就是monitorenter和monitorexit对应者;
下面还有一个monitorexit是对应,如果在锁里面出现了异常,那么监视器退出
你看第24行就出现了一个athrow
所以一个monitorenter对应2个monitorexit
但是在极端的情况下,一个monitorenter对应一个monitorexit
我们来看下下面的情况
在这种情况下就只有1个monitorenter对应1个monitorexit了
我们在使用javap -v Producer.class 来看下
public synchronized void aa(){
System.out.println("同步代码块");
}
在方法上面加了同步锁之后,就会显示ACC_SYNCHRONIZED,表示是一个同步方法
我们在来看下加了静态同步锁的区别
public synchronized void aa(){
System.out.println("同步代码");
}
public static synchronized void bb(){
System.out.println("静态同步代码");
}
可以看到同步锁前面多了一个ACC_STATIC,那么jvm就知道了那个是静态同步锁方法
为什么任何一个对象都可以成为一个锁?
管程就是monitors,也叫监视器;
每一个对象天生都带着一个对象监视器;
每一个被锁住的的对象都会和monitor关联起来;
什么是公平锁,什么是非公平锁?
公平锁,就是多个人干活,大家都能领取到任务;
非公平锁,就是一个人把所有人的活都干了;
我们来看下代码
public class Producer {
//默认非公平锁
ReentrantLock lock=new ReentrantLock();
int num=50;
public void aa(){
//加锁
lock.lock();
try {
if(num>0){
System.out.println("当前线程:"+Thread.currentThread().getName()+",剩余票数"+(num--));
}
}finally {
//释放锁
lock.unlock();
}
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
new Thread(()->{
for (int i = 0; i <51 ; i++) {
producer.aa();
}
},"a").start();
new Thread(()->{
for (int i = 0; i <51 ; i++) {
producer.aa();
}
},"b").start();
new Thread(()->{
for (int i = 0; i <51 ; i++) {
producer.aa();
}
},"c").start();
}
}
可以看到,线程a一个人把所有票都抢光了
我们在看下公平锁new ReentrantLock(true);设置为true
可以看到a,b,c三个线程都能买到票
为啥默认是非公平锁呢?
因为非公平锁不需要线程切换的开销,减少cpu空闲时间,要比公平锁的速度快,所以默认设置为非公平锁
什么时候使用公平锁,什么时候使用非公平锁?
如果是为了快速处理完一些程序,就是我一个人比那个2个人干活的速度还快,那么使用非公平锁,不用切换线程开销;
否则使用公平锁;
什么是可重入锁?
ReentrantLock和synchronized都是可重入锁;
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入;
自己可以获取自己的内部锁;
就是我大门的锁和屋子里面的锁是同一把,我一个钥匙就可以开2把锁;
我们来看下同步代码块的可重入锁
public class Producer {
Object o=new Object();
public void aa(){
new Thread(()->{
synchronized (o){
System.out.println("外层锁"+Thread.currentThread().getName());
synchronized (o){
System.out.println("中层锁"+Thread.currentThread().getName());
synchronized (o){
System.out.println("内层锁"+Thread.currentThread().getName());
}
}
}
},"aa").start();
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
producer.aa();
}
}
synchronized默认是可重入锁
在一个被synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码快时,是永远可以得到锁的;
可以看到,3个锁都是同一个线程,都能访问到锁;
可以重入锁指的是可重复递归调用的锁,在外层使用锁之后,在内存仍然可以使用,并且不会发送死锁;
我们来看下加在方法上面的可重入锁
public class Producer {
public synchronized void aa(){
System.out.println("我是aa开始"+Thread.currentThread().getName());
bb();
System.out.println("我是aa结束"+Thread.currentThread().getName());
}
public synchronized void bb(){
System.out.println("我是bb开始"+Thread.currentThread().getName());
cc();
System.out.println("我是bb结束"+Thread.currentThread().getName());
}
public synchronized void cc(){
System.out.println("我是cc开始"+Thread.currentThread().getName());
System.out.println("我是cc结束"+Thread.currentThread().getName());
}
public static void main(String[] args) throws Exception {
Producer producer=new Producer();
new Thread(()->{
producer.aa();
},"t1").start();
}
}
可以看到并没有发送死锁,拿的也是同一把锁,并且也达到了可重入的效果
接下来我们在看下ReentrantLock是否可以重入锁
public static void main(String[] args) throws Exception {
Lock lock=new ReentrantLock();
new Thread(()->{
//加锁
lock.lock();
try {
System.out.println("aa:"+Thread.currentThread().getName());
lock.lock();
try {
System.out.println("bb:"+Thread.currentThread().getName());
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放锁
lock.unlock();
}
},"t1").start();
}
可以看到已经实现了可以重入的效果
当我们使用lock和unlock的时候必须一一匹配,否则会造成死锁;
可重入锁的原理
每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针;
计数器默认为0,当有新的锁对象进来了,并且是当前线程,那么计数器加1,否则等待,直到线程释放该锁;
当jvm执行退出的时候,计数器减1,计数器为0的时候,表示锁已经释放;
接下来我们来看下死锁
public class Producer {
public static void main(String[] args) throws Exception {
Object a=new Object();
Object b=new Object();
new Thread(()->{
synchronized (a){
System.out.println("我是aa1:"+Thread.currentThread().getName());
synchronized (b){
System.out.println("我是bb1:"+Thread.currentThread().getName());
}
}
},"t1").start();
new Thread(()->{
synchronized (b){
System.out.println("我是bb2:"+Thread.currentThread().getName());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("我是aa2:"+Thread.currentThread().getName());
}
}
},"t2").start();
}
}
可以看到这个灯一直没有灭
产生死锁的原因?
系统资源不足;
两个线程访问的顺序不合适;
资源分配不当;
我们通过jvm的方式来排查下是否死锁
Found 1 deadlock. 找到一个死锁
t2等待锁xxx4900
t1 锁住xxx4900
我们还可以通过jconsole的命令来查看是否死锁
什么是线程中断机制?
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运;
interrupt :设置线程的中断状态为true,发起一个协商而不会立刻停止线程;
interupted :判断线程是否被中断并清楚当前中断状态;
这方法做了两件事;
1.返回当前线程的中断状态,测试当前线程是否已被中断;
将当前线程的中断状态清零并重新设置为false,清楚线程的中断状态;
isInterrupted: 测试此线程是否已被中断(通过检查中断标志位);
如何停止中断运行中的线程?
接下来我们来看下,使用volatile来中断线程
public class Producer {
//可见性 当前线程修改之后 可以被其他线程里面感知到
private static volatile boolean flag=false;
public static void main(String[] args) throws Exception {
new Thread(()->{
while (true){
if(flag){
System.out.println(Thread.currentThread().getName()+"已停止");
break;
}
System.out.println("你好");
}
},"t1").start();
//阻塞一下
Thread.sleep(100);
new Thread(()->{
flag=true;
},"t2").start();
}
}
当前t1线程感知到t2线程做出的改变,那么立马停止t1线程
我们还可以通过AtomicBoolean 原子类来实现 中断线程
public class Producer {
//原子boolean类 默认为false
private static AtomicBoolean atomicBoolean=new AtomicBoolean(false);
public static void main(String[] args) throws Exception {
new Thread(()->{
while (true){
if(atomicBoolean.get()){
//如果为true 停止循环
System.out.println(Thread.currentThread().getName()+"已停止");
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好");
}
},"t1").start();
//阻塞一下
Thread.sleep(100);
new Thread(()->{
//设置原子类为true
atomicBoolean.set(true);
},"t2").start();
}
}
我们还可以使用线程自带的api来中断线程
使用interrupt设置为true;
使用isInterrupted判断是否为true
但是在t1线程中千万不要写阻塞的代码,否则就会报错;
public static void main(String[] args) throws Exception {
Thread t1= new Thread(()->{
while (true){
//判断中断线程标志位 是否设置为true ,如果为true 则中断线程
if(Thread.currentThread().isInterrupted()){
//如果为true 停止循环
System.out.println(Thread.currentThread().getName()+"已停止");
break;
}
System.out.println("你好");
}
},"t1");
t1.start();
//阻塞一下
Thread.sleep(100);
new Thread(()->{
//协商t1线程中断 设置为true
t1.interrupt();
},"t2").start();
}
如果我们的isInterrupted在线程已经操作完了在去执行他,那么就会变成false
我们来看下例子
public class Producer {
public static void main(String[] args) throws Exception {
Thread t1 =new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(""+i);
}
System.out.println("t1线程:"+Thread.currentThread().isInterrupted());
},"t1");
t1.start();
new Thread(()->{
t1.interrupt();
System.out.println("t2线程:"+Thread.currentThread().isInterrupted());
},"t2").start();
//这个时候线程都执行完了 所以isInterrupted方法不会受到影响 所以为false
Thread.sleep(2000);
System.out.println("main:"+Thread.currentThread().isInterrupted());
}
}
我们对t1线程加一些阻塞看看什么效果?
public class Producer {
public static void main(String[] args) throws Exception {
Thread t1 =new Thread(()->{
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("t1线程已停止");
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好");
}
},"t1");
t1.start();
Thread.sleep(20);
new Thread(()->{
t1.interrupt();
},"t2").start();
}
}
可以看到报错了 sleep interrupted 睡眠中断
意思是当前你使用了interrupt方法,然后又使用了sleep,那么方法失效
抛出睡眠中断的异常,红灯没有关闭,所以这里不能加阻塞的代码
上面的代码,我们还可以在catch加一句代码 来解决
Thread.currentThread().interrupt();
可以看到,虽然还是报错,但是已经把红灯给停止了
我们在来看看interrupted方法
public static void main(String[] args) throws Exception {
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
System.out.println("-------------");
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
}
当我设置了interrupt,为啥最后一个interrupted变成了false呢?
因为当你调用interrupted的时候,先返回了标志位,然后再吧他自己设置为了false
我们来看下源码
在这里测试线程是否已经中断,如果传过来的是true,那么重置,也就是在底层设置为false
接下来我们在来看下LockSupport
LockSupport是什么?
用于创建锁和其他同步类的基本线程阻塞原语
就是阻塞和解除阻塞的意思
我们来看下代码
public class Producer {
public static void main(String[] args) throws Exception {
Object o=new Object();
new Thread(()->{
synchronized (o){
System.out.println("进入"+Thread.currentThread().getName());
try {
//阻塞
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"被唤醒");
}
},"t1").start();
Thread.sleep(1);
new Thread(()->{
//唤醒阻塞的线程
synchronized (o){
o.notify();
}
System.out.println(Thread.currentThread().getName()+"发出通知");
},"t2").start();
}
}
wait必须放在notify的前面;
不能先执行notify,否则报错;
wait和notify必须放在同步锁里面,并且持有同一把对象锁,要不然不知道你释放的是谁的锁
可以看到如果锁的对象不一样,那么t1就无法释放锁,一直在这里阻塞
我们在来看下Condition的阻塞和释放
public class Producer {
public static void main(String[] args) throws Exception {
Lock lock=new ReentrantLock();
//条件
Condition condition = lock.newCondition();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"进入");
lock.lock();
try {
//阻塞
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"释放锁");
},"t1").start();
Thread.sleep(1);
new Thread(()->{
lock.lock();
try {
//释放锁 唤醒线程
condition.signal();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"发出通知");
},"t2").start();
}
}
await和signal必须是同一个condition,并且必须要和lock结合使用,否则报错
先await在signal,signal不能在前面,否则报错
接下来我们来看下LockSupport的阻塞和释放锁
在unpark释放锁里面必须要放入,释放哪一个线程,才能释放对方的锁
public static void main(String[] args) throws Exception {
Thread t1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"进入");
//阻塞
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"释放锁");
},"t1");
t1.start();
Thread.sleep(1);
Thread t2=new Thread(()->{
//释放锁
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"发出通知");
},"t2");
t2.start();
}
那么我t1后执行,看看能否释放锁
可以看到顺序错了,也能释放锁,为什么呢?
我们来看下源码
在这个释放锁的方法里,我先给这个线程一个许可证
我们在看这个源码
如果当前线程有许可证,则消耗许可证,并往下走,如果没有许可证,那么阻塞
通过上面的源码分析,我们就可以明白顺序错了,也能往下执行了。
我们的许可证只有1个,并且不会积累;
可以看到下面虽然给了多个许可证,但是只能积累1个,所以在第二个阻塞哪里还会阻塞下去
为什么可以突破wait/notify的原有调用顺序?
因为unpark提前给了许可证,所以在park的时候拿到许可证,就不会阻塞了;
为什么唤醒两次后阻塞多次,但最终的结果还会阻塞线程?
因为unpark的许可证最多只能积累一个,所以还会造成阻塞;