不断的学习,我们才能不断的前进
一个好的程序员是那种过单行线马路都要往两边看的人

Java

Java的三大特性:封装、继承、多态

  • 封装:封装指的是隐藏具体属性和实现细节,仅对外提供公共访问方式。例如提供的setter和getter方法来访问私有属性。
  • 继承:继承是指将多个相同的属性和方法提取出来,新建一个父类。Java中一个类只能继承一个父类,且只能继承访问权限非private的属性和方法。 子类可以重写父类中的方法,命名与父类中同名的属性。 可以进行代码复用。
  • 多态:一个方法有多种实现结果,多态可以分为两种:设计时多态和运行时多态。
    • 设计时多态:即重载,是指Java允许方法名相同而参数不同(返回值可以相同也可以不相同)。
    • 运行时多态:即重写,是指Java运行根据调用该方法的类型决定调用哪个方法。

Java 基础

throw 和 throws

  • throw用在函数内,后面跟的是异常对象;throws 用在函数上,后面跟的是异常类。
  • throw 抛出具体的问题对象,执行到throw,功能就已经结束了; throws 用来声明异常,让调用者知道该功能可能出现的问题。
  • throws 表示出现异常的一种可能性,并不一定会发生这些异常

序列化
创建可复用的Java对象, 使用Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

JVM在序列化时会根据属性自动生成一个serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.

Transient关键字阻止该变量被序列化到文件中
可以实现深拷贝

comparable 接口和comparator
Comparable和Comparator接口被用来对对象集合或者数组进行排序。

  • Comparable接口被用来提供对象的自然排序,可使用它来提供基于单个逻辑的排序。
  • Comparator接口被用来提供不同的排序算法,可根据制定字段选择需要使用的Comparator来对指定的对象集合进行排序。
    Comparable需要比较对象实现该接口, Collections.sort()需要传入实现的Comparator接口,参数是两个对象。

泛型

Java 虚拟机里面没有范型类型对象,定义一个泛型类型,会自动提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名,类型变量会被擦除,并替换为其第一个限定类型来替换类型变量(T extends A,擦除后用A来替换T),对于无限定类型的变量则替换成Object

?/T extends Class > Object

类型擦除也会出现在泛型方法中,而且如果擦除了返回类型,假设返回类型为Object,编译器会插入强制类型转换,最后结果会进行强制转换。

如果类型擦除后与多态发生了冲突,会合成桥方法来保持多态(比如:子类没有成功重写父类的方法,父类有个泛型方法,被擦除后用Object替换,子类重写了这个方法,有具体的参数类型String,会导致子类没有成功的重写这个方法,而是变成了重载,因为方法的参数不一样,所以子类会自动生成桥方法。)。桥方法就是Java 编译器会自动帮我们生成对应的方法,而且能够覆盖泛型父类的泛型方法来帮我们实现重写,该方法内部会调用子类实现的方法。 所以子类会出现两个方法名一样,但是返回类型不一样的方法,这不是有问题吗?实际上这种情况只能由编译器来实现这种方法,手动实现的话会出现问题。

package base;
/**
 * 查看泛型擦除的作用
 * Javac Pair.java 生成 Pair.class 字节码文件
 * Javap -c -s Pair.java 可以看成泛型擦除后的信息
 * @param <T>
 */
public class Pair<T> {
	private T first;
	private T second;

	public Pair(T first, T second) {
		this.first = first;
		this.second = second;
	}

	public void setFirst(T newValue) {
		first = newValue;
	}

	public void setSecond(T newValue) {
		second = newValue;
	}
}

/**
 * 查看泛型冲突时,桥方法的作用
 * Javac T_B.java
 * Javap T_B.class
 */
public class T_A<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}
class T_B extends T_A<String> {
	@Override
	public void setValue(String value) {
		System.out.println("---B.setValue()---");
	}
}
// 使用javac javap命令查看T_B的class文件,
//会发现T_B多了一个桥方法:setValue(java.lang.Object)
class base.T_B extends base.T_A<java.lang.String> {
  base.T_B();
  public void setValue(java.lang.String);
  public void setValue(java.lang.Object);
}


TLAB
TLAB:Thread Local Allocation Buffer 的简写,基于 CAS 的独享线程(Mutator Threads)可以优先将对象分配在 Eden 中的一块内存,因为是 Java 线程独享的内存区没有锁竞争,所以分配速度更快,每个 TLAB 都是一个线程独享的

Card Table
Card Table 卡表:卡表的本质是用来解决跨代引用的问题,比如 年轻代中发生minor gc的频率很高,所以会经常扫描年轻代中的对象并进行标记,如果老年代中有对象引用了年轻代中的对象,那岂不是每次进行minor gc时也要进行全堆的扫描?jvm引入了卡表(card table)来解决这个问题。

卡表是通过卡标记(card marking)来解决的。以Hotspot虚拟机为例,卡表的设计,是将整个堆空间分割成一个个卡页(card page),HotSpot虚拟机的每个卡页大小为512字节(其他虚拟机也基本都为2的n次幂),而卡表本身为一个简单的字节数组,记录当前对应卡页的标记值。当判断一个卡页中有存在对象的夸代引用时,将这个页标记为脏页在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。

写屏障
写屏障:就是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑,相当于为引用赋值挂上的一小段钩子代码。
写屏障来维护卡表 : 写屏障就是在将引用赋值写入内存之前,先做一步mark card——即将出现跨代引用的内存块对应的卡页置为dirty。

只要有引用指向年轻代的对象,则使用写屏障将该页标记为脏页

三色标记法
黑色:本对象已访问过,而且本对象 引用到的其他对象 也全部访问过了。
白色:尚未访问过的对象。
灰色:本对象已访问过,但是本对象 引用到的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
访问步骤:

  1. 初始时,所有对象都在 【白色集合】中;
  2. 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
  3. 从灰色集合中获取对象:
    3.1 将本对象 引用到的其他对象 全部挪到 【灰色集合】中;
    3.2 将本对象 挪到【黑色集合】里面。

重复步骤3,直至【灰色集合】为空时结束。
结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收

几种反射的区别?

Class.class、class.forName、Object.getClass:

  • Class.class 获取类的模版信息,并加载到内存里面,并进行实例化。只会在执行newInstance才会进行初始化,创建一个对象。
  • class.forName 获取类的模版信息,并加载到内存里面,并进行实例化 和初始化(加载static静态信息)。
  • Object.getClass 获取类的模板信息,从对象实例里面获取。(内存里面存在该对象,所以肯定进行了实例化和初始化)
 Class object = Class.forName("reflection.Student");
 
 /**
 * Class.forName 使用类加载加载class信息,并进行初始化
 **/
 public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
    /**
    * 参数:包名+类名、是否进行初始化、类加载器
    **/
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;
    

class.forName 和 classloader.loadclass的区别
类加载的流程分为:加载、验证、解析、准备、初始化
class.forName 和 classloader.loadclass 都会执行加载、验证、解析、准备的工作流程,所以可以获取并访问该类的所有属性和方法。
但是 classloader.loadclass 得到的class 是没有进行初始化的;而class.forName是会进行初始化的,也就是执行静态代码块。

Demo:数据库驱动加载Class.froName(“com.mysql.jdbc.Driver”),就需要进行初始化,因为需要使用DriverManager

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    } <br>
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}

Java 异常?

Java异常是Java提供的一种识别及响应错误的一致性机制。
Throwable 是 Java 语言中所有错误与异常的超类,Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。

Error 类及其子类。

程序中无法处理的错误,表示运行应用程序中出现了严重的错误。这些错误是不受检异常,非代码性错误,JVM层面的错误,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;比如、OutOfMemoryError、NoClassDefFoundError(在编译期类存在,但是在类加载时找不到该类)。

Exception

程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常
RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如:NullPointerException、ArrayIndexOutBoundException。
此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理

编译时异常
定义: Exception 中除 RuntimeException 及其子类之外的异常。
特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。

Integer类?自动装箱?

// Integer a= 1212;调用的是 Integer.valueOf(1212),Interger在赋予的int数值在-128 - 127的时候,直接从cache中获取
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

讲讲HashMap?ConcurrentHashmap?

HashMap主要由数组和链表组成,他不是线程安全的。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。
put插入数据
往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash)。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8 且数组长度大于64,则会转换成红黑树,最后判断数组长度是否超过默认的长度负载因子也就是12,超过则进行扩容。

hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。

get查询数据
查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。
resize扩容
jdk1.7扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。jdk1.8对扩容进行了优化,采用更简单的判断逻辑,位置不变或索引+旧容量大小。:
这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?
扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。
因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,原数据hashcode高位第4位为0和高位为1的情况;
第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量),所以只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

jdk1.8的优化

  • 数组+链表改成了数组+链表或红黑树 (防止hash冲突,链表长度过长时降低时间复杂度)
  • 链表的插入方式从头插法改成了尾插法。(避免多线程并发加扩容时,可能会产生循环链表)
  • 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小。
  • 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
    为啥重写equals方法的时候需要重写hashCode方法呢?
    因为在java中,所有的对象都是继承于Object类。Object类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样。

线程安全的Map
多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大

Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。

Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现。
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。
ConcurrentHashmap主要使用CAS+自旋的方式保证了put方法的原子性,而HashTable则是方法上都加上了synchronized锁。

有序的Map
LinkedHashMap 和 TreeMap
LinkedHashMap内部维护了一个双向链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较

ConcurrentHashmap
ConcurrentHashmap在jdk1.7的时候是Segment+HashEntry,HashEntry用volatile关键字修饰来保证可见性,Segment是ConcurrentHashMap 的一个内部类,继承了ReentrantLock类,采用的是分段锁技术,也就是说当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment,在put的时候产生锁。

在每个Segment里面由HashEntry数组组成,可以看成一个小的HashMap。

ConcurrentHashmap在jdk1.8采用的是CAS+synchronized+Node实现,每个Node结点的值和next指针采用volatile关键字修饰来保证可见性。 采用跟hashMap一样的数据结构。
ConcurrentHashMap的put操作

  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树

快速失败?安全失败?

快速失败
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改

安全失败
安全失败在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

快速失败和安全失败是对迭代器而言的

引用类型有哪些?有什么区别?

引用类型主要分为强软弱虚四种:

  • 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
  • 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
  • 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
  • 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

抽象类和接口的区别?

抽象类

  • abstract关键字修饰,抽象方法所在的类必须声明为抽象类,但一个抽象类 可以没有抽象方法。
  • 一个抽象类若实现了接口,可以不实现该接口的所有方法
  • 一个非抽象类继承该类,必须实现所有的抽象方法;抽象类继承 可以不实现抽象方法
  • 抽象类不能使用new来进行初始化

抽象类:为了将一些相关类中的公共成员方法抽离出来,只声明方法,子类去进行各自不同的实现

接口

  • 用interface作为修饰符。接口是抽象方法和常量值的定义的集合。接口本质上是一种特殊的抽象类。这种抽象类中只包含方法和常量的定义
  • 接口中的方法默认都是public abstract,常量默认都是public static final
  • 接口可以实现多继承。
  • 没有构造方法,因此也不能用new操作符实例化。
  • jdk1.8,提供了使用关键字default的默认接口方法,提供一个默认实现。实现这个接口的类可以使用该默认方法或重写该方法,所以接口中可以存在公有静态方法。
  • 接口只有一个抽象方法时,可以使用lambda表达式.
  • 接口继承另外一个接口时,可以不重写抽象方法

谈谈Java的集合类和容器?

容器可以分为三种:Map(映射)、Set(集合)、List(列表),其中set 和list 都实现了Collection接口;map 实现了Map接口。java集合类

反射用过吗?什么原理?

Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象能够调用它的任意方法和属性;主要是利用class字节码文件来获取该class对象的所有信息。
获取class的三种方式:

  • Class.forName
  • 类名.class
  • 类型的对象.getClass()

这三种方式获取到的class都是同一个实例,因为在JVM里面,一个类只有一个Class模版

反射的应用
破坏单例模式:
动态代理:

动态代理了解吗?cglib什么区别?

动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

动态代理的常用实现方式是反射。反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。
主要通过Proxy.newProxyInstance生成代理对象,内部通过InvocationHandler接口的invoke方法来获取代理方法。

public Object getProxyInstance() { //生成代理类
		return Proxy.newProxyInstance(
				this.getClass().getClassLoader(),
				target.getClass().getInterfaces(),
				(proxy, method, args) -> {
					System.out.println("动态代理执行之前");
					//执行目标对象方法
					Object returnValue = method.invoke(target, args);
					System.out.println("动态代理执行之后");
					return returnValue;
				}
		);
	}

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。
CglibProxy通过创建子类来实现动态代理

静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

静态代理类需要和代理对象实现相同的接口

动态代理应用场景
动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

单例模式了解吗?

单例模式

无界队列和有界队列?

生产者消费者例子?

生产者消费者模式

Java是值传递还是引用传递,写一段对象拷贝的代码?

java里面是值传递,在传递引用类型(对象)的时候是传递对象的引用地址;传递值类型,传递的是实参的值;

  • 值传递:是指在调用一个有参函数时,会把实际参数复制一份传递到函数中。这样在函数中如果对参数进行修改,将不会影响到实际参数

  • 引用传递:是指在调用一个有参函数是,直接把实际参数的地址传递到函数中,那么,如果在函数中对参数所进行的修改,将影响到实际参数.

栈里面存放的是对象的引用,堆里面存放的是实际的地址。

拷贝
拷贝分为浅拷贝和深度拷贝,浅拷贝是实现对象引用的传递,拷贝之后的两个对象指向同一引用,这样的缺点就是修改一个对象的值之后,另一个对象的值也随之改变,但节省内存。深度拷贝是重新开辟空间存放对象的内容,这样可以实现两个对象的改变互不影响。
Object.clone()方法只能实现浅拷贝,可以通过构造方法的方式或者序列化的方式实现深拷贝。

class student implements Cloneable{
	private String name;
	private String age;
	public student(String name,String age){
		this.name=name;
		this.age=age;
	}
	public void setName(String name){
		this.name=name;
	}
	public String toString(){
		return name+" "+age;
	}
	@Override
	public Object clone() throws CloneNotSupportedException {
		return (student) super.clone();
	}
}
@Test
public void test(){
student s=new student("张三","1");
		student ss=s;
		System.out.println(s==ss); // == 是浅拷贝,深拷贝可以通过构造方法 和 重写clone接口
		ss.setName("王武");
		System.out.println(s.toString());
		System.out.println(ss.toString());

		student sss= (student) s.clone(); // 实现的深拷贝
		sss.setName("二码字");
		System.out.println(sss.toString());
		System.out.println(s.toString());
}

海量url怎么去重?

布隆过滤器,由一个很长的二进制向量和一系列随机hash映射函数组成,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
当输入一个key时,通过预先选择的多个哈希函数进行hash计算,获取key在二进制向量中的位置,并置为1;当查询key时,计算key的哈希值,然后判断对应的二进制位置是否都是1,是的话表示key可能存在,否则不存在。因为存在hash冲突的原因,所以key是不一定存在。
应用场景:

  • 海量URL去重
  • 避免缓存击穿

什么是一致性hash?

一致性hash算法是是一种hash算法,它能够在Hash输出空间发生变化时,引起最小的变动。一致性哈希将整个哈希输出空间设置为一个环形区域,从而减小了输出空间的变化对哈希结果的影响。

cpu 100% 遇到过吗?怎么解决的?

  1. 首先使用 top -c 找到 CPU 占用最高的进程;
  2. 然后使用 top -Hp PID 找到进程下 CPU 占用最高的线程 PID,并且将十进制 PID 转换成十六进制;
  3. 之后使用 jstack 命令导出进程快照;
  4. 最后用 cat 命令结合 grep 命令对十六进制线程 PID 进行过滤,可以定位到出现问题的代码。

线程调用start和run什么区别?

start方法是Thread线程类里面synchronized方法修饰的启动线程的方法,而run方法是Runable接口里面的抽象方法。

线程的 start方法启动后后,线程处于可运行状态,此时它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。JVM底层通过调用线程类的run()方法来完成实际的操作。

直接调用线程的 run() 方法,相当于调用普通的方法,并不会启动一个新的线程。

父类和子类有同一个属性,实例化子类时是否会包含父类的属性?

class Father{
	public String name="爸爸";
	public String getName(){
		return this.name;
	}
}
class Son extends Father{
	public String name="儿子";
	public String getFathName(){
		return super.getName();
	}
    // 这里有Override 重写父类的方法
	public String getName(){
		return this.name;
	}
}
@Test
public void test(){
        Father son=new Son();
		System.out.println(son.getName()); // 结果是son,调用的是方法,方法会被覆盖
        System.out.println(son.name); // 结果是爸爸,因为返回的是属性,返回的是父类的属性
}

java、多态性的体现,父类的引用指向其子类的实现,如果子类的方法重写了父类的方法,则调用子类对象时,将执行子类的方法,方法可以覆盖! 父类不能调用子类新写的方法。

什么是不可变对象?String 类为什么设计成不可变的?

不可变对象就是一个对象的状态在创建后就不能被修改的对象。
String类被final关键字修饰,意味着String类不能被继承和修改。
String被设计为不可变的原因:

  • HahsMap的key基本上是string类型,设置为不可变对象,那么就保证来对象的hash码是一致的。
  • java里面字符串是保存在字符串常量池里面的,如果两个字符串对象引用同一个常量池的对象,如果字符串对象允许改变,并且第二个对象修改了字符串对象,那么就会影响第一个对象,就会出现问题。

可以通过反射来破坏string的不可变。

@Test
	public  void  testString() throws Exception {
		String str="张三";
		System.out.println(str);

		Field[] fileds=String.class.getDeclaredFields();

		Field filed = String.class.getDeclaredField("value");
		filed.setAccessible(true);
		char[] value = (char[])filed.get(str);
		value[0]='王';
		System.out.println(str);
	}

如何自己实现阻塞队列?底层源码?

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程,阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器,,满足下面条件:

  • 取元素的时候,如果队列为空,消费者就会阻塞等待该队列非空。
  • 存元素的时候,如果队列满了,生产者就会阻塞等待非满。
    使用ReentrantLock 和 Condition 来达到指定通知唤醒。
    java中的阻塞队列有:
  • DelayQueue: 使用优先级队列实现的无界阻塞队列
  • ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
  • LinkedBlockingDeque: 由链表结构组成的有界阻塞队列。
    • 两个锁分别对应生产者锁 和 消费者锁
  • PriorityBlockingQueue: 支持优先级排序的无界阻塞队列.
  • SynchronousQueue: 不存储元素的阻塞队列
    • 不存储数据,用于传递数据.
    • 每一个put 操作必须等待一个take 操作,否则不能继续添加元素CyclicBarrie
  • LinkedBlockingDeque: 由链表结构组成的双向阻塞队列
  • DelayedWorkQueue:

在java.util.concurrent包下,PriorityQueue底层数据结果是堆

ArrayBlockingQueue

基于数组实现,保证并发的安全性是基于ReetrantLock 和Condition 实现的。其中有两个重要的成员变量putindex 和takeindex,putindex 就是指向数组中上一个添加完元素的位置的下一个地方,比如刚在index=1 的位置添加完,那么putindex 就是2,当index=数组的长度减一的时候,意味着数组已经到了满了,那么需要将putindex 置位0,原因是数组在被消费的也就是取出操作的时候,是从数组的开始位置取得,所以最开始的位置容易是空的,所以把要添加的位置置位0;takeindex 也是一样的,当takeindex 到了数组的长度减一的时候,也需要将takeindex 置为0。

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
        final Object[] items;
        int takeIndex;
        int putIndex;
        int count; // 阻塞队列的数量
        final ReentrantLock lock;

        private final Condition notEmpty; // 用于唤醒take操作
        private final Condition notFull; // 用于唤醒put操作
    }

LinkedBlockingQueue

LinkedBlockingQueue 采用的是以Node对象为结点的链表, 可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
LinkedBlockingQueue使用两个锁,添加和删除的锁是不一样的,这样能大大提高队列的吞吐量,也
意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据。

    public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
        private final int capacity;
        // 当前元素的数据
        private final AtomicInteger count = new AtomicInteger();

        transient Node<E> head;
        private transient Node<E> last;

        private final ReentrantLock takeLock = new ReentrantLock();
        private final Condition notEmpty = takeLock.newCondition();

        private final ReentrantLock putLock = new ReentrantLock();
        private final Condition notFull = putLock.newCondition();
    }

Java IO/NIO

阻塞IO模型

最传统的一种IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出IO 请求之后,内
核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block 状态.

非阻塞I/O

当用户线程发起一个read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个
error 时,它就知道数据还没有准备好,于是它可以再次发送read 操作。一旦内核中的数据准备
好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU.
但是存在一个问题:就是在非阻塞I/O模型中,需要不断的去询问内核数据是否就绪,这样会导致CPU 占用率非常高

I/O多路服用(NIO)

在多路复用IO模型中,会有一个线程不断去轮询多个socket 的状态,只有当socket 真正有读写事件时,才真正调用实际的IO 读写操作。因为在多路复用IO 模型中,只需要使用一个线程就可以管理多个
socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用IO 资源,所以它大大减少了资源占用。在Java NIO 中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用IO 模式,通过一个线程就可以管理多个socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO 比较适合连接数比较多的情况。

cookie、session、token区别?

cookie、session、token主要用于缓存用户的信息。

Cookie是存储在客户端的,以Key-value的形式存储在浏览器里面,value存储的是对应的SessionId 或者 Token。
即客户端不存储用户的任何信息,因为客户端是不安全的。

session

session是将要验证的信息存储在服务端,并以SessionId和数据进行对应(concurrentHashmap结构,key是SessionId,value是保存的数据),SessionId由客户端存储,客户端在请求时将SessionId也带过去,因此实现了状态的对应。

Session会引发一个问题,即多个后台服务器Session共享的问题

Token

Token是在服务端将用户信息经过加密后传给在客户端,每次用户请求的时候都会带上这一段token信息,因此服务端拿到此信息进行解密后就知道此用户是谁了。
Token相比较于Session的优点在于,当后台系统有多台时,由于是客户端访问时直接带着数据,因此无需做共享数据的操作。
Token 的优点:

  • 可以通过URL,POST参数或者是在HTTP头参数发送,因为数据量小,传输速度也很快
  • 由于串包含了用户所需要的信息,避免了多次查询数据库
  • 因为Token是以Json的形式保存在客户端的,所以JWT是跨语言的
  • 不需要在服务端保存会话信息,特别适用于分布式微服务

Java 锁和多线程

Java如何实现线程间通信?
Thread.join(),object.wait(),object.notify(),CountdownLatch,CyclicBarrier,FutureTask,Callable 。

1. 讲讲Java的锁?

锁的目的是当多线程访问共享资源时,提供的一种安全机制。
在Java里面锁分为共享锁和互斥锁、公平锁和非公平锁、悲观锁和乐观锁。
共享锁是指共享资源同时可以被多个线程持有,互斥锁同时只能有一个线程持有。
公平锁和非公平锁值的是线程进入阻塞队列的时候是否会去尝试获取锁,尝试获取锁的是非公平锁,公平锁会按队列的顺序进行加锁。
悲观锁就是认为无论在任何时刻都是线程不安全的,有synchronized、ReentrantLock;而乐观锁就是在读取数据的时候认为是线程安全的,在修改的时候才会做同步操作。
最常用的锁有:

synchronized与ReentrantLock

synchronized
synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。

执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。

synchronized对方法加上锁,使用的是ACC_SYNCHRONIZED来标识,而不是monitorenter和monitorexit。

synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁。
synchronized锁的范围

  • synchronized写在非静态方法上,锁的是方法的调用者(this)。(生成两个对象,是两个不同的锁)
  • static synchronized (写在静态方法上) 锁的是当前class类模板。类一加载就有了,而且类模版唯一。(无论多少个对象,都是同一把锁)
  • 在代码块上加锁,指定的对象进行加锁

ReentrantLock
相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点:

  • 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。
  • 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。
  • 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。

公平和非公平的区别在于,线程是否会去尝试获取锁

ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。

AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。

区别

  • Lock 是显示锁,需要手动开启和关闭,synchronized是隐式锁,自动释放
  • Lock只有代码块锁;synchronized有代码块锁和方法锁
  • Lock锁 ,JVM花费更少的调度时间,性能更好,扩展性高。
  • synchronized是Java内置的关键字;lock是java对象。
  • synchronized无法获取当前锁的状态;lock可以判断是否获取到了锁。
  • synchronized 线程1获取锁后进入阻塞,线程二会一直等待;lock锁就不一定会一直等待。

Condition 和Object 类锁方法的区别

  1. Condition 类的awiat 方法和Object 类的wait 方法等效
  2. Condition 类的signal 方法和Object 类的notify 方法等效
  3. Condition 类的signalAll 方法和Object 类的notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而object 的唤醒是随机的

tryLock、lock、lockInterruptibly

  1. tryLock 能获得锁就返回true,不能就立即返回false,tryLock可以增加时间限制,如果超过该时间段还没获得锁,返回false. (并不一定要获取锁)
  2. lock 能获得锁就返回true,不能的话一直等待获得锁.
  3. lock 和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而lockInterruptibly 会抛出异常.

读写锁

为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm 自己控制的.
java.util.concurrent.locks.ReadWriteLock类

分段锁

分段锁不是一种锁,而是一种思想, 参考jdk1.7的ConcurrentHashMap, 即使用的是分段锁数组+链表的形式, 每次锁的的只是一个数组项.

2. CAS的原理和问题?

CAS(CompareAndSwap,比较并交换),比较当前工作内存中的值 和主内存中的值,如果这个值是期望的,那么执行替换,否则一直 循环(自旋锁);主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:变量内存地址,V表示 ; 旧的预期值,A表示 ; 准备设置的新值,B表示。 当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

缺点

  • ABA问题:ABA的问题发生在CAS更新的过程中,比如两个线程都拿到同一个变量A=1,但是线程二把A的值改为3,再改回1。这个时候线程A拿到的1 是替换过后的1。

可以通过加乐观锁加版本号来解决这个问题。Java中有AtomicStampedReference来解决这个问题。

  • 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
  • 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

博客链接

3. 锁的优化机制?锁升级的过程?

锁的优化包括一些锁的优化机制和synchronized的锁升级策略。锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
锁优化机制
优化机制包括:偏向锁、轻量级锁、自旋锁、自适应自旋锁、锁消除、锁粗化:

  • 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。

偏向锁是在只有一个线程执行同步块时进一步提高性能

  • 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
  • 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,而且用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋的默认次数是10次
  • 自适应自旋锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
  • 重量级锁:重量级锁则是除了拥有锁的线程其他全部阻塞。 Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,。因此,这种依赖于操作系统Mutex Lock 所实现的锁我们称之为“重量级锁“.
  • 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除
  • 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

synchronized锁升级
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起,最后如果以上都失败就升级为重量级锁。

锁升级过程源码

AQS?

AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,是实现锁的一个框架,内部定义了很多锁相关的方法,我们熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的。

AQS内部维护了一个代表共享资源的状态变量volatile int state和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列),每个Node结点标识当前的状态值以及前驱后继结点的值,通过volatile来保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中(head结点表示当前占用的线程),等占有 state 的线程执行完临界区的代码释放资源( state 减 1)后,会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取 state,这里可以实现公平锁或者非公平锁(主要是通过判断当前线程是否会去尝试获取锁)。

参考资料

ReentrantLock来实现AQS
PS: 自己看源码,画个加锁,解锁的流程图
参考资料

8. 讲讲线程池的原理?

线程池的主要目的是控制运行的线程的数量,处理过程中将任务放入阻塞队列,然后在线程创建后
启动这些任务。他的主要特点为:线程复用;控制最大并发数;管理线程。线程池的好处有:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

如何创建线程

  • 继承Thread类、
  • 实现Runnable接口(重写run方法)、
  • 实现Callable接口(重写call方法,需要借助FutureTask这个类)
  • 线程池的方法

Callable接口可以返回结果或抛出检查异常,Runnable接口不可以。
FutureTask继承于RunnableFuture, RunnableFuture又继承于Runnable

FutureTask futureTask=new FutureTask(()->{
			System.out.println("call()");
			return 1024;
		});
new Thread(futureTask,"a").start();
new Thread(futureTask,"b").start(); // 结果被缓存,不会打印
// get()可能会阻塞,耗时较长
System.out.println((Integer) futureTask.get());

Callable方法提交线程的execute()和submit()区别

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功。

Future.get 可以获取到返回值

三大方法创建线程池
线程池的三大方法newSingleThreadExecutor(创建数量为1的线程池)、newFixedThreadPool(创建固定大小的线程池)、newCachedThreadPool(创建一个可根据需要创建新线程的线程);线程池在使用完之后,必须关闭。:

Executors的三大方法开启线程池,底层都是调用ThreadPoolExecutor方法,

阿里巴巴java手册推荐使用ThreadPoolExecutor的方式创建线程池,因为Executors的方式,容易造成OOM:

  • newSingleThreadExecutor 和 newFixedThreadPool 的阻塞队列(LinkedBlockingQueue)长度不限,可能堆积大量的请求,容易造成OOM
  • newCachedThreadPool 最大核心线程池个数Integer.MAX_VALUE,可能会创建大量线程,容易造成OOM。

maximumPoolSize可以根据电脑的核数来设置(CPU密集型)、判断程序中耗费IO操作的任务个数(IO密集型)

ThreadPoolExecutor的7大核心参数

public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
                          int maximumPoolSize, //最大核心池大小
                          long keepAliveTime,// 超时没人调用就关闭该线程
                          TimeUnit unit,//时间单元
                          BlockingQueue<Runnable> workQueue,//阻塞队列,超过这个值,线程池就设置为最大核心线程池大小。
                          ThreadFactory threadFactory, // 线程工厂,创建线程,默认不用修改
                          RejectedExecutionHandler handler) { // 拒绝策略,当线程池都在运行,而且阻塞队列也满了,再进来任务的话就执行的策略。
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

四大拒绝策略:
当任务大于最大线程池个数+阻塞队列的长度时就触发:

  1. AbortPolicy:当阻塞队列满了后,还进来任务就直接丢弃任务,抛出异常,这是默认策略
  2. CallerRunsPolicy:当队列满了,该任务只用调用者所在的线程来处理任务
  3. DiscardOldestPolicy:队列满了,尝试和最早的线程竞争,如果最早的线程结束就执行,没结束就丢弃当前任务,不抛出异常。
  4. DiscardPolicy:直接丢弃任务,也不抛出异常

线程池的工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会执行拒绝策略。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,=线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小==。

如何设计一个动态大小的线程池?

一个线程池应该包括以下四个组成部分:

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

应该包含的方法:

  • 创建线程池
  • 获取线程池
  • execute(Runnable task) 执行一个任务
  • execute(Runnable[] task) 批量执行任务
  • destory 销毁线程
  • addThread添加线程

9. 谈谈countwatch、CylicBarrier、Semaphore、AtomicInteger?

CountDownLatch:
通过一个计数器来实现,计数器的初始值是线程的数量,每当一个线程执行完毕之后,计数器的值减一,当计数器的值为0时表示所有的线程执行完毕。

  • await:调用await的线程会被挂起,直到计数器值为0时,才会继续执行。
  • countDown():计数器的值减一;
    @Test
	public void testCountDownLatch() throws InterruptedException {
		// 定义一个计数器
		CountDownLatch countDownLatch=new CountDownLatch(6);
		for(int i=0;i<6;i++){
			new Thread(()->{
				countDownLatch.countDown();
				System.out.println(Thread.currentThread().getName()+"go out");
			},String.valueOf(i)).start();
		}
		countDownLatch.await(); // 直到计数器为0,才执行下面的代码。
		System.out.println("close door");
	}

CylicBarrier
跟CountDownLatch类似,会阻塞一组线程直到某个事件的发生,区别在于所有的线程都必须等待到设置的阈值才能继续执行
有两个参数,一个是CylicBarrier的值,第二个参数是Runable接口的线程,当计数值达到第一个参数时就执行该线程。

  • await:方法告诉CylicBarrier,到达同步点,然后阻塞等待其他线程到达同步点。

第一个构造的参数,指的是需要几个线程一起到达,才可以使所有线程取消等待。第二个构造,额外指定了一个参数,用于在所有线程达到屏障时,优先执行该线程。

@Test
	public void testCycliBairer(){
		// 第一个参数是数字,第二个参数是Runable接口的线程,当计数值达到第一个参数时就执行该线程
		CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
			System.out.println("召唤神龙");
		});
		for(int i=0;i<7;i++){
			final int finalI = i;
			new Thread(()->{
				System.out.println(Thread.currentThread().getName()+"收集了"+ finalI +"课龙珠");
				try {
					cyclicBarrier.await(); // 等待,直到带到设置的计数器值
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					e.printStackTrace();
				}
			}).start();
		}
	}

Semaphore
Semaphore用来控制同一时间,资源可被访问的线程数量,一般可用于流量的控制。在多线程下用于协调各个线程,以保证能够正确的使用资源。

  • acquire:通过 acquire() 获取一个许可,然后操作共享变量,如果没有获取到,就进入等待状态
  • release: release() 释放一个许可;
    可以用来做秒杀。
    @Test
	public void testSemaphore() throws InterruptedException {
		// 线程数量;允许最多几个线程使用semaphore.acquire()
		Semaphore semaphore=new Semaphore(2,true);
		for(int i=1;i<=6;i++){
			new Thread(()->{
				try {
					semaphore.acquire();//获取
 					System.out.println(Thread.currentThread().getName()+"获取了车位");
					TimeUnit.SECONDS.sleep(2);
					System.out.println(Thread.currentThread().getName()+"离开了车位");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					semaphore.release();// 释放
				}

			},String.valueOf(i)).start();
		}
		TimeUnit.SECONDS.sleep(10);
	}

总结

  1. CountDownLatch 是一个线程等待其他线程,只有等待的线程会阻塞, CyclicBarrier 是多个线程互相等待(类似过山车)。
  2. CountDownLatch 是一次性的, CyclicBarrier 可以循环利用。
  3. CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作。
  4. Semaphore ,需要拿到许可才能执行,并可以选择公平和非公平模式

CountdownLatch源码解析
CyclicBarrier源码解析
Semaphore源码分析

CyclicBarrier实现了类似CountDownLatch的逻辑,它可以使得一组线程之间相互等待,直到所有的线程都到齐了之后再继续往下执行。
CyclicBarrier基于条件队列和独占锁来实现,而非共享锁。
CyclicBarrier可重复使用,在所有线程都到齐了一起通过后,将会开启新的一代。

CountDownLatch相当于一个“门栓”,一个“闸门”,只有它开启了,代码才能继续往下执行。通常情况下,如果当前线程需要等其他线程执行完成后才能执行,我们就可以使用CountDownLatch。
CountDownLatch基于共享锁实现。CountDownLatch是一次性的,“闸门”开启后,无法再重复使用。

AtomicInteger
在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一, juc包下为此类操作特意提供了一些同步类,使得使用更方便, 如AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等. 底层是通过自旋+CAS 实现的.

ThreadLocal 用过吗?实现机制?

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。ThreadLocal类主要解决的就是让每个线程绑定自己本地变量的值, 即线程本地存储

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal 内存泄露问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

sleep和wait的区别、yield、join?

  • sleep 是Thread中的方法,wait是Object中的方法
  • sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中,只有针对的notifyAll方法才能唤醒。
  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
  • sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

yield 会使当前线程让出CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU 时间片,但这又不是绝对的,有的操作系统对
线程优先级并不敏感.

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
为什么要join方法:主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法.

copyonWrite

线程调度、进程调度

线程调度

抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间.JVM使用的是抢占式调度方式.

协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行.

进程调度

先来先服务调度算法(FCFS)
在进程调度中采用FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机.

短作业(进程)优先调度算法
短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度. 缺点是 未考虑到紧迫型作业。
高优先权优先调度算法
系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程.

  • 非抢占式优先权算法: 系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成
  • 抢占式优先权调度算法: 系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)执行,重新将处理机分配给新到的优先权最高的进程.
    多级反馈队列调度算法
    有多个队列,队列的优先级从高到低,分配CPU的执行时间由低到高,也就是优先级越高的队列执行时间越低,最开始任务进入到第一级队列的末尾,在该队列内部使用FIFO分配任务,当任务在分配的CPU时间内执行完毕之后,就进入下一个任务,如果没有执行完毕,就进入到二级队列的末尾。如果当前执行到二级队列了,来了一个优先级更高的任务,则当前任务进入当前优先级队列末尾,转而去执行优先级更高的任务(抢占式任务)。

##

JVM

13. 对象的创建过程?

Java对象创建过程

14. JVM的垃圾回收机制?可达性分析的跟节点?

在Java内存结构里面,方法区、堆是线程共享的区域,栈、程序计数器是线程私有的,生命周期跟线程一样。
而Java堆和方法区这两个区域则有很多不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有在处于运行期间,我们才能知道程序究竟会创建那些对象,这部分回收是动态的。
垃圾回收算法有

新生代和老生代区别?

垃圾回收
新生代的回收算法是:标记复制,因为新生代存活的对象很少,所以把存活的对象复制到幸存者2区,并清空eden区和survivor1区,而且也不会出现碎片化的问题。
老年代采用的回收算法是:标记整理。老年代存活的对象很多,使用复制算法的话会复制大量的对象,所以采用标记整理算法,把存活的对象往内存空间一端移动,然后直接清理掉边界以外的内存。
老年代存活的对象更难被垃圾回收,新生代的对象都是朝生夕灭的。

Minor GC 与 Full GC 分别在什么时候发生?

垃圾回收

类加载机制了解吗?

类加载机制

什么是双亲委派模式?

类加载机制之双亲委派机制

Java内存模型

java内存模型

5. volatile原理?

java关键字,可以保持可见性、有序性,但是不能保证原子性。可以禁止指令重排优化。

因为volatile 变量的写入不是原子性操作,并不保证线程安全,

volatile要求每次使用之前都必须从主内存刷新最新的值,用于保证能看见其他线程队变量V所做的修改;

每次修改后volatile修饰的变量的值时都必须同步回主内存中,保证其他线程可以看到自己对变量V所做的修改;

volatile修饰的变量不会被指令重排优化,从而保证代码的执行顺序与程序的顺序相同。

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。
使用场景

  1. 状态标志
  2. 懒汉式单例模式

volatile 很适合只有一个线程修改,其他线程读取的情况。volatile 变量被修改之后,对其他线程立即可见。

内存屏障

6. 谈谈你对JMM内存模型的理解?为什么需要JMM?加锁为什么就可以保证内存屏障?

JMM(Java内存模型)是一种抽象的概念,它描述的是一组规则或规范。
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

线程加锁前,必须读取主内存的最新值到自己的工作内存;

线程解锁前,必须把共享变量的值刷新回主内存;
三大特性

原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。

有序性:在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

JMM的happen-before

happens-before 原则内容如下:程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
锁规则解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
volatile 规则volatile 变量的写,先发生于读,这保证了volatile 变量的可见性,简单的理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

内存抖动、内存泄漏

内存抖动:在GC垃圾回收进程执行任务的时候,会存在一个 STW (stop the world) 机制,他就会把其他所有线程都挂起。如果GC非常频繁地调用,那就会导致主线程不流畅,给用户的感觉就是卡顿。
内存抖动太频繁,导致大量对象频繁创建和销毁,会产生大量不连续的内存空间,如果此时有一个大对象需要申请内存,就有可能申请失败,导致OOM内存溢出。

内存泄漏:应该回收的对象,因为存在引用,导致没有被回收。比如java里面的ThreadLocal使用的是map类型,其中key是弱引用,在每次进行垃圾回收的时候,就会回收key,但是value是强引用,所以不会被回收到,就导致了内存泄漏。在使用ThreadLocal的时候要手动进行删除。

Java框架

用的什么rpc框架?

Nginx如何实现负载均衡策略?

消息队列MQ?

限流、熔断有了解吗?如何实现的

消息中间件如何解决消息丢失、消息乱序问题?

分布式事务保证一致性?

分布式锁的实现?


目录