JAVA学习/面试框架

目录

JVM

基础概念

类加载器

启动类加载器

扩展类加载器

系统类加载器

自定义加载器

类装载执行顺序

双亲委派模型

运行时内存区

数据区

指令区

垃圾收集器

判断对象是否存活

垃圾回收算法

多线程

线程状态

线程创建

线程方法

线程分类

synchronized

线程池

线程池具体实现举例

REDIS

快速原因

数据类型

优缺点

优点

缺点

高性能高并发

持久化

内存淘汰策略

过期键删除策略


JVM

基础概念

1.我们写出的java程序不能被机器直接识别执行,需要经过文件编译,编译顺序如下:

(1)通过JDK的javac,将java文件编译为字节码(.class)文件

(2)通过JVM将字节码文件编译成能被电脑识别的文件

2.java语言的跨平台性

        java文件会先被编译成字节码文件,然后再由不同机器上不同的JAVA虚拟机(JVM)编译为可供当前机器运行的文件。

        即字节码文件并不直接在机器上执行,而是由机器上的JAVA虚拟机编译后再执行,而每个系统平台都有自己的JVM,故能实现java的跨平台。

3.JDK(JAVA开发工具包)是java核心,包括了JRE(java运行环境)与jdk工具包(例如:java,javac,javadoc等),JVM则是java虚拟机

4.JVM子系统分为类加载器,运行时内存区与执行引擎

类加载器

        类加载器分为启动类加载器,扩展类加载器,系统类加载器,自定义加载器。

启动类加载器

        BootstrapClassLoader,用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(C++实现),无法被java程序直接引用

扩展类加载器

        ExtensionsClassLoader,用来加载 Java 的扩展库,由Java语言实现的,是Launcher的静态内部类。Java 虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类

系统类加载器

        AppClassLoader,负责在JVM启动时根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器,一般用户自定义的类加载器都以此类加载器作为父加载器,由Java语言实现。

【上述三个类加载器是JDK默认的三个类加载器,按照顺序前者是后者的父加载器,需要注意的是这里的父类并不是JAVA中的父子继承关系,而是由类加载器中有一个parentClassLoader字段设置的值指明的父加载器。】

自定义加载器

        通过继承 java.lang.ClassLoader类的方式实现

类装载执行顺序

        执行分为五个步骤:加载,验证,准备,解析,初始化。(也可分为加载,连接,初始化三步,其中连接包括验证,准备,解析,连接状态负责将类的二进制数据整合到JRE中)

加载:根据查找路径找到相应的 class 文件然后导入,会生成java.lang.Class对象。

验证:检查加载的 class 文件的正确性;

准备:给类中的静态变量分配内存空间;

解析:虚拟机将常量池中的符号引用替换成直接引用的过程。

         (符号引用可以理解为一个标示,而在直接引用直接指向内存中的地址)

初始化:对静态变量和静态代码块执行初始化工作,即执行执行类构造器<clinit>() 方法。

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

双亲委派机制的好处:

1.避免重复加载

2.防止核心API库被篡改

运行时内存区

运行时内存区分为数据区与指令区

数据区

数据区为线程共享数据区,可分为方法区和堆。

方法区用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

堆是JVM所管理的内存中最大的一块,在虚拟机启动时创建,用于存储对象实例。

指令区

数据区为线程独立数据区,可分为程序计数器,本地方法栈,虚拟机栈。

程序计数器用于保存当前线程所正在执行的字节码指令的地址/行号,用来保证每条线程切换后能回到当前正在执行的地方,所以是线程独立数据区。

虚拟机栈中包含多个帧栈,每个帧栈中存储局部变量表、操作数栈、动态链接、方法出口等信息,帧栈在方法被执行时创建(注意java虚拟机是线程私有,生命周期和线程相同)。

本地栈和虚拟机栈大体相同,区别为虚拟机栈用来管理java方法,本地栈用来管理本地方法,在方法前会带有native关键字。

垃圾收集器

JVM中的垃圾回收线程,在正常情况下是不会执行的(线程优先级最低),在虚拟机空闲或者当前堆内存不足时会触发执行,进行垃圾对象的回收。不需要程序员手动操作,由JVM自动执行。

判断对象是否存活

GC判断对象是否存活一般有两种方法:

1.引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
2.可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

垃圾回收算法

一般有四种算法:

1.标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
2.复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
3.标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
4.分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。(堆中一般1/3为新生代,2/3为老年代)

(下图是学习时自己列出的思维导图,有点模糊,用网页版可以大概看清)
watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZSBydXNo,size_20,color_FFFFFF,t_70,g_se,x_16

多线程

线程状态

线程中最重要的线程状态及其转换,如下图所示:

6ca41c2798624c3cb124f0b7961b9e5e.jpeg

 有几点需要注意:1.线程状态分为五种:创建,就绪,运行,阻塞,死亡

顺便一提进程的状态也是这五种,进程和线程的具体区别如下:

进程:资源分配最小单位,包含至少一个线程,每个进程都有独立的代码和数据空间,进程间的切换有较大开销

线程:CPU调度的最小单位,同一类线程共享代码和数据空间,线程有独立的运行栈和程序计数器,线程间的切换开销小

2.start方法是将线程置于就绪态而非运行态

3.每个程序运行至少启动两个线程,即main线程与垃圾收集线程

4.创建一个线程有四种方式:继承thread类,实现runable接口,实现callable接口,通过线程池创建

5.线程阻塞状态分为三种:等待阻塞,同步阻塞,其他阻塞

等待阻塞:wait()方法执行,用notify() 方法或 notifyAll()方法唤醒,属于Object类

同步阻塞:线程在获取对象的同步锁时如果该同步锁正在被其他线程占用,则进入同步阻塞

其他阻塞:例如sleep()方法,join()方法,I/O请求时(注意sleep(),join(),yield()属于Thread类)

其中需要注意的是wait方法会释放锁,sleep方法不会释放锁,而I/O操作会释放,join方法会释放Thread锁而不会释放Object锁(底层调用的是wait方法)

线程创建

继承thread类

    public class ThreadThread extends Thread {
        private String name;
        int i = 5;

        public ThreadThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始");
            while (i > 0){
                System.out.println(name + "运行  :  " + i);
                i--;
                try {
                    Thread.sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }
    }

实现runable接口

    public class ThreadRunable implements Runnable {
        private String name;
        int i = 5;

        public ThreadRunable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            while (i > 0){
                System.out.println(name + "运行  :  " + i);
                i--;
                try {
                    Thread.sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

继承Thread类与实现runable接口区别在于

1.后者更适合多个线程处理同一个资源

2.后者避免了JAVA只能单继承的限制

3.线程池只能放实现Runable或Callable的线程,不能放继承Thread类的线程

另外callable和runable的区别在于call()允许返回值,能抛出异常,run()则不可以

关于第一点的代码实现:

继承Thread类

    public void runanle() throws InterruptedException {
        Thread A = new ThreadThread("A");
        Thread B = new ThreadThread("B");
        A.start();
        B.start();
    }

运行结果

Thread-8开始
A运行  :  5
Thread-9开始
A运行  :  4
A运行  :  3
A运行  :  2
A运行  :  1
B运行  :  5
Thread-8结束
B运行  :  4
B运行  :  3
B运行  :  2
B运行  :  1
Thread-9结束

实现runable接口

    public void runanle() throws InterruptedException {
        ThreadRunable threadRunable = new ThreadRunable("test");
        new Thread(threadRunable,"A").start();
        new Thread(threadRunable,"B").start();
    }

运行结果

test运行  :  5
test运行  :  4
test运行  :  3
test运行  :  2
test运行  :  3
test运行  :  1

线程方法

1.join():等待该线程结束

e.g.

    public void runanle() throws InterruptedException {
        Thread A = new ThreadThread("A");
        Thread B = new ThreadThread("B");
        A.start();
        A.join();
        B.start();
    }

运行结果

Thread-8开始
A运行  :  5
A运行  :  4
A运行  :  3
A运行  :  2
A运行  :  1
Thread-8结束
Thread-9开始
B运行  :  5
B运行  :  4
B运行  :  3
B运行  :  2
B运行  :  1
Thread-9结束

e.g.2

    public void runanle() throws InterruptedException {
        Thread A = new ThreadThread("A");
        Thread B = new ThreadThread("B");
        A.start();
        B.start();
        B.join();
    }

运行结果

Thread-8开始
A运行  :  5
Thread-9开始
B运行  :  5
A运行  :  4
B运行  :  4
B运行  :  3
B运行  :  2
B运行  :  1
Thread-9结束
A运行  :  3
A运行  :  2
A运行  :  1
Thread-8结束

2.yield():让当前线程回到就绪态,需要注意的是该线程可以再次被线程调度程序选中运行

3.setPriority():更改线程优先级

        A.setPriority(Thread.MIN_PRIORITY);//1
        A.setPriority(Thread.NORM_PRIORITY);//5
        A.setPriority(Thread.MAX_PRIORITY);//10

4.interrupt():使线程在无限等待时能抛出,从而结束

e.g.

    public void runanle() throws InterruptedException {
        Thread A = new ThreadThread("A");
        A.start();
        A.interrupt();
    }

运行结果

Thread-8开始
A运行  :  5
A运行  :  4
A运行  :  3
A运行  :  2
A运行  :  1
Thread-8结束
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.csdn.controller.thread$ThreadThread.run(thread.java:46)

其他常见方法:
isAlive(): 判断一个线程是否存活。 
activeCount(): 程序中活跃的线程数。 
enumerate(): 枚举程序中的线程。 
currentThread(): 得到当前线程。 
isDaemon(): 一个线程是否为守护线程。 
setDaemon(): 设置一个线程为守护线程。
setName(): 为线程设置一个名称。 

线程分类

主线程:main()线程
当前线程:当前线程
后台线程:即守护线程,为其他线程提供服务的线程,也称为守护线程。(比如GC线程)

“用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束”
前台线程:是指接受后台线程服务的线程

synchronized

1.synchronized即Java中的同步锁,可以修饰的对象有代码块,方法,类。

2.synchronized的作用通俗讲即是:当一个线程访问被synchronized修饰的代码时,其他想要访问这段代码的线程将被阻塞。

3.在使用中synchronized一般与wait(),notify()方法一起使用,即wait(),notify()方法必须存在于synchronized修饰的对象中。

4.synchronized不可继承:这里是指子类重写父类被synchronized修饰的方法时必须加上synchronized关键字否则synchronized不生效。

synchronized实例:多线程实现循环打印ABC

线程:

    public class SynchronizedThread implements Runnable {

        private String name;
        private Object prev;
        private Object self;

        private SynchronizedThread(String name, Object prev, Object self) {
            this.name = name;
            this.prev = prev;
            this.self = self;
        }

        @Override
        public void run() {
            int count = 10;
            while (count > 0) {
                synchronized (prev) {
                    synchronized (self) {
                        System.out.print(name);
                        count--;

                        self.notify();
                    }
                    try {
                        prev.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }
    }

调用:

    public void runanle() throws InterruptedException {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        SynchronizedThread pa = new SynchronizedThread("A", c, a);
        SynchronizedThread pb = new SynchronizedThread("B", a, b);
        SynchronizedThread pc = new SynchronizedThread("C", b, c);

        new Thread(pa).start();
        Thread.sleep(100);
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
        Thread.sleep(100);
    }
}

结果:

ABCABCABCABCABCABCABCABCABCABC

线程池

优势:

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

线程池工作流程

cdb17fcb83fc49689c964a2a6fa23665.png

创建线程池

1.通过ThreadPoolExecutor创建,构造方法如下:

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                              int maximumPoolSize,//最大线程数
                              long keepAliveTime, //最大空闲时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列
                              ThreadFactory threadFactory,//线程工厂
                              RejectedExecutionHandler handler//饱和处理机制
) 

构造函数有四种:3fe0912d263946efa20ad030bde08487.png 

其中线程池的拒绝策略分为四种,即:

1 AbortPolicy(默认) : 丢弃任务并抛出RejectedExecutionException异常
2 CallerRunsPolicy: 由调用线程处理该任务
3 DiscardPolicy: 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的方式
4 DiscardOldestPolicy: 丢弃队列中最早的未被处理任务,然后重新尝试执行任务 

2.通过Executors静态类的方法创建,其中包含方法如下:

1 newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程在工作。
2 newFixedThreadPool :创建一个固定大小的线程池,每次提交一个任务就会创建一个线程,直到线程达到线程池的大小
3 newCachedThreadPool:创建一个可以缓存的线程池,如果线程池的大小超过了处理任务的线程,那么就会回收部分空闲线程
4 newScheduleThreadPool:创建一个大小无线的线程,线程池可以定时的以及周期的执行任务

线程池具体实现举例

这里使用经典的淘宝秒杀系统举例,例如:现在库存中有10件商品可供出售,有20个客户抢购,使用线程池实现这一例子。

任务类:

    //任务类
    public static class ThreadTask implements Runnable {
        private static int count = 10;
        private String name;

        public ThreadTask(String name) {
            this.name = name;
        }

        //线程具体实现
        public void run() {
            synchronized (ThreadTask.class) {
                if (count > 0) {
                    System.out.println(name + "使用" + Thread.currentThread().getName() + "秒杀成功,剩余" + count--);
                } else {
                    System.out.println(name + "使用" + Thread.currentThread().getName() + "失败,剩余" + count);
                }
            }
        }
    }

线程池实现分别使用上述的两种方式实现:

1.ThreadPoolExecutor实现

    //线程池
    @RequestMapping("/runanle")
    public void Pool() {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(15));
        for (int i = 0; i < 20; i++) {
            ThreadTask threadTask = new ThreadTask("客户" + i);
            pool.submit(threadTask);
        }
        pool.shutdown();
    }

结果:

客户0使用pool-1-thread-1秒杀成功,剩余10
客户3使用pool-1-thread-4秒杀成功,剩余9
客户1使用pool-1-thread-2秒杀成功,剩余8
客户2使用pool-1-thread-3秒杀成功,剩余7
客户4使用pool-1-thread-5秒杀成功,剩余6
客户5使用pool-1-thread-6秒杀成功,剩余5
客户6使用pool-1-thread-7秒杀成功,剩余4
客户7使用pool-1-thread-8秒杀成功,剩余3
客户8使用pool-1-thread-9秒杀成功,剩余2
客户9使用pool-1-thread-10秒杀成功,剩余1
客户10使用pool-1-thread-11失败,剩余0
客户11使用pool-1-thread-12失败,剩余0
客户12使用pool-1-thread-13失败,剩余0
客户13使用pool-1-thread-14失败,剩余0
客户14使用pool-1-thread-15失败,剩余0
客户15使用pool-1-thread-16失败,剩余0
客户16使用pool-1-thread-17失败,剩余0
客户17使用pool-1-thread-18失败,剩余0
客户18使用pool-1-thread-19失败,剩余0
客户19使用pool-1-thread-20失败,剩余0

2.使用Executors的newFixedThreadPool方法实现

    //线程池
    @RequestMapping("/Exrunanle")
    public void ExPool() {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            ThreadTask threadTask = new ThreadTask("客户" + i);
            pool.submit(threadTask);
        }
        pool.shutdown();
    }

结果:

客户0使用pool-1-thread-1秒杀成功,剩余10
客户2使用pool-1-thread-3秒杀成功,剩余9
客户1使用pool-1-thread-2秒杀成功,剩余8
客户3使用pool-1-thread-4秒杀成功,剩余7
客户4使用pool-1-thread-5秒杀成功,剩余6
客户5使用pool-1-thread-6秒杀成功,剩余5
客户6使用pool-1-thread-7秒杀成功,剩余4
客户7使用pool-1-thread-8秒杀成功,剩余3
客户8使用pool-1-thread-9秒杀成功,剩余2
客户9使用pool-1-thread-10秒杀成功,剩余1
客户10使用pool-1-thread-11失败,剩余0
客户11使用pool-1-thread-12失败,剩余0
客户12使用pool-1-thread-13失败,剩余0
客户13使用pool-1-thread-14失败,剩余0
客户14使用pool-1-thread-15失败,剩余0
客户15使用pool-1-thread-16失败,剩余0
客户16使用pool-1-thread-17失败,剩余0
客户17使用pool-1-thread-18失败,剩余0
客户18使用pool-1-thread-19失败,剩余0
客户19使用pool-1-thread-20失败,剩余0

REDIS

REDIS是使用C语言编写的,开源的高性能非关系型(NOSQL)key-value键值对数据库。

快速原因

1.完全基于内存

2.数据结构简单

3.使用多路 I/O 复用模型,非阻塞 IO

4.采用单线程,避免了多线程中切换,锁,死锁,竞争等一系列可能造成的性能消耗的行为

5.使用底层模型不同,Redis 直接构建了 VM 机制,不同于其他客户端之间通信的应用协议(一般的系统调用系统函数的话,会浪费一定的时间去移动和请求)

数据类型

Redis键的类型只能为字符串,值支持五种数据类型:

1.STRING        字符串,整数,浮点数

2.LIST        列表

3.SET        无序集合

4.HASH        包括键值对的无序散列表

5.ZSET        有序集合

优缺点

优点

1.读写性能优异
2.支持数据持久化
3.支持事务
4.数据结构丰富
5.支持主从复制

缺点

1.数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上
2.Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复
3.主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性
54.Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂

高性能高并发

高性能:用户第一次访问数据从硬盘上读取,会比较慢。然后可以将用户访问的这些数据存储到缓存上,之后的操作都是直接操作内存,速度会非常快。

高并发:如果用户操作请求很大,可以考虑将一部分数据转移至缓存,直接操作缓存能承受的请求量要远大于直接访问数据库的

持久化

内存淘汰策略

过期键删除策略

(下图是学习时自己列出的思维导图,有点模糊,用网页版可以大概看清)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZSBydXNo,size_20,color_FFFFFF,t_70,g_se,x_16

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值