JAVA面试知识点整理
JAVA知识点整理
JAVA关键字:
1、static
1.1、static的基本使用
1、static关键字介绍 总结:方便在没有创建对象的情况下进行调用。 也就是说,static修饰的不需要创建对象去调用,直接根据类名就可以去调用。 因为静态变量随着类加载时被完成初始化,他们在内存中仅有一个,且JVM也会只为他们分配一次内存。 使用场景:对象之间共享数据,方便访问。 2、static的使用方法: static一般用来修饰成员变量或方法(有一种特殊的用法,用static修饰内部类,普通类时不允许声明位静态的)。 2.1、static修饰内部类: public class Test3 { public static class InnerClass { InnerClass(){ System.out.println("============静态内部类"); } public void InnerMethod(){ System.out.println("============静态内部方法"); } } public static void main(String[] args) { //直接通过Test3类名访问静态内部类InnerClass InnerClass inner = new Test3.InnerClass(); //调用方法 inner.InnerMethod(); } } 结果: ============静态内部类 ============静态内部方法 2.2、static关键字修饰方法 修饰方法的时候,跟类一样,直接可以通过类名来进行调用。 2.3、static关键字修饰变量 被static修饰的成员变量叫做静态变量,也叫做类变量,说明这个变量时属于这个类的,而不是属于对象。没有被static修饰的成员变量叫做实例变量,说明这个变量时数据某个具体的对象。 2.4、static关键字修饰代码块 3、static关键字修饰的执行顺序 父类静态变量--父类静态代码块--子类静态变量--子类静态代码块--父类普通变量--父类普通代码块--父类构造函数--子类普通变量--子类普通代码块--子类构造函数。
1.2、深入理解static关键字
从上图中我们可以发现,静态变量存放在方法区,并且是被所有线程所共享。 解释一下上图的区域。 堆区: 1、存储的全部是对象,每一个对象都包含一个与之对应的Class的信息。 2、JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。 栈区: 1、每一个线程包含一个栈区,栈中只保存基本数据类型的对象和自定义对象的引用。 2、每一个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。 3、栈区分为3部分:基本类型变量区、执行环境上下文,操作指令区(存放操作指令)。 方法区: 1、又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。 2、方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
1.3、总结
特点: 1、static是一个修饰符,用于修饰成员(成员变量,成员函数,还有内部类)。 2、static修饰的成员被所有的对象共享。 3、static优先于对象存在,因为static的成员随着类的加载而加载。 4、static修饰的成员多了一种调用方式,可以直接被类名所调用。 5、static修饰的数据是共享数据,对象中的存储时特有的数据。 成员变量和静态变量的区别: 1、生命周期不同: 成员变量随着对象的创建而存在随着对象的回收而释放。 静态变量随着类的加载而存在随着类的消失而消失。 2、调用方式 成员变量只能被对象调用。 静态变量可以被对象调用,也可以用类名调用。(推荐用类名调用) 3、别名不用 成员变量也称为实例变量。 静态变量称为类变量。 4、数据存储位置不同 成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据。 静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据。 静态使用时需要注意的事项 1、静态方法只能访问静态成员。(非静态既可以访问静态,又可以访问非静态) 2、静态方法中不可以使用this或者super关键字。 3、主函数是静态的。
1.4、考点static
问题一:static出现的位置 public class Test3 { static String x = "1"; static int y =1; public static void main(String[] args) { static int z = 2;//static不应该出现在这里编译有错误 System.out.println(x + y + z); } } 只有类才存在静态的变量 方法只能对静态变量的操作 不能在方法内试图定义静态变量,否则的话会抛出编译错误。静态变量的本意是为了让所有的对象共享这个变量,如果在方法里面定义静态变量的话就存在逻辑错误了,也达不到你想要目的. 因为在方法中定义静态变量根本没有他的任何意义. 任何对象都有自己的方法,即使是静态方法,方法内的变量也是在方法调用时候才开始分配内存,所以想给成静态的在逻辑上存在问题
2、String、StringBuffer、StringBuilder的区别
相同点:都是对字符串进行操作。 区别: 1、String的值是不可变的,每次对String的操作,都有可能会产生新的String对象。 2、StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。 3、StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。 4、由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。 字符修改上: String:不可变字符串; StringBuffer:可变字符串、效率低、线程安全; StringBuilder:可变字符序列、效率高、线程不安全; 初始化: 初始化上的区别,String可以空赋值,后者不行,报错
String,StringBuffer,StringBuilder
3、final、finally、finalize的区别
final: 在java,final可以用来修饰类,方法和变量。 修饰类:当final修饰类的时,表明该类不能被其他类所继承。注意final类中所有的成员方法都会隐式的定义为final方法。 修饰方法: 原因:(1)把方法锁定,以防止继承类对其进行修改。 (2)效率,在早期的java版本中,会将final方法转化位内嵌调用,当时由于方法过去庞大,可能在性能上不会有多大的提示,因此在最近版本中,不需要final方法进行这些优化了。 修饰变量: final成员变量表示常量,只能被赋值一次,赋值后其值不会改变。所以在final修饰成员变量(属性),必须要显示初始化。 finally: 1、finally作为异常处理的一部分,它用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。(×)(这句话其实存在一定的问题) 2、只有与finally对应的try语句块得到执行的情况下,finally语句块才会执行。以上两种情况在执行try语句块之前已经返回或抛出异常,所以try对应的finally语句并没有执行。 3、因为我们在 try 语句块中执行了 System.exit (0) 语句,终止了 Java 虚拟机的运行。 4、因为finally用法特殊,所以会撤销之前的return语句,继续执行最后的finally块中的代码。 finalize: finalize()是在java.lang.Object里定义的,也就是说,每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。 特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。 使用finalize还需要注意一个事,调用super.finalize();一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。 所以,推荐不要使用finalize()方法,它跟析构函数不一样。
4、==和equals的区别
1、==是判断两个变量或实例是不是指向同一个内存空间,比较地址。 2、equals是判断两个变量或实例所指向的内存空间的值是不是相同。 Integer il = 127; Integer i2 = 127; System.out.println(il == i2); //True Integer m = 128; Integer n = 128; System.out.println(m == n); //false 原因:其实是因为Integer在常量池中的存储范围为[-128,127],127在这范围呢,因此是直接存储于常量池的,而128不在这范围内,所以会在堆内存中创建一个新的对象来保存这个值,所以m、n分别指向了两个不同的对象地址,所以导致了不相等。 其实 Objece类型的equals方法是直接通过==来比较的,和==没有任何区别。 所以其他调用equals方法是重写了equals方法。
JAVA集合:
1、HashTable和HashMap的区别
1、线程安去 HashTable是线程安全的,HashMap是线程不安全的。原因是因为HashTable所有元素的put-get操作都说是synchronized修饰的,而HashMap没有。 2、性能优劣 HashTable是线程安全的,每个方法都要阻塞其它线程,所有Hashtable性能较差,HashMap性能较好。 如果要线程安全又要保证性能,建议使用 JUC 包下的 ConcurrentHashMap。 3、NULL Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。 原因:Hashtable key 为 null 会直接抛出空指针异常,value 为 null 手动抛出空指针异常,而 HashMap 的逻辑对 null 作了特殊处理。 4、实现方式 可以看出两者继承的类不一样,Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。 5、容量扩容 HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。 当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
2、HashMap和ConcurrentHashMap的区别
1、安全 HashMap不支持并发操作,没有同步方法。 ConcurrentHashMap支持并发操作,通过继承 ReentrantLock(JDK1.7重入锁)/CAS和synchronized(JDK1.8内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。 理解: ConcurrentHashMap具体是怎么实现线程安全的呢肯定不可能是每个方法加synchronized,那样就变成了HashTable。 从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。 在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中: ConcurrentHashMap中默认是把segments初始化为长度为16的数组。 根据ConcurrentHashMap.segmentFor的算法,3、4对应的Segment都是segments[1],7对应的Segment是segments[12]。 (1)Thread1和Thread2先后进入Segment.put方法时,Thread1会首先获取到锁,可以进入,而Thread2则会阻塞在锁上: (2)切换到Thread3,也走到Segment.put方法,因为7所存储的Segment和3、4不同,因此,不会阻塞在lock(): 以上就是ConcurrentHashMap的工作机制,通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。 2、底层结构 JDK1.8之前HashMap的结构为数组+链表,JDK1.8之后HashMap的结构为数组+链表+红黑树;JDK1.8之前ConcurrentHashMap的结构为segment数组+数组+链表,JDK1.8之后ConcurrentHashMap的结构为数组+链表+红黑树 3、
3、HashMap怎么解决hash冲突问题
在 JDK1.8之前, HashMap底层采用的链表法来解决冲突。即使装载因子和 Hash函数设计的再合理,随着数据量的增加也会出现链表过长的情况,一旦链表过长,严重影响了 HashMap的性能。 在 JDK1.8中对 HashMap底层做了优化。当链表长度大于8时,链表就转化为红黑树,当链表小于8时,将红黑树转化为链表。因为当链表过长的时候,查找的效率将会变慢,利用红黑树快速增删改查的特性,可以提高 HashMap的性能,而链表不长时,红黑树的快速增删改查的特性就不太明显,并且红黑树的还有维护成本,因此当链表不长时,不需要将链表转化为红黑树。
4、HashMap的大小为什么是2的幂次
计算:当容量一定是2^n时,h & (length - 1) == h % length 为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1; 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
5、HashMap的扩容
//上图中说了默认构造方法与自定义构造方法第一次执行resize的过程,这里再说一下扩容的过程 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //扩容肯定执行这个分支 if (oldCap >= MAXIMUM_CAPACITY) { //当容量超过最大值时,临界值设置为int最大值 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //扩容容量为2倍,临界值为2倍 newThr = oldThr << 1; } else if (oldThr > 0) // 不执行 newCap = oldThr; else { // 不执行 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 不执行 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //将新的临界值赋值赋值给threshold @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //新的数组赋值给table //扩容后,重新计算元素新的位置 if (oldTab != null) { //原数组 for (int j = 0; j < oldCap; ++j) { //通过原容量遍历原数组 Node<K,V> e; if ((e = oldTab[j]) != null) { //判断node是否为空,将j位置上的节点 //保存到e,然后将oldTab置为空,这里为什么要把他置为空呢,置为空有什么好处吗?? //难道是吧oldTab变为一个空数组,便于垃圾回收?? 这里不是很清楚 oldTab[j] = null; if (e.next == null) //判断node上是否有链表 newTab[e.hash & (newCap - 1)] = e; //无链表,确定元素存放位置, //扩容前的元素地址为 (oldCap - 1) & e.hash ,所以这里的新的地址只有两种可能,一是地址不变, //二是变为 老位置+oldCap else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; /* 这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j]; 如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap] 举个栗子来说一下上面的两种情况: 设:oldCap=16 二进制为:0001 0000 oldCap-1=15 二进制为:0000 1111 e1.hash=10 二进制为:0000 1010 e2.hash=26 二进制为:0101 1010 e1在扩容前的位置为:e1.hash & oldCap-1 结果为:0000 1010 e2在扩容前的位置为:e2.hash & oldCap-1 结果为:0000 1010 结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。 现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1 那么在扩容后应该也这么计算呀,扩容后的容量为oldCap*2=32 0010 0000 newCap=32,新的计算 方式应该为 e1.hash & newCap-1 即:0000 1010 & 0001 1111 结果为0000 1010与扩容前的位置完全一样。 e2.hash & newCap-1 即:0101 1010 & 0001 1111 结果为0001 1010,为扩容前位置+oldCap。 而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位 是0,还是1。如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。 再来分析下loTail loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立): 第一次执行: e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素 loTail为空,所以loHead指向与e相同的node对象,然后loTail也指向了同一个node对象。 最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素 第二次执行: lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e, 也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向 的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。 第三次执行: 与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node ...... hiTail与hiHead的执行过程与以上相同,这里就不再做解释了。 由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍 历完链表。 这是(e.hash & oldCap) == 0成立的时候。 (e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表, 通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与 newTab[j+oldCap]上面去 */ do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; //尾节点的next设置为空 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //尾节点的next设置为空 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
6、JDK1.8对HashMap的改进
1、结构: 1.7:HashMap 里面是一个数组,然后数组中每个元素是一个单向链表,查找的时间复杂度为O(N),N为链表的长度; 1.8:HashMap结构:数组+链表+红黑树,查找的时间复杂度降低为O(logN). JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率) 2、插入方式: JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。 3、扩容后数据存储位置的计算方式也不一样: 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1) 而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
7、HashMap的key适合选用什么数据类型
由于HashMap的存储数据的特点是 : 无序,无索引,不能存储重复元素,所以这里容易想到的是类的HashCode()函数 HashMap是利用HashCode()来区别两个不同的对象。 而HashCode()是本地方法,是用C或C++来实现的,即该方法是直接返回对象的内存地址。 而基础类型里面没有hashcode()方法,而且不能设置为null,所以不允许HashMap中定义基础类型的key,而value也是不能定义为基础类型。 而在HashMap存储自定义对象的时候,需要自己再自定义的对象中重写其hashCode()方法和equals方法,才能保证其存储不重复的元素。 否则将存储多个重复的对象,因为每new一次,其就创建一个对象,内存地址是不同的。由于我们是只想看两个对象的某个属性是否相同来判断是否相同,所以就会出先找不到的情况,即返回hull。因为我们没有重写,他就会使用默认的hashcode()。对比的是两个对象的虚拟内存地址,而两个不同的对象,他们的内存地址是不一致的,所以就会返回为NULL。
8、ArrayList、Vector和LinkedList的区别
9、HashSet的底层实现
在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object();,
JAVA多线程:
1、进程和线程的区别
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。 区别: 与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的 根本区别:进程是资源分配最小单位,线程是程序执行的最小单位。 计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。
2、JAVA实现多线程的方式
第一种方式:
//通过创建Thread子类的方式实现功能--线程和任务绑定在一起,操作不方便。当多个线程要执行一个任务时,创建多个线程,这是就会有多个任务,虽然能定义一个static变量来实现你需要的功能,但是不符合逻辑。 public class Demo5 { public static void main(String[] args) { SubThread t0 = new SubThread(); SubThread t1 = new SubThread(); SubThread t2 = new SubThread(); SubThread t3 = new SubThread(); //开启线程 t0.start(); t1.start(); t2.start(); t3.start(); } } class SubThread extends Thread{ static int sum = 20;//大家共用这个变量 @Override public void run() { for (int i = 0; i < 5; i++) { //是为了把票都买完 System.out.println("剩余 票数:"+ --sum); } } }
第二种方式:
//将任务从线程中分离出来,哪个线程需要工作,就将任务交给谁,操作方便.创建一个类实现Runnable,作为任务类,实现run方法。然后,创建任务对象,创建线程将任务和线程绑定。 //这样做的好处是让多个线程调用一个任务对象,但是也又出现了线程安全问题,下面会引出锁的概念 public class Demo3 { public static void main(String[] args) { //任务对象 Ticket ticket = new Ticket(); //将任务与线程绑定 Thread t0 = new Thread(ticket); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); //开启线程 t0.start(); t1.start(); t2.start(); t3.start(); } } //任务类 class Ticket implements Runnable{ int sum = 20; boolean flag = true; public void run() { while (flag) { //让当前的线程睡100毫秒 //作用:让他暂时让出cpu try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (sum > 0) { System.out.println("剩余 票数:"+ --sum); }else { flag = ! flag; } } } }
第三种方式:
class Mythread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("*****come in call method()"); return 1024; } } /** * @auther zzyy * @create 2019-04-13 16:37 */ public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask(new Mythread()); new Thread(futureTask,"A").start(); Integer result = futureTask.get(); System.out.println(result); } } //实现Callable接口:从JDK1.5开始,最大的好处,可以有返回值,抛出异常可以排查错误。
3、线程安全的定义
在进行并发编程的时候我们需要确保程序在被多个线程并发访问时可以得到正确的结果,也就是实现线程安全。 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
4、线程安去产生的原因和解决方案
线程安全出现的根本原因: 1.存在两个或者两个以上的线程对象共享同一个资源; 2.多线程操作共享资源代码有多个语句。 解决方案: 方式一:同步代码块 格式:synchronize(锁对象){ 需要被同步的代码 } 同步代码块需要注意的事项: 1.锁对象可以是任意的一个对象; 2.一个线程在同步代码块中sleep了,并不会释放锁对象; 3.如果不存在线程安全问题,千万不要使用同步代码块; 4.锁对象必须是多线程共享的一个资源,否则锁不住。 方式二:同步函数(同步函数就是使用synchronized修饰一个函数) 同步函数注意事项: 1.如果函数是一个非静态的同步函数,那么锁对象是this对象; 2.如果函数是静态的同步函数,那么锁对象是当前函数所属的类的字节码文件(class对象); 3.同步函数的锁对象是固定的,不能由自己指定。 方式三:使用Lock锁: 步骤: 1、在成员位置创建一个ReentrantLock对象(该类实现了Lock接口,并添加了三个实用方法); 2、在可能会出现安全问题的代码之前使用Lock接口的lock方法获取锁; 3、在可能会出现安全问题的代码之后使用Lock接口的unlock方法释放锁。 发现:ReentrantLock类使用wait()和notify()会报错IllegalMonitorStateException(非法监视器状态异常),目前还不知道为啥。
5、Volatile关键字
1、volatile是java虚拟机提供的轻量级的同步机制。 特性: 保证可见性 不保证原子性:在各自的工作空间,A线程改了变量的值,没有及时的写到主内存了,造成数据丢失。(解决方法sync,JUC下面的AtomicInteger里面的方法) 禁止指令重排序 2、JMM(java内存模型):本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,规范中定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。(主内存与各个线程的工作空间) JMM关于同步的规定: 线程解锁前,必须把共享变量的值刷新回主内存。 线程加锁前,必须读取主内存的最新值到自己的工作内存。 加锁解锁是同意把锁。 JMM的特点: 可见性 原子性 有序性 volatile的使用场景: DCL(Double Check Lock 双端检索机制)
可见性:
指令重排序:
线程安全的保证:
6、Volatile和synchronized的区别
共性:volatile与synchronized都用于保证多线程中数据的安全 区别: (1)volatile修饰的变量,jvm每次都从主存(主内存)中读取,而不会从寄存器(工作内存)中读取。 而synchronized则是锁住当前变量,同一时刻只有一个线程能够访问当前变量 (2)volatile仅能用在变量级别,而synchronized可用在变量和方法中 (3)volatile仅能实现变量的修改可见性,无法保证变量操作的原子性。而synchronized可以实现变量的修改可见性与原子性
7、synchronized和lock的区别
1.)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中; 2.synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; 3.Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; 4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。 5.Lock可以提高多个线程进行读操作的效率。
8、synchronized的底层实现
对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synchronized方法而言,javac为其生成了一个ACCSYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACCSYNCHRONIZED修饰,则会先尝试获得锁。
9、sleep和wait的区别
它们最大本质的区别是:sleep()不释放同步锁,wait()释放同步锁. 1、这两个方法来自不同的类分别是Thread和Object 2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围) 4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 5、sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。
10、Java锁
11、线程池有哪几种
Java通过Executors提供四种线程池,分别为: newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 newWorkStealingPool jdk8增加了newWorkStealingPool(int parall),增加并行处理任务的线程池,不能保证处理的顺序。
12、线程池的组成部分
1、线程池管理器(ThreadPoolManager):用于创建并管理线程池 2、工作线程(WorkThread): 线程池中线程 3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。 4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。
13、线程的生命周期
新建:就是刚使用new方法,new出来的线程; 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行; 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能; 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态; 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
14、线程池参数
设计模式:
1、JAVA有哪些设计模式?
java的设计模式大体上分为三大类: 创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。 结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。 行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 设计模式遵循的原则有6个: 1、开闭原则(Open Close Principle) 对扩展开放,对修改关闭。 2、里氏代换原则(Liskov Substitution Principle) 只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 3、依赖倒转原则(Dependence Inversion Principle) 这个是开闭原则的基础,对接口编程,依赖于抽象而不依赖于具体。 4、接口隔离原则(Interface Segregation Principle) 使用多个隔离的借口来降低耦合度。 5、迪米特法则(最少知道原则)(Demeter Principle) 一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 6、合成复用原则(Composite Reuse Principle) 原则是尽量使用合成/聚合的方式,而不是使用继承。继承实际上破坏了类的封装性,超类的方法可能会被子类修改。 1. 工厂模式(Factory Method) 常用的工厂模式是静态工厂,利用static方法,作为一种类似于常见的工具类Utils等辅助效果,一般情况下工厂类不需要实例化。 1) 单例模式。单例模式对实例个数的控制并节约系统资源.在它的核心结构中只包含一个被称为单例类的特殊类,通过构造函数私有化和静态块以及提供对外访问的接口来实现.饿汉模式:单例实例在类的加载中就被创建.不需要判断,安全.private static MySingleton2 mySingleton = new MySingleton2(); 饿汉式 直接创建了对象懒汉模式:单例实例在第一次使用时被创建.需要if判断,不安全.private static MySingleton2 mySingleton=null 懒汉式 静态块中会进行判断登记模式(不常用可忽略).应用场景:如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。 2) 工厂模式。工厂模式主要是为创建对象提供了接口。应用场景如下:a、 在编码时不能预见需要创建哪种类的实例。b、 系统不应依赖于产品类实例如何被创建、组合和表达的细节 3) 观察者模式。(一个学生对应多个老师,一个老师对应多个学生).定义了对象间一对多的依赖关系,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。应用场景如下:a、对一个对象状态的更新,需要其他对象同步更新,而且其他对象的数量动态可变。b、对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。 4) 迭代器模式。迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。应用场景如下:当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,就应该考虑用迭代器模式。其实stl容器就是很好的迭代器模式的例子。 5) 代理模式为其他对象提供代理来控制对该对象的访问.应用场景如下:ngnix的反向代理(隐藏服务器)运用的就是代理模式. 6)适配器模式将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。应用场景如下:别人开发的控件要用咱们的应用服务(不识别啊),这时候适配器模式起作用了.让不兼容变为兼容.
2、单例模式饿汉式和懒汉式(手写)
//懒汉模式(线程安全) //线程安全 public class Singleton{ private static Singleton instance; private Singleton(){} public static synchronized Singleton getInstance(){ if(instance==null){ instance=new Singleton(); } return instance; } } //饿汉式是在类加载的时候创建实例,所以线程是安全的. public class EHan { //饿汉模式 //将构造函数私有化 private Singleton(){} //将对象实例化 private static EHan instance = new EHan(); //得到实例的方法 public static EHan getInstance() { return instance; } }
类的设计:
1、什么是面向对象
面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。面向对象有三大特征:封装性、继承性、多态性,其中封装性指的是隐藏了对象的属性和实现细节,仅对外提供公共的访问方式,这样就隔离了具体的变化,便于使用,提高了复用性和安全性。对于继承性,就是两种事物间存在着一定的所属关系,那么继承的类就可以从被继承的类中获得一些属性和方法;这就提高了代码的复用性。继承是作为多态的前提的。多态是说父类或接口的引用指向了子类对象,这就提高了程序的扩展性,也就是说只要实现或继承了同一个接口或类,那么就可以使用父类中相应的方法,提高程序扩展性,但是多态有一点不好之处在于:父类引用不能访问子类中的成员。
2、面向对象的三大特征
3、多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。 软件设计原则—开闭原则 对拓展开放,对修改关闭。
4、多态存在的必要性
1、多态:同一个对象,在不同时刻体现出来的不同状态。 2、多态的前提: 1)要有继承关系或实现关系(接口); 2)要有方法重写; 3)要有父类或者父接口引用指向子类`Fu f= new Zi(); 注意:形参实参形式的(形参父类实参子类)。 多态的好处 提高了代码的维护性(继承保证);提高了代码的扩展性。
5、重载和重写
重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写! 重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。 重载(Overload) 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。 每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。 最常用的地方就是构造器的重载。 方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。