Java面试——Java基础

零、基础概念的理解(个人理解)

1、Bean和类

类是对现实中某一类具体事物的总结描述,方便后续进行编程;
它一般由一些变量、和一些功能方法组成,比如我们可以对人、狗、猫进行抽象面试,定义三个Java类,分别叫class Person、class Dog、class Cat;再具体一点,类中通过姓名、年龄、性别等变量,对这一类事物进行更加详尽的面试,具体要描述那些方面,由编程目标决定。

在Java中,Bean就是类,“Bean”和“类”是对同一事物的不同描述方式。
这就类似于,“佛”的含义是“觉悟者”,那么“觉悟者”和“佛”就是对同一事物的不同描述。
为什么要把类叫做Bean呢?
Bean的词义是“豆子”;
类是对一类事物的总结描述,而在程序开发中具体使用,需要对其进行实例化、获取它的实例化对象,比如我们要获取某人的年龄,不能用类来获取,需要把类实例化为具体的对象张三、李四、王五…
有了具体的人,才有“获取其年龄”这一说。
这个过程,就像把豆子种到地里,结出很多小豆子非常类似,新的豆子与“种子豆子”结构一模一样,只是更为具体化了。
就是出于这种相似性,java中的类,又可以叫Bean;
进而类的实例化得到类对象,又可以描述为Bean的实例化得到Bean对象。

2、Java的特性

java是完全面向对象的计算机编程语言,其有封装、继承、多态三大特性。

1)我对于这句话的理解是,java在程序功能的具体实现过程,都是通过对象进行的、而不能通过抽象的、不具体的类进行。
2)然后封装就是,对现实中某一事物进行概况描述——>类,或者是对现实中某些功能进行概括性的描述——>接口;但是它们都不能用于做具体功能,实现具体功能时,需要用它们的实现对象才行。
3)继承就是,有关系的类与类之间、接口与接口之间,之前类中写过的、其它类也需要的变量、功能,要再在有关系的其它类中写一遍吗?
可以、但没必要,java支持通过继承实现父类所有的变量和功能方法,通过继承可以减少冗余、提高代码复用性、提高效率。
接口与接口之间,也可以进行继承,继承后子接口会拥有父接口的所有接口,但是一般没必要这样操作,因为接口就是定义一些没有具体的实现的方法,没有具体实现你再写一遍呗、反正不用你实现,能费多少事儿?还用继承?
4)多态,java支持一个接口有多个实现类,实现类必须重新接口方法,这也就让一个功能有了多种实现方式;而在接口实例化的时候,可以一下子获取所有的实现类对象,也可以获取多个实现类对象中的具体某一个。
多态,极大的提高了代码的灵活性和可扩展性。

一、面向对象编程

1、static关键字

static修饰的变量,叫静态变量,在类加载到内存的时候系统就会为静态变量分配专门的内存空间,一直到程序运行结束;
静态变量不属于类的任何一个对象,而是属于全体对象,一般通过类名访问,虽然可以通过对象名访问但一般都建议直接用类目访问;

static修饰的方法,叫静态方法;一般工具类的方法常被定义为静态方法,这样就可以用类名直接调用方法;静态方法中用到的全局变量必须是静态变量,方法内的局部变量可以是非静态的。

2、final关键字

final修饰的字段,不能被重写赋值,在定义时的值就是最终值;
final经常和static一起连用,被它们修饰的字段称为final常量,有点像配置文件的属性,程序启动、直到停止都是不变的,而且可以用类名直接访问,final常量的名字字母一般全部大写。

final修饰的实例方法,可以被子类继承,但不能被子类重写;

final修饰的类,称为最终类,不能被继承;

3、abstract关键字

修饰类,就叫抽象类;
修饰方法,就叫抽象方法(与之相对的是:实例方法);
修饰变量,就叫抽象变量(与之相对的是:实例变量)。
抽象方法和接口方法类似,但是可以有部分子类都需要进行的操作代码;
含有抽象方法的类都必须定义为抽象类;
子类继承抽象类,需要重写抽象类的抽象方法,否则子类也要定义为抽象类。

4、类的继承

1)子类会继承父类的所有实例属性和实例方法;
2)子类不会继承父类的构造方法,需要的话可以用super关键字调用;
3)静态成员不用继承,仍是用类名直接调用即可;
4)实例方法如不满足需求,子类可重写方法;
5)子类需要重写父类的抽象方法(如果有),否则子类也要定义为抽象类;
6)一个类只能继承一个父类。

5、接口的实现

接口多态:用接口对象调用接口方法,实际执行的是接口实现类重写后的方法,这就是接口的多态。

一个类可以实现多个接口(但只能继承一个父类)。

二、数组

1、数组的内存分析

数组是内存中一块连续的存储空间.

2、数组的特点+适用场景

通过下标可以快速访问数组的元素,不管数组的长度有多大,可以通过下标快速计算出每个元素的存储地址,访问速度快。
数组的缺点是插入/删除元素效率低。在插入元素时,可能需要数组扩容,需要复制/移动大量的元素。
所以数组适合应用于以访问为主,很少进行添加/删除操作的场景。

3、数组的算法(了解最好)

1)冒泡排序
2)选择排序
3)二分查找

4、Arrays工具类

大概知道就行,用的时候,可以具体看Arrays代码;
1)把数组转换为List列表
asList(T[] a)
2)copyOf(int[] original, int newLength)
复制original数组,新的数组长度是newLength
3)copyOfRange(int[] original, int from, int to)
把original数组中[from, to)范围内的元素复制到新数组中
4)binarySearch(int[] a, int key)
在a数组中采用二分查找,返回key元素的索引值
5)fill(int[] a, int fromIndex, int toIndex, int val)
把a数组中[from,to)范围的元素使用val填充
6)sort(int[] a)
排序
7)parallelSort(int[] a)
对a数组中元素并行 排序,适合于元素非常多的情况
8)sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
对a数组[from,to)范围的元素进行排序,根据comparator比较器比较大小
9)toString(int[] a)
可以把数组中元素转换为字符串
10)deepToString(Object[] a)
把多维数组转换为字符串

5、二维数组(了解)

就是数组元素是数组的那种

三、常用类

1、String

1)字符串常量池
在java中用引号引起来的字符串都会被放入常量池中,常量池中的变量采用享元模式。
a、String s1 = “hellow”;
String s2 = “hellow”;
若比较s1 == s2,结果是true,因为s1存的地址和s2一样,都指向系统分配给“hellow”的内存地址。
但一般字符串比较还是要用equls,因为==有时候不管用;java有自己的优化机制,常量池有的就尽量不重建,但是若java不确定以后会不会变化,它就会重建以确保不出错。
b、以下两行共创建了多少个String对象?
String s8 = new String(“world”);
String s9 = new String(“world”);
答案是3个;new出来的两个对象好理解,第三个是“world”,java也会给它分配一块内存。
2)StringBuilder/StringBuffer
String对象是不可变的,每次进行字符串连接都会生成新的字符串对象。
如果要频繁进行字符串连接,使用StringBuilder/StringBuffer。
StringBuffer/StringBuilder字符串对象是可变的,当创建StringBuilder对象后,通过append()、reverse()等方法可以改变这个字符串对象中的字符序列。最后可以调用StringBuilder对象的toString()方法转换为String对象。
StringBuilder/StringBuffer的区别:StringBuffer是线程安全的,StringBuilder不是线程安全的。

除了append()、reverse()、deleteCharAt()几个常要方法,还有一个setLength(0)方法,用于清空sb中已经拼上的字符,参数0就是清空所有,效果类似new StringBuilder/StringBuffer()

2、数学相关类

1)Math

java.lang.Math类,定义了一组与数学函数相关的操作,包括三角函数、对数操作、指数操作等。Math类构造方法是private私有的,不能创建Math对象,但Math类的所有方法都是静态方法,可以通过类名直接调用。
eg1:Math.random(),可以生成[0,1)范围内的随机数;
eg2:sqrt(n) n的平方根, cbrt(n)立方根, pow(a,b)返回a的b次方;
eg3:max(a,b)返回a与b的较大者, min(a,b)返回较小者;
了解即可,使用时、具体方法可看Math类中的描述。

2)Random

java.util.Random类,专门用于生成随机数的。提供两个构造方法,无参构造方法使用默认的种子(当前时间),另一个构造方法可以指定随机数的种子,相同种子的Random对象可以生成相同的随机数序列。
Random生成的随机数,比Math的种类更多、使用更方便
eg1:生成随机小数
Random random = new Random();
random.nextDouble();
eg2:生成随机整数
random.nextInt();
eg3:生成指定上限的随机整数, [0, limit)
random.nextInt(100); //生成[0,100)范围内的随机整数

3)DecimalFormat

java.text.DecimalFormat类可以对数字格式化, 常用的数字格式符有#与0,区别在于使用0格式符时,不足的位数会补0。
DecimalFormat df= new DecimalFormat(“###,###.0000”);
String text = df.format(1234.56);
System.out.println( text ); //1,234.5600

4)BigDecimal

如果进行科学计算、财务计算时,使用double可能不准确,可以使用BigInteger、BigDecimal类。
可以调用其add()、subtract()、multiply()、divide()进行加减乘除操作。

3、日期相关类

详看后端->《Java基础知识纪要》的【时间数据处理】

4、包装类

只需要知道Integer装箱、拆箱可能造成的问题即可,详看
后端->《Java基础知识纪要》的【变量的基本数据类型与包装类型】

四、集合与Map

1、collecion集合包括:List与Set

1)list

ArrayList与Vector在内存的存储方式都像数组一样,它们的特点都是易访问、不易扩容(和数组一样);只不过vector是线程安全的。
LinkList底层是双向链表,特点是:访问慢、但增/删容易。

2)set

HashSet无序、不可重复;
TreeSet有序、不可重复;
新建TreeSet的两种方式:
a、new的时候,在构造方法参数列表写一个compatator匿名内部类:

TreeSet<String> treeSet = new TreeSet<>(new Comparator<String>() {
	@Override
    public int compare(String o1, String o2) {
    	return o2.compareTo(o1);
    }
});

b、要放入set的元素,实现了comparable接口、并重写compameTo()方法;比如String就实现了comparable接口、并重写了compameTo()方法,所以可以直接放入TreeSet中

TreeSet<String> treeSet2 = new TreeSet<>();
treeSet2.addAll(treeSet1);	//treeSet1 = [dd, gg, jj, mm, xx]
System.out.println(treeSet2); 

2、Map

1)HashMap

数组+链表;
通过键对应的哈希值->(哈希函数)->哈希码->数组下标,
节点连到数组、或其他链表后边;
在这里插入图片描述
查询的时候,根据键对应的哈希值->(哈希函数)->哈希码->数组下标,找到对应链表,然后根据键的哈希值,找到对应的键值对。
map的键,如同set、是不可重复的,所以其哈希值注定不同(哈希码可能相同)。

2)TreeMap

TreeMap的创建与TreeSet类似,有两种方式:
a、new的时候,在构造方法参数列表写一个compatator匿名内部类:

TreeMap<String , Integer> treeMap = new TreeMap<>(new Comparator<String>() {
	@Override
	public int compare(String o1, String o2) {
		return o2.compareTo(o1);
	}
});

b、map的键值元素,实现了comparable接口、并重写compameTo()方法;比如String就实现了comparable接口、并重写了compameTo()方法,所以,以String为键值的、可以直接放入TreeMap中

3、Collections工具类

java.util.Collections工具类定义了一组对集合的操作
1)addAll( Collection, 元素), 可以向Collection中添加若干元素
Collections.addAll(list, “xx”, “oo”, “dd”, “jj”, “mm”);
2)sort()排序, 在JDK8前,对List排序使用Collections.sort(),在JDK8中List集合增加了自己的sort()

 Collections.sort(list);	//没有指定Comparator比较器要求List集合中 元素实现Comparable接口
 System.out.println(list);	//[dd, jj, mm, oo, xx]

五、异常

1、异常概述

在这里插入图片描述
运行时异常,不需要预先处理,解决靠规范合理的代码;
受检异常,需要预处理,包括抛出、捕获两种方式;
异常抛出后,就不断向上、交给调用者处理,最终一般都是上抛到JVM处理,JVM默认的异常处理方式是:中断程序+打印异常信息。
异常捕获语法:
try{
把有需要预处理的受检异常的代码入在try代码块中
}catch(异常类型 e){
捕获异常,进行预处理,
程序运行后,万一产生了异常,程序员提供的一预处理方案
}finally{
finally子句不是必需的,
不管是否有异常产生总是会执行
}

2、自定义异常及示例

很多时候,系统是否抛出异常,是根据业务需求来定的。比如程序中的数据或者执行与既定的业务需求不符,这就可以视为一种异常,与业务需求不符的异常需要由程序员来决定抛出。
抛出异常时要选择合适的异常类,以便更为明确地描述该异常情况,如果觉得java的异常类没有合适的,程序员可以自定义异常类。
自定义异常类都要继承Exception类;如果希望自定义运行时异常,则该异常类需要继承RuntimeException类(Exception的子类)。
定义异常类时一般提供两个构造方法,一个是无参构造方法,一个是带有String字符串参数的构造方法,这个字符串就作为异常的描述信息。

自定义异常应用示例:
在设置Person类属性时,如果设置的年龄超出范围,就抛出一个年龄越界的异常;如果设置的性别不合理,就抛出一个性别非法异常。

//person类
public class Person {
    private String name;
    private int age;
    private String gender;

    public String getName() {
        return name;
    }

    public Person setName(String name) {
        this.name = name;
        return this;
    }

    public int getAge() {
        return age;
    }

    public Person setAge(int age) {
        if (age >= 0 && age < 130 ) {
            this.age = age;
        }else {     //抛出一个年龄越界的异常
           //通过throw抛出一个异常对象;运行时异常,不需要额外处理
            throw new AgeOutOfBoundsException(age + "超出人的年龄范围");
        }
        return this;
    }

    public Person setGender(String gender) throws IllegalGenderException {
        //性别合理就赋值,不合理就抛出异常
        if ("男".equals(gender) || "女".equals(gender) ) {
            this.gender = gender;
        }else {     //抛出性别非法异常
            //throw抛出的受检异常对象,当前方法需要通过throws声明它,对于 setGender 方法来说,它就是受检异常
            throw new IllegalGenderException(gender + "不是合法的性别");
        }
        return this;
    }

    public String getGender() {
        return gender;
    }
}
//自定义年龄越界异常类,该异常类继承了RuntimeException类,是运行时异常
public class AgeOutOfBoundsException extends RuntimeException {
    //一般提供两个构造方法
    public AgeOutOfBoundsException() {
    }
    //有String参数的构造方法,字符串是异常 的信息
    public AgeOutOfBoundsException(String message) {
        super(message);
    }
}
//自定义非法性别异常类,  该异常类继承了Exception,而不是RuntimeException类,是受检异常
public class IllegalGenderException extends Exception {
    public IllegalGenderException() {
    }
    public IllegalGenderException(String message) {
        super(message);
    }
}
//测试
public class Test {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("lisi");
        //setAge()方法体中抛出的运行时异常,不需要预处理
        p1.setAge(23);
        //setGender方法声明抛出了受检异常IllegalGenderException,在调用时需要预处理
        try {
            p1.setGender("男");
        } catch (IllegalGenderException e) {
            e.printStackTrace();
        }
    }
}

3、全局异常处理

这里涉及到Spring的内容,所以在学习java基础的时候,老师并没有讲这个点;但是全局异常处理非常实用,而且面试也会经常问到。把它放到异常这个知识点下比较好记,所以在这里说一下

1)应用场景(必要性)

如果不做统一的异常处理,那么常常就需要在controller中的很多方法写try…catch…处理异常,这部分代码重复、冗余、且不优雅;最好是通过aop,把这部分工作隐形的完成。
全局异常处理,就是spring为了解决上述问题而提供的方法。

2)实现方法汇总

Spring统一/全局异常处理,一般有3种方式

一、定义一个异常处理类,实现HandlerExceptionResolver接口,重写resolveException()方法
这个方法有4个参数:HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4;通过这些参数,可以实现对异常的统一处理,返回固定格式的json串、或者返回一个页面。
这种方法比较麻烦,一般不推荐使用。

二、通过注解@ExceptionHandler实现
可以针对某个Controller,编写一个用于这个Controller的统一异常处理方法;
实现方式就是在这个类中写一个异常处理方法,并用@ExceptionHandler注解修饰就可以了;
还可以在这个注解中,指定方法要处理的异常种类

	@ExceptionHandler(BindException.class)
    public Result handleBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return Result.failure(message);
    }

三、通过注解@ControllerAdvice(配合@ExceptionHandler),实现全局异常处理
如果只实现@ExceptionHandler,就只能处理当前Controller中的异常,而如果@ControllerAdvice配合@ExceptionHandler则可以实现全局异常捕获、处理。
@ControllerAdvice是Spring3.2提供的新注解,可以对Controller中被@RequestMapping修饰的方法加一些处理逻辑,最常加的就是异常处理。
补充:因为异常处理形式,常常是返回统一格式的json串,所以这里@ControllerAdvice常用@RestControllerAdvice代替。

3)生产中最推荐的实现方式——实例代码

最推荐的实现方式,当然是用**@ControllerAdvice配合@ExceptionHandler作全局异常处理**了;下面给一段应用demo

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 处理连接异常
     */
    @ExceptionHandler(BindException.class)
    public Result handleBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return Result.failure(message);
    }
    
    /**
     * 处理SQL异常
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result handleSQLException(SQLIntegrityConstraintViolationException ex){
        //异常处理
    	//异常中带有Duplicate entry这类异常进行处理
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return Result.failure(msg);
        }
    	return Result.failure("添加员工失败");   
    }
    
}

六、注解

1、java.lang包中定义了一些基本的注解:

@Override注解告诉编译器检查方法覆盖(重写)是否符合规则.
@Deprecate注解表示已过时,当其他程序使用已过时的类或方法时,编译器会给出警告.
@FunctionalInterface注解表示是函数式接口. 如果接口中只有一个抽象方法可以定义为函数式接口. 函数式接口是为Lambda表达式准备的,即可以使用Lambda创建函数式接口实例.

2、自定义注解

语法:
[修饰符] @interface 注解名{
数据类型 属性名() default 默认值;
}

应用示例:

@Target(ElementType.TYPE)           //当前注解可以修饰类
@Retention(RetentionPolicy.RUNTIME) //保留到运行时
public @interface MyAnnotation {
    String name() default "hellow";  //定义name属性,默认值是hellow
}
@MyAnnotation(name = "hehe")
public class Test {
    public static void main(String[] args) {
        Class<Test> claxx = Test.class;
        //通过反射读取注解的属性值
        MyAnnotation annotation = claxx.getAnnotation(MyAnnotation.class);
        if ( annotation != null ){
            String name = annotation.name();
            System.out.println(name);
        }
    }
}

七、反射

反射就是Java中一种,根据类对象获取类、类方法、类属性等信息的,一种技术。
1)可获取的类信息
class.getModifiers() 返回类的修饰符
class.getSimpleName() 返回类的简易类名
class.getSuperclass() 返回父类
class.getInterfaces() 返回接口
2)可获取的方法信息
clss.getMethod(方法名, 形参列表) 返回指定方法签名的方法
method.invoke(对象名, 方法实参) 调用方法
3)可获取的字段信息
class.getFied(字段名) 返回指定名称的公共字段
class.getDeclaredField(字段名) 返回指定名称的任意权限的字段
field.set(对象名, 字段值) 设置对象的字段值
field.get(对象名) 返回对象的字段值
4)获取注解信息

八、多线程

1、基本概念

1)进程 (Process):是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。可以把进程简单的理解为正在操作系统中运行的一个程序。
2)线程 (Thread):是进程的一个执行单元/执行分支。进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程。

在操作系统中是以进程为单位分配资源,如CPU、虚拟存储空间等。多个线程共享同一个进程的资源。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。

3)主线程:JVM启动时会创建一个主线程,该主线程负责执行main方法/主线程就是运行main方法的线程。

4)Java中的线程不是孤立的,线程之间存在一些联系。如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程

5)串行、并发与并行:串行是指各个任务依次执行,并发是指在某段时间内交替完成多个任务;并行是同时完成多个任务。
并发可以提高事物的处理效率,即一段时间内可以处理或者完成更多的事情。并行是一种更为严格、理想的并发。
从硬件角度来说,如果是单核CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快速的在各个线程之间进行切换,对于用户来说,感觉是多个线程在同时执行。如果是多核心CPU,可以为不同的线程分配不同的CPU内核。

6)Java的内存模型

共享变量都存在主内存中,每个线程都有自己的本地内存,线程对变量的操作都是在本地内存中进行的。
当一个线程需要访问共享变量时,它需要将变量值从主内存中读取到本地内存中,对变量进行操作后,再将更新后的值写回主内存中。
这个过程包括三个步骤:读取、操作、写回,为了控制多个线程访问共享变量的安全性,我们肯定希望这个三个操作是原子操作。

7)线程安全

保证原子性、可见性、有序性;
原子性,读取、操作、写回操作,一旦开始不能被其它线程影响;
可见性,一个线程对共享变量的修改,其它线程都要能知道;如果严格遵循变量修改的原子性操作,可见性是可以被保证的;但很多时候为了追求性能,放弃原子性,这时可见性就不一定能被保证了。
有序性,在本线程内观察,三个操作都是严格按照先后顺序进行的。

8)Java提供的同步机制

为了保证多线程操作中,多个线程对于共享资源访问、操作的安全性(原子性、可见性、有序性),Java提供了多种同步/锁机制,包括synchronized、volatile关键字、ReEntrantLock可重入锁、ReadWriteLock读写锁等。
锁的原理,都是通过对共享资源访问权进行控制,保证访问的有序性、原子性,进行保证线程安全。

锁对比synchronized,更加灵活;
比如lock可以通过设置过期时间避免死锁;
也可以设置规定时间获取不到锁就放弃,避免长时间等待。

2、一些面试题

1)什么是线程?线程和进程有什么区别?

答:进程就是程序;
线程是进程中一个独立执行的路径。

2)多线程的实现方式

有四种实现方法:

继承Thread类,重写run()方法;

实现Runnable接口,重写run()方法;

实现Callable接口,重写call()方法;可以有返回值,通过Callable<>接口的泛型指定返回值的类型。

使用ExecutorService、Callable、Future实现有返回结果的多线程。

3)Java 关键字volatile 与 synchronized 作用与区别?

两者都是java提供的同步机制;

A、volatile只能用来修饰变量;
用volatile修饰的变量,可以保证有序性和可见性,但是不能保证原子性;
保证可见性:用volatile修饰的变量,在某线程进行读操作时总是去主内存中读取、而不是从线程本地内存读取,在进行写操作时,也会立即更新到主内存中。
保证有序性:用volatile修饰的变量,禁止cpu、编译器进行指令重排,进而保证有序性。

B、synchronized可以修饰方法、代码块,可以保证原子性、可见性、有序性;
一、用synchronized修饰方法,可以修饰普通方法、也可以修饰静态方法;
修饰普通方法的作用范围是同一个对象,若在不同线程、用不同对象调用改普通方法,锁无法起作用;
修饰静态方法,其作用域是全局的。

二、synchronized用在方法里面、锁对应的代码块,即常说的同步代码块;这中用法需要传一个参数,参数可以传对象、或者传类(xx.class);

synchronized(obj/xx.class) {
	......
} 
try {
	......
}catch() {
}

当参数传对象时,可以传this/注入一个本类对象、传对象名,这种情况其作用域是单个对象,在不同线程、用不同对象调用含同步代码块的方法,锁是不起作用的;

当参数传类,比如xx.class时,这时锁的作用域是全局生效的。

4)线程有哪些不同的线程生命周期?

create 新建状态

runnable 就绪状态/可运行状态

running 运行状态

blocked 阻塞状态

dead 终止状态

5)什么是死锁(Deadlock)?如何分析和避免死锁?

死锁是指多个线程因竞争资源而造成的一种相互等待的僵局,若无外力作用,这些线程任务都将无法向前推进。

避免死锁就要避免循环等待条件的产生、设置资源标识位以及执行顺序等。

6)什么是线程安全?Vector是一个线程安全类吗?

线程安全就是,多线程操作和多个线程单独、依次操作产生得结果相同。
一般认为Vector是一个线程安全的类,但实际上虽然Vector类中好多方法是用Synchronized修饰的,但是也有remove 和 contains等几个方法不是用Synchronized修饰的,这就需要在业务代码块中添加同步锁来保证线程安全了,所以严格来说不能直接说Vector是线程安全类。

7)Java中如何停止一个线程?

Thread类中的interrupt()方法可以中断线程,注意调用interrupt()方法仅仅是在当前线程打一个中断标志,并不是真正的停止线程;具体中断线程操作为:在测试、业务方法中用start()实例方法启动线程,在合适的节点用interrupt()方法给线程打中断标志,在子线程中提前设置代码——通过判断中断标志来退出子线程调用,可以用break关键字或者returen()退出。

Thread.interrupted()静态方法可以判断当前线程的中断状态,也可以调用thread.isInterrupted()实例方法判断当前线程的中断状态。这两个方法的区别在于: 静态方法interrupted()判断完中断状态后会清除中断标志, 实例方法isInterrupted()判断完后不会清除中断标志。

8)sleep()、suspend()和wait()之间有什么区别?

注:以上三个方法都是使线程进入blocked状态的

答:三个方法使用后线程的唤醒方式不同,sleep()固定时间后主动唤醒,suspend()是已经废弃的暂停方法、被resumen唤醒,wait()被notify()或者或者 notifyAll()唤醒。

9)什么是线程饿死,什么是活锁?

饿死:线程没有被合理的分配资源,都处于无限期的等待;

活锁:所有线程都认为自己的优先级别不够高,都在等待对方先执行。

10)什么是Java Timer类?如何创建一个有特定时间间隔的任务?

答:Timer可以看成一个定时器,用来安排以后在线程中执行的任务,可设置任务执行一次,或者定期重复执行。Timer可以调度TimerTask创建有特定时间间隔的任务,TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力;一个Timer可以调度任意多个TimerTask,它会将TimerTask存储在一个队列中,顺序调度,如果想两个TimerTask并发执行,则需要创建两个Timer。

11)什么是线程池? 为什么要使用它?

答:线程池用来管理多线程任务中的线程。一个线程需要创建,运行和销毁,如果客户端访问量过大,那么创建线程和销毁线程将是一笔很大、且有些浪费的开销;有了线程池,我们会在程序启动的时候事先放入几个线程,当有任务调用的时候就直接去取线程池中的线程,任务结束再将链接放回线程池而不是直接销毁。当然线程池本身也占用资源,所有容量也不易过多。

12)多线程中的忙循环是什么?

答: 忙循环就是程序员用循环让一个线程等待;不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存。在多核系统中,一个等待线程醒来的时候可能就在另一个内核运行了,这样会重建缓存,为了避免重建缓存和减少等待重建的时间就可以使用忙循环。

13)Thread 类中的start() 和run() 方法有什么区别?

start 是用来启动线程的,run()则是运行线程的 ;

start 在一个线程中只能调用一次,而run则可以多次调用;

查看Thread类的源码会发现start 有synchronized修饰,可见这个方法是线程安全的。

14)在多线程中,什么是上下文切换?

即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现多线程。时间片就是CPU分配给各个线程的时间,因为时间片非常短(一般为几十毫秒),所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书籍时,发现某个单词不认识,于是便打开中英文词典,但是在放下英文书籍之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

15)(相对于线程来说)Java中堆和栈有什么不同?

答:创建的对象是在堆中,堆内存是各线程共享的区域;而栈中内存是线程私有的区域、生命周期随着线程的消亡而消亡。系统为了提高运行效率会从堆中复制一份变量到栈内存中,运行完成后再刷新到堆内存中。

16)Thread类中的yield方法有什么作用?

答:让当前正在运行的线程回到就绪状态,让出cpu,等待cpu的再次调度。

17)Java中notify 和 notifyAll有什么区别?

答:notify是唤醒等待池中的某一个线程但是不指定是那个,notifyAll是唤醒等待池中的所有线程进入锁池中竞争对象。

18)Java多线程中调用wait() 和 sleep()方法有什么不同?

答:sleep()是Thread的静态方法;线程调用此方法会让线程暂时放弃cpu资源,但是不会释放对象锁;时间到后会自动苏醒;需要捕获异常

wait()方法必须放在同步控制方法或者同步语句块中使用;会暂时放弃对象锁;需要notify()方法将其唤醒。

19)有三个线程T1,T2,T3,怎么确保它们按顺序执行?

启动某个线程的时候分别加入join()方法,例如T3调用T2、然后T2调用T1,利用单线程池法

20)什么是ThreadLocal?

ThreadLocal是局部本地变量,为单个线程私有变量、不能共享,所以不存在线程安全问题;
相对的还有共享变量,线程不安全,需要用锁机制来维护。

21)Java线程池中submit() 和 execute()方法有什么区别?

答:两个方法都可以向线程池中提交任务,区别是:
execute()是Executor中的方法,用void修饰,即没有返回值;
submit() 是ExecutorService中的方法,有返回值,返回值为Future对象,可以用get方法获取执行结果。

22)Java中Runnable和Callable有什么不同?

答:区别是:
Runnable从jdk1.0就有,Callable从jdk5.0有;
callabel的call()方法可返回Future对象,Runnable的run()方法没有返回值。

九、Java的设计模式

设计模式是一套被反复使用、多数人知晓的、经过分类编目的代码设计经验的总结,旨在提高代码的可重用性、可读性和可靠性。
Java中共有23种设计模式,这些模式可以分为三大类:创建型模式、结构型模式和行为型模式。
在这里插入图片描述
需要注意的是,面试中问设计模式,经常不单单问一下概念,而是问你在实际开发中对于设计模式的应用,在什么地方、做什么项目、什么功能用到了?

其实一般,在开发中多多少少都会用到一些的设计模式的,只不过用的时候没有注意到。

另外在面试回到的时候,也不用说很多,举一些例子就可以。

1、观察者模式

观察者模式定义了对象间一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会收到通知并自动更新。

具体实现
如上,商城订单状态变化,对应的减库存、发短信、做统计等等模块也都跟着变化,具体实现是通过RabbitMQ,但用到的设计模式就是观察者模式。
同理,还有转盘抽奖,活动开始时给用户发通知、告知管理员等,也属于观察者模式。

2、策略模式

策略模式就是,定义一些列算法,并将每种算法封装起来,使它们可以互相替换,使用时根据选择取对应的算法具体执行。

具体应用:
最常见的,商城支付系统,就是用的策略模式(无论是单独的支付系统、还是商城系统的支付功能模块,都是采用策略模式),提供不同的支付方式(如微信、支付宝、信用卡、PayPal),根据用户选择、找具体的某种方式来执行支付操作

// 定义支付策略接口
interface PaymentStrategy {
    void pay(int amount);
}

// 实现具体的支付策略
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card");
    }
}

class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal");
    }
}

// 创建上下文使用策略
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// 使用策略模式
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPaymentStrategy(new CreditCardPayment());
        cart.checkout(100);

        cart.setPaymentStrategy(new PayPalPayment());
        cart.checkout(200);
    }
}

3、其他

1)单例模式:单例模式确保一个类只有一个实例,并提供一个全局访问点。
日志系统的日志记录器,一般都是单例模式,这样可以保证全系统统一的日志输出格式。

2)工厂模式:定义了一个创建对象的接口,但由子类决定实例化哪一个类。
也就是说,经常会用的多态,就是采用了工厂模式。

3)享元模式:包装类,比如Integer,其-127~128就是采用享元模式。

X、其他

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值