java基础面试题

240 阅读33分钟

Java面试

第一次写,主要整理一些面试题,题目是从https://juejin.cn/post/6844903567338242061#heading-1选一部分的。

基础

  • 面向对象的特征?

封装:

把客观事物封装成抽象的类,并且把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。也 就 是 说抽象数据类型对数据信息以及对数据的操作进行打包,将其变成一个不可分割 的实体,在这个实体内部,我们对数据进行隐藏和保密,只留下一些接口供外部调用。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

继承:

继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。

在本职上是特殊——一般的关系,即常说的is-a关系。子类继承父类,表明子类是一种特殊的父类,并且具有父类所不具有的一些属性或方法。从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性 ,当实现类用extends关键字继承了基类(父类)后,实现类就具备了这些相同的属性。

继承的类叫做子类(派生类或者超类),被继承的类叫做父类(或者基类)。

多态:

多态是同一个行为具有多个不同表现形式或形态的能力。

多态就是同一个接口,使用不同的实例而执行不同操作。

多态性是对象多种表现形式的体现。

多态的优点:

  1.消除类型之间的耦合关系

  2.可替换性

  3.可扩充性

  4.接口性

  5.灵活性

  6.简化性

 多态存在的三个必要条件

  1.继承

  2.重写

  3.父类引用指向子类对象

public class Main {
    Animal dog = new Dog();
    dog.eat();
    Animal cat = new Cat();
    cat.eat();
    Animal unknowAnimal = new UnknowAnimal();
    unknowAnimal.eat();
}

public class Animal {
    public void eat () {
        System.out.println("我不知道吃什么~~");
    }
}

public class Dog extends Animal{
    public viod eat () {
        System.out.println("吃狗粮");
    }
}

public class Cat extends Animal{
    public void eat () {
    System.out.println("吃猫粮");
}

public void UnknowAnimal extends Animal {
    
}
  • final, finally, finalize 的区别?

final: 是一个修饰符,修饰类表示该类不可被继承,修饰方法表示该类不可修改,修饰变量表示该类不可被修改

finally: 与try结合使用,无论程序中有无catch,都会执行finally中的代码

finalize: 是垃圾回收机制,不过一般不手动释放

  • int 与 Integer的区别?
int integer
基本类型 包装类型
速度快 速度较慢
在栈中 在堆中
初始值为 0 初始值为null

1.new Integer生成的是两个对象,内存地址不同.

2.包装类Integer和基本数据类型int比较时,java会自动包装为int

3.非new生成的integer变量指向的是静态常量池中的cache数组中存储的指向Integer对象;而new integer生成的变量指向堆中,两者在内存中的对象引用地址不同。

4.Integer的值会进行缓存[-128到127]之外的数,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个Integer对象。

5.int可以自动装箱变成integer类型, integer可以自动拆箱成int类型

  • 重载和重写的区别?

重载 Overload

表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。

重写 Override

表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是private类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。

  • 抽象类和接口有什么区别?

抽象类:

1、抽象类使用abstract修饰;

2、抽象类不能实例化,即不能使用new关键字来实例化对象;

3、含有抽象方法(使用abstract关键字修饰的方法)的类是抽象类,必须使用abstract关键字修饰;

4、抽象类可以含有抽象方法,也可以不包含抽象方法,抽象类中可以有具体的方法;

5、如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类;

6、抽象类中的抽象方法只有方法体,没有具体实现;

接口

1、接口使用interface修饰;

2、接口不能被实例化;

3、一个类只能继承一个类,但是可以实现多个接口;

4、接口中方法均为抽象方法;

5、接口中不能包含实例域或静态方法(静态方法必须实现,接口中方法是抽象方法,不能实现)

  • 说说反射的用途及实现

概念:

当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为 Java 并不是动态语言,但是它却又一个非常突出的动态相关的机制,俗称:反射。 Reflection 是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类和对象的内部属性。

用途:

①、在运行时判断任意一个对象所属的类

②、在运行时构造任意一个类的对象

③、在运行时判断任意一个类所具有的成员变量和方法(通过反射设置可以调用 private)

④、在运行时调用人一个对象的方法

⑤、获得 Class 对象

(1)使用 Class类的 forName() 静态方法:

public static Class<?> forName(String className)

…… 在JDBC开发中常用此方法加载数据库驱动: ……java Class.forName(driver)

(2)、直接获取某一个对象的 class,比如:

Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;

(3)、调用某个对象的getClass() 方法,比如:

StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();

⑥、判断是否为某个类的实例

一般地,我们用 instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的 isInstance()方法来判断是否为某个类的实例,它是一个 Native 方法:

  public native boolean isInstance(Object obj);

实现:

通过反射来生成对象主要有两种方式。

(1)使用 Class 对象的 newInstance() 方法来创建对象对应类的实例。

Class<?> c  = String.calss;
Object str = c.getInstance();

(2)、先通过 Class 对象获取制定的 Constructor 对象,在调用 Constructor 对象的 newInstance() 方法来创建实例。这种方法可以用指定的构造器构造类的实例。

  //获取String所对应的Class对象
   Class<?> c = String.class;
  //获取String类带一个String参数的构造器
  Constructor constructor = c.getConstructor(String.class);
  //根据构造器创建实例
  Object obj = constructor.newInstance("23333");
  System.out.println(obj);

  • 说说自定义注解的场景及实现

1、什么是注解?

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些数据。 Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java注解可以通过反射获取注解内容。在编译器生成类文件时,注解可以被嵌入到字节码中。Java虚拟机可以保留注解内容,在运行时可以获取到注解内容。

注解本身没有具体的功能,它相当于一个标注,而这个标注具体的作用和意义需要我们自己实现。一般都是先判断类或属性是否被该注解修饰再通过反射来获取注解属性再实现具体业务功能。

2.注解的分类

注解大体上分为三种:标记注解一般注解元注解@Override用于标识,该方法是继承自超类的。这样,当超类的方法修改后,实现类就可以直接看到了。而 @Deprecated注解,则是标识当前方法或者类已经不推荐使用,如果用户还是要使用,会生成编译的警告。

3.使用场景?

登陆、权限拦截、日志处理,以及各种 Java 框架,如 Spring,Hibernate,JUnit 提到注解就不能不说反射,Java 自定义注解是通过运行时靠反射获取注解

4.实现

内置注解 Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。 1、作用在代码的注解是

@Override - 检查该方法是否是重载方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

@Deprecated - 标记过时方法。如果使用该方法,会报编译警告。

@SuppressWarnings - 指示编译器去忽略注解中声明的警告。

2、作用在其他注解的注解(或者说元注解)是:

@Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。

@Documented - 标记这些注解是否包含在用户文档中。

@Target - 标记这个注解应该是哪种 Java 成员。

@Inherited - 标记这个注解是继承于哪个注解类(默认注解并没有继承于任何子类)

3、从 Java 7 开始,额外添加了 3 个注解:

@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。

@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

元注解

1、@Retention

@Retention annotation指定标记注释的存储方式:

RetentionPolicy.SOURCE - 标记的注释仅保留在源级别中,并由编译器忽略。

RetentionPolicy.CLASS - 标记的注释在编译时由编译器保留,但Java虚拟机(JVM)会忽略。

RetentionPolicy.RUNTIME - 标记的注释由JVM保留,因此运行时环境可以使用它。

**2、@Documented **

@Documented 注释表明,无论何时使用指定的注释,都应使用Javadoc工具记录这些元素(默认情况下,注释不包含在Javadoc中)。有关更多信息,请参阅 Javadoc工具页面。

3、@Target

@Target 注释标记另一个注释,以限制可以应用注释的Java元素类型。目标注释指定以下元素类型之一作为其值。

ElementType.TYPE 可以应用于类的任何元素。

ElementType.FIELD 可以应用于字段或属性。

ElementType.METHOD 可以应用于方法级注释。

ElementType.PARAMETER 可以应用于方法的参数。

ElementType.CONSTRUCTOR 可以应用于构造函数。

ElementType.LOCAL_VARIABLE 可以应用于局部变量。

ElementType.ANNOTATION_TYPE 可以应用于注释类型。

ElementType.PACKAGE 可以应用于包声明。

ElementType.TYPE_PARAMETER

ElementType.TYPE_USE

4、@Inherited

@Inherited 注释表明注释类型可以从超类继承。当用户查询注释类型并且该类没有此类型的注释时,将查询类的超类以获取注释类型(默认情况下不是这样)。此注释仅适用于类声明。

5、@Repeatable

Repeatable Java SE 8中引入的,@Repeatable注释表明标记的注释可以多次应用于相同的声明或类型使用(即可以重复在同一个类、方法、属性等上使用)。

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface myAnnotation {
    
}
  • HTTP 请求的 GET 与 POST 方式的区别

1.get是从服务器上获取数据,post是向服务器传送数据。

2.get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。post是通过HTTPpost机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。

3.对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。

4.get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB。

5.get安全性非常低,post安全性较高。

  • session 与 cookie 区别

(1)Cookie以文本文件格式存储在浏览器中,而session存储在服务端它存储了限制数据量。它只允许4kb它没有在cookie中保存多个变量。

(2)cookie的存储限制了数据量,只允许4KB,而session是无限量的

(3)我们可以轻松访问cookie值但是我们无法轻松访问会话值,因此它更安全

(4)设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。

  • session 分布式处理

第一种:粘性session

原理:粘性Session是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了A服务器上,如果负载均衡器设置了粘性Session的话,那么用户以后的每次请求都会转发到A服务器上,相当于把用户和A服务器粘到了一块,这就是粘性Session机制。

优点:简单,不需要对session做任何处理。

缺点:缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的session信息都将失效。

适用场景:发生故障对客户产生的影响较小;服务器发生故障是低概率事件。

实现方式:以Nginx为例,在upstream模块配置ip_hash属性即可实现粘性Session。

第二种:服务器session复制

原理:任何一个服务器上的session发生改变(增删改),该节点会把这个 session的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要session,以此来保证Session同步。 优点:可容错,各个服务器间session能够实时响应。

缺点:会对网络负荷造成一定压力,如果session量大的话可能会造成网络堵塞,拖慢服务器性能。

实现方式:

① 设置tomcat ,server.xml 开启tomcat集群功能

Address:填写本机ip即可,设置端口号,预防端口冲突。

② 在应用里增加信息:通知应用当前处于集群环境中,支持分布式 在web.xml中添加选项

第三种:session共享机制

使用分布式缓存方案比如memcached、Redis,但是要求Memcached或Redis必须是集群。

使用Session共享也分两种机制,两种情况如下:

① 粘性session处理方式

原理:不同的 tomcat指定访问不同的主memcached。多个Memcached之间信息是同步的,能主从备份和高可用。用户访问时首先在tomcat中创建session,然后将session复制一份放到它对应的memcahed上。memcache只起备份作用,读写都在tomcat上。当某一个tomcat挂掉后,集群将用户的访问定位到备tomcat上,然后根据cookie中存储的SessionId找session,找不到时,再去相应的memcached上去session,找到之后将其复制到备tomcat上。

第四种:session持久化到数据库

原理:就不用多说了吧,拿出一个数据库,专门用来存储session信息。保证session的持久化。

优点:服务器出现问题,session不会丢失

缺点:如果网站的访问量很大,把session存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

第五种terracotta实现session复制

原理:Terracotta的基本原理是对于集群间共享的数据,当在一个节点发生变化的时候,Terracotta只把变化的部分发送给Terracotta服务器,然后由服务器把它转发给真正需要这个数据的节点。可以看成是对第二种方案的优化。

  • JDBC 流程

1、加载驱动 2、连接数据库 3、编写sql语句 4、执行sql语句

Class.forName("com.mysql.jsbc.Driver");
Connection conn = Driver.getConnection(url, usrename, password);
PreparedStatement pre = conn.preparedStatement(sql);
pre.executeQuery();
  • MVC 设计思想

MVC(Model View Controller)是一种软件设计的框架模式,它采用模型(Model)-视图(View)-控制器(controller)的方法把业务逻辑、数据与界面显示分离。把众多的业务逻辑聚集到一个部件里面,当然这种比较官方的解释是不能让我们足够清晰的理解什么是MVC的。用通俗的话来讲,MVC的理念就是把数据处理、数据展示(界面)和程序/用户的交互三者分离开的一种编程模式。

注意!MVC不是设计模式!

MVC框架模式是一种复合模式,MVC的三个核心部件分别是

1:Model(模型):所有的用户数据、状态以及程序逻辑,独立于视图和控制器

2:View(视图):呈现模型,类似于Web程序中的界面,视图会从模型中拿到需要展现的状态以及数据,对于相同的数据可以有多

3:Controller(控制器):负责获取用户的输入信息,进行解析并反馈给模型,通常情况下一个视图具有一个控制器

  • equals 与 == 的区别

equals()

方法用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断;

"=="

比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。

集合

  • List 和 Set 区别
  1. list有序,set无序
  2. list可重复,set不可有重复元素
  3. list中元素可为空,set中不可
  • List 和 Map 区别

List:是存储单列数据的集合,存储的数据是有序并且是可以重复的

Map:存储双列数据的集合,通过键值对存储数据,存储 的数据是无序的,Key值不能重复,value值可以重复

  • Arraylist 与 LinkedList 区别

ArrayList和LinkedList的大致区别:

1.ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。

2.对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。

3.对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

他们在性能上的有缺点:

1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。

2.在ArrayList集合中添加或者删除一个元素时,当前的列表所所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。

3.LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。

4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

  • ArrayList 与 Vector 区别
  1. Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比;

  2. ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;

  3. Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。

  • HashMap 和 Hashtable 的区别
HashMap Hashtable
1. key有一个可为空 1. key不可为空
2. 父类是AbstractMap 2. 父类是Dictionary
3. 线程安全 3. 线程不安全
4. 初始化为16,每次扩容为上次的2倍  4. 初始化为11,每次扩容为上次的2n+1
5. 计算哈希值方法不同
  • HashSet 和 HashSet 区别
HashSet HashMap
1. 实现了set接口 1. 实现了map接口
2. 存储的是对象 2. 存储的是键值对
3. 使用add()方法把对象添加到set中 3. 使用put()方法将键值对添加到map中
4. 用hashCode()计算hash值,相等的话再用equals()判断对象是否相等  4. 使用键对象来计算hashCode()
5. 对比hashmap较慢 较快
  • HashMap 和 ConcurrentHashMap 的区别

  • HashMap 的工作原理及代码实现

  • ConcurrentHashMap 的工作原理及代码实现

线程

  • 创建线程的方式及实现
  1. Thread

继承 Thread,重写run()方法,在创建对象调用start()启动线程

public class ThreadTest extends Thread{

    @Override
    public void run() {
        System.out.println("thread线程··");
    }

    public static void main(String[] args) {
       new ThreadTest().start();
    }
}

  1. 实现Runnable

实现Runable接口,重写run()方法,创建本类实例作为Thread的参数,再通过thread。start()启动线程

public class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println("runnable线程");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableTest());
        thread.start();
    }
}
  1. 使用Callable和Future 实现Callable和Future实现线程
public static void main(String[] args) {

        FutureTask<Integer> future = new FutureTask<>(
                // Callable中call()方法的lambda,返回数字6
                () -> 6
        );
        // 实际以Callable对象启动线程
        new Thread(future, "线程1").start();
        try {
            // get()方法会阻塞,知道线程执行完成
            System.out.println("结果:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • sleep() 、join()、yield()有什么区别

Thread类常用函数及功能

  1. sleep()

sleep()使当前线程停滞,sleep()线程在指定时间内不会被执行,而且也不会释放锁

sleep()下高的优先级和低优先级线程都有相同执行的机会

  1. yield()

yeild()使线程回到可执行状态,线程可能从可行性状态变成执行状态

线程优先级高的先执行,而且也不会释放锁

public class SleepAndYieldComparetor extends Thread {

    String name;

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            Thread.yield(); // Thread.sleep()
            System.out.print(name + "" + i + "   ");
        }
    }

    public static void main(String[] args) {
        SleepAndYieldComparetor one = new SleepAndYieldComparetor();
        SleepAndYieldComparetor two = new SleepAndYieldComparetor();
        one.name = "张三";
        two.name = "李四";
        one.setPriority(Thread.MAX_PRIORITY);
        two.setPriority(Thread.MIN_PRIORITY);
        two.start();
        one.start();
    }
}

  1. stop()

使当前执行的线程立即停止下来,会导致后面的代码不能被执行到,可能会出现部分数据缺失,资源不能被释放,不推荐使用

  1. interrupt()

一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序.

              Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态,

             两个解释为什么会相冲突呢, 一个解释是可以中断一个线程. 一个解释说不会中断一个正在运行的线程.  仔细看会发现其中的奥秒. interrupt方法不会中断一个正在运行的线程.就是指线程如果正在运行的过程中, 去调用此方法是没有任何反应的. 为什么呢, 因为这个方法只是提供给 被阻塞的线程, 即当线程调用了.Object.wait, Thread.join, Thread.sleep三种方法之一的时候, 再调用interrupt方法, 才可以中断刚才的阻塞而继续去执行线程.
  1. *join()

join(0) 等待一个线程直到死亡,join(100)等待100个线程死亡

  1. suspend(), resume()

线程同步

synchronized, wait, notify

线程的同步需要依靠上面两个函数和一个同步块实现.

   同步, 即两个线程为了一件事情协同合作执行. 例如, 缓冲区的读写操作. 在实际应用中, 例如:/

       有一个缓冲区, 由两个线程进行操作, 一个写线程, 一个读线程. 写线程完成对缓冲区的写入, 读线程完成对线程的读取, 只有当缓冲区写入数据时, 读线程才可以读取, 同样. 只有当缘冲区的数据读出时,  写线程才可以向缘冲区写入数据.

wait()方法

  在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。   当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。   唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。   waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通 过,但在运行时会发生IllegalMonitorStateException的异常。

线程的同步需要依靠上面两个函数和一个同步块实现.

   同步, 即两个线程为了一件事情协同合作执行. 例如, 缓冲区的读写操作. 在实际应用中, 例如:/

       有一个缓冲区, 由两个线程进行操作, 一个写线程, 一个读线程. 写线程完成对缓冲区的写入, 读线程完成对线程的读取, 只有当缓冲区写入数据时, 读线程才可以读取, 同样. 只有当缘冲区的数据读出时,  写线程才可以向缘冲区写入数据.

这两个是JDK的过期方法. suspend()函数,可使线程进入停滞状态. 通过suspend()使线程进入停滞状态后,除非收到resume()消息,否则该线程不会变回可执行状态

  • 说说 CountDownLatch (共享锁)原理

CountDownLatch在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,并且其操作都是原子操作,该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。比如下面的例子就是有两个操作,一个是读操作一个是写操作,现在规定必须进行完写操作才能进行读操作。所以当最开始调用读操作时,需要用await()方法使其阻塞,当写操作结束时,则需要使count等于0。因此count的初始值可以定为写操作的记录数,这样便可以使得进行完写操作,然后进行读操作。

  • 说说 CyclicBarrier 原理

栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

  • 说说 Semaphore 原理

实现了经典的信号量机制。信号量其实是一个非负整数表示可用共享资源的数目,主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。其内部是用一个计数器控制对共享资源的访问,计数据大于0资源允许访问,等于0是拒绝访问共享资源的,在访问共享资源前要先调用acquire方法,如果计数据大于0则可以访问资源,访问后要调用release方法释放共享资源,如果等于0则调用线程阻塞于此方法。

首先可以肯定一个简单的计数器是不能实现同步的,因为计数器本身的同步也是需要解决的,Semaphore是其通一个链式的queue来实现的AbstractQueuedSynchronizer,其中一个成员是private volatile int state;调用acquire方法会检查是否大于0,调用release方法会使state加1,AbstractQueuedSynchronizer其中有一个内部类Node,Node有一个成员Thread,这个成员就是存储调acquire阻塞时的线程,所以AbstractQueuedSynchronizer其实是一个包含计数器的,线程链式队列

  • 说说 Exchanger 原理

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的。

Exchanger类提供了两个方法,String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待。

  • 说说 CountDownLatch 与 CyclicBarrier 区别

CountDownLatch是不可重置的,所以无法重用;而CyclicBarrier则没有这种限制,可以重用。 CountDownLatch的基本操作组合是countDown/await。调用await的线程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程里countDown,只要次数足够即可。所以说CountDownLatch操作的是事件。 CyclicBarrier的基本操作组合,则就是await。当所有的伙伴(parties)都调用了await,才会继续进行任务,并自动进行重置。注意,正常情况下,CyclicBarrier的重置都是自动发生的,如果我们调用reset方法,但还有线程在等待,就会导致等待线程被打扰,抛出BrokenBarrierException异常。CyclicBarrier侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。

  • ThreadLocal 原理分析

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

原理

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:

  • 讲讲线程池的实现原理

线程池的几个主要参数的作用

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize: 规定线程池有几个线程(worker)在运行。
  2. maximumPoolSize: 当workQueue满了,不能添加任务的时候,这个参数才会生效。规定线程池最多只能有多少个线程(worker)在执行。
  3. keepAliveTime: 超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并且超过了keepAliveTime设定的时间,就会消亡。
  4. unit: 生存时间对于的单位
  5. workQueue: 存放任务的队列
  6. threadFactory: 创建线程的工厂
  7. handler: 当workQueue已经满了,并且线程池线程数已经达到maximumPoolSize,将执行拒绝策略。

任务提交后的流程分析

用户通过submit提交一个任务。线程池会执行如下流程:

判断当前运行的worker数量是否超过corePoolSize,如果不超过corePoolSize。就创建一个worker直接执行该任务。—— 线程池最开始是没有worker在运行的 如果正在运行的worker数量超过或者等于corePoolSize,那么就将该任务加入到workQueue队列中去。 如果workQueue队列满了,也就是offer方法返回false的话,就检查当前运行的worker数量是否小于maximumPoolSize,如果小于就创建一个worker直接执行该任务。 如果当前运行的worker数量是否大于等于maximumPoolSize,那么就执行RejectedExecutionHandler来拒绝这个任务的提交。

  • 线程池的几种方式

1、newFixedThreadPool

它有两个重载方法,代码如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
 }

建立一个线程数量固定的线程池,规定的最大线程数量,超过这个数量之后进来的任务,会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。

创建固定线程数量线程池, corePoolSize 和 maximumPoolSize 要一致,即核心线程数和最大线程数(核心+非核心线程)一致,Executors 默认使用的是 LinkedBlockingQueue 作为等待队列,这是一个无界队列,这也是使用它的风险所在,除非你能保证提交的任务不会无节制的增长,否则不要使用无界队列,这样有可能造成等待队列无限增加,造成 OOM。

正确的创建固定线程数线程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

public static ExecutorService createFixedThreadPool() {
        int poolSize = 5;
        int queueSize = 10;
        ExecutorService executorService = new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize), threadFactory, new ThreadPoolExecutor.AbortPolicy());
        return executorService;
    }

上面代码是创建一个 5 个线程的固定数量线程池,这里线程存活时间没有作用,所以设置为 0,使用了 ArrayBlockingQueue 作为等待队列,设置长度为 10 ,最多允许10个等待任务,超过的任务会执行默认的 AbortPolicy 策略,也就是直接抛异常。ThreadFactory 使用了 Guava 库提供的方法,定义了线程名称,方便之后排查问题。

2、newSingleThreadExecutor

建立一个只有一个线程的线程池,如果有超过一个任务进来,只有一个可以执行,其余的都会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。使用 LinkedBlockingQueue 作为等待队列。

这个方法同样存在等待队列无限长的问题,容易造成 OOM,所以正确的创建方式参考上面固定数量线程池创建的方式,只是把 poolSize 设置为 1 。

3、newCachedThreadPool

缓存型线程池,在核心线程达到最大值之前,有任务进来就会创建新的核心线程,并加入核心线程池,即时有空闲的线程,也不会复用。达到最大核心线程数后,新任务进来,如果有空闲线程,则直接拿来使用,如果没有空闲线程,则新建临时线程。并且线程的允许空闲时间都很短,如果超过空闲时间没有活动,则销毁临时线程。关键点就在于它使用 SynchronousQueue 作为等待队列,它不会保留任务,新任务进来后,直接创建临时线程处理,这样一来,也就容易造成无限制的创建线程,造成 OOM。

正确的创建缓存型线程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

    public static ExecutorService createCacheThreadPool(){
        int coreSize = 10;
        int maxSize = 20;
        return new ThreadPoolExecutor(coreSize, maxSize, 10L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), threadFactory, new ThreadPoolExecutor.AbortPolicy());
    }

4、newScheduledThreadPool

计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池。使用的是 DelayedWorkQueue 作为等待队列,这中类型的队列会保证只有到了指定的延时时间,才会执行任务。

正确的创建缓存型线程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(2, threadFactory);
        executorService.scheduleAtFixedRate(task,0L,5L, TimeUnit.SECONDS);
        latch.await();
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "executing");
        }
    } 
  • 线程的生命周期