java面试题之-javase篇(持续更新)

一、基础

01、变量,常量

Java的数据类型分为两大类:

  • 基本数据类型:包括数值类型 、字符类型 、 布尔类型 。

    数值类型:字节类型(byte),短整型(short),整形(int),长整型(long),单精度(float),双精度(double)

    内存占用:字节类型(1字节),短整型(2字节),整形(4字节),长整型(8字节),单精度(4字节),双精度(8字节)

    初值值:字节类型(0),短整型(0),整形(0),长整型(0),单精度(0.0),双精度(0.0)

    字符类型:字符类型(char),内存占用(2字节),初始值(null)

    布尔类型:布尔类型(boolean),内存占用(1字节),初始值(false)

  • 引用数据类型:包括 类 、 数组 、 接口 。

02、两个小面试题

+=符号的扩展

public static void main(String[] args){
	short s = 1;
	s+=1;
	System.out.println(s);
}
分析:
s += 1 逻辑上可以看作是 s = s + 1 计算结果被提升为int类型,再向short类型赋值时发生错误,因为不能将取值范围大的类型赋值到取值范围小的类型。
但是, s=s+1是进行两次运算,+= 是一个运算符,只运算一次,并带有强制转换的特点,s += 1 就是 s = (short)(s + 1) ,因此程序没有问题编译通过,运行结果是2

常量和变量的运算

public static void main(String[] args){
	byte b1=1;
	byte b2=2;
	byte b3=1 + 2;
	byte b4=b1 + b2;
	System.out.println(b3);//正确
	System.out.println(b4);//错误
}
分析: 
b3 = 1 + 2 , 1 和 2 是常量 ,为固定不变的数据,在编译的时候(编译器javac),已经确定了 1+2 的结果并没有超过byte类型的取值范围,可以赋值给变量 b3 ,因此 b3=1 + 2 是正确的。
反之,b4 = b2 + b3,b2 和 b3 是变量,变量的值是可能变化的,在编译的时候,编译器javac不确定b1+b2的结果是什么(两个byte类型的数据相加结果可能会超出byte类型所能表示的范围,因此编译器会将结果以int类型处理)因此会将结果以int类型进行处理,所以int类型不能赋值给byte类型,因此编译失败

03、数组

数组定义的三种方式“

//方式一:数组存储的数据类型[] 数组名字 = new 数组存储的数据类型[长度];

//方式二:数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};

//方式三:数据类型[] 数组名 = {元素1,元素2,元素3...};

//注意:数组中只能保存类型一致的数据

04、类

类的三大特性:封装,继承,多态

05、抽象类和接口的对比

先说概念:

  • 抽象类:
    • 概念:用abstract关键字,修饰的类叫做抽象类,抽象类不可以实例化(也就是不能new)
    • 继承特点:类只能单继承
    • 特点:
      • 包含抽象方法的类一定是抽象类,抽象类中不一定包含抽象方法
      • 抽象类的子类必须重写其中的所有抽象方法,否则该子类也是一个抽象类。
      • 抽象类和普通类差不多,各种形式的成员变量都可以声明,也可以有构造方法
  • 接口:
    • 概念:接口就是一个规范和抽象类比较相似,只不过比抽象类更加抽象。
    • 继承特点:接口支持多继承
    • 特点:
      • 在接口中成员变量都静态成员变量,默认会加上public static final修饰符。
      • 接口中的所有方法默认为抽象方法,默认会加上public关键字。但是:JDK8之后,把接口和抽象类做的更加贴近了,即接口中可以有静态成员方法
      • 声明接口使用interface关键字,默认是public类型。
      • 实现接口时要实现接口中声明的所有原有的抽象方法。

对比分析:

对比项抽象类接口
构造器可以有一定没有
实现方式使用extends关键字使用implement关键字
修饰符可以是public,protected,default默认public,也只能是public
是否有方法实现可已有只能是抽象方法实现(JDK8以后可以),普通的不可以有

06、权限修饰符

  • 在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,

    publicprotecteddefaultprivate
    同一类中
    同一包中(子类和无关类)
    不同包中的子类
    不同包中的无关类
  • 可见,public具有最大权限,private则是最小权限。

    编写代码时,如果没有特殊的考虑,建议这样使用权限:
    - 成员变量使用 private ,隐藏细节。
    - 构造方法使用 public ,方便创建对象。
    - 成员方法使用 public ,方便调用方法。
    
  • 注意:不加权限修饰符,其访问能力与default修饰符相同

07、final、finally、finalize区别,怎么使用?

  • final:用于声明属性方法,分别表示属性不可交变,方法不可覆盖,类不可继承

  • finally:是异常处理语句结构的一部分,表示总是执行。

  • finalize:Object类的一个方法,在垃圾回收器执行的时候会被调用。当该方法被系统调用时则代表该对象即将死亡,但需要注意的是,我们主动行为上去调用该方法不一定会导致对象死亡,这是一个被动的方法,不需要我们调用。

08、重载和重写区别?

在说重载和重写的之前先说一下如何声明一个方法,声明一个方法包括以下几部分:

  • 访问修饰符 返回值类型 方法名 参数列表 方法体

  • 例如:public int count(int a,int b){ 方法体 }

重载:

  • 发生在同一个类中,重载的方法就是两个方法
  • 特点:
    • 1、方法名必须相同
    • 2、方法的参数列表一定不一样
    • 3、访问修饰符和返回值类型可以相同也可以不同
  • 总结:重载只要关注的就是参数列表

重写:

  • 发生在父类和子类中

  • 特点

    • 1、方法名相同
    • 2、参数相同
    • 3、但是具体的实现不同
  • 总结:重写主要关注的是方法体,一般都是子类对父类方法的增强,或者接口的实现类进行重写接口中的方法。

二、进阶

01、Object类中方法有哪些?

在这里插入图片描述

02、equals和==的区别?

  • equals可以判断这两个对象是否是相同的,这里的相同有默认自定义两种方式。

  • 默认是地址比较:如果没有覆盖重写equals方法,那么Object类中默认地址比较,只要不是同一个对象(同一对象地址相同),结果必然为false。

  • 自定义对象内容比较:如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法。

  • ==比较的是地址

    • 基本类型:比较的是值是否相同;
    • 引用类型:比较的是引用是否相同;
  • 如果不重写equals方法,二者比较的是一样的东西

03、String,StringBuilder,StringBuffer的区别?

在JDK5之后,使用的是StringBuilder,在JDK5之前使用的是StringBuffer.

StringStringBufferStringBuilder
值是不可变的,导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间值可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量可变类,速度更快
不可变可变可变
线程安全线程不安全
多线程操作字符串单线程操作字符串
  • 注意:和变量进行拼接操作也不一定都是使用StringBuilder

04、自动装箱与拆箱?

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的,装箱、拆箱动作可以自动完成。例如:

Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
//注意:如果数值范围在-128到127会默认从常量池获取对象

i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

接下来说一下intInteger吧:

  • intInteger的区别:

    int是基本数据类型,直接存储的数值,默认是0
    Integer是int的包装类,是个对象,存放的是对象的引用,必须实例化之后才能使用,默认是null
    
  • intInteger比较的特性

    • == :比较的是地址

      Integer i = 4;
      Integer j = 5;
      System.out.println(i==j);//true
      /*数值类型int自动装箱成对象Integer,实际上是调用了Integer.valueOf()。
       *如果数值范围在-128到127会默认从常量池获取对象,引用地址相同。
       *超过范围会new对象出来,在堆中重新分配内存,指向这个新内存的引用地址必然不相同。
       *int 和 Integer 比较时,会自动进行Integer的拆箱操作,即比较的是数值。
       *但是:如果是new出来的对象,都在堆中重新分配内存,引用地址必然不相同。
       */
      
    • equals 数值的比较

      //Object里默认的equals方法比较的是地址 ==
      //Integer里重写了equals()方法,比较的是value
      //下面是Integer重写Object类中的equals方法
      public boolean equals(Object obj) {
          if (obj instanceof Integer) {
              return value == ((Integer)obj).intValue();
          }
          return false;
      }
      
  • 笔试题,关于 == 、 equals

    public static void compareInt2Integer() {
        /**
         * i、j实际上都是调用了 Integer.valueOf(127),都是从常量池取的,所有内存地址一样,true
         */
        Integer i = 127;
        Integer j = 127;
        System.out.println(i == j);    	 // true
        System.out.println(i.equals(j)); // true
    
        /*到
         * m、n 实际上都是调用了 Integer.valueOf(128),进行自动装箱操作,但是超过范围-128~127,会新建对象
         * m、n 都是新创建的对象,会在堆中重新分配内存,地址不同,false
         */
        Integer m = 128;
        Integer n = 128;
        System.out.println(m == n);      //false
        System.out.println(m.equals(n)); //true
    
        /**
         * k取的是缓存,h是新创建的对象,false
         * 注意:只要是new出来的都是在堆区分配内存,这点是毋庸置疑的
         */
        Integer k = 127;
        Integer h = new Integer(127);
        System.out.println(k == h);   //false
        System.out.println(k.equals(h));//true
    
        /**
         * new 操作符会分配内存,a、b都是新创建的对象
         * == 比较的是引用的地址,false
         */
        Integer a = new Integer(127);
        Integer b = new Integer(127);
        System.out.println(k == h);  //false
        System.out.println(k.equals(h)); //true
    
        /**
         * Integer.valueOf() 会优先判断常量池缓存,缓存范围是-128~127,超过范围会new一个对象,
         * w 实际也是调用了 Integer.valueOf(127)
         * y 只是显示调用了 Integer.valueOf(127)
         * w、y 本质都是从常量池中拿取值,所以地址是相同的
         */
         Integer w = 127;
         Integer y = Integer.valueOf(127);
         System.out.println(w == y); // true
         System.out.println(w.equals(y)); //true
      
        /**
         * Integer.valueOf() 会优先判断常量池缓存,缓存范围是-128~127,超过范围会new一个对象,
         * w 实际也是调用了 Integer.valueOf(128)
         * y 只是显示调用了 Integer.valueOf(128)
         * w、y 超过范围,所以会在堆区开辟内存,两个值不相等。
         */
        Integer w = 128;
        Integer y = Integer.valueOf(128);
        System.out.println(w == y); // false
        System.out.println(w.equals(y)); //true
    
        /**
         * Integer和int == 比较,Integer会自动拆箱成int,数值比较,true
         * Integer和int进行算术运算符 + - * / 等,会自动拆箱
         */
        Integer x = 128;
        int z = 128;
        System.out.println(x == z); //true
        System.out.println(x.equals(z)); //true
    }
    
    /*
     * 总结:
     *	如果是Integer和Integer之间的==比较,那么当超过范围会因为创建新的对象导致二者不相等。
     *	如果是Integer和int之间的==比较,不管Integer的值为多少,在比较之前都会被自动拆箱,比较的是值
     */
    

05、集合

在这里插入图片描述
集合分为单列集合和双列集合:

  • 单列集合为Collection接口下的实现类
  • 双列集合为Map接口下的实现类

Collection接口下面有:List和Set

List:ArrayList(底层数组),LinkedList(底层链表)

查找多使用ArrayList,增删多使用LinkedList

Set:HashSet(hash算法)—>LinkedHashSet(链表实现),TreeSet(二叉树)

Set中存储自定义对象,并保证元素唯一

HashSet是value为null的,只是用key的HashMap

Map接口下的实现类有:HashMap(无序)—>LinkedHashMap(有序)

06、遍历Map的方式?

使用MAP中两个方法:

  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。

  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

方式一:分析步骤:

  1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法:keyset()

  2. 遍历键的Set集合,得到每一个键。

  3. 根据键,获取键所对应的值。方法:get(K key)

    public class MapDemo01 {
        public static void main(String[] args) {
            //创建Map集合对象 
            HashMap<String, String> map = new HashMap<String,String>();
            //添加元素到集合 
            map.put("C", "第一");
            map.put("C++", "第二");
            map.put("JAVA", "第三");
            
            //获取所有的键  获取键集
            Set<String> keys = map.keySet();
            // 遍历键集 得到 每一个键
            for (String key : keys) {
              	//key  就是键
                //获取对应值
                String value = map.get(key);
                System.out.println(key+"的语言排名是:"+value);
            }  
        }
    }
    

方式二:分析步骤

  • Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry将键值对的对应关系封装成了对象,即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对对象(Entry)中获取对应的键与对应的值。

  • 1、获取Map集合中,所有的键值对对象(Entry),以Set集合形式返回。方法:entrySet()。

  • 2、遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。

  • 3、通过键值对对象(Entry),获取Entry对象中的键与值。 方法:getkey() getValue()

    public class MapDemo02 {
        public static void main(String[] args) {
            // 创建Map集合对象 
            HashMap<String, String> map = new HashMap<String,String>();
            // 添加元素到集合 
            map.put("C", "第一");
            map.put("C++", "第二");
            map.put("JAVA", "第三");
    
            // 获取 所有的 entry对象  entrySet
            Set<Entry<String,String>> entrySet = map.entrySet();
    
            // 遍历得到每一个entry对象
            for (Entry<String, String> entry : entrySet) {
               	// 解析 
                String key = entry.getKey();
                String value = entry.getValue();  
                System.out.println(key+"的语言排名是:"+value);
            }
        }
    }
    

07、异常?

在这里插入图片描述

异常的父类是Throwable类,在Throwable类下有两个异常类:Exception,Error

  • Error类:专门用来处理严重影响程序运行的错误,可是通常程序设计者不会设计程序代码去捕捉这种错误,其原因在于即使捕捉到它,也无法给予适当的处理。

    虚拟机错误:Virtual Machine Error,内存溢出:Out Of Memory Error,栈溢出:Stack Over Flow

  • Exception 类:包含了一般性的异常,这些异常通常在捕捉到之后,便可做妥善的处理,以确保程序继续运行。

    • Exception类中可细分为:编译时期异常运行时期异常

      • 编译时期异常:checked异常。在编译时期,就会去检查,如果有异常并且没有去处理,则会编译失败。

      • 运行时期异常:runtime异常。在运行时期,才会检查异常,在编译时期,运行异常不会被编译器检测(不报错)

        数组越界异常,算术异常,类加载异常,空指针异常

异常的处理方式:两种

  • 第一种:使用java默认的的异常处理机制,也就是我们不做捕获,出现异常直接停止运行。

  • 第二种:我们自己进行处理,使用5个关键字:try,catch,finally,throw,throws

    try-catch-finally
    - 把可能发生异常的代码使用try包裹起来,然后在catch中进行捕获,如果捕获到,我们可以自己处理,程序继续运行,也可以向外抛出。
    
    - 如果向外抛出就要用到throw关键字了,我们在catch中抛出异常,不过这种做法没有实际意义,程序还是会停止执行,如果有程序调用这种会抛出异常的方法,对其异常进行处理程序也是可以运行的
    
    - 使用throws关键字在指定方法上抛出异常,如果方法内的程序代码可能会发生异常,且方法内又没有使用任何的代码块来捕捉这些异常时,则必须在声明方法时一并指明所有可能发生的异常,并使用throws关键字抛出,以便让调用此方法的程序得以做好准备来捕捉异常。也就是可能产生异常的方法自己不捕获,让调用此方法的的方法进行捕获,如果调用此方法的方法不进行捕获,也可将其声明出去,再让别调用的方法进行捕获,大家都不处理,逐层抛出,不过这样最后会导致程序终止运行。
    

异常处理顺序:

多个异常分别处理。
多个异常一次捕获,多次处理。
多个异常一次捕获一次处理。

08、Synchronized 和 Lock 区

  • 1、Synchronized 内置的Java关键字, Lock 是一个Java接口
  • 2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  • 3、Synchronized 会自动释放锁(被修饰的类或者方法执行结束会自动释放),lock 必须要手动释放锁!如果不释放锁,死锁
  • 4、Synchronized 线程 1(获得锁之后阻塞)、线程2(等待,傻傻的等),Lock锁就不一定会等待下去
  • 5、Synchronized 可重入锁,不可以中断的,非公平
  • 6,Lock 可重入锁,可以判断锁,非公平还是公平(可以自己设置,参考ReentrantLock的构造方法)
  • 7、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码

09、讲讲实例化(new的时候)顺序,比如父类静态数据 ,父类构造函数、父类属性、子类静态数据,子类构造函数

此题考察的是类加载器实例化时进行的操作步骤(加载–>连接->初始化)。

  • 父类静态代变量、父类静态代码块、
  • 子类静态变量、子类静态代码块、
  • 父类非静态变量(父类实例成员变量)、父类构造函数、
  • 子类非静态变量(子类实例成员变量)、子类构造函数。

当我们进行new实例化进行操作的时候,JVM都进行了什么操作呢?

  • 当我们使用new关键字进行实例化的时候,会进行如下几步处理:
    • 加载:此阶段加载类的字节码信息到内存
    • 链接:此阶段进行验证,分配默认值,符号引用转直接引用
    • 初始化:为成员进行赋值等

使用
​ 对实例进行操作比如sons.toString()

对于我们的实例而言,我们只关注链接和初始化阶段即可:

  • 链接阶段:我们按照类结构(成员变量(先),方法(后))的顺序(不是书写顺序),先对变量进行赋默认值0,对象的话为null。
  • 初始化阶段:就是对对象进行赋值,比如c执行initc()进行赋值。

10、反射的原理,反射创建类实例的三种方式是什么

  • JAVA反射机制是在运行状态中,对于任意一个,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个属性和方法,这种动态获取类的信息以及动态调用对象的方法和属性的功能称为java语言的反射机制。

  • 方式一:如果知道一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取

    实例:Class cls1=Class.forName(classPath);

  • 方式二:如果已知具体的类,通过类的Class获取,该方式最为安全可靠,程序性能最高

    Class cls2=Cat.class;

  • 方式三:通过类加载器来获取类的对象

    1、先拿到Cat的类加载器

    ClassLoader classLoader=cat.getClass().getClassLoader();

    2、通过类加载器得到对应的对象

    Class cls=classLoader.loadClass(classPath);

11、反射中,Class.forName 和 ClassLoader 区别?

  • 1、Class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块,当然还可以在改方法中指定是否执行静态块。

    public static Class<?> forName(String className){....}
    public static Class<?> forName(String className, boolean initialize,ClassLoader loader){....}
    
  • 2、ClassLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

    public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
  • Class类其实也是使用的ClassLoader类加载器加载的

12、this和super关键字

this关键字主要有三个应用:

  • 1、this调用本类中的属性,也就是类中的成员变量;
  • 2、this调用本类中的其他方法;
  • 3、this调用本类中的其他构造方法,调用时要放在构造方法的首行。

super关键字的用法有三种:

  • 1、在子类的成员方法中,访问父类的成员变量
  • 2、在本类的成员方法中,访问本类的另一个成员方法
  • 3、在本类的构造方法中,访问本类的另一个构造方法

13、说一说你对Object 类对象中 hashCode 和 equals 方法的理解,在什么场景下需要重新实现这两个方法?

hashCode方法:是通过hash算法计算对象的hashCode值

equals方法:比较两个对象的地址是否相同,一般继承Object类的对象会对该方法进行重写

equals与hashcode的关系:

  • equals相等两个对象,则hashcode一定要相等。
  • 但是hashcode相等的两个对象不一定equals相等(可能存在哈希冲突,导致两个不同的对象的hash值相同)。

14、java 中 IO 流分为几种?

  • 按功能来分:输入流(input)、输出流(output)。

  • 按类型来分:字节流和字符流。

  • 字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。

三、线程和线程池

01、创建线程的方式以及优缺点

方式一: 通过继承 Thread 类实现多线程,重写该类的run()方法,然后创建子类实例,调用start方法运行线程。

  • 优点:代码编写最简单直接操作。
  • 缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差。

方式二:通过实现 Runnable 接口实现多线程

  • 1、编写一个类实现Runnable接口
  • 2,重写父接口中的run()方法,将要执行的语句放入方法体中
  • 3,创建Runnable接口子类的实例化对象,把它丢到Thread类中
  • 4,通过 Thread 类的 start()方法,启动多线程序
  • 为什么可以这样做:在 Thread 类之中,有这样一个构造方法,可以将一个 Runnable 接口的实例化对象作为参数去实例化 Thread 类对象
  • 优点:线程类可以实现多个几接口,可以再继承一个类。
  • 缺点:没返回值,不能直接启动,需要通过构造一个 Thread 实例传递进去启动。

方式三:实现Callable接口来实现多线程,类似于Runnable接口,可以创建一个可以被其他线程执行的实例

  • 无论使用什么样的方式创建多线程,我们都需要使用Thread的start()方法来启动线程,通过查Thread类发现,此类的构造方法中只能接收Runnable接口的实现类。

  • 1、创建一个类实现Callable接口(通过泛型指定返值的类型)

  • 2、把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类

  • 3、把FutureTask的实例放到Thread中用来创建线程

  • 优点:有返回值,拓展性也高 ,Callable可以有返回值,返回值在FutureTask里面,使用get方法获取返回值

  • 缺点:Jdk5以后才支持,需要重写call()方法,结合多个类比如 FutureTask 和 Thread 类

    public class CallableTest01 {
        public static void main(String[] args) {
            MyThread2 myThread2=new MyThread2();
            //2.把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类
            FutureTask futureTask=new FutureTask(myThread2);
            //3.把FutureTask的实例放到Thread中用来创建线程
            new Thread(futureTask,"Thread_A").start();
           
            new Thread(new FutureTask<String>(new MyThread2()),"Thread_B").start();
          
            try {
                String returnValue=(String) futureTask.get();//此方法会产生阻塞,一般往后放
                System.out.println(returnValue);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    
    //注意:泛型的参数等于方法的返回值
    //1.创建一个类实现Callable接口(通过泛型指定返值的类型)
    class MyThread2 implements Callable<String>{
        @Override
        public String call() throws  InterruptedException{
            System.out.println("Callable.call.....");
            return "aismall";
        }
    }
    

02、线程状态有哪些?

任何线程一般具有五种状态,即创建就绪运行阻塞终止

线程新建好之后是不会自己执行的,调用start方法进入就绪状态,然后获得执行权后进入运行状态,通过调用方法可以阻塞线程,run方法执行结束或者调用stop方法线程死亡。

堵塞状态

  • 一个正在执行的线程在某些特殊情况下,如被人为挂起将让出 CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()suspend()wait()等方法,线程都将进入堵塞状态,堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。

  • 等待阻塞 :运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤 醒,wait()是 Object 类的方法

  • 同步阻塞:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。

  • 其他阻塞状态:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态,当sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。

03、线程等待:wait和sleep的对比

1、来自不同的类:

  • wait是Object类中的方法

  • sleep是Thread类中的方法

    sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,
    但是它的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
    在调用sleep()方法的过程中,线程不会释放对象锁。
    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
    

2、关于锁被释放

  • wait会释放锁
  • sleep不会释放锁

3、使用的范围不同

  • wait必须在同步代码块中
public synchronized void set(){
   try {
         // 睡眠线程,会被放入线程的等待池中,等待其它线程唤醒
         wait();
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
  //调用notify方法,唤醒等待池中最先被睡眠的的此类(持有该类对象锁的线程)的线程
   notify();
 }
  • sleep可以在任何地方休眠

04、线程之间的通信

Java 是通过 Object 类的 waitnotifynotifyAll,这几个方法来实现线程间的通信的,又因为所有的类都是从 Object 继承的,所以任何类都可以直接使用这些方法。下面是这三个方法的简要说明:

  • wait:调用此方法的线程进入睡眠状态,直到其它线程调用该线程的 notify 方法才能被唤醒。
  • notify:唤醒同一对象(synchronize锁住的对象)监听器中调用 wait 的第一个线程。(等待的线程会去等待池中排队,最先调用等待方法的线程优先排队,自然优先被唤醒)。
  • notifyAll:唤醒同一对象监听器中调用 wait 的所有线程,具有最高优先级的线程首先被唤醒并执行。

05、mt.run() : 和 mt.start()的区别

前景提要:

  • 假设我有一个继承了Thread类的MyThread类,然后创建一个该类的对象:MyTread mt=new MyThread();
  • 执行:在main方法中执行mt.run() 和 mt.start()有什么区别?
  • 分析:
    • 执行mt.run();:类似于一个类执行了自己的成员方法,就是一个普通的类,属于单线程
    • 执行mt.start();多线程的开启形式,会开辟一个新的栈空间,执行run方法, 每次执行mt.start();方法,JVM都会请求OS(操作系统)开辟一条线程,然后在jvm中创建这个线程的栈内存。

06、什么是线程死锁?

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)

例如:线程A拿着资源1,想要线程B的资源2,线程B拿着资源2,想要线程A资源1

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

07、构成死锁的条件以及如何预防死锁发生?

构成死锁的条件是:

  • 互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。其实就是对资源加了同步锁(例如:synchronize)

  • 占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。

  • 不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。

  • 循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

  • 当以上四个条件均满足,必然会造成死锁,相反,而只要上述条件之一不满足,就不会发生死锁

只要打破四个必要条件之一就能有效预防死锁的发生:

  • 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
  • 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
  • 打破占有且等待条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
  • 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

08、什么是线程池?

好多同学知道如何创建线程池,但是让他们描述什么是线程池的时候,却说不清楚了,下面就来谈谈什么是线程池以及一些概念!

线程池

  • 是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
  • 如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 假设一个服务器完成一项任务,创建线程时间T1 ,在线程中执行任务的时间T2,销毁线程时间为T3,如果:T1 + T3 远大于 T2,则可以采用线程池,大大缩短T1、T3时间,以提高服务器性能。

一个线程池包括以下四个基本组成部分:

  • 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
  • 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
  • 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  • 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

当线程池中有任务需要执行时,线程池会判断:

  • 如果池里的核心线程数没有占满就会新建线程池进行任务执行,
  • 如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;
  • 如果任务队列也满了,并且线程池没有达到最大线程数,就会新建非核心线程来执行任务;
  • 如果超过了最大线程数,就会执行拒绝策略。

在这里插入图片描述

09、创建线程池的方式?

创建线程池可以使用Executors类中的静态方法来创建线程池,但是不推荐,因为底层还是调用ThreadPoolExecutor类来创建线程池

创建线程池推荐使用ThreadPoolExecutor类,该类的构造方法中有七大参数,用来设置线程池的属性:

 public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                           int maximumPoolSize,//最大核心线程池大小
                           long keepAliveTime,//存活时间,没人调用时的存活时间,过期自动释放
                           TimeUnit unit,//超时单位
                           BlockingQueue<Runnable> workQueue,//阻塞对列
                           ThreadFactory threadFactory, //创建线程的工厂,用来创建线程,一般不动
                           RejectedExecutionHandler defaultHandler)//拒绝策略
                           {
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);
  }

拒绝策略有四种:

  • RejectedExecutionHandler接口的四个实现类,也是ThreadPoolExecutor类中的内部子类(使用默认的拒绝策略即可)

    默认拒绝策略:AbortPolicy())

不同拒绝策略的区别:

  • new ThreadPoolExecutor.AbortPolicy() :队列满了,再创建线程,不处理这个创建过程,抛出异常

    最大承载线程个数:最大核心线程池+阻塞对列

  • new ThreadPoolExecutor.CallerRunsPolicy() :哪来的去哪里

  • new ThreadPoolExecutor.DiscardPolicy() :队列满了,丢掉任务,不会抛出异常

  • new ThreadPoolExecutor.DiscardOldestPolicy() :队列满了,尝试去和最早的竞争,也不会抛出异常!

10、线程池的状态有哪些?

参考链接:https://www.cnblogs.com/shujiying/p/11924465.html

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
在这里插入图片描述

1、RUNNING

  • 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

  • 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

  • private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

2、 SHUTDOWN

  • 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
  • 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP

  • 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
  • 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING

  • 状态说明:当所有的任务已终止,ctl记录的任务数量为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

  • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING,当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

  • 状态说明:线程池彻底终止,就变成TERMINATED状态。
  • 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

注意:线程池的状态及工作线程数的表示是使用线程池类中原子类的 ctl 属性一部分二进制位表示状态一部分表示工作线程数) 。

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彤彤的小跟班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值