synchronized关键字

并发编程系列synchronized-wait原理讨论

synchronized关键字的blocking

在多线程编程的情况下,假设我们定义了一把锁,如果现在有10个线程来获取这把锁那么肯定只有第一个线程可以获取到锁,从而进入临界区(所谓临界区就是被锁保护起来的代码块);其他获取不到锁的线程都会被阻塞(关于阻塞你就可以理解为CPU放弃调度这个线程了),但是这些被阻塞的线程JVM是怎么处理的呢?先看一张图在这里插入图片描述
上图t1获取锁,如果在t1没有释放的情况下其他线程也来获取锁,结果肯定是获取不到,从而进入阻塞状态,但是这些被阻塞的线程如果不存某种关系将来唤醒的时候就很麻烦(比如先唤醒谁呢?有人肯定会说那肯定先唤醒最先阻塞的那个线程啊,关键是JVM如何知道哪个线程最先阻塞的呢?)为了解决这个麻烦JVM设计了一个EntryList的双向链表的队列来维护这些阻塞的线程;如上图这样 t2到tn被维护到了这个队列,当t1释放锁之后会去这个队列当中唤醒一个线程来获取锁,这里请读者们思考一些问题;到底是唤醒一个,还是全部唤醒呢?如果唤醒一个是随机唤醒还是顺序唤醒,如果是顺序唤醒是正序还是倒序呢?笔者直接给出答案,当t1释放锁的时候会从EntryList当中唤醒一个线程,而且顺序唤醒,而且倒序的,也就是先唤醒tn这个线程;但是值得注意的synchronized关键字是倒序唤醒,但是如果你使用ReentrantLock那么则是正序唤醒;那么这个结论如何证明了----笔者将会通过三个角度来证明
1、通过一个简单java应用来证明 synchronized 是倒叙唤醒而不是随机唤醒
首先来看一个简单的java应用,代码如下(先仔细阅读以下代码,下文我会对代码做解释,博客里面所有代码放到文末链接,读者可以自己下载);

package com.qwang.aqs;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class TestSynchronized {
    static List<Thread> list = new ArrayList<>();
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName()+"开始执行");
                    try {
                        //这里的睡眠没有什么意义,仅仅为了控制台打印的时候有个间隔 视觉效果好
                        TimeUnit.MILLISECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t" + i);//给每个线程去了一个名字 t1 t2 t3 ....

            list.add(t);
        }
        System.out.println("---启动顺序 调度顺序或者说获取锁的顺序讲道理是正序0--9----");
     
        synchronized (lock) {
            for (Thread thread : list) {
                //这个打印主要是为了看到线程启动的顺序
                System.out.println(thread.getName()+"启动顺序--正序0-9" );
                thread.start();//  CPU 调度?
                
                //这个睡眠相当重要,如果没有这个睡眠会有很大问题
                //这里是因为线程的start仅仅是告诉CPU线程可以调度了,但是会不会立马调度是不确定的
                //如果这里不睡眠 就有有这种情况出现
                // 主线程执行t1.start--Cpu没有调度t1--继续执行主线程t2-start cpu调度t2--然后再调度t1
                //虽然我们的启动顺序是正序的(t1--t2),但是调度顺序是错乱的  t2---t1
                
                TimeUnit.MILLISECONDS.sleep(1);
             }
            System.out.println("-------执行顺序--正序9-0");
        }
    }
}


代码非常简单主线程main启动,然后实例化了10个线程对象t0-t9;继而把这个10个线程添加到一个List当中(注意这里仅仅是实例化了十(10)个线程,并没有启动,如果将来启动这10个线程他们的run方法里面的代码也非常简单,就是获取lock这把锁,然后打印一句话);添加到数组之后main线程接着往下执行;mian线程获取锁(这里一定能获取成功,因为那10个线程还没启动,锁处于自由状态,所以能被main获取);获取到锁之后main线程执行了一个for循环从list当中依次顺序获取到上面存入到list当中的那10个线程(由于ArrayList是有序的)故而取出的顺序肯定是有序的(t0-t9);取出来之后依次调用star方法启动这些线程;但是这里需要注意的是虽然我们已经保证取出来的线程是顺序的(t0-t9),而且我们也保证了这些线程的start方法是顺序调用的,但是你依然没法保证这些线程的调度(也就是我们常说的执行)顺序;为了保证t0-t9的调度顺序我这里在线程start之后,让main线程sleep了1毫秒;这样就能保证t0-t9线程的调度或者说执行顺序;至于为什么要保证他们的调度顺序?

  			 来解释一下为什么需要保证这个调度顺序呢?
             这里所有代码的意图就是顺序启动线程(顺序调度线程),这些线程启动之后会去拿锁(lock)
             肯定拿不到,因为这个时候锁被主线程持有
             主线程还在for循环没有释放锁,所以在for循环里面启动的线程都是拿不到锁的
             那么这些那不到锁的线程就会阻塞
             也就t0----t9阳塞之后他们被存到了一个队列当中
             这个JVM的源码中可以证明,我后面给大家看源码,
             总之你现在记住所有拿不到锁的线程会阻塞进入到Entrylist这个队列当中
             然后主线程执行完for循环后会释放放锁
             继而会去这个队列当中去唤醒一个个线程————随机唤醒还是顺序唤醒呢?
             假设是顺序唤醒,是倒序还是正序唤醒呢?
             需要证明这个问题,就要保证所有因为拿不到锁而进入到这个队列当中的线程
             他们的顺序必须是有序的,这样后面从他们的执行结果才能分析;
             假设你 进入到阻塞队列的时候都是随机的,那么后面唤醒线程执行的时候必然也是随机的
             那么则无法证明唤醒是否具备有序性
             为了保证进入到队列当中的线程调度是有序的,主线程睡眠很有必要  
             那么为什么主线程睡眠1下就能保证这些线程的顺序调度呢?这个问题读者可以思考一下后而我会重点分析
             好了现在我们来看结果

---启动顺序 调度顺序或者说获取锁的顺序讲道理是正序0--9----
t0启动顺序--正序0-9
t1启动顺序--正序0-9
t2启动顺序--正序0-9
t3启动顺序--正序0-9
t4启动顺序--正序0-9
t5启动顺序--正序0-9
t6启动顺序--正序0-9
t7启动顺序--正序0-9
t8启动顺序--正序0-9
t9启动顺序--正序0-9
-------执行顺序--正序9-0
t9开始执行
t8开始执行
t7开始执行
t6开始执行
t5开始执行
t4开始执行
t3开始执行
t2开始执行
t1开始执行
t0开始执行

从上图可以看出首先10个线程的启动顺序(由于主线程睡眠了1毫秒故而启动顺序其实等于调度顺序)是t0-t9;因为在启动线程的时候主线程没有释放锁,所以t0-t9都因为拿不到锁进入了队列(EntryList),又因为t0-t9的调度(启动)顺序保证了,所以进入队列的顺序也保证了(t0先进入队列,t9最后进入队列);但是在主线程释放锁的时候,唤醒线程的顺序是都倒序的,先唤醒t9,最后唤醒t0;这里的结果可以说明JVM在从队列当中唤醒的时候是唤醒一个,而不是全部唤醒,因为如果是全部唤醒,那么这些线程的执行顺序肯定是乱的,只有唤醒一个,而且还是顺序唤醒才能保证执行顺序是具备规则的(t9-t0),而且是倒序唤醒的;那么这队列存在哪里呢?在java语言里使用synchronized关键字如果变成了一把重量锁(关于什么是重量锁下次分析),那么这个锁对象(本文当中的lock对象——Object lock = new Object())会关联一个C++对象——ObjectMonitor对象;这个监视器对象当中记录了持有当前锁的线程,记录了锁被重入的次数,同时他还有一个属性EntryList用来关联那些因为拿不到锁而被阻塞的线程;如下图所示(先不要关心WaitSet)
在这里插入图片描述
好了到此为止你应该明白了我们在程序当中使用synchronized关键字的大概原理了吧,总结一下。当我们使用synchronized关键字来保护临界区的代码的时候,如果多线程并发情况下,持有锁的线程只有一个;其他竞争锁而不得线程会进入到当前锁对象关联的ObjectMonitor对象当中的双向链表的一个队列当中,并且是顺序进入的;但是当持有锁线程释放锁之后会从这个队列当中唤醒一个线程;从队列的末尾唤醒一个(先进后出);如果你没有看懂,请务必把文章翻上去在去看一遍或者从文章末尾把代码下载过去自己运行一遍,然后好好体会一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值