做公司网站哪家 上海,wordpress产品展示主题,网站服务器搭建,sw网站建设最近发现自己的Java基础知识还是有点薄弱#xff0c;刚好有点空闲时间进行再补一补#xff0c;然后进行整理一下#xff0c;方便自己以后复习。其实个人认为Java基础还是很重要的#xff0c;不管从事Java后端开发还是Android开发#xff0c;Java这块的基础还是重中之重刚好有点空闲时间进行再补一补然后进行整理一下方便自己以后复习。其实个人认为Java基础还是很重要的不管从事Java后端开发还是Android开发Java这块的基础还是重中之重可以多去学习一下Java各种类和数据结构的写法进行学习基础正确使用 equals 方法尽量使用 字符串.equals(变量)方法推荐使用java.util.Objects#equals(JDK7 引入的工具类)Objects.equals(null,SnailClimb);// falsejava.util.Objects#equals源码public static boolean equals(Object a, Object b) {// 可以避免空指针异常。如果anull的话此时a.equals(b)就不会得到执行避免出现空指针异常。return (a b) || (a ! null a.equals(b));}BigDecimal浮点数之间的等值判断基本数据类型不能用来比较包装数据类型不能用 equals 来判断,会造成精度丢失问题,不要使用构造方法BigDecimal(double)方式吧double值转化为BigDecimal对象推荐使用BigDecimal(String)方法使用使用 BigDecimal 来定义浮点数的值再进行浮点数的运算操作。BigDecimal a new BigDecimal(1.0);BigDecimal b new BigDecimal(0.9);BigDecimal c new BigDecimal(0.8);BigDecimal x a.subtract(b);// 0.1BigDecimal y b.subtract(c);// 0.1System.out.println(x.equals(y));// trueBigDecimal 的大小比较:a.compareTo(b) : 返回 -1 表示小于0 表示 等于 1表示 大于。BigDecimal a new BigDecimal(1.0);BigDecimal b new BigDecimal(0.9);System.out.println(a.compareTo(b));// 1基本数据类型与包装数据类型的使用标准【强制】所有的 POJO 类属性必须使用包装数据类型。【强制】RPC 方法的返回值和参数必须使用包装数据类型。【推荐】所有的局部变量使用基本数据类型。Arrays.asList()使用指南Arrays.asList()将数组转换为集合后,底层其实还是数组并没有实现修改集合的方法所以不能使用其修改集合的相关方法add/remove/clear方法会抛出UnsupportedOperationException异常传递的数组必须是对象数组而不是基本类型(需要使用包装数据类型)。如何将数组转换成ArrayList:1.最简便的方法(推荐)List list new ArrayList(Arrays.asList(a, b, c))2.使用 Java8 的StreamInteger [] myArray { 1, 2, 3 };List myList Arrays.stream(myArray).collect(Collectors.toList());//基本类型也可以实现转换(依赖boxed的装箱操作)int [] myArray2 { 1, 2, 3 };List myList Arrays.stream(myArray2).boxed().collect(Collectors.toList());Collection.toArray()方法使用的坑该方法是一个泛型方法 T[] toArray(T[] a); 如果toArray方法中没有传递任何参数的话返回的是Object类型数组。String [] s new String[]{dog, lazy, a, over, jumps, fox, brown, quick, A};List list Arrays.asList(s);Collections.reverse(list);slist.toArray(new String[0]);//没有指定类型的话会报错// new String[0]起一个模板的作用指定了返回数组的类型0是为了节省空间因为它只是为了说明返回的类型不要在 foreach 循环里进行元素的 remove/add 操作remove元素使用Iterator方式如果是并发操作需要对Itreator对象加锁。foreach循环会抛出ConcurrentModificationException异常String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?可变性String 类中使用 final 关键字修饰字符数组来保存字符串 private final char value[]所以 String 对象是不可变的.而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰线程安全性String 中的对象是不可变的也就可以理解为常量线程安全。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁所以是线程安全的。StringBuilder 并没有对方法进行加同步锁所以是非线程安全的。性能每次对 String 类型进行改变的时候都会生成一个新的 String 对象然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作而不是生成新的对象并改变对象引用。对于三者使用的总结操作少量的数据: 适用String单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer在 Java 中定义一个不做事且没有参数的构造方法的作用Java 程序在执行子类的构造方法之前如果没有用 super() 来调用父类特定的构造方法则会调用父类中“没有参数的构造方法”。因此如果父类中只定义了有参数的构造方法而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法则编译时将发生错误因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。接口和抽象类的区别接口的方法默认是 public所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现)而抽象类可以有非抽象的方法。接口中除了static、final变量不能有其他变量而抽象类中则不一定。一个类可以实现多个接口但只能继承一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。接口方法默认修饰符是public抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰)。从设计层面来说抽象是对类的抽象是一种模板设计而接口是对行为的抽象是一种行为的规范。构造方法的特性名字与类名相同。没有返回值但不能用void声明构造函数。生成类的对象时自动执行无需调用。 与 equals(重要) : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象(基本数据类型比较的是值引用数据类型比较的是内存地址)。equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况情况1类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时等价于通过“”比较这两个对象。情况2类覆盖了 equals() 方法。一般我们都覆盖 equals() 方法来比较两个对象的内容是否相等若它们的内容相等则返回 true (即认为这两个对象相等)。hashCode 与 equals (重要)hashCode() 的作用就是获取哈希码也称为散列码它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 在散列表中才有用在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码进而确定该对象在散列表中的位置。hashCode()与equals()的相关规定如果两个对象相等则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true两个对象有相同的hashcode值它们也不一定是相等的因此equals 方法被覆盖过则 hashCode 方法也必须被覆盖hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode()则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)Java异常处理try 块用于捕获异常。其后可接零个或多个catch块如果没有catch块则必须跟一个finally块。catch 块用于处理try捕获到的异常。finally 块无论是否捕获或处理异常finally块里的语句都会被执行。当在try块或catch块中遇到return语句时finally语句块将在方法返回之前被执行。在以下4种特殊情况下finally块不会被执行在finally语句块第一行发生了异常。 因为在其他行finally块还是会得到执行在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 若该语句在异常语句之后finally会执行程序所在的线程死亡。关闭CPU。当try语句和finally语句中都有return语句时在方法返回之前finally语句的内容将被执行并且finally语句的返回值将会覆盖原始的返回值。Java序列化中如果有些字段不想进行序列化如何做对于不想进行序列化的变量使用transient关键字修饰。transient关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化当对象被反序列化时被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量不能修饰类和方法。Java 中只有值传递Java程序设计语言总是采用按值调用。方法得到的是所有参数值的一个拷贝即方法不能修改传递给它的任何参数变量的内容。一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。一个方法可以改变一个对象参数的状态。一个方法不能让对象参数引用一个新的对象。容器List,Set,Map三者的区别List(对付顺序的好帮手) List接口存储一组不唯一(可以有多个元素引用相同的对象)有序的对象Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象但Key不能重复典型的Key是String类型但也可以是任何对象。Arraylist 与 LinkedList 区别是否保证线程安全ArrayList 和LinkedList 都是不同步的也就是不保证线程安全底层数据结构 Arraylist 底层使用的是** Object 数组LinkedList 底层使用的是双向链表**数据结构(JDK1.6之前为循环链表JDK1.7取消了循环。注意双向链表和双向循环链表的区别)插入和删除是否受元素位置的影响① ArrayList 采用数组存储所以插入和删除元素的时间复杂度受元素位置的影响。 比如执行add(E e) 方法的时候 ArrayList 会默认在将指定的元素追加到此列表的末尾这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储所以插入删除元素时间复杂度不受元素位置的影响都是近似 O(1)而数组为近似 O(n)。4.是否支持快速随机访问LinkedList 不支持高效的随机元素访问而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index) 方法)。内存空间占用ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。RandomAccess接口RandomAccess 接口中什么都没有定义,标识实现这个接口的类具有随机访问功能(知识标识没有具体作用)。在 binarySearch()方法中它要判断传入的list 是否 RamdomAccess 的实例如果是调用indexedBinarySearch()方法如果不是那么调用iteratorBinarySearch()方法实现了 RandomAccess 接口的list优先选择普通 for 循环 其次 foreach,未实现 RandomAccess接口的list优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,)大size的数据千万不要使用普通for循环ArrayList 与 Vector 的区别为什么要用Arraylist取代VectorVector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。Arraylist不是同步的所以在不需要保证线程安全时建议使用Arraylist通过ArrayList 源码探索其扩容机制ArrayList有三种方式来初始化构造方法源码如下/*** 默认初始容量大小*/private static final int DEFAULT_CAPACITY 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA {};/***默认构造函数使用初始容量10构造一个空列表(无参数构造)*/public ArrayList() {this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** 带初始容量参数的构造函数。(用户自己指定容量)*/public ArrayList(int initialCapacity) {if (initialCapacity 0) {//初始容量大于0//创建initialCapacity大小的数组this.elementData new Object[initialCapacity];} else if (initialCapacity 0) {//初始容量等于0//创建空数组this.elementData EMPTY_ELEMENTDATA;} else {//初始容量小于0抛出异常throw new IllegalArgumentException(Illegal Capacity: initialCapacity);}}/***构造包含指定collection元素的列表这些元素利用该集合的迭代器按顺序返回*如果指定的集合为nullthrows NullPointerException。*/public ArrayList(Collection extends E c) {elementData c.toArray();if ((size elementData.length) ! 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() ! Object[].class)elementData Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData EMPTY_ELEMENTDATA;}}以无参数构造方法创建 ArrayList 时实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时才真正分配容量。即向数组中添加第一个元素时数组容量扩为10。add 方法/*** 将指定的元素追加到此列表的末尾。*/public boolean add(E e) {//添加元素之前先调用ensureCapacityInternal方法ensureCapacityInternal(size 1); // Increments modCount!!//这里看到ArrayList添加元素的实质就相当于为数组赋值elementData[size] e;return true;}ensureCapacityInternal() 方法//得到最小扩容量private void ensureCapacityInternal(int minCapacity) {if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 获取默认的容量和传入参数的较大值minCapacity Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);}当 要 add 进第1个元素时minCapacity为1在Math.max()方法比较后minCapacity 为10。ensureExplicitCapacity() 方法//判断是否需要扩容private void ensureExplicitCapacity(int minCapacity) {modCount;// overflow-conscious codeif (minCapacity - elementData.length 0)//调用grow方法进行扩容调用此方法代表已经开始扩容了grow(minCapacity);}grow() 方法/*** 要分配的最大数组大小*/private static final int MAX_ARRAY_SIZE Integer.MAX_VALUE - 8;/*** ArrayList扩容的核心方法。*/private void grow(int minCapacity) {// oldCapacity为旧容量newCapacity为新容量int oldCapacity elementData.length;//将oldCapacity 右移一位其效果相当于oldCapacity /2//我们知道位运算的速度远远快于整除运算整句运算式的结果就是将新容量更新为旧容量的1.5倍int newCapacity oldCapacity (oldCapacity 1);//然后检查新容量是否大于最小需要容量若还是小于最小需要容量那么就把最小需要容量当作数组的新容量if (newCapacity - minCapacity 0)newCapacity minCapacity;// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE//如果minCapacity大于最大容量则新容量则为Integer.MAX_VALUE否则新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。if (newCapacity - MAX_ARRAY_SIZE 0)newCapacity hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData Arrays.copyOf(elementData, newCapacity);}hugeCapacity() 方法。private static int hugeCapacity(int minCapacity) {if (minCapacity 0) // overflowthrow new OutOfMemoryError();//对minCapacity和MAX_ARRAY_SIZE进行比较//若minCapacity大将Integer.MAX_VALUE作为新数组的大小//若MAX_ARRAY_SIZE大将MAX_ARRAY_SIZE作为新数组的大小//MAX_ARRAY_SIZE Integer.MAX_VALUE - 8;return (minCapacity MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}java 中的 length 属性针对数组,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.java 中的 length() 方法针对字符串,如果想看这个字符串的长度则用到 length() 这个方法.java 中的 size()方法针对泛型集合,如果想看这个泛型有多少个元素,就调用此方法来查看.ArrayList源码中的ensureCapacity方法最好在 add 大量元素之前用 ensureCapacity 方法以减少增量重新分配的次数/**如有必要增加此 ArrayList 实例的容量以确保它至少可以容纳由minimum capacity参数指定的元素数。** param minCapacity 所需的最小容量*/public void ensureCapacity(int minCapacity) {int minExpand (elementData ! DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. Its already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity minExpand) {ensureExplicitCapacity(minCapacity);}}HashMap 和 Hashtable 的区别线程是否安全 HashMap 是非线程安全的HashTable 是线程安全的HashTable 内部的方法基本都经过synchronized 修饰(要保证线程安全的话就使用 ConcurrentHashMap)效率因为线程安全的问题HashMap 要比 HashTable 效率高一点。另外HashTable 基本被淘汰不要在代码中使用它对Null key 和Null value的支持HashMap 中null 可以作为键这样的键只有一个可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null直接抛出NullPointerException。初始容量大小和每次扩充容量大小的不同 ①创建时如果不指定容量初始值Hashtable 默认的初始大小为11之后每次扩充容量变为原来的2n1。HashMap 默认的初始化大小为16。之后每次扩充容量变为原来的2倍。②创建时如果给定了容量初始值那么 Hashtable 会直接使用你给定的大小而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用2的幂作为哈希表的大小。底层数据结构 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化当链表长度大于阈值(默认为8)时将链表转化为红黑树以减少搜索时间。Hashtable 没有这样的机制。HasMap 中带有初始容量的构造函数public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity 0)throw new IllegalArgumentException(Illegal initial capacity: initialCapacity);if (initialCapacity MAXIMUM_CAPACITY)initialCapacity MAXIMUM_CAPACITY;if (loadFactor 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException(Illegal load factor: loadFactor);this.loadFactor loadFactor;this.threshold tableSizeFor(initialCapacity);}tableSizeFor方法保证了 HashMap 总是使用2的幂作为哈希表的大小。/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {int n cap - 1;n | n 1;n | n 2;n | n 4;n | n 8;n | n 16;return (n 0) ? 1 : (n MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n 1;}集合框架底层数据结构总结ListArraylist Object数组Vector Object数组LinkedList 双向链表(JDK1.6之前为循环链表JDK1.7取消了循环)SetHashSet(无序唯一): 基于 HashMap 实现的底层采用 HashMap 来保存元素LinkedHashSet LinkedHashSet 继承与 HashSet并且其内部是通过 LinkedHashMap 来实现的。TreeSet(有序唯一) 红黑树(自平衡的排序二叉树。)MapHashMap JDK1.8之前HashMap由数组链表组成的数组是HashMap的主体链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化当链表长度大于阈值(默认为8)时将链表转化为红黑树以减少搜索时间LinkedHashMap LinkedHashMap 继承自 HashMap所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外LinkedHashMap 在上面结构的基础上增加了一条双向链表使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作实现了访问顺序相关逻辑。详细可以查看《LinkedHashMap 源码详细分析(JDK1.8)》Hashtable 数组链表组成的数组是 HashMap 的主体链表则是主要为了解决哈希冲突而存在的TreeMap 红黑树(自平衡的排序二叉树)并发synchronized 关键字synchronized关键字解决的是多个线程之间访问资源的同步性synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。synchronized关键字最主要的三种使用方式修饰实例方法: 作用于当前对象实例加锁进入同步代码前要获得当前对象实例的锁修饰静态方法: :也就是给当前类加锁会作用于类的所有对象实例。访问静态 synchronized 方法占用的锁是当前类的锁而访问非静态 synchronized 方法占用的锁是当前实例对象锁。修饰代码块: 指定加锁对象对给定对象加锁进入同步代码库前要获得给定对象的锁。总结 synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中字符串常量池具有缓存功能双重校验锁实现对象单例(线程安全)public class Singleton {private volatile static Singleton instance;private Singleton() {}public static Singleton getInstance() {//先判断对象是否已经实例过没有实例化过才进入加锁代码if (instance null) {//类对象加锁synchronized (Singleton.class) {if (instance null) {instance new Singleton();}}}return instance;}}instance采用 volatile 关键字修饰也是很有必要的 instance new Singleton(); 这段代码其实是分为三步执行为 instance分配内存空间初始化 instance将 instance指向分配的内存地址但是由于 JVM 具有指令重排的特性执行顺序有可能变成 1-3-2。指令重排在单线程环境下不会出先问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 getInstance() 后发现 instance不为空因此返回 instance但此时 instance还未被初始化。使用 volatile 可以禁止 JVM 的指令重排保证在多线程环境下也能正常运行。synchronized 关键字和 volatile 关键字的区别volatile关键字是线程同步的轻量级实现所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升实际开发中使用 synchronized 关键字的场景还是更多一些。多线程访问volatile关键字不会发生阻塞而synchronized关键字可能会发生阻塞volatile关键字能保证数据的可见性但不能保证数据的原子性。synchronized关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性而 synchronized关键字解决的是多个线程之间访问资源的同步性。ThreadLocal通常情况下我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢 JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值可以将ThreadLocal类形象的比喻成存放数据的盒子盒子中可以存储每个线程的私有数据。如果你创建了一个ThreadLocal变量那么访问这个变量的每个线程都会有这个变量的本地副本这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。ThreadLocal原理Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是null只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们实际上调用这两个方法的时候我们调用的是ThreadLocalMap类对应的 get()、set()方法。ThreadLocal类的set()方法public void set(T value) {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}最终的变量是放在了当前线程的 ThreadLocalMap 中并不是存在 ThreadLocal 上ThreadLocal 可以理解为只是ThreadLocalMap的封装传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap而ThreadLocalMap可以存储以ThreadLocal为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal 对象的话会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的ThreadLocalMap的 key 就是 ThreadLocal对象value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 是 map结构是为了让每个线程可以关联多个 ThreadLocal变量。这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。ThreadLocalMap是ThreadLocal的静态内部类。ThreadLocal 内存泄露问题ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以如果 ThreadLocal 没有被外部强引用的情况下在垃圾回收的时候会 key 会被清理掉而 value 不会被清理掉。这样一来ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话value 永远无法被GC 回收这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况在调用 set()、get()、remove() 方法的时候会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法线程池线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息例如已完成任务的数量。使用线程池的好处降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度。 当任务到达时任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性。 线程是稀缺资源如果无限制的创建不仅会消耗系统资源还会降低系统的稳定性使用线程池可以进行统一的分配调优和监控。实现Runnable接口和Callable接口的区别如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。备注 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable taskObject resule))。执行execute()方法和submit()方法的区别1)execute() 方法用于提交不需要返回值的任务所以无法判断任务是否被线程池执行成功与否2)submit() 方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象通过这个Future对象可以判断任务是否执行成功并且可以通过future的get()方法来获取返回值get()方法会阻塞当前线程直到任务完成而使用 get(long timeoutTimeUnit unit)方法则会阻塞当前线程一段时间后立即返回这时候有可能任务没有执行完。如何创建线程池《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建而是通过 ThreadPoolExecutor 的方式这样的处理方式能更加明确线程池的运行规则规避资源耗尽的风险Executors 返回线程池对象的弊端如下FixedThreadPool 和 SingleThreadExecutor 允许请求的队列长度为 Integer.MAX_VALUE 可能堆积大量的请求从而导致OOM。CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE 可能会创建大量线程从而导致OOM。方式一通过构造方法实现通过构造方法实现.png方式二通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutorFixedThreadPool 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时线程池中若有空闲线程则立即执行。若没有则新的任务会被暂存在一个任务队列中待有线程空闲时便处理在任务队列中的任务。SingleThreadExecutor 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池任务会被保存在一个任务队列中待线程空闲按先入先出的顺序执行队列中的任务。CachedThreadPool 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定但若有空闲线程可以复用则会优先使用可复用的线程。若所有线程均在工作又有新的任务提交则会创建新的线程处理任务。所有线程在当前任务执行完毕后将返回线程池进行复用。对应Executors工具类中的方法如图所示进程和线程进程进程是程序的一次执行过程是系统运行程序的基本单位因此进程是动态的。系统运行一个程序即是一个进程从创建运行到消亡的过程。在 Java 中当我们启动 main 函数时其实就是启动了一个 JVM 的进程而 main 函数所在的线程就是这个进程中的一个线程也称主线程。线程线程与进程相似但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源但每个线程有自己的程序计数器、虚拟机栈和本地方法栈所以系统在产生一个线程或是在各个线程之间作切换工作时负担要比进程小得多也正因为如此线程也被称为轻量级进程。线程与进程的关系,区别及优缺点从 JVM 角度说进程和线程之间的关系一个进程中可以有多个线程多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。总结 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的而各线程则不一定因为同一进程中的线程极有可能会相互影响。线程执行开销小但不利于资源的管理和保护而进程正相反程序计数器、虚拟机栈和本地方法栈是线程私有的堆和方法区是线程共享的程序计数器为什么是私有的?程序计数器主要有下面两个作用字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制如顺序执行、选择、循环、异常处理。在多线程的情况下程序计数器用于记录当前线程执行的位置从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。需要注意的是如果执行的是 native 方法那么程序计数器记录的是 undefined 地址只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。所以程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。虚拟机栈和本地方法栈为什么是私有的?虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。本地方法栈 和虚拟机栈所发挥的作用非常相似区别是 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。所以为了保证线程中的局部变量不被别的线程访问到虚拟机栈和本地方法栈是线程私有的。堆和方法区堆和方法区是所有线程共享的资源其中堆是进程中最大的一块内存主要用于存放新创建的对象 (所有对象都在这里分配内存)方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。并发与并行的区别并发 同一时间段多个任务都在执行 (单位时间内不一定同时执行)并行 单位时间内多个任务同时执行。为什么要使用多线程先从总体上来说从计算机底层来说 线程可以比作是轻量级的进程是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外多核 CPU 时代意味着多个线程可以同时运行这减少了线程上下文切换的开销。从当代互联网发展趋势来说 现在的系统动不动就要求百万级甚至千万级的并发量而多线程并发编程正是开发高并发系统的基础利用好多线程机制可以大大提高系统整体的并发能力以及性能。使用多线程可能带来什么问题?并发编程的目的就是为了能提高程序的执行效率提高程序运行速度但是并发编程并不总是能提高程序运行速度的而且并发编程可能会遇到很多问题比如内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。线程的生命周期和状态Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态Java 线程的状态线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁由上图可以看出线程创建之后它将处于 NEW(新建) 状态调用 start() 方法后开始运行线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态它只能看到 RUNNABLE 状态所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。当线程执行 wait()方法之后线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时在没有获取到锁的情况下线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。什么是上下文切换多线程编程中一般线程的个数都大于 CPU 核心的个数而一个 CPU 核心在任意时刻只能被一个线程使用为了让这些线程都能得到有效执行CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用这个过程就属于一次上下文切换。概括来说就是当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态以便下次再切换会这个任务时可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。什么是线程死锁?如何避免死锁?认识线程死锁多个线程同时被阻塞它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞因此程序不可能正常终止。如下图所示线程 A 持有资源 2线程 B 持有资源 1他们同时都想申请对方的资源所以这两个线程就会互相等待而进入死锁状态。线程死锁示意图产生死锁必须具备以下四个条件互斥条件该资源任意一个时刻只由一个线程占用。请求与保持条件一个进程因请求资源而阻塞时对已获得的资源保持不放。不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源。循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。如何避免线程死锁只要破坏产生死锁的四个条件中的其中一个就可以了。破坏互斥条件这个条件我们没有办法破坏因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。破坏请求与保持条件一次性申请所有的资源。破坏不剥夺条件占用部分资源的线程进一步申请其他资源时如果申请不到可以主动释放它占有的资源。破坏循环等待条件靠按序申请资源来预防。按某一顺序申请资源释放资源则反序释放。破坏循环等待条件。sleep() 方法和 wait() 方法区别和共同点两者最主要的区别在于sleep 方法没有释放锁而 wait 方法释放了锁 。两者都可以暂停线程的执行。Wait 通常被用于线程间交互/通信sleep 通常被用于暂停执行。wait() 方法被调用后线程不会自动苏醒需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。我们调用 start() 方法时会执行 run() 方法为什么我们不能直接调用 run() 方法new 一个 Thread线程进入了新建状态;调用 start() 方法会启动一个线程并使线程进入了就绪状态当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作然后自动执行 run() 方法的内容这是真正的多线程工作。 而直接执行 run() 方法会把 run 方法当成一个 main 线程下的普通方法去执行并不会在某个线程中执行它所以这并不是多线程工作。总结 调用 start 方法方可启动线程并使线程进入就绪状态而 run 方法只是 thread 的一个普通方法调用还是在主线程里执行。final,static,this,super 关键字总结final 关键字final关键字主要用在三个地方变量、方法、类。对于一个final变量如果是基本数据类型的变量则其数值一旦在初始化之后便不能更改如果是引用类型的变量则在对其初始化之后便不能再让其指向另一个对象。当用final修饰一个类时表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。使用final方法的原因把方法锁定以防任何继承类修改它的含义static 关键字static 关键字主要有以下四种使用场景修饰成员变量和成员方法: 被 static 修饰的成员属于类不属于单个这个类的某个对象被类中所有对象共享静态变量 存放在 Java 内存区域的方法区。静态方法不能调用非静态方法和非静态成员变量。静态变量 存放在 Java 内存区域的方法区。静态代码块代码执行顺序(静态代码块 —非静态代码 —构造方法) 该类不管创建多少对象静态代码块只执行一次.静态内部类(只能修饰内部类)它的创建是不需要依赖外围类的创建。不能使用任何外围类的非static成员变量和方法。静态导包this 关键字this关键字用于引用类的当前实例代表对本类对象的引用指向本类对象super 关键字super关键字用于从子类访问父类的变量和方法 代表对父类对象的引用指向父类对象使用 this 和 super 要注意的问题在构造器中使用 super() 调用父类中的其他构造方法时该语句必须处于构造器的首行否则编译器会报错。另外this 调用本类中的其他构造方法时也要放在首行。this、super不能用在static方法中。