大白话讲一些基础底层知识

1.Jdk1.7到Jdk1.8 java虚拟机发⽣了什么变化?

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,⽽是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是⽅法区的具体实现,之所以元空间所占的内存改成本地内存,官⽅的说法是为了和JRockit统⼀,不过额外还有⼀些原因,⽐如⽅法区所存储的类信息通常是⽐较难确定的,所以对于⽅法区的⼤⼩是⽐较难指定的,太⼩了容易出现⽅法区溢出,太⼤了⼜会占⽤了太多虚拟机的内存空间,⽽转移到本地内存后则不会影响虚拟机所占⽤的内存。

 

2.说⼀下HashMap的Put⽅法

先说HashMap的Put⽅法的⼤体流程:

  1. 根据Key通过哈希算法与与运算得出数组下标
  2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置
  3. 如果数组下标位置元素不为空,则要分情况讨论

a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中。(先扩容再添加)

b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node(分如下三种情况)(先添加再扩容)

  • 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
  • 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8且数组长度>64,那么则会将该链表转成红⿊树
  • 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法

 

3.ReentrantLock中的公平锁和⾮公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以是否公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

(ReentrantLock 默认非公平,且要注意,虽然是可重入,但你 lock了几次,你就要unLock几次=》 state = 0 表示锁可获取了,每重入一次+1,释放锁-1)

 

4.如何查看线程死锁

  • linux中,可使用如下命令
 jstack 进程号
  • Mysql查询死锁
#1.查询是否锁表
show open tables where In_use >0;
#2.查询进程
show processlist;
#3.查看正在锁的事务
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
#4.查看等待锁的事务
select * from INFORMATION_SCHEMA.INNODB_LOCKS_WAITS;

 

5.Volatile和Synchronized

  • 区别:
    Synchronized关键字用来加锁。 Volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。
  • Volatile能不能保证线程安全?
    不能,volatile只能保证线程的可见性,不能保证原子性。
  • DLC 双重检测锁->成员变量为什么要加 volatile修饰?
    Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

 

6.JAVA线程锁机制是怎样的?

1、JAVA的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级
锁,重量级锁对应不同的锁状态。

markword问题参考 https://blog.csdn.net/evelynnJava/article/details/124028745

2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。

 

7.Sychronized的偏向锁、轻量级锁、重量级锁

需知:每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

  1. 偏向锁: 在锁对象的对象头中记录⼀下当前获取到该锁的线程ID(只是做了个标记),该线程下次如果⼜来获取该锁就可以直接获取到了。(在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,java6后加了该锁,也说明syn是可重入的。偏向锁可以降低无竞争开销,它不是互斥锁,不存在线程竞争的情况,省去了再次同步判断的步骤(获取锁),大大提升了程序运行性能)
  2. 轻量级锁: 由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
  3. 重量级锁: 如果轻量锁⾃旋次数过多仍然没有获取到锁(自旋耗cpu),则会升级为重量级锁,重量级锁会导致线程阻塞。(OS处理,串行)

⾃旋锁说明: ⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

流程图:
在这里插入图片描述
实际上,锁的细节更多,如图中有【匿名偏向锁】以及【偏向锁是否已启动】,此处提供一个demo说明。

//TimeUnit.SECONDS.sleep(5);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

说明: 如上案例,如果注释了sleep五秒,打印出来 00000001 00000000 00000000 00000000 (无锁状态) 和 01101000 11110001 11110101 00000000(轻量级锁) , 并不是说我们的理论不对, 而是 jvm内部会默认延迟 偏向锁启动,大概4秒, 此时结果就会如流程图所示。所以我们测试时需要放开 注释的部分!

延迟偏向原因:jvm启动明显是个多线程环境,而偏向锁机制本身就是考虑到大多场景下,锁只有一个线程获得。 所以这种明确多线程的场景下,还要给每个对象都贴个偏向锁的标志,浪费资源,所以jvm会延迟偏向锁)

放开后,第一个结果打印出 00000101 00000000 00000000 00000000( 低地址变成101->其他bit却都是0,表示匿名偏向锁) ,说明此时,jvm默认预备启动了一个偏向锁,还没有记录到线程id, 但是它随时等着别人来用它 , 第二句打印出 00000101 01000000 10000111 00000010(正常的偏向锁)

简单理解:syn最开始,没有线程进入时,为 无锁状态, 有1个线程,则变为偏向锁;有数个线程,则变为 轻量级锁 ( 该在 syn内, 其实有个 while循环 做自旋 , 除了一个进入的线程, 后面线程在 while中自旋等待, 但自旋到了一定次数后, 如果上一个进入的还没有释放锁, jvm觉得竞争有点激烈,会变为 重量级锁)

优化: 重量锁除锁粗化,锁消除(利用JIT)外,其实也可以用自旋优化,在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。 自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

 

8.谈谈你对AQS的理解。AQS如何实现可重入锁?

1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
2、 在AQS中,维护了一个信号量state和一个个线程组成的双向链表队列(用Node类封装每个线程,每个节点有head和tail指向前后)。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。 在不同的场景下,有不同的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。

符一张AQS设计的思想图:
****
 

9.三个重要的并发工具

1.有A,B,C三个线程,如何保证三个线程同时执行?

  • CountDownLatch

await()阻塞线程
countDown()令计数器减1,减到0时线程不再阻塞

场景1 让多个线程等待:模拟并发,让并发线程一起执行

public class CountDownLatchTest {
    
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    //准备完毕……运动员都阻塞在这,等待号令
                    countDownLatch.await();
                    String parter = "【" + Thread.currentThread().getName() + "】";
                    System.out.println(parter + "开始执行……");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        Thread.sleep(2000);// 裁判准备发令
        countDownLatch.countDown();// 发令枪:执行发令
    }
}

场景2 让单个线程等待:多个线程(任务)完成后,进行汇总合并

public class CountDownLatchTest2 {
    public static void main(String[] args) throws Exception {

        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
                    System.out.println(Thread.currentThread().getName()+" finish task" + index );

                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        // 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
        countDownLatch.await();
        System.out.println("主线程:在所有任务运行完成后,进行结果汇总");

    }
}
  1. 功能相比CountDownLatch 更强的栅栏,支持计数器重置等
  • CyclicBarrier

场景一 利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景

public class CyclicBarrierTest3 {

    public static void main(String[] args) {

        AtomicInteger counter = new AtomicInteger();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5, 5, 1000, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                (r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
                new ThreadPoolExecutor.AbortPolicy());

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
                () -> System.out.println("裁判:比赛开始~~"));

        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.submit(new MyThread(cyclicBarrier));
        }

    }
    static class MyThread extends Thread{
        private CyclicBarrier cyclicBarrier;
        public MyThread(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                int sleepMills = ThreadLocalRandom.current().nextInt(1000);
                Thread.sleep(sleepMills);
                System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
                cyclicBarrier.await();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
        }
    }

}

3.如何在并发情况下保证三个线程依次执行?

  • Semaphore

acquire() 表示阻塞并获取许可
release() 表示释放许可

场景一 三个线程顺序打印 One Two Three

//也可以用volatile修饰一个变量 int state,每个子线程去判断变量state=n时执行
public class SemaphoneTest1 {
    private static Semaphore s1= new Semaphore(1);
    private static Semaphore s2= new Semaphore(1);
    private static Semaphore s3= new Semaphore(1);

    public static void main(String[] args) throws InterruptedException {
        //s1 和 s2先调用一次
        s1.acquire();
        s2.acquire();
        //由于构造方法中为1,后面s1 和 s2再调 acquire会阻塞住,直到调用 release()方法
        new Thread(()->{
            while (true){
                try {
                    s1.acquire();
                    System.out.println("Two");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                s2.release();
            }
        }).start();
        new Thread(()->{
            while (true) {
                try {
                    s2.acquire();
                    System.out.println("Three");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                s3.release();
            }
        }).start();
        new Thread(()->{
            while (true) {
                try {
                    s3.acquire();
                    System.out.println("One");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                s1.release();
            }
        }).start();
    }
}
结果: One Two Three....顺序打印

场景二 信号量更多的用于限流,例同时只能处理5个请求的限流器

public class SemaphoneTest2 {

    /**
     * 实现一个同时只能处理5个请求的限流器
     */
    private static Semaphore semaphore = new Semaphore(5);

    /**
     * 定义一个线程池
     */
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));

    /**
     * 模拟执行方法
     */
    public static void exec() {
        try {
            semaphore.acquire(1);
            // 模拟真实方法执行
            System.out.println("执行exec方法" );
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            semaphore.release(1);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        {
            for (; ; ) {
                Thread.sleep(100);
                // 模拟请求以10个/s的速度
                executor.execute(() -> exec());
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值