Java·JVM·JUC(五)

什么是设计模式,描述几个常用的设计模式

什么是设计模式

设计模式就是经过前任无数次的实践总结出的,设计过程中可以反复使用的,可以解决特定问题的设计方法。

常用的设计模式

单例模式(懒汉式、饿汉式)

步骤:
1、构造方法私有化,让除了自己类能创建,其他类都不能创建。
2、在自己的类中创建一个单实例(懒汉模式是在需要的时候才创建,饿汉模式是一开始就创建)
3、提供一个方法获取该实例对象

工厂模式

spring IOC就是使用了工厂模式,对象的创建都交给一个工厂去创建。

代理模式

spring AOP就是使用的动态代理模式。一般是指接口的实现。

观察者模式

对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理,看代码:

public interface Person {
//小王和小李通过这个接口可以接收到小美发过来的消息
void getMessage(String s);
}

这个接口相当于小王和小李的电话号码,小美发送通知的时候就会拨打getMessage这个电话,拨打电话就是调用接口,看不懂没关系,先往下看

public class LaoWang implements Person {

private String name = "小王";

public LaoWang() {
}

@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);
}

}

public class LaoLi implements Person {

private String name = "小李";

public LaoLi() {
}

@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);
}

}

代码很简单,我们再看看小美的代码:

public class XiaoMei {
List<Person> list = new ArrayList<Person>();
public XiaoMei(){
}

public void addPerson(Person person){
list.add(person);
}

//遍历list,把自己的通知发送给所有暗恋自己的人
public void notifyPerson() {
for(Person person:list){
person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");
}
}
}

我们写一个测试类来看一下结果对不对

public class Test {
public static void main(String[] args) {

XiaoMei xiao_mei = new XiaoMei();
LaoWang lao_wang = new LaoWang();
LaoLi lao_li = new LaoLi();

//小王和小李在小美那里都注册了一下
xiao_mei.addPerson(lao_wang);
xiao_mei.addPerson(lao_li);

//小美向小王和小李发送通知
xiao_mei.notifyPerson();
}
}

装饰者模式

对已有的业务逻辑进一步的封装,使其增加额外的功能,如Java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。那我们应该怎么来写代码呢?首先,我们需要写一个Food类,让其他所有食物都来继承这个类,看代码:

public class Food {

private String food_name;

public Food() {
}

public Food(String food_name) {
this.food_name = food_name;
}

public String make() {
return food_name;
};
}

代码很简单,我就不解释了,然后我们写几个子类继承它:

//面包类
public class Bread extends Food {

private Food basic_food;

public Bread(Food basic_food) {
this.basic_food = basic_food;
}

public String make() {
return basic_food.make()+"+面包";
}
}

//奶油类
public class Cream extends Food {

private Food basic_food;

public Cream(Food basic_food) {
this.basic_food = basic_food;
}

public String make() {
return basic_food.make()+"+奶油";
}
}

//蔬菜类
public class Vegetable extends Food {

private Food basic_food;

public Vegetable(Food basic_food) {
this.basic_food = basic_food;
}

public String make() {
return basic_food.make()+"+蔬菜";
}

}

这几个类都是差不多的,构造方法传入一个Food类型的参数,然后在make方法中加入一些自己的逻辑,如果你还是看不懂为什么这么写,不急,你看看我的Test类是怎么写的,一看你就明白了

public class Test {
public static void main(String[] args) {
Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));
System.out.println(food.make());
}
}

看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包。

简述 CMS 与 G1 机制的区别

区别一: 使用范围不一样

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

区别二: STW的时间

CMS收集器以最小的停顿时间为目标的收集器。

G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

区别三: 垃圾碎片

CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片

G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

集合类中的 List 和 Map 的线程安全版本是什么,如何保证线程安全的?

  1. 线程安全与线程不安全集合
    集合类型中存在线程安全与线程不安全的两种,常见例如:
    ArrayList —– Vector
    HashMap —–HashTable
    但是以上都是通过 synchronized 关键字实现,效率较低

  2. Collections 构建的线程安全集合,Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的,如Collections.synchronizedList()

  3. java.util.concurrent 并发包下
    CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全

Java 的线程有哪些状态,转换关系是怎么样的?

  1. 新建(NEW):新创建了一个线程对象。

  2. 可运行(RUNNABLE**)**:线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

简述 Java AQS 的原理以及使用场景

AQS核心思想

如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

Java 异常有哪些类型?

image-20220411090142921

可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)

  • 可查异常(编译器要求必须处置的异常):

正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

  • 不可查异常(编译器不要求强制处置的异常)

包括运行时异常(RuntimeException与其子类)和错误(Error)。

hashmap 和 hashtable 的区别是什么?

  1. 线程安全性不同。HashMap线程不安全;Hashtable 中的方法是Synchronize的。
  2. key、value是否允许null。HashMap的key和value都是可以是null,key只允许一个null;Hashtable的key和value都不可为null。
  3. 迭代器不同。HashMap的Iterator是fail-fast迭代器;Hashtable还使用了enumerator迭代器。
  4. hash的计算方式不同。HashMap计算了hash值;Hashtable使用了key的hashCode方法。
  5. 默认初始大小和扩容方式不同。HashMap默认初始大小16,容量必须是2的整数次幂,扩容时将容量变为原来的2倍;Hashtable默认初始大小11,扩容时将容量变为原来的2倍加1。
  6. 是否有contains方法。HashMap没有contains方法;Hashtable包含contains方法,类似于containsValue。
  7. 父类不同。HashMap继承自AbstractMap;Hashtable继承自Dictionary。

简述 Spring 注解的实现原理

注解如何实现

1、接口使用****@interface****定义。

2、通过继承以下注解,实现功能:

元注解**@Target,@Retention,@Documented,@Inherited**

Spring注解的实现原理

【两种处理策略】
(1)类级别的注解:如@Component、@Repository、@Controller、@Service以及JavaEE6的@ManagedBean和@Named注解,都是添加在类上面的类级别注解。
Spring容器根据注解的过滤规则扫描读取注解Bean定义类,并将其注册到Spring IoC容器中。

(2)类内部的注解:如@Autowire、@Value、@Resource以及EJB和WebService相关的注解等,都是添加在类内部的字段或者方法上的类内部注解。
SpringIoC容器通过Bean后置注解处理器解析Bean内部的注解。

Spring 3.0 新增AnnotationConfigApplicationContext注解处理器实现原理

AnnotationConfigApplicationContext注解处理器是Spring boot提供的一个比较常用的注解处理器。我们先看看它的工作原理。

第一步:初始化读取器AnnotationBeanDefinitionReader

第二步:初始化扫描器AnnotationBeanDefinitionScanner

第三步:调用父GenericApplicationContext的无参构造方法初始化一个BeanFactory。这里初始化使用的是DefaultListableBeanFactory。

第四步:注册Bean配置类

第五步:刷新上下文

Java 线程和操作系统的线程是怎么对应的?Java线程是怎样进行调度的?

Java 线程和操作系统的线程是怎么对应

从实际意义上来讲,操作系统中的线程除去new和terminated状态,一个线程真实存在的状态,只有:

  • ready:表示线程已经被创建,正在等待系统调度分配CPU使用权。
  • running:表示线程获得了CPU使用权,正在进行运算
  • waiting:表示线程等待(或者说挂起),让出CPU资源给其他线程使用

对于Java中的线程状态:

无论是Timed WaitingWaiting还是Blocked,对应的都是操作系统线程的**waiting(等待**)状态。
Runnable状态,则对应了操作系统中的readyrunning状态。

JVM在设计上,就已经声明:

虚拟机中的线程状态,不反应任何操作系统线程状态

Java线程是怎样进行调度的

  1. Java在调度机制上采用的时抢占式的线程调度机制。

  2. Java线程在运行的过程中多个线程之间式协作式的。

第一个《深入理解Java虚拟机》一书中所说的Java采用抢占式线程调度,是对的,但是他是站在JVM的层面上说的,是站在线程调度的角度来说的,确实,在JVM层面上,我们的线程都是由JVM负责调度与打断的,当某个线程的时间片用完了,JVM就会剥夺其对CPU的使用权,交给另一个线程使用(这里就可以理解为抢占)。

第二个,说Java线程是协作式的,也是对的,但是他是站在Java api的层面上来说的,是站在多线程并发运行的角度来说的,确实,你会发现,一个线程调用另一个线程的interrupt() 方法中断线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。如果真的想要中断当前线程,我们可以对其“中断标志位”进行判断,如果是true我们就在代码层面上停止当前线程的执行逻辑。这里就充分的体现了线程协作的思想,“你不能强行中断我,只要我主动放弃你才能占有我的cpu”

Java 线程池里的 arrayblockingqueue 与 linkedblockingqueue 的使用场景和区别

BlockingQueue接口定义了一种阻塞的FIFO queue,每一个BlockingQueue都有一个容量,让容量满时往BlockingQueue中添加数据时会造成阻塞,当容量为空时取元素操作会阻塞。

ArrayBlockingQueue是一个由数组支持的有界阻塞队列。在读写操作上都需要锁住整个容器,因此吞吐量与一般的实现是相似的,适合于实现“生产者消费者”模式。

ArrayBlockingQueue与LinkedBlockingQueue实现

ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:

LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE

ArrayBlockingQueue与LinkedBlockingQueue的比较

相同点:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;

不同点

  1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构;
  2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLocktakeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。

题目答案均为转载,题目先后顺序按各大厂总出题次数排列!