【无标题】

java 大厂面试题

  • intern()
  • 两数之和
  • 可重入锁
    • LockSupport
      • 简单使用
      • 面试题
  • AbstractQueuedSynchronizer之AQS
    • 进一步理解锁和同步器的关系

出自 尚硅谷 大厂面试题3

intern()

public class StringPool58demo {
   public static void main(String[] args) {
       String s1 = new StringBuilder("j").append("ava").toString();
       System.out.println(s1);
       System.out.println(s1.intern());
       System.out.println(s1 == s1.intern()); // false

       String s2 = new StringBuilder("ali").append("baba").toString();
       System.out.println(s2);
       System.out.println(s2.intern());
       System.out.println(s2 == s2.intern()); // true
   }
}

why?
【无标题】_第1张图片

sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时他需要对静态常量字段根据指定常量值(ConstantValue)做默认初始化,此时被sun.misc.Version.launcher静态常量字段所引用的 “java” 字符创字面量就被intern到HotSpot VM的字符串常量池–StringTable里了

两数之和

【无标题】_第2张图片

 public static void main(String[] args) {
        int[] nums = new int[]{2, 7, 11, 15};
        int target = 9;
        int[] ints = twoSum(nums, target);
        System.out.println(Arrays.toString(ints));
    }

    public static int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int res = target - nums[i];
            if (map.containsKey(res)) {
                return new int[]{map.get(res), i};
            }
            map.put(nums[i], i);
        }
        return null;
    }
    

可重入锁


/**
 * 可重入锁 : 可重复递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
 *
 * 在一个Synchronized 修饰的方法或代码块的内部
 * 调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class SyncCode {
    public static void main(String[] args) {
        Object objectA = new Object();

        new Thread(()->{
            synchronized (objectA){
                System.out.println("outer");
                synchronized (objectA){
                    System.out.println("middler");
                    synchronized (objectA){
                        System.out.println("inner");
                    }
                }
            }
        },"t1").start();
    }
}

public class SyncCode {
    public synchronized static void m1( ) {
        System.out.println("=== 外");
        m2();
    }
    public synchronized static void m2( ) {
        System.out.println("=== 中");
        m3();
    }
    public synchronized static void m3( ) {
        System.out.println("=== 内");
    }
    public static void main(String[] args) {
         SyncCode.m1();
    }
}

原理
【无标题】_第3张图片
每个锁对象拥有一个锁计数器和一个指向持有该锁的指针

当执行monitorenter时,如果目标所对象的计数器为零,那么说明他没有被其他线程持有,java虚拟机会将该对象的持有线程设置为当前线程,并且将其计数器加1

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁

当执行monitorexit时,java虚拟机则需将锁计数器减1。计数器为零代表锁已释放

public class SyncCode {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try{
            System.out.println("==外层");
            lock.lock();
            try{
                System.out.println("==中层");
                lock.lock();
                try{
                    System.out.println("==内层");
                } finally{
                    lock.unlock();
                }

            } finally{
                lock.unlock();
            }

        }finally{
            lock.unlock();
        }

    }
}

LockSupport

线程等待唤醒机制(wait/notify)

LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程
请添加图片描述
synchronized 或 lock 约束

  1. 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  2. 必须要先等待后唤醒,线程才能被唤醒

LockSupport类中的park等待和unpark唤醒

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

LockSupport类使用了一种名为Permit(许可)的概念来做阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
permit只有两个值 1 和 0 ,默认是零
可以把许可看成(0,1)信号量(Semphore),但与Semaphore不同的是,许可的累加上线是1

简单使用

public class SyncCode {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "被唤醒");
        }, "a");
        a.start();
        Thread b = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ " 去唤醒");
            LockSupport.unpark(a);
        }, "b");
        b.start();
    }
}
public class SyncCode {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "被唤醒");
        }, "a");
        a.start();
        Thread b = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+ " 去唤醒");
            LockSupport.unpark(a);
        }, "b");
        b.start();
    }
}


正常+无锁块要求
之前错误的先唤醒后调用,LockSupport照样支持

形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个
当调用park方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出
  • 如果无凭证,就必须阻塞等待凭证可用
    而unpark则相反,他会增加一个凭证,但凭证最多只能有一个,累加无效

面试题

为什么可以先唤醒线程后阻塞线程?
因为unpark增加一个凭证,之后在调用park方法,就可以名正言顺的凭证消费,故不会阻塞
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两侧unpark和调用一次unpark效果是一样的,只会增加一个凭证;而调用两次park去需要消费两个凭证,证不够,不能放行

AbstractQueuedSynchronizer之AQS

是用来构建锁或者其他同步器组件的重量级基础框架及整个juc体系的基石,通过内置的FIFO 队列 来完成资源获取线程的排队工作,并通过一个 int类型的变量表示锁的持有状态
和AQS有关的
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore

进一步理解锁和同步器的关系

锁,面向锁的使用者,定义了程序员和锁的交互的使用层API,隐藏了实现细节,你调用即可
同步器,面向锁的实现者,比如java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等
【无标题】_第4张图片

如果共享资源被占用,就需要一定的阻塞等待唤醒机制保证锁分配,这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。他将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋锁以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的控制效果
【无标题】_第5张图片

【无标题】_第6张图片

你可能感兴趣的:(java,java)