Linux下环境变量配置

下载jdk linux版压缩包然后解压
vim /etc/profile

  1. export JAVA_HOME=/usr/local/jdk
  2. export PATH=$JAVA_HOME/bin:$PATH
  3. export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

source /etc/profile

面向对象思想的理解

面向过程 vs面向对象

面向过程更多是以 “执行者” 的角度来思考问题

而面向对象更多是以 “组织者” 的角度思考问题

“面向过程”的思维,我考虑的是如何去实现一个方法,比如设计一个算法

“面向对象”的思维,我考虑的是如何选择合适的工具,如何组织到一起干成一件事

面向对象三大特性

  • 封装
  • 继承
    • 多为复用,父类写好共用的,子类只需要写好扩展的即可
  • 多态
    • 父类引用不同的子类对象产生不同的表现,比如设计接口解耦

JDK、JRE、JVM

  • JDK: Java Development Kit Java开发工具包,提供Java开发环境
    • 包含编译Java源文件的编译器Javac,还有调试和分析工具
  • JRE: Java Runtime Environment,Java 运行环境,包含Jvm以及一些基础类库
  • JVM: Java Virtual Machine ,Java 虚拟机 提供执行字节码文件的能力

只需要Java运行环境只安装JRE即可,所以Java程序是支持跨平台的,但JVM不支持跨平台
image.png

== 和 equals

== 比较的是值

比较基本的数据类型:比较的是数值,本地变量存放在栈帧中

比较引用类型:比较引用指向的值(地址),实例对象存放在堆内存里,比较的就是指向堆的地址

equals 默认比较的是地址,这个方法最初定义在Object上,默认的实现就是比较地址

自定义的类,如果需要比较内容就需要重写equals方法,比如String类

//new对象的方式就是引用 在堆内存中分配了空间 指向堆的地址
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";
//相加 相当于new了一个对象
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
//两个变量相加得到的也是变量 这里得到的就是new出来的
final String s10 = s3+s4;
System.out.println(s5==s10);//false

final

  • 修饰类,类不可变,不可继承,所以抽象类不可以用final修饰
    • 比如String类不可变性
  • 修饰方法,该方法不可重写
    • 比如模板方法,固定算法
  • 修饰变量,就是常量
    • 修饰基本数据类型,值就不能改
    • 修饰引用类型,引用的指向不能改
final Student student = new Student("Andy");
//注意,这种操作是可以的
student.setName("yudi");

类初始化和实例初始化

类初始化

一个类要创建实例需要先加载并初始化该类

  • main方法所在的类需要先加载和初始化

一个子类要初始化需要先初始化父类

一个类的初始化就是执行<clinit>()方法,此方法由JVM生成,可在编译后的.class文件中看到

  • <clinit>()方法由静态类变量显示赋值代码和静态代码块组成
  • 按从上到下的顺序执行
  • 该方法只执行一次

实例初始化

实例初始化执行<init>()方法,也就是构造方法

  • <init>()方法可能重载多个,有几个构造器就有几个<init>方法
  • <init>()方法由非静态实例变量显示赋值代码和非静态代码块,对应构造器代码组成
  • 非静态实例变量显示赋值代码和非静态代码块从上到下顺序执行,而对应构造器的代码最后执行
  • 每次创建实例对象,调用对应构造器,执行的就是对应的<init>方法
  • <init>方法的首行是super()super(实参列表),即对应父类的<init>方法,super方法写或不写都会在
  • 子类如果重写父类的方法,通过子类对象调用的一定是子类重写过的代码

线程安全?

一个线程安全的对象,在多线程环境下对同一资源的访问,不需要加入额外的同步控制,比如不需要手动加锁,调用这个对象的行为都可以获得正确的结果,我们就说这个对象线程安全

多个线程对同一对象的操作可能会同时运行这段代码

如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

如果多个线程访问同一个资源,那么就需要上锁,才能保证数据安全

比较常用的方式是采用synchronized关键字给代码块或方法加锁

如果每个线程访问的是各自的资源。那么就不需要考虑线程安全问题,这个时候就可以放心的使用非线程安全对象,比如在方法内创建对象(局部变量)

String、StringBuffer、StringBuilder

  • String是final类型,每次声明的都是不可变对象
    • 所以每次操作都会产生新的String对象,然后重新指向新的String对象
  • StringBuffer和StringBuilder都是在原对象上进行操作
    • 所以如果经常要改变字符串内容,建议使用这两个类

StringBuffer VS StringBuilder

  • StringBuffer 线程安全,后者线程不安全
  • 线程不安全但性能更高,所以优先使用StringBuilder
  • 优先级 StringBuilder > StringBuffer > String

安全和性能都需要考虑

抽象类与接口

JDK8之前:

  • 语法:
    • 抽象类:方法可以有抽象,也可以有非抽象,有构造器
    • 接口:方法都是抽象;属性都是常量,默认有public static final修饰
  • 设计:
    • 抽象类:同一类事物的抽取,比如针对Dao层的操作的封装,如BaseDao,BaseServiceImpl
    • 接口:通常更像是一种标准的制定,定制系统之间对接的标准
    • 例子:
      • 单体项目:分层开发,interface作为各层之间的纽带,在controller中注入UserService,在service中注入UserDao
      • 分布式项目:面向服务开发,抽取服务service,这个时候,就会产生服务的提供者和消费者两个角色
      • 这两个角色之间的纽带依然是接口

JDK8之后

  • 接口里面可以有实现的方法,要在方法的声明上加上default或者static

区分几个概念:

  • 多继承、多重继承、多实现
    • 多重继承:A->B->C (爷孙三代)
    • 多实现:Person implements Test1,Test2(符合多项国际化标准)
    • 多继承:接口可以多继承,类只能单继承

递归与迭代

n的阶乘与斐波那契数列(递归)

n的阶乘

public static int getResult(int n){
    if (n < 0){
        throw new IllegalArgumentException("非法参数");;
    }
    if( n==1 || n == 0){
        return 1;
    }
    return getResult(n-1)*n;
}

斐波那契数列

规律:每个数等于前两个数之和

public static int getFeiBo(int n){
    if(n < 1){
        throw new IllegalArgumentException("不能小于1");
    }
    if(n == 1 || n == 2){
        return n;
    }
    return getFeibBo(n-1) + getFeiBo(n-2);
}

有n步台阶,一次只能上1步或2步,共有多少种走法?

n=1, f(1)=1

n=2, f(2)=2

n=3,f(3)=f(1)+f(2)

n=x,f(x)=f(x-2)+f(x-1)

递归

递归的写法直接使用上面的斐波那契数列的代码

循环迭代

public static int loop(int n) {
    if (n < 1) {
        throw new IllegalArgumentException("不能小于1");
    }
    if (n == 1 || n == 2) {
        return n;
    }
    //初始化走到第二级台阶的走法 最后走两步
    int one = 2;
    //初始化走到第一级台阶的走法 最后走一步
    int two = 1;
    int sum = 0;
    for (int i = 3; i <= n; i++) {
        //最后跨两步 + 最后跨一步的走法
        sum = two + one;
        two = one;
        one = sum;
    }
    return sum;
}

int和Integer

//new 对象 表示在堆内存开辟了空间
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);
System.out.println(i1 == i2);//false
//普通数据类型赋值给引用类型 自动装箱机制 
//使用反编译工具可以看到Integer有一个缓存的数组
Integer i3 = 126;
Integer i4 = 126;
int i5 = 126;
//引用类型和基本数据类型比较的时候会自动拆箱
System.out.println(i3 == i4);//true
System.out.println(i3 == i5);//true
//Integer范围是-128~127 
//128就越界了 就会new一个对象 new Integer(128)
Integer i6 = 128;
Integer i7 = 128;
int i8 = 128;
System.out.println(i6 == i7);//false
//引用类型和基本数据类型比较的时候会自动拆箱
System.out.println(i6 == i8);//true

引用类型相比较,要看是否越界;如果是引用类型和基本数据类型比较,就只用看数值

重载与重写

  • 重载:发生在一个类里面,方法名相同,参数列表不同,跟返回类型没关系

    • 例如以下不构成重载,编译会报错
      double add()
      int add()
      
  • 重写:子类重写父类的方法

ArrayList和LinkedList

ArrayList是数组,一段连续的内存空间 LinkedList是双向链表,不连续的内存空间

简单的结论

  • ArrayList查找快,因为是连续的内存空间,方便寻址,但删除插入慢,因为需要数据迁移
  • LinkedList查找慢,因为需要指针一个个的寻址,但插入删除快,因为只需要改变前后节点的指针指向即可

ArrayList默认长度是10

如存储1000个对象的信息,ArrayList更省内存,ArrayList只需要初始化好空间,避免后期扩容浪费资源

如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小

LinkedList是双向链表,除了元素,还有首尾的两个指针,而且每个节点都有指向上一个和下一个的指针,查找第N个元素的时间复杂度是O(N)

ArrayList使用在查询比较多,但是插入和删除比较少的情况,而LinkedList用在查询比较少而插入删除比较多的情况
(1)如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
( 2 ) 如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList对象要优于ArrayList对象;
ArrayList扩容

  • 创建一个新数组,新数组的长度是老数组的1.5倍(通过位运算的方式)

    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
  • 将原数组的数据迁移到新数组

    elementData = Arrays.copyOf(elementData, newCapacity);
    

    ArrayList与Vector

ArrayList线程不安全,效率高,常用

Vector线程安全,效率相对较低,内部使用了synchronized

HashMap与HashSet

HashSet的底层是HashMap,HashMap的key是唯一的,所以HashSet也就实现了不可重复

存入HashSet的值是HashMap的key,value是HashSet源码中自己new的Object对象

private static final Object PRESENT = new Object();

HashMap

HashMap在put的时候存在线程安全问题,如果只是get就不存在线程安全问题

key可以为null,HashMap会进行特殊处理,放在table[0]的位置

数据结构

  • 数组
  • 链表
  • 红黑树(Jdk1.8开始)

hashmap的初始容量为2的n次幂,hashmap会维护2的n次幂

当数组长度为2的n次幂的时候,不同的key算得的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

因为 n 永远是2的次幂,所以 n-1 通过 二进制表示,永远都是尾端以连续n个1的形式表示

就算手动设置的大小不为2的n次幂,hashmap也会根据值的大小设置成相近的n的2次幂

简而言之:为什么维护2的n次幂?因为可以减少hash碰撞

元素存储 为什么要采用Hash算法?

遍历数组里的内容逐个比对太捞了,使用哈希算法解决效率问题

底层使用数组存储数据,哈希表的本质就是数组,数组元素是链表

hash算法,存入的每一个key可以通过hashCode()方法计算得到一个int类型的数值,称之为hash值

hash值与数组长度做位运算(%运算)
hashmap优化成(n - 1) & hash(与运算比%运算要更快,效果和hash % n求余是一样的),得到元素存入数组中的位置下标

随着元素不断添加,就可能出现hash碰撞,即需要存储到数组的不同的对象求出来的数组下标相同 ,这个时候需要比较,就需要用到equals和==进行比较的方法

如果key相同则替换旧的值,否则形成链表,新存入map中的node的next指针指向已经存在的

以上是JDK1.7之前,JDK1.8之后做了优化
元素太多了链表就会比较长,会优化成红黑树

红黑树就相当于在数组和链表中取了一个平衡

HashMap底层定义了一个常量static final int TREEIFY_THRESHOLD = 8;

JDK8并不是一上来就用红黑树,默认先使用链表,当链表长度>=TREEIFY_THRESHOLD -1时才使用红黑树

当红黑树元素个数小于等于6时,再从红黑树转回链表

为什么这样子?因为链表的插入比红黑树要快,插入红黑树要进行比较然后左移右移

红黑树

插入顺序为1,2,3,4,5,6,7,8 最终形成的效果如下
image.png
红黑树是一个接近平衡的二叉搜索树,解决链表查询的速度问题
左节点小于父节点,右节点大于父节点
查找,插入,删除等操作,时间复杂度最坏情况为O(logn)
新插入的节点默认是红色的,可能经过后续的变色和旋转改变

红黑树的逻辑约束

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点(nil,虚节点)是黑色
  4. 不能有连在一起的红节点
  5. 每个红色节点的两个子节点一定都是黑色,红节点的父节点一定是黑色的,黑节点的子节点也可以是黑色
  6. 任意一个节点到每个叶子节点的路径都包含数量相同的黑节点,黑色完美平衡

平衡措施

  1. 变色
  2. 自旋
    • 左子节点为黑色,右子节点为红色,需要变色或左旋
      • image.png
      • image.pngimage.png
    • 左子节点是红色,且左子节点的左子节点也是红色,需要变色或右旋
      • image.png

扩容机制

当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75
也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16
0.75=12的时候,就把数组的大小扩展为2*16=32即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作
所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。
比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

扩容死锁

底层数组扩容的本质是创建一个新的数组,然后将数据迁移

如果是多个线程的场景下扩容时就会申请多个数组

就会出现扩容死锁现象,线程在转移数据的过程中可能造成死锁,CPU可能就会飙到100%

为什么会出现这种情况,因为hashmap在扩容的时候,老数据迁移到新的数组上,可能会在链表上形成一个闭环,get操作时就会陷入这个循环中出不来

java8进行了优化,多使用了四个指针躲过链表闭环

使用注意事项

说明:HashMap使用HashMap(int initialCapacity) 初始化
公式:initialCapacity = (需要存储的元素个数 / 负载因子) + 1
注意负载因子(即loaderfactor)默认为0.75,如果暂时无法确定初始值大小,请设置为16。
HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize需重建hash表,严重影响性能。

LinkedHashMap

继承HashMap,和hashmap相似

每个元素都有个指针指向下一个插入的元素,这样遍历的时候会根据插入的顺序来遍历

而不是hashmap那样无序

TreeMap

底层就是红黑树

HashTable、HashMap、ConcurrentHashMap

HashTable

线程安全,内部有使用synchronized关键字给方法上锁,多个线程操作同一对象会产生阻塞现象,性能较低

HashMap

线程不安全,内部没有锁,优点:效率高,缺点:线程不安全,可能出现死锁情况

HashMap底层可以看上面

ConcurrentHashMap

key不允许为null

插入数据时先判断对应的数组位置是不是空的,如果是就用CAS算法插入节点,如果不为空就加锁,T1线程操作成功后T2线程才能进行操作

put操作用到了循环,插入失败就自旋

使用分段锁,将锁的粒度变小。只锁一个对象,兼顾了线程安全和性能

如何选择

  • 不是多个线程访问同一资源,优先使用HashMap,因为常用在方法内,是局部变量
  • 成员变量多个线程共享访问,就使用ConcurrentHashMap

IO流的分类及选择

字节流基类:

InputStream、OutputStream

字符流基类:

Reader、Writer

  • 字节流——二进制文件
  • 字符流——文本文件

需要解析文件内容时用字符流

SerialVersionUID的作用

ObjectOutputStream.writeObject() 方法会根据类的结构生成序列化版本号

ObjectInputStream.readObject() 方法拿当前的类的结果生成版本号,会比较两个版本号如果一致就反序列化

根据业务需求,在类中再加了一个字段,这个时候类的结构发生了变化,反序列化就会出错

在类中自定义一个序列化ID,及时类结构发生了变化,但序列化ID一致,依然可以反序列化成功老版本的

新版本升级,兼容老版本

创建线程的方式

  • 继承Thread类
  • 实现Runable接口
  • 实现Callable接口(可以获取线程执行之后的返回值)

本质上都是继承Thread类,通过new一个Thread类的对象的start方法开一个线程

实现Runable接口可以避免单继承的局限性

在实际开发中通常采用线程池来完成Thread创建,更好的管理线程资源

main方法不是一个人在战斗,后台有一个GC线程用于垃圾回收

线程的生命周期

  • 新建:就是刚使用new方法,new出来的线程;
  • 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

image.png

sleep和wait

  1. 所属的类不同
    sleep方法是定义在Thread上
    wait方法是定义在Object上
  2. 对于锁资源的处理方式不同
    sleep不会释放锁
    wait会释放
  3. 使用范围
    sleep可以使用在任何代码块
    wait必须在同步方法或同步代码块执行

为什么wait要定义在Object中,而不是定义在Thread中?

Java的锁是对象级别的,而不是线程级别的

为什么wait必须写在同步代码块中?

避免CPU切换到其他线程,而其他线程又提前执行了notify方法,就永远唤不醒,这样就达不到预期(先wait再由其他线程唤醒),所以需要一个同步锁来保护

ThreadLocal

为每个线程创建一个副本,实现了线程的上下文传递对象

实现副本的源码分析

ThreadLocal如何实现副本,分析一下get方法的源码

public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //找到当前线程对应的map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //以ThreadLocal为key,获取对应的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取对应的value 就是对应的数据
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

需要结合ThreadLocal的set方法才能更好的理解

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map不为空就存放一个键值对
        //this指的是threadLocal
        //value是副本
        map.set(this, value);
    else
        //为空就创建一个map
        createMap(t, value);
}

可以看出每个线程拥有对应的map用来保存数据

应用场景

数据库连接,不管是Mybatis还是Hibernate本质都是基于JDBC的连接

让线程内多次获取到的Connection对象是同一个

volatile

当多个线程操作共享数据时 彼此不可见 使用volatile可以解决此问题

synchronized关键字: 同步锁 可以解决这个问题 但是线程多了效率就变低了 适合较轻量级的应用

volatile关键字:让多个线程操作共享数据时 彼此可见

volatile关键字特性:

  1. 内存可见性(当一个线程修改volatile变量的值时,另一个线程就可以实时看到此变量的更新值)
  2. 禁止指令重排(volatile变量之前的变量执行先于volatile变量执行,volatile之后的变量执行在volatile变量之后)
  3. volatile不具备互斥性 synchronized关键字具备
  4. 不能保证变量的原子性

CAS

compare and swap 比较交换

期望值和内存里的一样就 变成更新值 不然不进行操作

只能有一个线程进行替换操作

可以利用此特性实现自旋锁
image.png

jdk提供的 java.util.concurrent.atomic 包下就有很多底层使用CAS算法的类