Java面试 —— Java基础

Java 基础

Java三大特性.

封装: 方法、属性聚集在类中
继承: 通过继承关系可以实现功能和属性的扩展
多态: 父类引用子类对象

1、 == 与 equals

在说明上述两个的区别前,需要了解Java中基本数据类型和引用类型是如何存储的。
基本数据类型存放于栈帧中,存放的是变量的值。而引用变量类型在栈中存放的是引用的地址,内容是存放在堆中的。例如,int x = 12,由于x是基本数据类型,栈中存放的是变量的值12,。而String s = new Striing(“zs”)s是通过new运算符生成的,是对象,其栈存放的是"zs"在堆中的内存地址0x11。
在这里插入图片描述
Java中的"=="有两种比较方式。对于基本类型来说,比较的是值,而对于引用类型来说,比较的是内存地址。

equals,默认是Objec的equals方法,该方法比较的是对象在内存中的地址,等价于通过"=="比较两个对象。源码如下:

public boolean equals(Object obj) {
return (this == obj);
}

一般,这并不是我们想要的,通过重写可以实现值比较。String的比较就重写了equals方法

import java.util.Scanner;
class Main{
    public static void main(String args[]){
        String s1 = new String("zs"); 
        String s2 = new String("zs");
        System.out.println(s1 == s2);  //false
        String s3 = "zs";
        String s4 = "zs";
        System.out.println(s3 == s4);//true
        System.out.println(s3 == s1);//false
        String s5 = "zszs";
        String s6 = s3+s4;
        System.out.println(s5 == s6);//false
        final String s7 = "zs"; 
        final String s8 = "zs";
        String s9 = s7+s8;
        System.out.println(s5 == s9);  //true
        final String s10 = s3+s4;  //final 说明s10只能被赋值一次 但是是通过s3+s4相当于new
        System.out.println(s5 == s10);//false


    }

}
注意: Java中有字符串常量池,对于一个字符串值,会首先在常量池中寻找是否有该字符串值出现,有则直接调用,无则重新创建。因此s3和s4指向的是同一字符串引用。而 String s6 = s3+s4,java中通过"+"连接两个字符串变量创建新的字符串本质上是 通过StringBuilder创建一个新的对象来创建因此s6指向的是堆中内存,而s5是字符串常量,指向的是字符串常量池的地址。

Java中通过"+"连接两个字符串常量会将其结果也转化为字符串常量。因此s9和s5都指向
final String s10 = s3+s4; final 说明s10只能被赋值一次 但是s10是通过两个字符串变量s3和s4+创建出来的,是一个对象。而 s5是字符串常量池的引用,两者是不同的。

Java传参问题

Java中数组作为参数传递的是引用。

package 第七届;
/**
* @author JohnnyLin
* @version Creation Time:2021年4月14日 下午7:42:01
	数组是传值还 是传引用
*/
public class Test {
	public static void fun(int []a) {//传递引用
		a[0] = 1100;
		
		
	}
	public static void main(String[] args) {
		int [] b = {1,2,3,4,5,6,7,8};
		fun(b);
		System.out.println(b[0]); //1100

	}

}

package 第七届;
/**
* @author JohnnyLin
* @version Creation Time:2021年4月14日 下午7:42:01
*/
public class Test {

	public static void fun_Basic(int a) {
		a = 999;
	}
	public static void main(String[] args) {

		int c = 456;
		fun_Basic(c);
		System.out.println(c);//456

	}

}

从上面例子可以看出。
基本数据类型(int、long、double)作为函数参数传递,是按值传递。
引用类型(类对象、数组)作为函数参数传递,是按引用传递。

在这里插入图片描述本质上传递的都是值,只不过对于引用变量来说其值为引用地址。

public class Person {
    private String name;
   // 省略构造函数、Getter&Setter方法
}

public static void main(String[] args) {
    Person xiaoZhang = new Person("小张");
    Person xiaoLi = new Person("小李");
    swap(xiaoZhang, xiaoLi);
    System.out.println("xiaoZhang:" + xiaoZhang.getName());
    System.out.println("xiaoLi:" + xiaoLi.getName());
}

public static void swap(Person person1, Person person2) {
    Person temp = person1;
    person1 = person2;
    person2 = temp;
    System.out.println("person1:" + person1.getName());
    System.out.println("person2:" + person2.getName());
}

输出结果

person1:小李
person2:小张
xiaoZhang:小张
xiaoLi:小李

在这里插入图片描述

2、ArrayList 和 LinkedList的区别

本质上与数组和双向链表的区别一样。

  1. 数据结构的实现上: ArrayList 是动态数组的实现。LinkedList是双向链表的数据结构实现。
  2. 随机访问的效率上:
    按索引访问: ArrayList由于是基于数组实现的,地址是连续的。查找可根据索引计算偏移量,所以查找效率较高。
    按值访问: 两者皆需遍历查找,效率一样。
  3. 增加和删除效率上:尾部增加和删除,两者的效率是不相上下的。 若是头部和中间位置的增加和删除,显而易见数组需要移动大量元素,效率比链表低。
  4. 线程安全: 二者都是不同步的,不保证线程安全。
  5. 空间占用: LinkedList比ArrayList更占内存。因为其每个节点不但存储数据还存储指向前一个节点和指向后一个节点的两个引用。

3、 ArrayList的扩容机制

ArrayList数组初始化容量为10。当数组容量不够时会进行扩容,扩容过程如下:
1、申请一个新的数组,该数组容量为原来的1.5倍。使用的是位运算

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);

2、 将旧数组的元素赋值到新数组。 Arrays工具类。

elementData = Arrays.copyOf(elementData, newCapacity)

4、 HashSet的存储原理

HashSet的存储主要是保证其元素的唯一性。
HashSet底层是借助HashMap实现存储的的,其值作为HashMap 的key

public boolean add(E e) {
return map.put(e, PRESENT)==null; // 返回null说明插入成功 该key原先不存在
}

HashMap保证key的唯一性本身借助了Hash算法。
什么是Hash算法呢?
按照常规操作,当往数组中添加元素,保证唯一性时,是遍历数组中的元素逐个比较。这种方法虽然可行,但是当数据量很大时就显得很低效。为此采取了新的方法也就是hash算法。
采用hash算法,通过计算存储对象的HashCode,然后在与数组长度-1做位运算,得到我们要存储在哪个下标下,如果此时计算的位置下没有元素,就直接存储,不做比较。
但是,随着数组元素的增多,就可能发生"Hash冲突"(“Hash碰撞”),这个时候我们就要用到equals来比较两个对象的内容是否相同。如相同则不插入,否则形成链表存储在该位置。这一点上,jdk1.8做了优化,当元素不断添加,同一位置形成的链表可能会越来越长,因此其优化为红黑树。

5、为什么重写equals要重写HashCode

重写对象equals和hashCode

HashMap是底层实现时数组加链表
A.当put元素时:

1.首先根据put元素的key获取hashcode,然后根据hashcode算出数组的下标位置,如果下标位置没有元素,直接放入元素即可。
2.如果该下标位置有元素(即根据put元素的key算出的hashcode一样即重复了),则需要已有元素和put元素的key对象比较equals方法,如果equals不一样,则说明可以放入进map中。这里由于hashcode一样,所以得出的数组下标位置相同。所以会在该数组位置创建一个链表,后put进入的元素到放链表头,原来的元素向后移动。

B.当get元素时:

根据元素的key获取hashcode,然后根据hashcode获取数组下标位置,如果只有一个元素则直接取出。如果该位置一个链表,则需要调用equals方法遍历链表中的所有元素与当前的元素比较,得到真正想要的对象。
可以看出如果根据hashcdoe算出的数组位置尽量的均匀分布,则可以避免遍历链表的情况,以提高性能。
所以要求重写hashmap时,也要重写equals方法。以保证他们是相同的比较逻辑。

equals 和hashCoe方法:
在Java中,每个类都有equals和hashCode方法,一般来说,如果要override的话,两个方法必须同时被override。
对象的比较必须重写equals方法,否则默认使用的父类的equals方法本质是使用==进行比较的。
而equals方法需要使用到对象的hashCode方法,该方法默认返回的是对象的内存地址,因此也需要重写,否则即使是相同对象equals结果也是不相同的。因此equals方法和HashCode方法在逻辑上要保持一致。
如果只重写hashCode而不重写equals在使用HashMap的get方法获得某个对象时会找不到。
hashmap什么时候需要重写equals和hashcode方法

6、 switch的参数类型

Java中switch语句所支持的参数类型有三类:

  1. 基本数据类型:byte、short、char、int

  2. 引用数据类型: Byte、 Short、 Character、Integer(即上述四种类型的包装类)、String、

  3. 枚举类型
    其实switch只支持整型,其他数据类型都是转换成整型后才使用的switch。

  4. byte、short、char(ASCII码)自动类型提升为int,而这四种基本类型所对应的封装类,因为自动拆箱机制也可以作为参数。

  5. Switch借助hashCode()和equals()实现了对String的支持——先使用hashCode(还是整型类型的)进行初步判断,然后使用equal()进行二次校验。

7、 Integer和int类型的比较


Integer a = 1;
Integer b = 1;
Integer c = 500;
Integer d = 500;
System.out.print(a == b); //true
System.out.print(c == d);	//false
System.out.print(a.equals(b));	 //true

Integer类型,如果数值是在 -128 到127范围之间是会被缓存的。在这范围内,赋值是直接从缓存中取,不会有新的对象产生。也就是说,a和b实际上是指向同一个对象。Integer a = 1 是自动装箱会调用Integer。valueOf(int)方法. 该方法API解释如下:

public static Integer valueOf​(int i)
Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.

对应方法实现如下:

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

而一旦超过这个范围,即使值相同也会创建一个新的对象。所以c和d是指向不同对象。
Integer类型的equals方法是被重写过的。会先比较类型是否相同,如果类型相同才比较值是否相等。如果类型相同且值也相等就返回true。因此a.equals(b)为true。

        Integer i = 127;  //自动装箱调用Integer.valueOf() 该值在内部类IntegerCache的属性中
        Integer j = new Integer(127); //new产生一个对象 该对象的value属性被赋值为127
        Integer k = new Integer(127);  //new产生一个对象 该对象的value属性被赋值为127
        System.out.println(i == j); //false
        System.out.println(i.equals(j));//true
        System.out.println(j == k);  //不同堆对象 false
        System.out.println(j.equals(k));//true

Integer的拆装箱问题:

int i=0;
Integer j = new Integer(0);
System.out.println(i==j);		//false
System.out.println(j.equals(i));  //true

在jdk1.5之后,提供了拆装箱。
i== j,基本类型与封装类型在进行比较的时候,会自动将基本类型装箱。而Integer j = new Integer(0)产生一个新的对象,在堆中。两者的地址不一样,所以 i == j 的结果 为false。
j.equals(i)结果为true。

除此之外,Character封装类型也提供了缓存机制:

public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character©;
}

字符的值小于127就会被缓存。大于127就会重新构建一个对象

Character a = 'c';
Character b = 'c';
Character c = '等';
Character d = '等';
System.out.println(a==b);   // true
System.out.println(c==d);   // false

Float同Double , 没有缓存,所以每次就是直接new一个对象

Double a = 1.0;
Double b = 1.0;
Double c = 500.0;
Double d = 500.0;
System.out.println(a==b);  // false
System.out.println(c==d);  // false

8. ArrayList扩容机制

ArrayList初始容量为10,当超过容量时。会重新申请空间,申请的空间大小为原来的1.5倍(通过右移运算,oldCapacity + oldCapacity >> 1)。然后将旧数组赋值到新开辟的数组空间上。

9. 类型转换

类型转换包括两种,一种是自动转换,另一种是强制转换。

自动转换

  • 范围小的类型可以自动转换为(赋值给)范围大类型。
  • 范围小的类型遇到范围大的类型,自动转为范围大的类型。
int a = 1;
double b = a;		// 1赋值给范围大的类型(double)
double d = a +3.14; //1遇上范围大的3.14

错误:
int a = 10 +3.14; //范围大的不能自动转为范围小的,需通过强转。因为10遇上3.14会自动转化为double类型,但是最终结果是要赋值给整型变量a,所以需要强转。

各类型的范围大小:

char < int <整数<float<double<字符串
范围最大为字符串。

		char c = 'A';
		System.out.println(c);//输出 A
		System.out.println(c+0);	//输出字符A的ASCII码 65
		System.out.println(0+c);	//输出字符A的ASCII码 65
		System.out.println(0+'0');	//输出字符0的ASCII码 48
		System.out.println('0'+c);	//输出AJ加上'0'的ASCII码值之和

强制转换

范围大的赋值给范围小的,必须强转。

通用写法:
范围小的 = (小类型) 范围大的;

特殊:
不能写成float f = 1.345;
原因是1.345为double类型,不能自动转为范围更小的float类型,但是float比较特殊也可以无需强转,只要在后面加上f标记即可,如下:
float x = 123.45f;

关于字符与整形之间的转换:

        int x = 'a'; //小范围赋值给大范围 :自动转换
        System.out.println('a');// a
        System.out.println(x);//97
        // 'a' + 3 -- >小范围'a'遇上大范围3自动变成97
        System.out.println('a'+3); //100
        System.out.println((char)('a'+3)); //d 范围大的转小的 强制转换

10、抽象类和接口

抽象类是从多个具体类中抽象出来的父类,抽象类就是一个模板,使用abstract关键字修饰。因此抽象类:

  1. 跟普通类一样,可以存在成员变量,普通方法,构造方法,静态属性和静态方法。
  2. 抽象类中可以存在构造方法。因为抽象类中存在抽象方法所以不能被实例化。抽象类的构造器仅仅是提供给子类调用的,不能用于实例化。
  3. 抽象类可以被继承。继承的子类要么实现所有抽象方法,要么自己也被声明为抽象。

接口

  1. 在接口中只有方法的声明,没有实现体。
  2. 在接口中只有常量,任何成员变量的定义在编译时都会默认加上public static final
  3. 接口中的所有方法,永远都被public 修饰,且默认public abstract类型。
  4. 接口没有构造方法,也不能实例化接口。

注意:
JDK1.8中对接口进行了增强,接口可以有default、static方法。


public interface Haha {
    default void haha()    {
        System.out.println("接口里也能有普通方法的,我去");
    }
}

例题:
下面有关java 抽象类和接口的区别

11、 Object类及其方法

Object类自带的方法

getClass(): 返回对象的运行时类(class<>)

package com.JavaSe.objectDemo;

class Father{}
public class Son extends  Father {
    public static void main(String[] args) {
        Father father = new Son();

        System.out.println( father.getClass()); //class com.JavaSe.objectDemo.Son
        System.out.println( father.getClass().getName() ); //com.JavaSe.objectDemo.Son

    }
}

与getClass()类似的方法还有两个:
一个是Class类中的forName()方法,也是在运行时,根据传入的类名去加载类,然后返回与类关联的Class对象。同时因为是动态加载,在 编译时可以没有这个类,也不会对类名进行校验,所以可能抛出ClassNotFoundException异常。

另一个是类名.class
与上面两种方法不同之处在于,它返回的是编译时类型。也就是在编译时,类 加载器将类加载到JVM中,然后 返回一个初始化的Class对象、

12、HashSet

 Set<Integer> set = new HashSet<>();
 System.out.println(set.add(11));
 System.out.println(set.add(11));

第一次add时候
HashSet map.put(e, PRESENT)返回null

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet的add方法调用putVal(……)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

第二次add的时候,HashMap的 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 会将旧值覆盖掉。返回该值。

13、字符常量和字符串常量的区别

  1. 形式上: 字符常量由单引号括起来的一个字符。字符串常量由双引号括起来若干个字符,可以为0个。
  2. 含义上:字符常量相当于一个整型值(ASCCII码)。而字符串常量是引用对象,存储的是引用地址。
  3. 占用大小上: 字符型常量大小为2个字节。字符串采用一种更灵活的方式进行存储,没有固定大小。取决于编码和字符(比如说是汉子还是 字母)。

在这里插入图片描述

14、既然有了字节流,为什么还要有字符流?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

Java中提供字符流是为了给字符操作提供更方便、更高效的方法。如果知道某个文本文件的编码类型那么可以使用字符流。比如说,全是中文的。那么如果按字节流的话,需要读两个才能成为一个中文字符,有了字符流之后,既保证了读数据的准确性又保证了效率。
本质上来说所有的读写操作都是以字节为单位进行。字符只是根据编码对字节流进行翻译的结果。
字节流用来处理二进制数据,比如图片数据。字符流用于处理字符和字符串

异常

java异常处理 Exception、error、运行时异常和一般异常有何异同

在这里插入图片描述

异常(Exception)根据其在编译时期还是运行时期去检查异常可分为:checked异常和runtime异常:
runtime异常:又称运行时期异常,此类型的异常在运行时期检查;在编译时期,运行异常并不会检测,就不会出现,只有在运行到相关代码时才会出现;RuntimeException自身及其子类异常都属于runtime异常;checked异常:又称编译时期异常,此类型的异常在编译时期就会检查,而且是必须处理的,如果没有处理,就会导致编译失败;除了runtime异常之外的其他异常(包括Exception自身)都属于checked异常;

出现 java.lang.OutOfMemoryError: PermGen space 错误的原因及解决方法

线程

创建线程的方式的四种方式

  1. 继承Thread 类创建线程
  2. 实现Runable接口创建线程
  3. 通过Callable 和FutureTask创建线程
  4. 通过线程池创建线程

继承Thread类

这种方式是通过继承Thread类并重写run()方法,然后创建该类实例,调用该实例的start()方法。

package com.thread.createThread;


public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread的线程创建");
    }

    public static void main(String[] args) {

        MyThread thread = new MyThread();
        thread.start();
        System.out.println("执行main方法");
    }
}
	

输出结果

执行main方法
继承Thread的线程创建

Thread类的start()方法

 public synchronized void start() {
        /**
         * A zero status value corresponds to state "NEW".
         * 0 状态表示新建
         */
         //如果不是新建状态则抛出异常
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented【减少】.
			 */
		//将该线程加入线程组
        group.add(this);

        boolean started = false;
        try {
        	//调用本地start0()方法为该线程分配线程栈,并调用该 线程run方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

原理

由于新创建的线程是调用start()方法,该方法会根据status是否为0判断是否是新建态
如果,不为0,则不是新建态抛出异常。否则将该线程加入线程组中。然后调用本地方法start0,
JVM会创建出一个新线程,并且为线程创建方法调用栈和程序计数器,此时线程处于就绪状态(Runnable),当线程获取CPU时间片后,线程会进入到运行状态(Running),然后调用该线程的run()方法。这是真正的多线程工作。

注意
由于是创建了新线程,所以主线程(main方法所在的线程)和子线程(MyThread 创建的线程)是并发进行的。两者并不干扰。但是如果是在main方法中调用了 run() 方法则认为是调用用了普通对象的run方法,主线程需要等待其执行完,

实现Runable接口

这种方式通过实现Runnable接口重写run方法,然后将该类的对象通过Thread的构造方法传入。实际上线程对象还是Thread实例。


package com.thread.createThread;

/**
 * 实现Runnable接口方式的创建线程
 */
public class TargetThread implements Runnable {
    @Override
    public void run() {
        System.out.print("实现Runnable接口方式的创建线程:");
        System.out.println( Thread.currentThread().getName());
    }

    public static void main(String[] args) {

        System.out.println( Thread.currentThread().getName());
        TargetThread target= new TargetThread();
        new Thread(target).start();
        System.out.println("main方法……");

    }
}

执行结果:

main
main方法……
实现Runnable接口方式的创建线程:Thread-0

原理

Thread类中


	/* What will be run. */
    private Runnable target;
    public void run() {
        if (target != null) {
            target.run();
        }
    }

通过构造方法传入的target会赋值给成员变量target。当执行run方法时,target不为null,调用实现类的run方法。

还可以借助Lambda方式实现

package com.thread.createThread;

/**
 *  lambda表达式: 实现Runnable接口方式的创建线程
 */
public class TargetThread2{

    public static void main(String[] args) {

        System.out.println( Thread.currentThread().getName());


        Thread target = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.print("实现Runnable接口方式的创建线程2:");
                        System.out.println( Thread.currentThread().getName());
                    }
                }
        );
        target.start();
        System.out.println("main方法……");

    }
}

通过实现接口并重写run方法可以创建一个线程。借助接口可以实现多继承,因此使用Runable接口的好处就是使得那些已经继承了其他类的类可以实现多线程。

实现Callable接口

上述两种实现方式的run方法都是没有返回值的。如果需要执行的任务带返回值就需要通过实现Callable接口的方式:
创建一个CallableTarget类,实现Callable接口,实现带有返回值的call方法,然后根据FutureTask的有参构造方法,传入CallableTarget创建一个任务task。然后根据Thread的有参构造方法传入FutureTask来创建一个线程thread,调用Thread的start方法可以执行任务。

FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入实现Callable接口的类给FutureTask,直接调用其run()方法或者放入线程池执行,之后可以在外部通过FutureTask的get()方法异步获取执行结果,因此FutureTask非常适用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask还可以确保即使外部多次调用run方法,它都会只执行一次Runnable或Callable任务。另外,futureTask.get()会阻塞主线程,一直等到子线程执行完毕并返回后才能继续执行主线程后面的代码。
可以通过isDone()来判断子线程是否已经执行完了。

package com.thread.createThread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTarget implements Callable {

    @Override
    public String call() throws Exception {
        return "通过实现Callable接口创建线程";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        System.out.println( Thread.currentThread().getName() );

        CallableTarget target = new CallableTarget();

        //FutureTask的run方法会调用Callable接口实例的call方法
        FutureTask task = new FutureTask<>(target);

        Thread thread = new Thread(task);
        //Thread调用传入target(就是通过构造方传入的task)的run方法(线程启动时调用通过start)
        thread.start();
        //Callable的call方法返回值 通过FutureTask 的get()返回 该方法会阻塞主线程
        String s = (String) task.get();
        System.out.println(s);


    }


}


main
通过实现Callable接口创建线程

Callable内部有缓存机制,即使有不同打的线程调用也不会重复执行。

原理

Thread类的start方法默认调用run()方法,run()方法会调用自身实例变量target的run()方法,也就是我们通过Thread构造方法传入的FutureTask,而FutureTask的方法会调用Callable接口实例的call方法。

	//Thread类的run() 方法
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
	//FutureTas类的run()方法
	 public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                	//调用Callable实例的call()方法
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

Runnable、Callable、Future、FutureTask

Runnable

Runnable 是一个接口,只要创建一个类实现该接口并重写run()方法,就可以将该类的实例作为Thread的构造方法的传入参数传入创建Thread,线程运行时就会调用该实例的run方法。

Callable
Callable接口与Runnable接口类似,也是一个接口。所不同的是该接口的call()方法有 返回值,可以供程序接收执行的结果。

Future

Future接口进一步对Runnable和Callable的实例进行封装,定义了一些方法: 取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get() 方法,带有超时时间来获取执行结果的get方法。


public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask 实现RunnableFuture接口,而RunnableFuture继承Runnable接口和Future接口。可以认为FutureTask是Future接口的实现类。

public class FutureTask<V> implements RunnableFuture<V> {
...
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

通过线程池创建线程

线程池本质是一个HashSet,多余的人与会放在阻塞队列中。

package com.thread.createThread;

import java.util.concurrent.*;

public class ThreadPool {

    public static void main(String[] args) {
        //ThreadPoolExecutor(int corePoolSize:线程池的的线程数量
        // int maximumPoolSize:线程池允许的最大线程数量
        // long keepAliveTime:线程最大空闲时间
        // TimeUnit unit:
        // BlockingQueue<Runnable> workQueue: 阻塞队列
        // )
        ExecutorService executorService = new ThreadPoolExecutor(1, 10, 30L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)); //创建线程池
        executorService.execute(
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("线程池方式创建线程!");
                    }
                }
        );
    }
}

线程同步
多线程之间是如何同步的
一、 synchronized

  1. 同步代码块
    表示线程在执行时会将object对象上锁。这个对象可以是任意类的对象,也可以使用this关键字或者是class对象,

Lock

Lock和Synchronized的区别:

  1. Lock是一个接口,Synchronized是Java内置的语言实现,是Java关键字。
  2. Lock发生异常时,如果没有主动通过unlock释放掉占有的锁,则很可能造成死锁现象;因此需要在finally块中释放掉锁。而Synchronized发生异常时,会自动释放掉线程占有的锁,因此不会造成死锁现象。
  3. Lock是 可中断锁。可以让等待锁的线程响应中断。而synchronized是不可中断锁,使用synchronized时,等待的线程会一直等待下去,不能响应中断。
  4. 通过Lock可以知道有没有成功获取锁(通过tryLock方法:如果获取了锁,则返回true;否则返回false。在那不到锁是会一直在那等待),而synchronized却无法办到。
  5. Lock可以实现公平锁,synchronized不保证公平锁。

数据结构

HashMap

HashMap的get(key)原理

get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可

源码:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
	  /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
	final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;//Entry对象数组
	Node<K,V> first,e; //在tab数组中经过散列的第一个位置
	int n;
	K k;
	/*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/
	//也就是说在一条链上的hash值相同的
        if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
	/*检查第一个Node是不是要找的Node*/
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同
                return first;
	  /*检查first后面的node*/
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
				/*遍历后面的链表,找到key值和hash值都相同的Node*/
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

底层实现原理:
简单说下HashMap的实现原理:

首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

HashMap的put(key,value)原理

1、 初始化table
table数组是否为空或者length==0,如果是则调用resize()初始化。
2、计算hash值。
根据key计算hash值,采用key的hashcode的低16位与高16位进行一个异或运算来保留高位特征,以便得到的hash值更加均匀分布。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hashmap的hash方法为什么需要让高16位参与异或运算的原因

当数组的长度很短时,只有低位数的hashcode值能参与运算。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。并且使得高16位和低16位的信息都被保留了。
而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值的二进制会向0靠拢,采用|运算计算出来的值的二进制会向1靠拢

3、 插入或更新节点

根据计算得来的hash值,(n - 1) & hash 得到插入数组的下标i,然后进行判断

3.1 如果数组下标位置为空(table[i] == null)
那么说明该数组下标下没有hash冲突的元素,直接新建节点添加。
3.2 等于下标首元素,(tabe[i].hash= =hash && (table[i].key = = key || (key != null && key.equals(table[i].key)))

判断table[i]的首个元素是否和key一样,如果是,则说明是同一个hash且同一个key,那么则直接更新value。

3.3 数组下标存的是红黑树,( table[i] instanceof TreeNode )
判断table[i] 是否为treeNode,即table[i]是否是红黑树。如果是,则直接在树中插入键值对。

3.4 数组下标存的是链表

如果上面的判断条件都不满足,说明table[i]存储的是一个链表,那么遍历链表,判断是否存在已有元素的key与插入键值对的key相等。如果是,则更新value,如果没有,那么在链表末尾插入一个新节点。插入之后判断链表长度是否大于8。大于8的话把链表转为红黑树。

4 、 扩容

插入成功后,判断实际存在的键值对数量是否超过了最大容量threshold(一般是数组长度*负载因子0.75),如果超过,进行扩容,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中

加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率

hashmap并发条件下会成环。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.tab为空则创建 
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.计算index,并对null做处理  
    // 3.插入元素
  //(n - 1) & hash 确定元素存放在哪个数组下标下
  //下标没有元素,新生成结点放入中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 下标中已经存在元素,即存在Hash冲突了,开始处理冲突
    else {
        Node<K,V> e; K k;
        // 节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)是否hash值相等且key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 判断该链为红黑树 
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 判断该链为链表 
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    /*
                    如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,
            treeifyBin首先判断当前hashMap的长度,如果不足64,只进行
                      resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
                    */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    ++modCount;
    // 超过最大容量 就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

相关扩展
static final int TREEIFY_THRESHOLD = 8; // 链表长度大于这个值时转红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点个数小于这个数时转链表
hashmap的为什么链表长度大于8时才红黑树化

HashMap:为什么容量总是为2的次幂

在计算机中,位运算的速度比取模运算要快很多。而当数组长度是2的n次方幂时满足一个公式: (数组长度length - 1 ) & hash == hash % length

HashMap 的扩容机制
扩容为原来的两倍,然后重新计算元素在新数组的下标位置。发现: 下标位置应为旧数组下标或者为旧数组下标加上旧数组长度。取决于第(n + 1)位是0还是是1,是0则为前者是1则为后者。

LoadFactor负载因子为什么是0.75
loadFactor,扩容阈值,默认为0.75,当HashMap中的元素大于容器容量 * loadFactor时就会触发扩容机制。
之所以选0.75,是一个这种的办法,在这个比例下,降低了红黑树的复杂性和高度,提高了查找效率,再者空间利用率也比较高。负载因子为1时,过大,虽然空间利用率上去了,但是时间效率降低了。
负载因子为0.5时,太小,虽然时间效率提升了,但是空间利用率降低了。

HashMap 在多线程操作下可能发生的线程安全问题

结合HashMap的源码分析可以发现,多线程情况下。HashMap扩容时会发生链表成环导致get操作时死循环。
主要原因在于:

在扩容的过程中使用头插法将oldTable 中的单链表节点插入到newTable的单链表中,newTable中的单链表会倒置oldTable中的单链表。在多线程同时扩容的情况下就可能导致扩容后的HashMap存在一个有环的单链表,从而导致后续执行get操作时,触发死循环,引发CPU100&问题。

HashMap扩容死循环问题解析

HashMap的扩容机制

构造hash表时,如果不指明初始大小,则不进行初始化。只当执行put操作时调用resize()进行初始化,初始长度initialCapacity为16。如果链表数组table达到了 数组长度*loadFactor(加载因子,也就是填充比,DEFAULT_LOAD_FACTOR为0.75),则重新调整hashMap为原来的两倍。这里用的是左移一位。

newCap = oldCap << 1

Dubbo

dubbo的负载均衡策

random loadbalance

默认策略,随机调用调用provider。

roundrobin loadbalance

这个的话默认均匀地将流量打到各机器上。可以调整权重,让性能差的机器权重小一些,流量少一些。

leastactive loadbalance

这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求

consistanthash loadbalance

一致性哈希算法,相同参数的请求发送到一个provider实例上,provider挂掉时,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。

既然有HTTP请求,为什么还要用rpc调用?

编码效率问题:对于http1.1协议来说,报头采用文本编码,非常占用字节数。而自定义tcp协议,报头字节数也只有16B,极大地精简了传输内容。

rpc库相对http容器,更多的是封装了“服务发现”,“负载均衡”,“熔断降级”一类面向服务的高级特性。可以这么理解,rpc框架是面向服务的更高级的封装。如果把一个http servlet容器上封装一层服务发现和函数代理调用,那它就已经可以做一个rpc框架了。

Linux 基础

  1. Linux终止一个前台进程可能用到的命令和操作是 Ctrl + C ,终止后台进程才是 kill

参考文章

MySQL 基础

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值