java面试宝典书籍-JavaSE基础

知识:java面试宝典  

JavaSE基础  注:知识都来源于Java面试宝典书籍,此处只为学习

一、Java面向对象      Standard Edition

1. 面向对象的特性以及理解

  • 封装:将对象进行封装,封装后可以通过调用对象的方式调用对象的属性,使一切变得有序
  • 继承:父类、子类,继承方法
  • 多态:方法重写(修改方法里的内容)、方法重载(方式所携带的参数不同)   、对象造型(引用类型转换)
  • 抽象:提取一类对象共有的特征

2. 访问修饰符

  • public 
  • protected
  • default
  • private
权限修饰符
修饰符 当前类 同包 子类 其他包
public true true true true
protected true true true false
default true true false false
private true false false false

 

 

 

 

 

 

3. clone

使用场景:当对象A需要多次调用,而在此期间,需要修改A的一些值但又不想要影响后面A对象的原值,这时就需要将A复制一份

4. new和clone的区别

  • 共同点:都能创建对象,同时分配内存
  • 不同点:clone不会调用构造方法,new会调用构造方法;clone()能快速创建一个已有对象的副本,即创建对象并且将已有对象中所有属性值克隆;new只能在JVM中申请一个空的内存区域,对象的属性值要通过构造方法赋值

5. clone对象的使用

// 复制引用

Person p1 = new Person();
Person p2 = p1;

System.out.println(p1);
System.out.println(p2);

// 结果:打印输出两个值一样,说明指向同一个对象,这是复制引用操作

// -------------------------------------------------------------

// 复制对象

Person p1 = new Person();
Person p2 = (Person) p1.clone();

System.out.println(p1);
System.out.println(p2);

// 结果:输出两个不同的值,说明指向的是不同的对象,这是clone操作

6. 深拷贝和浅拷贝

  • 默认的Object.clone()方法,对于引用类型成员变量拷贝只是拷贝“值”即地址,没有在堆中开辟新的内存空间
  • 重写clone()方法,对于引用类型成员变量,重新在堆中开辟新的内存空间

java面试宝典书籍-JavaSE基础_第1张图片

 

二、JavaSE语法

1. Java中有无goto语句

goto在Java中是保留字,目前还未在java中使用;还有一个保留字是const,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字

2. &和&&

  • &:按位与
  • &&:逻辑与

3. 跳出当前的多重嵌套循环

在最外层循环前加一个标记A,然后使用break A;可以跳出多重循环(break可跳出当前循环)。注:最好不要使用多重嵌套循环,这会使得程序变得复杂

4. equals和hashCode

  • 如果两个对象相同(equals 方法返回 true),那么它们的 hashCode 值一定要相同;如果两个对象的 hashCode 相同,它们并不一定相同。注:对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)

  • equals:自反性、对称性、传递性、一致性;而且对于任何非 null 值的引用 x,x.equals(null)必须返回 false

  • 高效equals:1) 使用==操作符检查"参数是否为这个对象的引用"  2) 使用 instanceof 操作符检查"参数是否为正确的类型"  3) 对于类中的关键属性,检查参数传入对象 的属性是否与之相匹配  4) 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性  5) 重写 equals 时总是要重写 hashCode  6) 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解     

  • “重写”时想到了排序--compare

//实现Comparator进行排序
Collections.sort(stuList,new Comparator(){
    @Override
    public int compare(Object o1, Object o2) {
        return ((Student) o2).getAge() - ((Student) o1).getAge();
    }
});

5. String--引用数据类型,属于final类,无法被继承

6. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

java方法调用只支持参数的值传递。场景:在javaweb中写Service层时候,从传入的数据数据封装为对象,即方法类的参数多为对象,可以在实现层去改里面的参数,但是其他方法的参数对象里面的属性值并未改变也不需要改变。

7. overload和override

  • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问

8. overload不能根据返回类型来区分

函数的返回值只是作为函数运行之后的一个“状态”,它是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”,因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。

9. char型变量中可以存储一个中文汉字

java使用的编码为unicode编码,char占两个字节,而一个汉字占两个字节

10. 抽象类(abstract class)和接口(interface)

相同:

  • 不能够实例化

  • 可以将抽象类和接口类型作为引用类型 、

  • 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类

不同:

抽象类:

  • 抽象类中可以定义构造器
  • 可以有抽象方法和具体方法
  • 接口中的成员全都是 public 的
  • 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
  • 抽象类中可以包含静态方法
  • 一个类只能继承一个抽象类

接口:

  • 接口中不能定义构造器
  • 方法全部都是抽象方法
  • 抽象类中的成员可以是 private、默认、protected、public
  • 接口中定义的成员变量实际上都是常量
  • 接口中不能有静态方法
  • 一个类可以实现多个接口
public abstract class Employee
{
   private String name;
   private String address;
   private int number;
   public Employee(String name, String address, int number)
   {
      System.out.println("Constructing an Employee");
      this.name = name;
      this.address = address;
      this.number = number;
   }
   public double computePay()
   {
     System.out.println("Inside Employee computePay");
     return 0.0;
   }
   public void mailCheck()
   {
      System.out.println("Mailing a check to " + this.name
       + " " + this.address);
   }
   public String toString()
   {
      return name + " " + address + " " + number;
   }
   public String getName()
   {
      return name;
   }
   public String getAddress()
   {
      return address;
   }
   public void setAddress(String newAddress)
   {
      address = newAddress;
   }
   public int getNumber()
   {
     return number;
   }
}
interface Animal {
   public void eat();
   public void travel();
}

11. 抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由 本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,
也是矛盾的。synchronized 和方法的实现细节有关, 抽象方法不涉及实现细节,因此也是相互矛盾的。https://www.cnblogs.com/ibelieve618/p/6410910.html    作者:自学开发的老司机 

12. 静态变量和实例变量

  • 静态变量:是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝
  • 实例变量:必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存

13. == 和 equals

  • ==:运算符。如果比较的是基本数据类型,则比较的是基本类型的值;如果比较的是对象,则比较的是地址值
  • equals:方法。不能用于基本数据类型的变量,如果没有对equals()进行重写,则比较的是引用类型的变量所指向的对象的地址

14. break和continue

  • break:跳出一个循环
  • continue:跳出本次循环,执行下一次循环

15. String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?

没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s 原先指 向一个 String 对象,内容是 "Hello",然后我们对 s 进行了“+”操作,那么 s 所指向的那个对象是否发生了改变呢? 答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。           StringBuffer

三、Java中的多态

1. Java实现多态的机制

靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

// 多态,通过父类指向具体子类的实例对象
Animal dog = new Dog();

四、Java的异常处理

1. Java中异常处理分为那些类

  • 编译时异常--CheckedException(强制性异常):Java 程序必须显式处理 Checked 异常。如果程序没有处理 Checked 异常,该程序在编译时就会发生错误无法编译。处理方法:1) 当前方法知道如何处理该异常,则用 try...catch 块来处理该异常 2) 当前方法不知道如何处理,则在定义该方法时声明抛出该异常
  • 运行时异常--RuntimeException(非强制性异常):只有当代码在运行时才发行的异常,编译时不需要 try catch

2. 

public int getNum(){
    try {
        int a = 1.0 / 0;
        return 1;
    } catch (Exception e){        
        return 2;
    } finally {
        return 3;
    }
}

// 最终结果:返回值为3

3. error和exception--Error 类和 Exception 类的父类都是 Throwable 类

  • Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类 错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止

  • Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常--又与异常类相联系        系统异常和普通异常

4. Java异常处理类机制

  • 所有的异常的根类:java.lang.Throwable    有Error和Exception两个类

5. 5个常见的异常

  • java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象

  • java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常

  • java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符

  • java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生

  • java.lang.IllegalArgumentException 方法传递参数错误

  • java.lang.ClassCastException 数据类型转换异常

  • java.lang.NoClassDefFoundException 未找到类定义错误

  • SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误

  • java.lang.InstantiationException 实例化异常

  • java.lang.NoSuchMethodException 方法不存在异常

6. throw 和 throws

throw:

  • throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理

  • throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常

throws:

  • throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理
  • throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型
  • throws 表示出现异常的一种可能性,并不一定会发生这种异常

7. final、finally、finalize

  • final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承

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

  • finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法 被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对 象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用

五、JavaSE常用API

1. Math.round()--四舍五入

2. switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。 从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的

3. 数组有没有 length() 方法?String 有没有 length() 方法?

数组没有 length()方法,而是有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆

4. String、StringBuffer、StringBuilder

  • String 和 StringBuffer/StringBuilder,它们都可以储存和操作字符串

  • String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的

  • StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改

  • StringBuilder 是 Java5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被 synchronized 修饰,因此它的效率理论上也比 StringBuffer 要高

5. 什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder对象的 append 方法连接字符串性能更好

  • String的"+"和StringBuilder/StringBuffer的append--在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder
  • 一般情况进行字符串拼接用+就可以,系统内部会进行优化,但是如果是循环拼接,则需要用StringBuilder的append来实现
        String str1 = "love";
        for(int i=0;i<10;i++)
        {
            //系统会在这里创建StringBuilder,然后进行append,这样会增加内存消耗
            str1 += i;
        }
        //StringBuilder
        StringBuilder str2 = new StringBuilder("love2");
        for(int i=0;i<10;i++)
        {
            //这里的StringBuilder是在外部创建的,就一个,所以不会增加内存消耗
            str2.append(i);
        }

6.

class StringEqualTest {
public static void main(String[] args) {
    String s1 = "Programming";
    String s2 = new String("Programming");
    String s3 = "Program";
    String s4 = "ming";
    String s5 = "Program" + "ming";
    String s6 = s3 + s4;
    System.out.println(s1 == s2); //false
    System.out.println(s1 == s5); //true
    System.out.println(s1 == s6); //false
    System.out.println(s1 == s6.intern()); //true
    System.out.println(s2 == s2.intern()); //false

    // intern():   得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与
    //             String 对象的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符  
    //             串将被添加到常量池中,然后返回常量池中字符串的引用; 
}

7. Java中的日期和时间

1) 获得年月日时分秒

1. public class DateTimeTest {
2.     public static void main(String[] args) {
3.         Calendar cal = Calendar.getInstance();
4.         System.out.println(cal.get(Calendar.YEAR));
5.         System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
6.         System.out.println(cal.get(Calendar.DATE));
7.         System.out.println(cal.get(Calendar.HOUR_OF_DAY));
8.         System.out.println(cal.get(Calendar.MINUTE));
9.         System.out.println(cal.get(Calendar.SECOND));
10.        // Java 8
11.        LocalDateTime dt = LocalDateTime.now();
12.        System.out.println(dt.getYear()); 
13.        System.out.println(dt.getMonthValue()); // 1 - 12
14.        System.out.println(dt.getDayOfMonth());
15.        System.out.println(dt.getHour());
16.        System.out.println(dt.getMinute());
17.        System.out.println(dt.getSecond());
18.    }
19. }

2) 获得毫秒数    1970 1:1 0:0

1. Calendar.getInstance().getTimeInMillis(); //第一种方式
2. System.currentTimeMillis(); //第二种方式
3. // Java 8
4. Clock.systemDefaultZone().millis();

3) 获得某月的最后一天

1. //获取当前月第一天:
2. Calendar c = Calendar.getInstance();
3. c.add(Calendar.MONTH, 0);
4. c.set(Calendar.DAY_OF_MONTH,1);//设置为 1 号,当前日期既为本月第一天
5. String first = format.format(c.getTime());
6. System.out.println("===============first:"+first);
7.
8. //获取当前月最后一天
9. Calendar ca = Calendar.getInstance();
10. ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
11. String last = format.format(ca.getTime());
12. System.out.println("===============last:"+last);
13.
14. //Java 8 
15. LocalDate today = LocalDate.now(); 
16. //本月的第一天
17. LocalDate firstday = LocalDate.of(today.getYear(),today.getMonth(),1); 
18. //本月的最后一天
19. LocalDate lastDay =today.with(TemporalAdjusters.lastDayOfMonth()); 
20. System.out.println("本月的第一天"+firstday); 
21. System.out.println("本月的最后一天"+lastDay);

4) 格式化日期

1. import java.text.SimpleDateFormat;
2. import java.time.LocalDate;
3. import java.time.format.DateTimeFormatter;
4. import java.util.Date;
5. class DateFormatTest {
6.
7.     public static void main(String[] args) {
8.          SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
9.          Date date1 = new Date();
10.         System.out.println(oldFormatter.format(date1));
11.
12.         // Java 8
13.         DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
14.         LocalDate date2 = LocalDate.now();
15.         System.out.println(date2.format(newFormatter));
16.     }
17. }

5)打印昨天的当前时刻

1. import java.util.Calendar;
2. class YesterdayCurrent {
3.     public static void main(String[] args){
4.         Calendar cal = Calendar.getInstance();
5.         cal.add(Calendar.DATE, -1);
6.         System.out.println(cal.getTime());
7.     }
8. }
9.
10.
11. //java-8
12. import java.time.LocalDateTime;
13. class YesterdayCurrent {
14.     public static void main(String[] args) {
15.         LocalDateTime today = LocalDateTime.now();
16.         LocalDateTime yesterday = today.minusDays(1);
17.         System.out.println(yesterday);
18.     }
19. }

六、数据类型

1. 基本数据类型

2. String引用数据类型

3. 

// 以下语句是否正确

short s1 = 1;
s1 = s1 + 1;

// 不正确   s1 = s1 + 1 中 1 是int类型,s1+1 运算结果也是 int 型需要强制转换类型才能赋值给 short 
//          型

s1 += 1;

// 正确,+= 有隐含的强制类型转换的作用

4. int 和 Integer

  • int : 基本数据类型
  • Integer:包装类
  • 自动装箱和拆箱操作

java面试宝典书籍-JavaSE基础_第2张图片

5. 

java面试宝典书籍-JavaSE基础_第3张图片

result:true和false

  • 首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不是值而是引用简单的说,
  • 如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1==f2 的结果是 true,而 f3==f4 的结果是 false

6. String类常用的方法

java面试宝典书籍-JavaSE基础_第4张图片

7. String、StringBuffer、StringBuilder的区别

可变与不可变

  • String:字符串常量,在修改时不会改变自身;若修改,等于重新生成新的字符串对象\

  • StringBuffer:在修改时会改变对象自身,每次操作都是对 StringBuffer 对象本身进行修改,不是生成新的对象;使用场景:对字符串经常改变情况下,主要方法:append(),insert()等

线程安全与否

  • String:对象定义后不可变,线程安全

  • StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区 大量数据

  • StringBuilder:是线程不安全的,适用于单线程下操作字符串缓冲区大量数据

 

共同点

  • StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)

8. 数据类型之间的转换

  • 字符串如何转基本数据类型:调用基本数据类型对应的包装类中的方法 parseXXX(String)或 valueOf(String)即可返回相应基本类型
  • 基本数据类型如何转字符串:1) 将基本数据类型与空字符串(" ")连接(+)即可获得其所对应的字符串  2) 另一种方法是调用 String类中的 valueOf()方法返回相应字符串

七、Java的IO

1. Java中有几种类型流

方向:

  • 输入流--inputStream 
  • 输出流--outputStream

实现功能:

  • 节点流--如FileReader
  • 处理流--BufferReader

处理数据的单位:

  • 字符流--继承于InputStream和OutputStream
  • 字节流--继承于InputStreamReader和OutputStreamReader

2. 字节流如何转为字符流

  • 字节输入流转字符输入流通过InputStreamReader实现,该类的构造函数可以传入 InputStream 对象
  • 字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象

3. 如何将一个java对象序列化到文件里

在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用

//对象输出流
ObjectOutputStream objectOutputStream = 
                   new ObjectOutputStream(new FileOutputStream(new File("D://obj")));
objectOutputStream.writeObject(new User("zhangsan", 100));
objectOutputStream.close();


//对象输入流
ObjectInputStream objectInputStream = 
                  new ObjectInputStream(new FileInputStream(new File("D://obj")));
User user = (User)objectInputStream.readObject();
System.out.println(user);
objectInputStream.close();

4. 字节流和字符流的区别

  • 1个字节和2个字节
  • 字节流可以处理所有类型数据,只要是处理纯文本数据就要优先考虑使用字符流
  • 如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点。

5. 如何实现对象克隆

  • 实现Cloneable接口并重写Object类中的clone()方法
  • 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

java面试宝典书籍-JavaSE基础_第5张图片

java面试宝典书籍-JavaSE基础_第6张图片

6. java序列化,如何实现java序列化

implements Serializable标注该对象是可被序列化的

八、Java集合

1. HashMap排序题

import java.util.*;

/**
 * 题目描述
 *     已知一个HashMap集合,根据User中的age进行排序
 *
 * HashMap本身就不是可排序的,但它的子类LinkedHashMap<>可排序
 */


class User{
    private String userName;
    private int age;

    public User() {

    }

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}


public class HashTest {

    public static void main(String[] args) {
        HashMap users = new HashMap<>();
        users.put(1,new User("张三",25));
        users.put(2,new User("李四",26));
        users.put(3,new User("王五",27));

        System.out.println(users);

        HashMap sortHashMap = sortHashMap(users);
        System.out.println(sortHashMap);

    }

    public static HashMap sortHashMap(HashMap map){
        // 首先拿到 map 的键值对集合
        Set> entrySet = map.entrySet();
        // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法
        List> list = new ArrayList>(entrySet);
        // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现
        Collections.sort(list, new Comparator>() {
            @Override
            public int compare(Map.Entry o1, Map.Entry o2) {
                //按照要求根据 User 的 age 的倒序进行排
                return o2.getValue().getAge()-o1.getValue().getAge();
            }
        });
        //创建一个新的有序的 HashMap 子类的集合
        LinkedHashMap linkedHashMap = new LinkedHashMap();
        //将 List 中的数据存储在 LinkedHashMap 中
        for(Map.Entry entry : list){
            linkedHashMap.put(entry.getKey(), entry.getValue());
        }
        return linkedHashMap;
    }
}

2. 集合安全性问题

  • ArrayList、HashSet、HashMap没有加锁,所以线程并不安全
  • Collections工具类提供API,使其线程变得安全
Collections.synchronizedCollection(c)
Collections.synchronizedList(list)
Collections.synchronizedMap(m)
Collections.synchronizedSet(s)

3. ArrayList内部实现

https://www.cnblogs.com/maoyali/p/8805975.html   作者:maoyl

4. 并发集合和普通集合

  • 普通集合:普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性
  • 同步(线程安全)集合:线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了
  • 并发集合:通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率   ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque

5.

List的三个子类特点

  • ArrayList 底层结构是数组,底层查询快,增删慢

  • LinkedList 底层结构是链表型的,增删快,查询慢   基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还 存储下一个元素的地址

  • vector 底层结构是数组 线程安全的,增删慢,查询慢

Map的三个子类特点

  • HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键
  • HashTable:线程安全,低效,不支持 null 值和 null 键
  • LinkedHashMap:是 HashMap 的一个子类,保存了记录的插入顺序
  • SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)

Set的两个子类特点

  • HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法

  • LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMp)

6. List、Map和Set

结构特点

  • List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合

  • List 中存储的数据是有顺序,并且允许重复

  • Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的

  • Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的)

实现类:如       5

区别

  • List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过 list.get(i)方法来获取集合中的元素

  • Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复

  • Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 TreeSet 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator接口来自定义排序方式

7. HashMap和HashTable

  • HashMap 是线程不安全的, Map 的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全,HashMap 的效率要较 HashTable 的效率高一些

  • HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值

  • HashTable 是 sychronize,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步

8. 数组、链表

简介:计算机处理数据,首先将数据一部分或全部存储到内存中,然后再对内存中的数据进行各种处理

1) 数据存储方式

java面试宝典书籍-JavaSE基础_第7张图片

  • 当内存控件有足够大的连续空间,可以把数据连续放在内存中
  • 当内存中只有一些离散空间,想要连续存储数据,1) 移动内存中数据,这样可能造成其他数据的丢失  2) 将数据离散地进行存储,每块内存的存储空间标记下一块数据的存储地址
  • 内存中的存储形式可以分为连续存储和离散存储两种,对应即为数组和链表

2) 区别

数组是将元素在内存中连续存储的:

优缺点

  • 因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高
  • 存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大

链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)

3) 使用场景

  • 数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建 的线性表较稳定

  • 链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表

9. ArrayList和LinkedList

  • ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向

  • LinkedList 使用了循环双向链表数据结构

  • LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表  (涉及到节点)

10. List a = new ArrayList()和Arraylist a = new ArrayList()的区别

  • 前者是一个List对象,有些Arraylist有但是List没有的属性,它就不能再用了
  • 后者保留了ArrayList的所有属性      所以需要用到 ArrayList 独有的方法的时候不能用前者

11. 要对集合进行更新操作时,ArrayList和LinkedList哪一个合适

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

  • 如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针

  • 如果集合数据是对于集合新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数据

  • 二分法查找:ArrayList优于LinkedList

  • 链表开头插入数据:LinkedList优于ArrayList

java面试宝典书籍-JavaSE基础_第8张图片\

12. 两个队列结构模拟堆栈结构

  • 队列:先进后出
  • 堆栈:先进先出

13. Collection和Map的集成体系

java面试宝典书籍-JavaSE基础_第9张图片

 

java面试宝典书籍-JavaSE基础_第10张图片

14. Map 中的 key 和 value 可以为 nul

  • HashMap 对象的 key、value 值均可为 null
  • HahTable 对象的 key、value 值均不可为 null
  • 且两者的的 key 值均不能重复,若添加 key 相同的键值对,后面的 value 会自动覆盖前面的 value,但不会报错

九、Java的多线程和并发库

1. 传统线程

https://www.cnblogs.com/snow-flower/p/6114765.html   作者:移动开发snow_flower

1) 使用类Thread和接口Runnable实现

// 第一种方式

class MyThread extends Thread{
	int j=20;
	public void run(){
		for (int i = 0; i < 20; i++) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(this.getName()+",i="+j--);
		}
	}
}


// 第二种方式

class MyRunnable implements Runnable{
	int j=20;
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+",j="+this.j--);
		}
	}
	
}

2) 定时器Timer和TimerTask

/**
 * 题目要求:使用定时器,间隔4s执行一次,再间隔2s执行一次,以此类推
 */

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * @author Archerlu
 *
 */
public class TimerAndTimerTrack extends TimerTask{
    private static volatile int count = 0;
    @Override
    public void run() {
        count = (count +1)%2;
        System.err.println("Boob boom ");
        new Timer().schedule(new TimerAndTimerTrack(), 2000+2000*count);
    }

    public static void main (String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerAndTimerTrack(), 2000);
        while (true) {
            System.out.println(new Date().getSeconds());
            try {
                Thread.sleep(1000);  // 相当于计时器
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}


3) 线程互斥与同步

当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性

  • 间接相互制约--互斥。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待
  • 直接相互制约--同步。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理,那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态
  • 对于互斥可以这样理解,线程 A 和线程 B 互斥访问某个资源则它们之间就会产个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步
/**
 * 题目要求:子线程运行执行 10 次后,主线程再运行 5 次。
 * 同步:synchronization
 * 互斥:mutual exclusion
 *
 * 线程生命周期:
 *      1. 新建状态:用new关键字和Thread类或其子类建立一个线程对象后,
 *                  该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,
 *                  通过调用start方法进入就绪状态(runnable)
 *      2. 就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,
 *                   处于线程就绪队列(尽管是采用队列形式,
 *                   事实上,把它称为可运行池而不是可运行队列。
 *                   因为cpu的调度不一定是按照先进先出的顺序来调度的),
 *                   等待系统为其分配CPU。等待状态并不是执行状态,
 *                   当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系
 *                   统挑选的动作称之为“cpu调度”。
 *                   一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
 *      3. 运行状态:处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态
 *      4. 阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
 *      5. 死亡状态:当线程的run()方法执行完,或者被强制性地终止,就认为它死去。
 */

class Bussiness{
    private boolean subFlag = true;

    public synchronized void mainMethod(){
        while (subFlag){
            try {
                wait();
            } catch (Exception e){
                e.printStackTrace();
            }
        }

        for (int i=0; i<5;i++){
            System.out.println(Thread.currentThread().getName()+
                    " : main thread running loop count -- " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        subFlag = false;
        notify();
    }

    public synchronized void subMethod(){
        while (!subFlag){
            try {
                wait();
            } catch (Exception e){
                e.printStackTrace();
            }
        }

        for (int i=0; i<10;i++){
            System.err.println(Thread.currentThread().getName()+
                    " : sub thread running loop count -- " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        subFlag = false;
        notify();
    }


}

public class SM {

    public static void main(String[] args) {
        final Bussiness bussiness = new Bussiness();
        // 子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3;i++){
                    bussiness.subMethod();
                }
            }
        }).start();

        // 主线程
        for (int i=0;i<3;i++){
            bussiness.mainMethod();
        }

    }
}
/**
 * 子父线程交替执行,子线程执行5次。父线程执行3次,一共执行10个循环
 */
public class TestThread {

    public static void main(String[] args) {
        //创建静态类对象(因为该类继承了Thread类,也可以等于创建了一个线程)
        final JS1c js1c = new JS1c();
        //设置对象线程名
        js1c.setName("子线程");
        //启动线程
        js1c.start();
        //main函数的程序入口运行父线程
        for (int i = 0; i < 10; i++) {
            //设置父线程名
            Thread.currentThread().setName("父线程");
            //调用静态类方法
            js1c.MainThread(i);
        }
    }
}

/**
 *创建继承Thread的集成调用方法的工具类,重写run方法,父线程直接调用方法,子线程通过run方法启动并调用方法。
 */
class JS1c extends Thread{
    //控制线程先运行子线程的控制选项,父线程先wait挂起,子线程执行完唤醒父线程,子线程再wait挂起,实现交替执行
    private boolean flag = true;
    /**
     * 重写线程启动方法
     */
    public void run() {
        for (int i = 0; i < 10; i++) {
            //调用本类中的方法
            SubThread(i);
        }
    }
    /**
     * 父线程调用方法
     * synchronized锁的应用,实现了“一个门只有一把锁一个人使用的原理”,只有在有锁的人使用完毕后在重新分配锁的使用权
     * 1.flag初始值为true,优先进入if选项,挂起线程
     * 3.线程被唤醒,进入for循环输出语句,修改flag值为true,唤醒子线程,循环进入if选项,挂起线程
     * 5.(循环执行之前的唤醒、挂起操作,知道程序结束)。。。。。。
     * @param i   记录循环输出次数的记录值
     */
    public synchronized void MainThread(int i) {
        if (flag)
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        for (int j = 0; j < 3; j++) {
            System.out.println(Thread.currentThread().getName() + ":i=" + (i+1) + ",j=" + j);
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        flag = true;
        this.notify();
    }
    /**
     * 子线程调用方法
     * synchronized锁的应用,实现了“一个门只有一把锁一个人使用的原理”
     * 2.flag初始值为true,不进入if选项,直接修改flag值为false,并唤醒父线程,循环进入if选项,挂起线程
     * 4.线程被唤醒,进入for循环输出语句,修改flag值为false,唤醒父线程,循环进入if选项,挂起线程
     * 6.(循环执行之前的唤醒、挂起操作,知道程序结束)。。。。。。
     * @param i    记录循环输出次数的记录值
     */
    public synchronized void SubThread(int i) {
        if (!flag)
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        for (int j = 0; j < 5; j++) {
            System.out.println(Thread.currentThread().getName() + ":i=" + (i+1) + ",j=" + j);
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        flag = false;
        this.notify();
    }
}

4) 线程局部变量ThreadLocal

  • ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据

  • 每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是 ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量

  • ThreadLocal 的应用场景:订单处理、银行转账、

/**
 *  1. 在关联数据类中创建 private static ThreadLocal
 */
public class SerialNum {


    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal(){
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get(){
        return ((Integer) (serialNum.get())).intValue();
    }

    public static void main(String[] args) {
        for (int i=0; i<5;i++){
            System.out.println(get());
        }

    }
}

 

2. 在Util类中创建ThreadLocal

3. 在Runnable中创建ThreadLocal

5) 多线程共享数据

  • 多个线程行为一致,共同操作一个数据源也就是每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做

  • 多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同Runnable对象。例如,银行存取款

/**
 *  1. 多个线程行为一致共同操作一个数据
 */

// 共享数据类
class ShareData {

    private int num = 10;
    public synchronized void inc(){
        num++;
        System.out.println(Thread.currentThread().getName()+":invoke inc method ="+ num);

        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

    }
}

// 多线程类
class RunnableCusToInc implements Runnable{

    private ShareData shareData;

    public RunnableCusToInc(ShareData shareData) {
        this.shareData = shareData;
    }

    @Override
    public void run() {
        for (int i=0 ;i<5 ;i++){
            shareData.inc();
        }
    }
}

public class Test{
    public static void main(String[] args) {
        ShareData shareData = new ShareData();

        for (int i=0;i<4;i++){
            new Thread(new RunnableCusToInc(shareData),"Thread"+i).start();
        }
    }
}
/**
 *  1. 多个线程行为不一致共同操作一个数据
 */

// 共享数据类
class ShareData1 {

    private int num = 10;
    public synchronized void inc(){
        num++;
        System.out.println(Thread.currentThread().getName()+":invoke inc method ="+ num);

        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

    }
}

// 封装共享数据类
class RunnableCusToInc1 implements Runnable{
    // 封装共享数据类
    private ShareData1 shareData;

    public RunnableCusToInc1(ShareData1 shareData) {
        this.shareData = shareData;
    }

    @Override
    public void run() {
        for (int i=0 ;i<5 ;i++){
            shareData.inc();
        }
    }
}

// 封装共享数据类
class RunnableCusToInc2 implements Runnable{
    // 封装共享数据类
    private ShareData1 shareData;

    public RunnableCusToInc2(ShareData1 shareData) {
        this.shareData = shareData;
    }

    @Override
    public void run() {
        for (int i=0 ;i<5 ;i++){
            shareData.inc();
        }
    }
}

public class Test2{
    public static void main(String[] args) {
        ShareData1 shareData = new ShareData1();

        for (int i=0;i<4;i++){
            if(i%2 == 0){
                new Thread(new RunnableCusToInc1(shareData),"Thread"+i).start();
            }else {
                new Thread(new RunnableCusToInc2(shareData),"Thread"+i).start();
            }

        }
    }
}

2. 线程并发库(迷茫。。。)

3. 多线程面试题

1) 多线程的创建方式

/**
 *  继承Thread类:
 *  但Thread本质上也是实现了Runnable接口的一个实例,
 *  它代表一个线程的实例,并 且,启动线程的唯一方法就是通过Thread类的start()实例方法。
 *  start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
 *  这种方式实现多线程很简单,通过自己的类直接extendThread,并复写run()方法,
 *  就可以启动新线程并执行自己定义的run()方法。
 *  例如:继承Thread类实现多线程,并在合适的地方启动线程
 */

public class MyThread extends Thread{

    public void run(){
        System.out.println("MyThread.run()");
    }

    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();

        m1.start();
        m2.start();
    }
}
2. 实现Runnable接口的方式实现多线程,并且实例化Thread,传入自己的Thread实例,调用自己的run()方法

java面试宝典书籍-JavaSE基础_第11张图片

// 3. 使用ExecutorService、Callable、Future实现具有返回结果的多线程

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;

/**
 * 具有返回值的线程
 */

@SuppressWarnings("unchecked")
public class Test3 {

    public static void main(String[] args) throws ExecutionException,InterruptedException {
        System.out.println("---------程序开始运行----------");
        Date date1 = new Date();

        int taskSize = 5;
        // 创建一个线程包
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List list = new ArrayList();
        for (int i=0; i>>"+f.get().toString());
        }

        Date date2 = new Date();
        System.out.println("--------程序结束运行---------,程序运行时间["+(date2.getTime()-date1.getTime())+"毫秒]");

    }
}

class MyCallable implements Callable{

    private String taskNum;

    public MyCallable(String taskNum) {
        this.taskNum = taskNum;
    }

    @Override
    public Object call() throws Exception {
        System.out.println(">>>"+taskNum+"任务启动");
        Date dateTmp1 = new Date();
        Thread.sleep(1000);
        Date dateTmp2 = new Date();

        Long time = dateTmp2.getTime() - dateTmp1.getTime();
        System.out.println(">>>"+taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间["+time+"毫秒]";
    }
}

2) 在Java中wait和sleep方法的不同?

  • 最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。

3) synchronized和volatile关键字的作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的

  • 禁止进行指令重排序

作用:

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取                               synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住

  • volatile仅能使用在变量级别      synchronized则可以使用在变量、方法、和类级别的

  • volatile仅能实现变量的修改可见性,并不能保证原子性        synchronized则可以保证变量的修改可见性和原子性

  • volatile不会造成线程的阻塞            synchronized可能会造成线程的阻塞

  • volatile标记的变量不会被编译器优化       synchronized标记的变量可以被编译器优化

4) 分析线程并发访问代码解释原因


class Counter{
    private volatile int count = 0;

    public void inc(){
        try{
            Thread.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        count++;
    }

    @Override
    public String toString() {
        return "Counter{" +
                "count=" + count +
                '}';
    }
}

public class VolatileTest {

    public static void main(String[] args) {
        final Counter counter = new Counter();
        for (int i=0;i<1000;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    counter.inc();
                }
            }).start();
        }
        System.out.println(counter);
    }
}


// result:不一定或等于1000
// 在java的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某
// 一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值 load 到线程
// 本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在
// 修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值
// 就产生变化了。

// 主函数中开启了 1000 个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了
// 自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。
// 因此结果就不可能等于 1000了,一般都会小于 1000

5. 什么是线程池,如何使用?

将多个线程对象放到容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高了代码的执行效率

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

6. 常用的线程池

  • newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序

    执行

  • newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

  • newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小

  • newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求

  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求

7. 线程池了解

  • 线程池的定义
  • 如何创建
  • 常用的线程池

线程池好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

8. 线程池的启动策略

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们

  • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务  b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列  c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务  d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”

  •  一个线程完成任务时,它会从队列中取下一个任务来执行

  • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

java面试宝典书籍-JavaSE基础_第12张图片

9. 如何控制某个方法允许并发访问线程个数

import java.util.concurrent.Semaphore;

/**
 *  先了解 Semaphore--信号量
 *  转载例子:
 *      一条四车道,长100米,假设每辆车(考虑车身和车距)占用车道10米,
 *      也就是说这条干道满载运行也只容纳40辆车
 *      如果有120辆车要通过的话(为简单起见,一波40辆,分成3波),就必须要红绿信号灯来调度了
 *      对于最前面的一波来说,它们看到绿车,就通过,第一波全通过后,红绿灯变为红色,如此循环
 *
 *  对于这个例子:
 *      每次先放进来五个线程,这五个线程自己去抢,先抢到,谁先去抢信号
 */
public class SemaphoreTest {

    // 使用Semaphore控制,构造函数并初始化5个信号,这样最多只能有5个线程并发访问,
    // 对于5个线程就排队等候,走一个来一个
    static Semaphore semaphore = new Semaphore(5,true);

    public static void main(String[] args) {
        for (int i=0; i<100; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test();
                }
            }).start();
        }
    }

    public static void test() {

        try{
            // 申请一个请求
            semaphore.acquire();
        }catch (InterruptedException el){
            el.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+"进来了");

        try{
            // 为什么要用sleep,主要是为了暂停当前线程,
            // 把cpu片段让出给其他线程,减缓当前线程的执行
            Thread.sleep(1000);
        }catch (InterruptedException el){
            el.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+"走了");

        // 释放一个请求
        semaphore.release();
    }
}

10. 三个线程a、b、c并发运行,b、c需要a线程的数据,如何实现

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.util.concurrent.Semaphore;

/**
 *  问题描述:
 *      三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现
 *  问题分析:
 *      ThreadA、ThreadB、ThreadC,ThreadA 用于初始化数据 num,
 *      只有当 num 初始化完成之后再让 ThreadB 和 ThreadC 获取到初始化后的变量 num
 *
 *      线程不确定性--不能保证ThreadA就一定先于ThreadB和ThreadC前执行,就算 ThreadA
 *      先执行了,我们也无法保证 ThreadA 什么时候才能将变量 num 给初始化完成。
 *      因此我们必须让 ThreadB 和 ThreadC去等待 ThreadA 完成任何后发出的消息
 *
 *      需要解决问题:
 *          一:是让 ThreadB 和 ThreadC 等待 ThreadA 先执行完
 *          二:是 ThreadA 执行完之后给 ThreadB 和 ThreadC 发送消息
 */

public class ThreadCommunication {

    // 定义一个变量作为数据
    private static int num;

    // SemapHore构造函数
    // 线程通过 semaphore.acquire()方法获取 permit,
    // 如果当前 semaphore 有 permit
    // 则分配给该线程如果没有则阻塞该线程直到 semaphore
    private static Semaphore semaphore = new Semaphore(0);

    public static void main(String[] args) {

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 模拟耗时操作之后初始化变量num
                    Thread.sleep(1000);
                    num = 1;

                    System.out.println("首先要开启线程A");

                    // 初始化参数后释放两个permit
                    semaphore.release(2);

                }catch (InterruptedException el){
                    el.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个
                   semaphore.acquire();
                }catch (InterruptedException el){
                    el.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num);
            }
        });

        Thread threadC  = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个
                    semaphore.acquire();
                }catch (InterruptedException el){
                    el.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num);
            }
        });

        // 同时开启三个线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

11. 同一个类中的2个方法都加了同步锁,多个线程能同时访问同一个类中的这两个方法吗?

Lock不行,synchronized可以

Lock锁:Lock 可以让等待锁的线程响应中断,Lock 获取锁,之后需要释放锁。如下代码,多个线程不可访问同一个类中的 2 个加了 Lock 锁的方法

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *  Lock锁可以让等待锁的线程响应中断
 *  Lock获取锁,之后需要释放锁
 *
 *  多个线程不可访问同一个类中的 2 个加了 Lock 锁的方法
 */

public class qq {

    private int count = 0;
    // 设置锁
    private Lock lock = new ReentrantLock();

    // 方法1
    public Runnable run1 = new Runnable() {
        @Override
        public void run() {
            // 加锁
            lock.lock();
            while(count<1000){
                try{
                    // 打印是否执行该方法
                    System.out.println(Thread.currentThread().getName()+"run1:"+count++);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            lock.unlock();
        }
    };

    // 方法2
    public Runnable run2 = new Runnable() {
        @Override
        public void run() {
            // 加锁
            lock.lock();
            while(count<1000){
                try{
                    // 打印是否执行该方法
                    System.out.println(Thread.currentThread().getName()+"run2:"+count++);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            lock.unlock();
        }
    };

    public static void main(String[] args) throws InterruptedException{
        // 创建该对象的方法1
        qq t = new qq();

        // 获取该对象的方法1
        new Thread(t.run1).start();

        // 获取该对象的方法2
        new Thread(t.run2).start();

    }
}

synchronized

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *  使用 synchronized 时,当我们访问同一个类对象的时候,是同一把锁,
 *  所以可以访问该对象的其他 synchronized 方法
 */

public class qq_1 {

    private int count = 0;

    // 方法1
    public Runnable run1 = new Runnable() {
        @Override
        public void run() {
            // 设置关键字synchronized,以当前类为锁
            synchronized (this) {
                while (count < 1000) {
                    try {
                        // 打印是否执行该方法
                        System.out.println(Thread.currentThread().getName() + "run1:" + count++);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    // 方法2
    public Runnable run2 = new Runnable() {
        @Override
        public void run() {
            synchronized (this) {
                while (count < 1000) {
                    try {
                        // 打印是否执行该方法
                        System.out.println(Thread.currentThread().getName() + "run2:" + count++);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    public static void main(String[] args) throws InterruptedException{
        // 创建一个对象
        qq_1 t = new qq_1();

        // 获取该对象的方法1
        new Thread(t.run1).start();

        // 获取该对象的方法2
        new Thread(t.run2).start();

    }
}

12. 什么情况导致线程死锁,怎么解决?

1) 定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

2) 死锁产生的必要条件

  • 互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待

  • 不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)

  • 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放

  • 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求

java面试宝典书籍-JavaSE基础_第13张图片

3) 死锁例子

/**
 *  例子阐述:
 *       当 DeadLock 类的对象 flag==1 时(td1),先锁定 o1,睡眠 500 毫秒
 *       而 td1 在睡眠的时候另一个 flag==0 的对象(td2)线程启动,先锁定 o2,睡眠 500 毫秒
 *       td1 睡眠结束后需要锁定 o2 才能继续执行,而此时 o2 已被 td2 锁定
 *       td2 睡眠结束后需要锁定 o1 才能继续执行,而此时 o1 已被 td1 锁定
 *       td1、td2 相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁
 */

public class DeadLock implements Runnable{

    public int flag = 1;

    // 静态对象是类的所有对象共享的
    private static Object o1 = new Object(),o2 = new Object();

    @Override
    public void run() {
        System.out.println("flag="+flag);
        if (flag == 1){
            synchronized (o1){
                try{
                    Thread.sleep(500);
                }catch (Exception e){
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;

        // td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的
        // td2的run()可能在td1的run()之前
        new Thread(td1).start();
        new Thread(td2).start();
    }
}

4) 如何避免死锁

加锁顺序--线程按照一定顺序加锁

/**
 *  例子阐述:
 *      加锁顺序
 */

public class DeadLock_2 {

    public int flag = 1;

    // 静态对象是类的所有对象共享的
    private static Object o1 = new Object(),o2 = new Object();

    public void money(int flag){
        this.flag = flag;
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+" "+"flag 的值"+"1");
                }
            }
        }

        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+" "+"flag 的值"+"0");
                }
            }
        }
    }

    public static void main(String[] args) {
        final DeadLock_2 td1 = new DeadLock_2();
        final DeadLock_2 td2 = new DeadLock_2();
        td1.flag = 1;
        td2.flag = 0;

        // td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的
        // td2的run()可能在td1的run()之前
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                td1.flag = 1;
                td1.money(1);
            }
        });

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 让t2等待t1执行完
                    t1.join();  // 核心代码,让 t1 执行完后 t2 才会执行
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                td2.flag = 0;
                td1.money(0);
            }
        });

        t2.start();

    }
}

加限时锁

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *  例子阐述:
 *      加限时锁
 */

public class DeadLock_3 {

    public int flag = 1;

    // 静态对象是类的所有对象共享的
    private static Object o1 = new Object(),o2 = new Object();

    public void money(int flag){
        this.flag = flag;
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+" "+"flag 的值"+"1");
                }
            }
        }

        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+" "+"flag 的值"+"0");
                }
            }
        }
    }

    public static void main(String[] args) {
        final Lock lock = new ReentrantLock();
        final DeadLock_3 td1 = new DeadLock_3();
        final DeadLock_3 td2 = new DeadLock_3();
        td1.flag = 1;
        td2.flag = 0;

        // td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的
        // td2的run()可能在td1的run()之前
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                String tName = Thread.currentThread().getName();

                td1.flag = 1;

                try{
                    // 获取不到锁,就等 5 秒,如果 5 秒后还是获取不到就返回 false
                    if (lock.tryLock(5000, TimeUnit.MILLISECONDS)){
                        System.out.println(tName+"获取到锁");
                    }else {
                        System.out.println(tName+"获取不到锁");
                        return;
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }

                try{
                    td1.money(1);
                }catch (Exception e){
                    System.out.println(tName+"出错了");
                }finally {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+"释放锁");
                    lock.unlock();
                }

            }
        });

        t1.start();

        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String tName = Thread.currentThread().getName();

                td1.flag = 1;

                try{
                    // 获取不到锁,就等 5 秒,如果 5 秒后还是获取不到就返回 false
                    if (lock.tryLock(5000, TimeUnit.MILLISECONDS)){
                        System.out.println(tName+"获取到锁");
                    }else {
                        System.out.println(tName+"获取不到锁");
                        return;
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }

                try{
                    td1.money(0);
                }catch (Exception e){
                    System.out.println(tName+"出错了");
                }finally {
                    System.out.println("当前的线程是"+Thread.currentThread().getName()+"释放锁");
                    lock.unlock();
                }

            }
        });

        t2.start();

    }
}

13. Java多线程间的通信怎么实现?

1) 共享变量

public class MySignal {

    // 共享的变量
    private boolean hasDataToProcess = false;

    // 取值
    public boolean isHasDataToProcess() {
        return hasDataToProcess;
    }

    // 存值
    public void setHasDataToProcess(boolean hasDataToProcess) {
        this.hasDataToProcess = hasDataToProcess;
    }

    public static void main(String[] args) {
        // 同一个对象
        final MySignal my = new MySignal();

        // 线程1设置hasDataToProcess值为true
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                my.setHasDataToProcess(true);
            }
        });
        t1.start();

        // 线程2设置hasDataToProcess值为true
        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 等待线程1完成后取值
                    t1.join();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                my.setHasDataToProcess(true);
                System.out.println("t1改变以后的值:"+my.isHasDataToProcess());
            }
        });
        t2.start();
    }
}

2) wait/notify机制

以资源为例,生产者生产一个资源,通知消费者就消费掉一个资源,生产者继续生产资源,消费者消费资源,以此循环


// 资源类
class Resource {

    private String name;
    private int count = 1;
    private boolean flag = false;

    public synchronized void set(String name){
        // 生产资源
        while(flag){
            try {
                // 线程等待。消费者消费资源
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        this.name = name + "---"+count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag = true;
        // 唤醒等待中的消费者
        this.notifyAll();

    }

    public synchronized void out(){
        // 消费资源
        while(!flag){
            try {
                // 线程等待。生产者生产资源
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag = false;

        // 唤醒生产者,生产资源
        this.notifyAll();
    }
}

// 生产者
class Producer implements Runnable{

    private Resource res;

    public Producer(Resource res) {
        this.res = res;
    }

    // 生产者生产资源
    @Override
    public void run() {
        while (true){
            res.set("商品");
        }
    }
}

class Consumer implements Runnable{

    private Resource res;

    public Consumer(Resource res) {
        this.res = res;
    }

    @Override
    public void run() {
        while(true) {
            res.out();
        }
    }
}

public class ProducerConsumerDemo{
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);

        t1.start();
        t2.start();
    }
}

14. 线程和进程的区别

  • 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位

  • 线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位

  • 特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验

  • 注:多线程编程对于其他程序是不友好的,占据大量的CPU资源

15. 同步线程及线程调度相关方法

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态

​​​​​​​16. 启动一个线程是调用run()方法还是调用start()方法?

  • 启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行
  • run()方法是线程启动后要进行回调(callback)的方法

十、Java内部类

1. 内部嵌套类(Static Nested Class)和内部类(Inner Class)的不同

  • Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化

  • 内部类:需要在外部类实例化后才能实例化,其语法看起来挺诡异的

java面试宝典书籍-JavaSE基础_第14张图片

原因:Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo 和 main 方法都是静态方法,静态方法中没有 this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做

 

 

 

 

 

 

 

 

你可能感兴趣的:(Java)