汇总大厂-校招/社招 Java面试题

如需下载整套资料。链接放在最后了啦!

String对象中的replace和replaceAll的区别?

replace方法:支持字符和字符串的替换。

public String replace(char oldChar, char newChar)
 
public String replace(CharSequence target, CharSequence replacement)

replaceAll方法:基于正则表达式的字符串替换。

public String replaceAll(String regex, String replacement)

测试代码:

String str = "Hello Java. Java is a language.";
System.out.println(str.replace("Java.", "c++"));//打印 Hello c++ Java is a language.
System.out.println(str.replaceAll("Java.", "c++"));//打印 Hello c++ c++is a language.

打印结果:

Hello c++ Java is a language.
Hello c++ c++is a language.

​​​​​​​

Java接口方法的修饰符可以是()

答案:C

分析:

  • 接口中的访问权限修饰符只能是 public 或 default
  • 接口中的方法必须要实现类实现,所以不能使用 final
  • 接口中所有的方法默认都是 abstract,通常 abstract 省略不写

什么是Linux?

  • Linux 是一种基于 UNIX 的操作系统,最初是由 Linus Torvalds 引入的
  • 它基于 Linux 内核,可以运行在由 Intel,MIPS,HP,IBM,SPARC 和 Motorola 制造的不同硬件平台上
  • Linux 另一个受欢迎的元素是它的吉祥物,一个名叫 Tux 的企鹅形象

java中操作字符串都有哪些类?它们之间有什么区别?

Java 中,常用的对字符串操作的类有 String、StringBuffer、StringBuilder

  • String : final 修饰,String 类的方法都是返回 new String。即对 String 对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
  • StringBuffer : 对字符串的操作的方法都加了synchronized,保证线程安全。
  • StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以 new StringBuilder 对象,调用 StringBuilder 对象的 append()、replace()、delete() 等方法修改字符串。

Java 程序中怎么保证多线程的运行安全?

线程的安全性问题体现在:

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

导致原因:

  • 缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

如何防止表单重复提交?

网络延迟时重复点击提交按钮,会发生重复提交表单的问题。

解决办法:

  • 数据库主键唯一
  • 提交成功后页面重定向
  • 按钮提交后隐藏或不可再点击
  • 后台生成页面 token,页面表单提交携带 token,后台进行校验

为什么Redis所有数据放到内存中?

  • Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘,所以 Redis 具有高速读写和数据持久化的特征
  • 如果程序直接与磁盘交互,磁盘 IO 速度会严重影响 Redis 的性能
  • 内存的硬件成本降低,使得 Redis 更受欢迎

continue语句的作用

  • 结束本次循环,循环体后续的语句不执行
  • 继续进行循环条件的判断,进行下一次循环体语句的执行

List、Set、Sorted Set最多能存放多少元素?

官方 FAQ 上说明,理论上 List、Set、Sorted Set 可以放 2 的 32 次方个元素

谈谈双亲委派模型

Parents Delegation Model,这里的 Parents 翻译成双亲有点不妥,类加载向上传递的过程中只有单亲;parents 更多的是多级向上的意思。

除了顶层的启动类加载器,其他的类加载器在加载之前,都会委派给它的父加载器进行加载,一层层向上传递,直到所有父类加载器都无法加载,自己才会加载该类。

双亲委派模型,更好地解决了各个类加载器协作时基础类的一致性问题,避免类的重复加载;防止核心API库被随意篡改。

JDK 9 之前

  • 启动类加载器(Bootstrp ClassLoader),加载 /lib/rt.jar、-Xbootclasspath
  • 扩展类加载器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加载 /lib/ext、java.ext.dirs
  • 应用程序类加载器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加载 CLASSPTH、-classpath、-cp、Manifest
  • 自定义类加载器

JDK 9 开始 Extension ClassLoader 被 Platform ClassLoader 取代,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader

类加载代码逻辑

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  // 首先,检查请求的类是否已经被加载过了
  Class c = findLoadedClass(name);
  if (c == null) {
    try {
      if (parent != null) {
        c = parent.loadClass(name, false);
      } else {
        c = findBootstrapClassOrNull(name);
      }
    } catch (ClassNotFoundException e) {
      // 如果父类加载器抛出ClassNotFoundException
      // 说明父类加载器无法完成加载请求
    }
    if (c == null) {
      // 在父类加载器无法加载时
      // 再调用本身的findClass方法来进行类加载
      c = findClass(name);
    }
  }
  if (resolve) {
    resolveClass(c);
  }
  return c;
}

Dubbo的安全调用

  • Dubbo 和 Zookeeper 基本都是部署在内网,不对外网开放
  • Zookeeper 的注册可以添加用户权限认证
  • Dubbo 通过 Token 令牌防止用户绕过注册中心直连
  • 在注册中心上管理授权
  • 增加对接口参数校验
  • 提供IP、服务黑白名单,来控制服务所允许的调用方

Java中有几种基本数据类型?它们分别占多大字节?

基本数据类型

  • byte:1个字节,8位
  • short:2个字节,16位
  • int:4个字节,32位
  • long:8个字节,64位
  • float:4个字节,32位
  • double:8个字节,64位
  • boolean:官方文档未明确定义,依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1位,但是实际中会考虑计算机高效存储因素
  • char:2个字节,16位

补充说明:字节的英文是 byte,位的英文是 bit

详细说明可以参考:

可重入锁与不可重入锁之间的区别与性能差异?

可重入锁

指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。

为了避免死锁的发生,JDK 中基本都是可重入锁。

下面我们来测试一下 synchronized 和 java.util.concurrent.lock.ReentrantLock 锁的可重入性

  • 测试 synchronized 加锁 可重入性

    package constxiong.concurrency.a019;

    /**

    • 测试 synchronized 加锁 可重入性

    • @author ConstXiong

    • @date 2019-09-20 15:55:27
      */
      public class TestSynchronizedReentrant {

      public static void main(String[] args) {
      new Thread(new SynchronizedReentrant()).start();
      }

    }

    class SynchronizedReentrant implements Runnable {

    private final Object obj = new Object();
    
    /**
     * 方法1,调用方法2
     */
    public void method1() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + " method1()");
            method2();
        }
    }
    
    /**
     * 方法2,打印前获取 obj 锁
     * 如果同一线程,锁不可重入的话,method2 需要等待 method1 释放 obj 锁
     */
    public void method2() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + " method2()");
        }
    }
    
    @Override
    public void run() {
        //线程启动 执行方法1
        method1();
    }
    

    }

打印结果:

Thread-0 method1()
Thread-0 method2()
  • 测试 ReentrantLock 的可重入性

    package constxiong.concurrency.a019;

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    /**

    • 测试 ReentrantLock 的可重入性

    • @author ConstXiong

    • @date 2019-09-20 16:24:52
      */
      public class TestLockReentrant {

      public static void main(String[] args) {
      new Thread(new LockReentrant()).start();
      }

    }

    class LockReentrant implements Runnable {

    private final Lock lock = new ReentrantLock();
    
    /**
     * 方法1,调用方法2
     */
    public void method1() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " method1()");
            method2();
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 方法2,打印前获取 obj 锁
     * 如果同一线程,锁不可重入的话,method2 需要等待 method1 释放 obj 锁
     */
    public void method2() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " method2()");
        } finally {
            lock.unlock();
        }
    }
    
    @Override
    public void run() {
        //线程启动 执行方法1
        method1();
    }
    

    }

打印结果:

Thread-0 method1()
Thread-0 method2()

测试不可重入锁

我在 JDK 中没找到不可重入锁,所以考虑自己实现一下。两种方式:通过 synchronized wait notify 实现;通过 CAS + 自旋方式实现

1) synchronized wait notify 方式实现

package constxiong.concurrency.a019;


/**
 * 不可重入锁,通过 synchronized wait notify 实现
 * @author ConstXiong
 * @date 2019-09-20 16:53:34
 */
public class NonReentrantLockByWait {

    //是否被锁
    private volatile boolean locked = false;
    
    //加锁
    public synchronized void lock() {
        //当某个线程获取锁成功,其他线程进入等待状态
        while (locked) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //加锁成功,locked 设置为 true
        locked = true;
    }
    
    //释放锁
    public synchronized void unlock() {
        locked = false;
        notify();
    }

}

2) 通过 CAS + 自旋 方式实现

package constxiong.concurrency.a019;

import java.util.concurrent.atomic.AtomicReference;


/**
 * 不可重入锁,通过 CAS + 自旋 实现
 * @author ConstXiong
 * @date 2019-09-20 16:53:34
 */
public class NonReentrantLockByCAS {
    
    private AtomicReference<Thread> lockedThread = new AtomicReference<Thread>();

    public void lock() {
        Thread t = Thread.currentThread();
        //当 lockedThread 持有引用变量为 null 时,设置 lockedThread 持有引用为 当前线程变量
        while (!lockedThread.compareAndSet(null, t)) {
            //自旋,空循环,等到锁被释放
        }
    }
    
    public void unlock() {
        //如果是本线程锁定的,可以成功释放锁
        lockedThread.compareAndSet(Thread.currentThread(), null);
    }
}

测试类

package constxiong.concurrency.a019;

/**
 * 测试不可重入锁
 * @author ConstXiong
 * @date 2019-09-20 18:08:55
 */
public class TestLockNonReentrant{

    public static void main(String[] args) {
        new Thread(new LockNonReentrant()).start();
    }

}


class LockNonReentrant implements Runnable {
    
//    private final NonReentrantLockByWait lock = new NonReentrantLockByWait();
    private final NonReentrantLockByCAS lock = new NonReentrantLockByCAS();
    
    /**
     * 方法1,调用方法2
     */
    public void method1() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " method1()");
            method2();
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 方法2,打印前获取 obj 锁
     * 如果同一线程,锁不可重入的话,method2 需要等待 method1 释放 obj 锁
     */
    public void method2() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " method2()");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        //线程启动 执行方法1
        method1();
    }
}

测试结果,都是在 method1,调用 method2 的时候,导致了死锁,线程一直等待或者自旋下去。

image_20190920181445.png

参考:

输入流和输出流的区别

  • 输入输出的方向是针对程序而言,向程序中读入数据,就是输入流;从程序中向外写出数据,就是输出流
  • 从磁盘、网络、键盘读到内存,就是输入流,用 InputStream 或 Reader
  • 写到磁盘、网络、屏幕,都是输出流,用 OuputStream 或 Writer

Javascript正则表达式使用方式有哪些?

一、字面量的形式

var expression = /pattern/flags;
flags参数
i:忽略大小写
g:全局匹配
gi:全局匹配+忽略大小写

二、使用 RegExp 对象

var reg = new RegExp(expression, destStr);
RegExp.$1 是 RegExp 的一个属性,指的是与正则表达式匹配的第一个子匹配(以括号为标志)字符串。以此类推,RegExp.2, RegExp.3, ..RegExp.$99总共可以有99个匹配
  • test()方法:测试正则是否匹配字符串

    正则.test(字符串)
    如:
    /^\d/.test(‘1a’)
    new RegExp(“1a”, ‘i’).test(‘1a1a’)

  • search()方法:在字符串搜索符合正则的内容,搜索到就返回出现的位置,搜索失败就返回 -1

    字符串.search(正则)
    如:
    ‘1a1a’.search(/^\d/); //返回0

  • match()方法:

    stringObject.match(searchvalue)
    stringObject.match(regexp)
    如:
    ‘1a1a’.match(/^\d/); //返回[“1”, index: 0, input: “1a1a”, groups: undefined]

  • replace()方法:

    replace([RegExp|String],[String|Function])
    如:
    ‘1a1a’.replace(/^\d/, 2); //返回"2a1a"

  • exec()方法:捕获组,仅 RegExp 对象可用

    如:
    ar r = new RegExp(“(1a)”, ‘i’); r.exec(‘1a1a’); RegExp.$1;

如何避免sql注入?

1、概念

SQL 注入(SQL Injection),是 Web 开发中最常见的一种安全漏洞。

可以用它来从数据库获取敏感信息、利用数据库的特性执行添加用户、导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

**2、造成 SQL 注入的原因 **

程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 脚本,程序在接收后错误的将攻击者的输入作为 SQL 语句的一部分执行,导致原始的查询逻辑被改变,执行了攻击者精心构造的恶意 SQL 语句。

如 从用户表根据用户名 ConstXiong 和密码 123 查用户信息

select * from user where username = 'ConstXiong' and password = '123'

恶意修改用户名参数 ConstXiong -> ConstXiong’ or 1=1 –

select * from user where username = 'ConstXiong' or 1=1 --' and password = '123'

![Image 1][]![Image 1][]

SQL 中 – 是注释标记,如果上面这个 SQL 被执行,就可以让攻击者在不知道任何用户名和密码的情况下成功登录。

3、预防 SQL 注入攻击的方法

  • 严格限制 Web 应用的数据库的操作权限,给连接数据库的用户提供满足需要的最低权限,最大限度的减少注入攻击对数据库的危害
  • 校验参数的数据格式是否合法(可以使用正则或特殊字符的判断)
  • 对进入数据库的特殊字符进行转义处理,或编码转换
  • 预编译 SQL(Java 中使用 PreparedStatement),参数化查询方式,避免 SQL 拼接
  • 发布前,利用工具进行 SQL 注入检测
  • 报错信息不要包含 SQL 信息输出到 Web 页面

[Image 1]:

什么是触发器,MySQL都有哪些触发器?

触发器是指一段代码,当触发某个事件时,自动执行这些代码

MySQL 数据库中有六种触发器:

  • Before Insert
  • After Insert
  • Before Update
  • After Update
  • Before Delete
  • After Delete

使用场景:

  • 可以通过数据库中的相关表实现级联更改
  • 实时监控表中字段的更改做出相应处理

注意:滥用会造成数据库及应用程序的维护困难

并发编程的缺点?

  1. Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间。
  2. 容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致。
  3. 多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重。
  4. 对编程人员的技术要求较高,编写出正确的并发程序并不容易。
  5. 并发程序易出问题,且难调试和排查;问题常常诡异地出现,又诡异地消失。

jsp的4种作用域?

  • page (当前页面作用域):相当于 Java 关键字中 this。在这个作用域中存放的属性值,只能在当前页面中取出。对应 PageContext 类
  • request (请求作用域):范围是从请求创建到请求消亡这段时间,一个请求可以涉及的多个页面。jsp:forward 和 jsp:include 跳转到其他页面,也在作用域范围。对应 ServletRequest 类
  • session (会话作用域):范围是一段客户端和服务端持续连接的时间,用户在会话有效期内多次请求所涉及的页面。session 会话器,服务端为第一次建立连接的客户端分配一段有效期内的属性内存空间。对应 HttpSession 类
  • application (全局作用域):范围是服务端Web应用启动到停止,整个Web应用中所有请求所涉及的页面。当服务器开启时,会创建一个公共内存区域,任何客户端都可以在这个公共内存区域存取值。对应 ServletContext 类

什么是 happens-before 原则?

Java 中 happens-before 原则,是在 JSR-133 中提出的。

原文摘要:

• Each action in a thread happens-before every subsequent action in that thread.
• An unlock on a monitor happens-before every subsequent lock on that monitor.
• A write to a volatile field happens-before every subsequent read of that volatile.
• A call to start() on a thread happens-before any actions in the started thread.
• All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
• If an action a happens-before an action b, and b happens before an action c, then a happensbefore c.
• the completion of an object’s constructor happens-before the execution of its finalize method (in the formal sense of happens-before).

翻译过来加上自己的理解就是:

  • 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(这里涉及到 CPU 指令重排,所以需要加入内存屏障保证有序性)
  • 管程锁定规则:对一个锁的解锁操作,先行发生于后续对这个锁的加锁操作。这里必须强调的是同一个锁。
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程 join() 规则:被调用 join() 方法的线程的所有操作先行发生于 join() 的返回。
  • 传递性规则:操作 a 先发生于操作 b,操作 b 先发生于操作 c,则操作 a 先发生于操作 c。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法。

MyBatis 中如何配置连接中断或执行超时?

Mybatis 的 XML 配置中,在 节点中添加子节点 ,name=defaultStatementTimeout,设置等待数据库响应的超时时间。

<settings>
  <!-- 设置超时时间,它决定数据库驱动等待数据库响应的秒数 -->
  <setting name="defaultStatementTimeout" value="25"/>
</settings>

介绍一下Redis

Redis 是一款使用 C 语言编写的高性能 key-value 数据库,开源免费,遵守 BSD 协议。

特点:

  • 性能极高,能到 100000 次/s 读写速度
  • 支持数据的持久化,对数据的更新采用Copy-on-write技术,可以异步地保存到磁盘上
  • 丰富的数据类型,String(字符串)、List(列表)、Hash(字典)、Set(集合)、Sorted Set(有序集合)
  • 原子性:Redis 的所有操作都是原子性的,多个操作通过 MULTI 和 EXEC 指令支持事务
  • 丰富的特性:key 过期、publish/subscribe、notify
  • 支持数据的备份,快速的主从复制
  • 节点集群,很容易将数据分布到多个Redis实例中

十进制100转换成八进制是多少?

100 = 1*(8*8) + 4*(8) + 4*(1)

八进制:144

Java中八进制数必须以0开头,0144

MyBatis 是如何将 sql 执行结果转换为目标对象并返回的?有哪些映射形式?

  • 方式一、 标签使用 resultType 参数,传递 Java 类,sql 中 select 的字段名保持与 Java 类属性名称一致

  • 方式二、使用 标签,定义数据库列名和对象属性名之间的映射关系

  • 方式三、使用注解 select 的字段名保持与接口方法返回的 Java 类或集合的元素类的属性名称一致

    方式一

    select * from user where id = #{id}

    方式二

    select * from user where id = #{id}




    方式三
    @Select(“select * from user”)
    List selectAllUsers();

根据解析得到 ResultMap 结合 sql 执行结果,通过反射创建对象,根据映射关系反射填充返回对象的属性

源码体现在 DefaultResultSetHandler 的 handleResultSets 方法

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
        ResultMap resultMap = resultMaps.get(resultSetCount);
        handleResultSet(rsw, resultMap, multipleResults, null);
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
        while (rsw != null && resultSetCount <resultSets.length) {
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
                String nestedResultMapId = parentMapping.getNestedResultMapId();
                ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
                handleResultSet(rsw, resultMap, null, parentMapping);
            }
            rsw = getNextResultSet(stmt);
            cleanUpAfterHandlingResultSet();
            resultSetCount++;
        }
    }

    return collapseSingleResultList(multipleResults);
}

列举一些你知道的打破双亲委派机制的例子。为什么要打破?

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

举例说明数据库死锁

加锁顺序不一致可能会导致死锁:

1、事务 1 持有 id = 1 的行锁,更新 id = 2 的行数据;事务 2 持有 id = 2 的行锁,更新 id = 1的行数据

image_20200725145411.png

2、在范围查询更新时,加锁是一条记录一条记录挨个加锁的,数据行被加锁顺序不一样也会导致死锁

事务1                                        
update table set name = 'A' where id <100;

事务2
update table set name = 'B' where age > 25;

ArrayList和LinkedList的区别是什么?

  • ArrayList 基于动态数组实现的非线程安全的集合;LinkedList 基于双向链表实现的非线程安全的集合。
  • 扩容问题:ArrayList 使用数组实现,无参构造函数默认初始化长度为 10,数组扩容是会将原数组中的元素重新拷贝到新数组中,长度为原来的 1.5 倍(扩容代价高);LinkedList 不存在扩容问题,新增元素放到集合尾部,修改相应的指针节点即可。
  • LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用节点,一个指向前一个元素,一个指向下一个元素。
  • 对于随机 index 访问的 get 和 set 方法,一般 ArrayList 的速度要优于 LinkedList。因为 ArrayList 直接通过数组下标直接找到元素;LinkedList 要移动指针遍历每个元素直到找到为止。
  • 新增和删除元素,一般 LinkedList 的速度要优于 ArrayList。因为 ArrayList 在新增和删除元素时,可能扩容和复制数组;LinkedList 实例化对象需要时间外,只需要修改节点指针即可。
  • LinkedList 集合不支持高效的随机访问(RandomAccess)
  • ArrayList 的空间浪费主要体现在在list列表的结尾预留一定的容量空间;LinkedList 的空间花费则体现在它的每一个元素都需要消耗存储指针节点对象的空间。

都是非线程安全,允许存放 null

测试代码

public static void main(String[] args) {
    ArrayList<Integer> arrayList = new ArrayList<Integer>();
    LinkedList<Integer> linkedList = new LinkedList<Integer>();
    int size = 10000 * 1000;
    int index = 5000 * 1000;
    
    System.out.println("arrayList add " + size);
    addData(arrayList, size);
    System.out.println("linkedList add " +  + size);
    addData(linkedList, size);
    System.out.println();
    
    System.out.println("arrayList get " + index + " th");
    getIndex(arrayList, index);
    System.out.println("linkedList get " + index + " th");
    getIndex(linkedList, index);
    System.out.println();
    
    System.out.println("arrayList set " + index + " th");
    setIndex(arrayList, index);
    System.out.println("linkedList set " + index + " th");
    setIndex(linkedList, index);
    System.out.println();
    
    System.out.println("arrayList add " + index + " th");
    addIndex(arrayList, index);
    System.out.println("linkedList add " + index + " th");
    addIndex(linkedList, index);
    System.out.println();
    
    System.out.println("arrayList remove " + index + " th");
    removeIndex(arrayList, index);
    System.out.println("linkedList remove " + index + " th");
    removeIndex(linkedList, index);
    System.out.println();
    
    System.out.println("arrayList remove Object " + index);
    removeObject(arrayList, (Object)index);
    System.out.println("linkedList remove Object " + index);
    removeObject(linkedList, (Object)index);
    System.out.println();
    
    System.out.println("arrayList add");
    add(arrayList);
    System.out.println("linkedList add");
    add(linkedList);
    System.out.println();
    
    System.out.println("arrayList foreach");
    foreach(arrayList);
    System.out.println("linkedList foreach");
    foreach(linkedList);
    System.out.println();
    
    System.out.println("arrayList forSize");
    forSize(arrayList);
    System.out.println("linkedList forSize");
//        forSize(linkedList);
    System.out.println("cost time: ...");
    System.out.println();
    
    System.out.println("arrayList iterator");
    ite(arrayList);
    System.out.println("linkedList iterator");
    ite(linkedList);
}
    
private static void addData(List<Integer> list, int size) {
    long s1 = System.currentTimeMillis();
    for (int i = 0; i <size; i++) {
        list.add(i);
    }
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void getIndex(List<Integer> list, int index) {
    long s1 = System.currentTimeMillis();
    list.get(index);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void setIndex(List<Integer> list, int index) {
    long s1 = System.currentTimeMillis();
    list.set(index, 1024);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void addIndex(List<Integer> list, int index) {
    long s1 = System.currentTimeMillis();
    list.add(index, 1024);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void removeIndex(List<Integer> list, int index) {
    long s1 = System.currentTimeMillis();
    list.remove(index);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void removeObject(List<Integer> list, Object obj) {
    long s1 = System.currentTimeMillis();
    list.remove(obj);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void add(List<Integer> list) {
    long s1 = System.currentTimeMillis();
    list.add(1024);
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void foreach(List<Integer> list) {
    long s1 = System.currentTimeMillis();
    for (Integer i : list) {
        //do nothing
    }
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void forSize(List<Integer> list) {
    long s1 = System.currentTimeMillis();
    int size = list.size();
    for (int i = 0; i <size; i++) {
        list.get(i);
    }
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

private static void ite(List<Integer> list) {
    long s1 = System.currentTimeMillis();
    Iterator<Integer> ite = list.iterator();
    while (ite.hasNext()) {
        ite.next();
    }
    long s2 = System.currentTimeMillis();
    System.out.println("cost time: " + (s2-s1));
}

JDK1.8,win7 64位。结果

arrayList add 10000000
cost time: 3309
linkedList add 10000000
cost time: 1375
 
arrayList get 5000000 th
cost time: 0
linkedList get 5000000 th
cost time: 53
 
arrayList set 5000000 th
cost time: 0
linkedList set 5000000 th
cost time: 44
 
arrayList add 5000000 th
cost time: 3
linkedList add 5000000 th
cost time: 45
 
arrayList remove 5000000 th
cost time: 3
linkedList remove 5000000 th
cost time: 46
 
arrayList remove Object 5000000
cost time: 31
linkedList remove Object 5000000
cost time: 131
 
arrayList add
cost time: 0
linkedList add
cost time: 0
 
arrayList foreach
cost time: 30
linkedList foreach
cost time: 128
 
arrayList forSize
cost time: 5
linkedList forSize
cost time: ...
 
arrayList iterator
cost time: 6
linkedList iterator
cost time: 113

思考:

  • arrayList add 10000000 cost time: 3293;linkedList add 10000000 cost time: 1337
  • arrayList add 1000000 cost time: 22 ; linkedList add 1000000 cost time: 1011
  • 跑另外一组数据,size 设为 1000 * 1000,得出当size增加,ArrayList 的 add操作的累计时间增长更快
  • 千万别在循环中调用 LinkedList 的 get 方法,耗时会让你崩溃
  • 代码例子中,“新增和删除元素,一般 LinkedList 的速度要优于 ArrayList” 并不成立,可以思考一下原因。

源码分析参考:

常用的设计模式有哪些?

创建型

  • 工厂模式与抽象工厂模式 (Factory Pattern)(Abstract Factory Pattern)
  • 单例模式 (Singleton Pattern)
  • 建造者模式 (Builder Pattern)
  • 原型模式 (Prototype Pattern)

结构型

  • 适配器模式 (Adapter Pattern)
  • 装饰器模式 (Decorator Pattern)
  • 桥接模式 (Bridge Pattern)
  • 外观模式 (Facade Pattern)
  • 代理模式 (Proxy Pattern)
  • 过滤器模式 (Filter、Criteria Pattern)
  • 组合模式 (Composite Pattern)
  • 享元模式 (Flyweight Pattern)

行为型

  • 责任链模式(Chain of Responsibility Pattern)
  • 观察者模式(Observer Pattern)
  • 模板模式(Template Pattern)
  • 命令模式(Command Pattern)
  • 解释器模式(Interpreter Pattern)
  • 迭代器模式(Iterator Pattern)
  • 中介者模式(Mediator Pattern)
  • 策略模式(Strategy Pattern)
  • 状态模式(State Pattern)
  • 备忘录模式(Memento Pattern)
  • 空对象模式(Null Object Pattern)

详细可以参考:

关于内存回收正确的是()

答案:A

分析:

  • 内存由 JVM 负责释放
  • 程序员无法直接释放内存
  • 垃圾回收时间不确定

MyBatis 与 Hibernate 的区别

  • MyBatis 不完全是一个 ORM 框架,它需要程序员自己编写 SQL;Hibernate 可以做到无 SQL 对数据库进行操作
  • MyBatis 直接编写原生 SQL,可以严格控制 SQL 执行性能,灵活度高,快速响应需求变化;Hibernate 会根据模型配置自动生成和执行 SQL 语句,面对多变的需求,灵活度没那么高
  • MyBatis 书写 SQL 可能依赖数据库特性,导致应用程序数据库可移植性差;Hibernate 可以屏蔽掉数据库差异,数据库可移植性好
  • MyBatis 考验程序编写 SQL 的功底,编写大量 SQL,效率可能不高;Hibernate 对象关系映射能力强,可以节省很多代码,提高开发效率
  • MyBatis 没法根据模型自动初始化数据库中的表;Hibernate 是根据模型的配置生成 DDL 语句在数据库中自动初始化对应表、索引、序列等

Dubbo如何设置超时时间?

Dubbo 可以在提供端(provider) 和 消费端(consumer) 设置超时间

provider:

  • 系统向外提供的 facade 请求超时时间,默认1000 ms

  • provider 接受到请求时,会把整个处理逻辑执行完,不管你是否设置了时间;dubbo 只会在方法执行完,判断是否超时,如果超时,记一个 warn 日志

    <dubbo:provider timeout=“” >

consumer:

  • 调用外部系统接口的超时时间,默认1000 ms

  • 请求发出后,线程处于阻塞状态,线程的唤醒条件是超时和收到 provider 返回

    <dubbo:consumer timeout=“” >

provider 和 consumer 都设置了超时时间,Dubbo 会默认优先使用 consumer 的配置

官方文档:

  • 方法级优先,接口级次之,全局配置再次之
  • 如果级别一样,则消费方优先,提供方次之
  • 其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。
  • 建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置

MyBatis 的源码中的核心类有哪些?如何实现框架功能的?

  • Configuration: 配置类
  • Environment: 环境信息
  • SqlSessionFactoryBuilder: SqlSessionFactory 构造者类
  • SqlSessionFactory: SqlSession 工厂类
  • SqlSession: 执行 SQL 的一次会话
  • XMLConfigBuilder: MyBatis xml 配置文件构造者类
  • XMLMapperBuilder: Mapper xml 配置文件构造者类
  • MapperBuilderAssistant: Mapped 匹配信息构造者类,如构造添加MappedStatement
  • XMLStatementBuilder: Mapper xml 配置文件中 SQL 标签的构造者类,构造 MappedStatement
  • MappedStatement: 通过 Mapper xml 或注解,生成的 select|update|delete|insert Statement 的封装
  • MapperProxy: Mapper 接口的代理类
  • MapperMethod: Mapper 接口的方法,包含匹配的 SQL 执行种类和具体方法签名等信息
  • Executor: 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护
  • BaseExecutor: SqlSession 中的基本数据库的增删改查的执行器,涉及本地缓存与数据库查询
  • CachingExecutor: 带缓存的执行器,涉及二级缓存,未命中走本地缓存逻辑
  • ResultMap: 返回值类型匹配的类
  • SqlSource: 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回该对象
  • BoundSql: 动态生成的 SQL 语句以及相应的参数信息
  • StatementHandler: Statement 处理接口,封装 JDBC Statement 操作
  • ParameterHandler: 参数处理接口,负责对用户传递的参数转换成 JDBC Statement 所需要的参数
  • ResultSethandler: 执行结果处理接口
  • TypeHandler: 返回类型处理接口

框架如何实现,这个问题的细节就特别多了,画了一张我个人理解的图

image_20201013135322.png

Mysql的SQL语句是否区分大小写?

不区分,下面 sql 都是可以的,如:

  • SELECT VERSION();
  • select vErSION();

同样的复杂度,为什么插入排序比冒泡排序更受欢迎?

前面了解了 冒泡排序 和 插入排序,时间复杂度、空间复杂度都相同:

  • 最好情况时间复杂度:O(n)
  • 最坏情况时间复杂度:O(n2)
  • 平均情况下的时间复杂度:O(n2)
  • 空间复杂度:O(1),稳定排序算法

但为什么实际开发中插入排序使用偏多呢?

原因如下:

  • 针对同一个数组,冒泡排序和插入排序,最优情况下需要交互数据的次数是一样(即原数组的逆序度一样)
  • 每次数据交换,冒泡排序的移动数据要比插入排序复杂。冒泡排序进行了 3 次赋值,插入排序进行了 1 次赋值

代码对比:

//冒泡排序
int temp = array[j + 1];
array[j+1] = array[j];
array[j] = temp;
hasSwitch = true;//有数据交换

//插入排序
if (array[j] > value) {
    array[j+1] = array[j];
} else {
    break;
}

测试代码:

package constxiong.interview.algorithm;

import java.util.Random;

/**
 * 测试冒泡排序
 * @author ConstXiong
 * @date 2020-04-10 09:36:54
 */
public class CompareBubbleAndInsertionSort {
    
    public static void main(String[] args) {
        //生成两个一样长度的随机数组
        int length = 10000;
        int[] array_1 = generateArray(length);
        int[] array_2 = new int[length]; 
        System.arraycopy(array_1, 0, array_2, 0, length);
        print(array_1);
        print(array_2);
        
        //比较冒泡排序与插入排序的耗时
        long array_1_start = System.currentTimeMillis();
        bubbleSort(array_1);
        System.out.println("bubbleSort cost time : " + (System.currentTimeMillis() - array_1_start));
        long array_2_start = System.currentTimeMillis();
        insertionSort(array_2);
        System.out.println("insertionSort cost time : " + (System.currentTimeMillis() - array_2_start));
        
        //打印排序后的两个数组,看看结果是否正确
        print(array_1);
        print(array_2);
    }
    
    /**
     * 生成随机数组
     * @param length
     * @return
     */
    private static int[] generateArray(int length) {
        Random r = new Random();
        int[] array = new int[length];
        for (int i = 0; i <array.length; i++) {
            array[i] = r.nextInt(length);
        }
        return array;
    }
    
    /**
     * 冒泡排序
     * @param array
     */
    private static void bubbleSort(int[] array) {
        for (int i = 0; i <array.length; i++) {
            //提前退出冒泡循环的标志
            boolean hasSwitch = false;
            //因为使用 j 和 j+1 的下标进行比较,所以 j 的最大值为数组长度 - 2
            for (int j = 0; j <array.length - (i+1); j++) {
                if (array[j] > array[j + 1]) {
                    int temp = array[j + 1];
                    array[j+1] = array[j];
                    array[j] = temp;
                    hasSwitch = true;//有数据交换
                }
            }
            //没有数据交换退出循环
            if (!hasSwitch) {
                break;
            }
        }
    }
    
    /**
     * 插入排序
     */
    private static void insertionSort(int[] array) {
        for (int i = 1; i <array.length; i++) {
            int j = i - 1;
            int value = array[i];
            for (; j >= 0; j--) {
                if (array[j] > value) {
                    array[j+1] = array[j];
                } else {
                    break;
                }
            }
            array[j+1] = value;
        }
    }
    
    /**
     * 打印数组
     * @param array
     */
    private static void print(int[] array) {
        for(int i : array) {
            System.out.print(i);
        }
        System.out.println();
    }

}

打印结果:

image_20200410224517.png

随着数组长度的提升,冒泡排序比插入排序多出的耗时也随之增多。

构造方法的参数太多,如何解决?

开发中经常会遇到构造方法的参数很多,需要确认参数个数和位置;容易出现参数传错位的问题,而且 bug 不好排查。

如果使用默认构造方法,提供 public set 方法,又会把构造对象属性的修改权限放开,导致对象的属性数据安全问题。

这时候,可以使用 Builder 者模式。

package constxiong.interview.design;

/**
 * 对象人
 * @author ConstXiong
 */
public class Person {

    /**
     * id
     */
    private final int id;
    
    /**
     * 姓名
     */
    private final String name;
    
    /**
     * 性别
     */
    private final String sex;
    
    /**
     * 身高
     */
    private final Double height;
    
    /**
     * 体重
     */
    private final Double weight;
    
    public static class Builder {
        private int id;
        private String name;
        private String sex;
        private Double height;
        private Double weight;
        
        public Builder() {
        }
        
        public Builder id(int id) {
            this.id = id;
            return this;
        }
        
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        
        public Builder sex(String sex) {
            this.sex = sex;
            return this;
        }
        
        public Builder height(Double height) {
            this.height = height;
            return this;
        }
        
        public Builder weight(Double weight) {
            this.weight = weight;
            return this;
        }
        
        public     Person build() {
            return new Person(this);
        }
    }
    
    private Person(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.sex = builder.sex;
        this.height = builder.height;
        this.weight = builder.weight;
    }
    
}

创建 Person 对象的代码

Person person = new Person.Builder()
                .id(1)
                .name("ConstXiong")
                .sex("男")
                .height(1.70)
                .weight(150.0)
                .build();

Builder 模式需要注意是,Builder 类是静态内部类、类的构造方法是 private 的且参数为 Builder 对象。

Builder 模式不仅可以解决构造过程数据安全、参数过多、可读性的问题,还可以自动填充参数、为生成对象前对参数之间的关系进行合法校验等…

Builder 模式也带了新的问题:

  • 创新对象前,必须创建 Builder 对象,多一些性能开销,对性能要求极高的场景下慎用。
  • Builder 模式跟 1、2 两种方式比,代码行数更多,显得有点啰嗦。

Oracle中排序对性能的影响?

order by 只有满足如下情况才会使用索引:

  • order by中的列必须包含相同的索引并且索引顺序和排序顺序一致
  • 不能有 null 值的列

所以排序的性能并不高,尽量避免 order by

高并发下,如何安全地修改同一行数据?

  • 可以将数据加载到缓存中,利用 CAS 方式进行更新
  • 也可以将所有请求放到同一个消息队列里,异步返回,按顺序执行更新

注意:

  • 如果使用悲观锁,在并发请求量很大的情况下,会导致服务和数据连接数耗尽,系统卡死

Array和ArrayList有何区别?

Array 即数组,声明方式可以如下:

int[] array = new int[3];
int array [] = new int[3];
int[] array = {1, 2, 3};
int[] array = new int[]{1, 2, 3};

定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。

ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。

List list = new ArrayList(3);
list.add(1);
list.add("1");
list.add(new Double("1.1"));
list.add("第四个元素,已经超过初始长度");
for (Object o : list) {
    System.out.println(o);
}

怎么确保一个集合不能被修改?

使用 JDK中java.util.Collections 类,unmodifiable*** 方法赋值原集合。

当再修改集合时,会报错 java.lang.UnsupportedOperationException。从而确保自己定义的集合不被其他人修改。

public class TestCollectionUnmodify {
 
    static List<String> list = new ArrayList<String>();
    static Set<String> set = new HashSet<String>();
    static Map<String, String> map = new HashMap<String, String>();
    
    static {
        list.add("1");
        list.add("2");
        list.add("3");
        
        set.add("1");
        set.add("2");
        set.add("3");
        
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
    }
    
    public static void main(String[] args) {
        list = Collections.unmodifiableList(list);
        set = Collections.unmodifiableSet(set);
        map = Collections.unmodifiableMap(map);
        listModify();
        setModify();
        mapModify();
    }
    
    public static void listModify() {
        list.add("4");
    }
    
    public static void setModify() {
        set.add("4");
    }
    
    public static void mapModify() {
        map.put("3", "4");
    }
}

PS:guava工具类也可完成改功能

ReadWriteLock如何使用?

ReadWriteLock,读写锁。
ReentrantReadWriteLock 是 ReadWriteLock 的一种实现。

特点:

  • 包含一个 ReadLock 和 一个 WriteLock 对象
  • 读锁与读锁不互斥;读锁与写锁,写锁与写锁互斥
  • 适合对共享资源有读和写操作,写操作很少,读操作频繁的场景
  • 可以从写锁降级到读锁。获取写锁->获取读锁->释放写锁
  • 无法从读锁升级到写锁
  • 读写锁支持中断
  • 写锁支持Condition;读锁不支持Condition

示例1–根据 key 获取 value 值

private ReadWriteLock lock = new ReentrantReadWriteLock();//定义读写锁
 
//根据 key 获取 value 值
public Object getValue(String key){
    //使用读写锁的基本结构
    lock.readLock().lock();//加读锁
    Object value = null;
    try{
        value = cache.get(key);
        if(value == null){
            lock.readLock().unlock();//value值为空,释放读锁
            lock.writeLock().lock();//加写锁,写入value值
            try{
                //重新检查 value值是否已经被其他线程写入
                if(value == null){
                    value = "value";//写入数据
                }
            }finally{
                lock.writeLock().unlock();
            }
            lock.readLock().lock();
        }
    }finally{
        lock.readLock().unlock();
    }
    return value;
}

示例2–多线程环境下的读写锁使用

package constxiong.interview;
 
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * 测试可重入 读写锁
 * @author ConstXiong
 * @date 2019-06-10 11:19:42
 */
public class TestReentrantReadWriteLock {
    
    private Map<String, Object> map = new HashMap<String, Object>();
    
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    
    /**
     * 根据 key 获取 value
     * @param key
     * @return
     */
    public Object get(String key) {
        Object value = null;
        lock.readLock().lock();
        try {
            Thread.sleep(50L);
            value = map.get(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
        return value; 
    }
    
    /**
     * 设置key-value
     * @param key
     * @return
     */
    public void set(String key, Object value) {
        lock.writeLock().lock();
        try {
            Thread.sleep(50L);
            map.put(key, value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
 
    //测试5个线程读数据,5个线程写数据
    public static void main(String[] args) {
        final TestReentrantReadWriteLock test = new TestReentrantReadWriteLock();
        final String key = "lock";
        final Random r = new Random();
        for (int i = 0; i <5; i++) {
            new Thread(){
                @Override
                public void run() {
                    for (int j = 0; j <10; j++) {
                        System.out.println(Thread.currentThread().getName() + " read value=" + test.get(key));
                    }
                }
            }.start();
            
            new Thread(){
                @Override
                public void run() {
                    for (int j = 0; j <10; j++) {
                        int value = r.nextInt(1000);
                        test.set(key, value);
                        System.out.println(Thread.currentThread().getName() + " write value=" + value);
                    }
                }
            }.start();
        }
    }
    
}

linux指令-mv

移动文件、目录;修改文件名或目录名

mv test.log test1.txt  将文件 test.log 重命名为 test1.txt
mv llog1.txt log2.txt log3.txt /test3 将文件 log1.txt,log2.txt,log3.txt 移动到 /test3 目录中 
mv -i log1.txt log2.txt 将文件 log1.txt 改名为 log2.txt,如果 log2.txt 已经存在,则询问是否覆盖
mv * ../ 移动当前文件夹下的所有文件到上一级目录

Executors创建线程池有哪几种方式?

Executors如何创建线程池?

Executors 类是从 JDK 1.5 开始就新增的线程池创建的静态工厂类,它就是创建线程池的,但是很多的大厂已经不建议使用该类去创建线程池。原因在于,该类创建的很多线程池的内部使用了无界任务队列,在并发量很大的情况下会导致 JVM 抛出 OutOfMemoryError,直接让 JVM 崩溃,影响严重。

但是 Executors 类究竟是如何使用的?

1. newFixedThreadPool

创建定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程。

package constxiong.concurrency.a011;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 测试创建定长线程池
 * @author ConstXiong
 */
public class TestNewFixedThreadPool {

    public static void main(String[] args) {
        //创建工作线程数为 3 的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        //提交 6 个任务
        for (int i = 0; i <6; i++) {
            final int index = i;
            fixedThreadPool.execute(() -> {
                try {
                    //休眠 3 秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " index:" + index);
            });
        }
        
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4秒后...");
        
        //关闭线程池后,已提交的任务仍然会执行完
        fixedThreadPool.shutdown();
    }
    
}

打印结果:

pool-1-thread-2 index:1
pool-1-thread-3 index:2
pool-1-thread-1 index:0
4秒后...
pool-1-thread-1 index:4
pool-1-thread-3 index:5
pool-1-thread-2 index:3

2. newCachedThreadPool

创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制。

package constxiong.concurrency.a011;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 测试创建可缓存的线程池
 * @author ConstXiong
 */
public class TestNewCachedThreadPool {
    
    public static void main(String[] args) {
        //创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        for (int i = 0; i <6; i++) {
            final int index = i;
            cachedThreadPool.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " index:" + index);
            });
        }
        
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4秒后...");
        
        cachedThreadPool.shutdown();
        
    }
    
}

打印结果可以看出,创建的线程数与任务数相等

pool-1-thread-1 index:0
pool-1-thread-3 index:2
pool-1-thread-6 index:5
pool-1-thread-4 index:3
pool-1-thread-5 index:4
pool-1-thread-2 index:1
4秒后...

3. newScheduledThreadPool

创建定长线程池,可执行周期性的任务。

package constxiong.concurrency.a011;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 测试创建定长线程池,可执行周期性的任务
 * @author ConstXiong
 */
public class TestNewScheduledThreadPool {

    public static void main(String[] args) {
        //创建定长线程池,可执行周期性的任务
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        
        for (int i = 0; i <3; i++) {
            final int index = i;
            //scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务
            scheduledThreadPool.scheduleWithFixedDelay(() -> {
                    System.out.println(Thread.currentThread().getName() + " index:" + index);
            }, 0, 3, TimeUnit.SECONDS);
        }
        
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4秒后...");
        
        scheduledThreadPool.shutdown();

    }
}

打印结果:

pool-1-thread-1 index:0
pool-1-thread-3 index:2
pool-1-thread-2 index:1
pool-1-thread-1 index:0
pool-1-thread-2 index:1
pool-1-thread-3 index:2
4秒后...

4. newSingleThreadExecutor

创建单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行。

package constxiong.concurrency.a011;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 测试单线程的线程池
 * @author ConstXiong
 */
public class TestNewSingleThreadExecutor {
    
    public static void main(String[] args) {
        //单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        
        //提交 3 个任务
        for (int i = 0; i <3; i++) {
            final int index = i;
            singleThreadPool.execute(() -> {
                
                //执行第二个任务时,报错,测试线程池会创建新的线程执行任务三
                if (index == 1) {
                    throw new RuntimeException("线程执行出现异常");
                }
                
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " index:" + index);
            });
        }
        
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4秒后...");
        
        singleThreadPool.shutdown();
    }

}

打印结果可以看出,即使任务出现了异常,线程池还是会自动补充一个线程继续执行下面的任务

pool-1-thread-1 index:0
Exception in thread "pool-1-thread-1" 
java.lang.RuntimeException: 线程执行出现异常
    at constxiong.concurrency.a011.TestNewSingleThreadExecutor.lambda$0(TestNewSingleThreadExecutor.java:21)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
4秒后...
pool-1-thread-2 index:2

5. newSingleThreadScheduledExecutor

创建单线程可执行周期性任务的线程池。

package constxiong.concurrency.a011;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 测试单线程可执行周期性任务的线程池
 * @author ConstXiong
 */
public class TestNewSingleThreadScheduledExecutor {

    public static void main(String[] args) {
        //创建单线程可执行周期性任务的线程池
        ScheduledExecutorService singleScheduledThreadPool = Executors.newSingleThreadScheduledExecutor();
        
        //提交 3 个固定频率执行的任务
        for (int i = 0; i <3; i++) {
            final int index = i;
            //scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务
            singleScheduledThreadPool.scheduleAtFixedRate(() -> {
                System.out.println(Thread.currentThread().getName() + " index:" + index);
            }, 0, 3, TimeUnit.SECONDS);
        }
        
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("4秒后...");
        
        singleScheduledThreadPool.shutdown();
    }
    
}

打印机结果可以看出 0-2 任务都被执行了 2 个周期

pool-1-thread-1 index:0
pool-1-thread-1 index:1
pool-1-thread-1 index:2
pool-1-thread-1 index:0
pool-1-thread-1 index:1
pool-1-thread-1 index:2
4秒后...

6. newWorkStealingPool

创建任务可窃取线程池,空闲线程可以窃取其他任务队列的任务,不保证执行顺序,适合任务耗时差异较大。

package constxiong.concurrency.a011;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 测试可任务窃取线程池
 * @author ConstXiong
 */
public class TestNewWorkStealingPool {

    public static void main(String[] args) {
        //创建 4个工作线程的 任务可窃取线程池,如果不设置并行数,默认取 CPU 总核数
        ExecutorService workStealingThreadPool = Executors.newWorkStealingPool(4);
        
        for (int i = 0; i <10; i++) {
            final int index = i;
            workStealingThreadPool.execute(() -> {
                try {
                    //模拟任务执行时间为 任务编号为0 1 2 的执行时间需要 3秒;其余任务200 毫秒,导致任务时间差异较大
                    if (index <= 2) {
                        Thread.sleep(3000);
                    } else {
                        Thread.sleep(200);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " index:" + index);
            });
        }
        
        try {
            Thread.sleep(10000);//休眠 10 秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("10秒后...");
    }
    
}

打印结果可以看出,线程 ForkJoinPool-1-worker-0 把3-9的任务都执行完

ForkJoinPool-1-worker-0 index:3
ForkJoinPool-1-worker-0 index:4
ForkJoinPool-1-worker-0 index:5
ForkJoinPool-1-worker-0 index:6
ForkJoinPool-1-worker-0 index:7
ForkJoinPool-1-worker-0 index:8
ForkJoinPool-1-worker-0 index:9
ForkJoinPool-1-worker-1 index:0
ForkJoinPool-1-worker-3 index:2
ForkJoinPool-1-worker-2 index:1
10秒后...

tcp为什么要三次握手,两次不行吗?为什么?

  • 两次握手只能保证单向连接是畅通的。

Step1 A -> B : 你好,B。

Step2 A <- B : 收到。你好,A。

这样的两次握手过程, A 向 B 打招呼得到了回应,即 A 向 B 发送数据,B 是可以收到的。

但是 B 向 A 打招呼,A 还没有回应,B 没有收到 A 的反馈,无法确保 A 可以收到 B 发送的数据。

  • 只有经过第三次握手,才能确保双向都可以接收到对方的发送的 数据。

Step3 A -> B : 收到,B。

这样 B 才能确定 A 也可以收到 B 发送给 A 的数据。

参考

jQuery中get与eq方法的区别

  • get() 返回的是一个 html 对象
  • eq() 返回的是一个 jQuery 对象

MyBatis 如何支持延迟加载?现实原理是什么?

支持延迟加载的配置:
在配置文件的****标签内设置参数

  • lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态
  • aggressiveLazyLoading:开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需加载
  • lazyLoadTriggerMethods:指定对象的哪些方法触发一次延迟加载

resultMap 中配置 或

配置与测试示例

//配置文件
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
    <setting name="lazyLoadTriggerMethods" value=""/>
</settings>


//Mapper xml
<select id="selectUserWithLazyInfo" resultMap="UserWithLazyInfo">
    select * from user where id = 1
</select>

<resultMap id="UserWithLazyInfo" type="constxiong.po.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="info" javaType="constxiong.po.Info" select="constxiong.mapper.InfoMapper.selectInfoByUserId" column="id"/>
</resultMap>


//InfoMapper
public interface InfoMapper {
    @Select("select * from info where user_id = #{userId}")
    @Results(value = {@Result(column="user_id", property = "userId")})
    Info selectInfoByUserId(int userId);
}


//测试代码
System.out.println("------ selectUserWithLazyInfo ------");
User user = userMapper.selectUserWithLazyInfo();
System.out.println(user);
System.out.println(user.getInfo());


//打印 User 对象里的 Info 为空,使用 getInfo 能够查询对应的值
------ selectUserWithLazyInfo ------
User{id=1, name='ConstXiong1', mc='null', info=null, articles=null}
Info{userId=1, name=大熊}

实现原理:

支持延迟加载是通过字节码增强实现的,MyBatis 3.3 及以上默认使用了 javassist,3.3 以前使用 cglib 实现。

我本地用的 MyBatis 3.5.5,使用了 javassist 增强,核心源码如下

//DefaultResultSetHandler getRowValue 获取每条的查询数据,resultMap 中如果包含懒加载 rowValue 在 createResultObject 方法通过 javassist 代理增强
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //对象数据,通过 javassist 代理增强
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        //根据从数据库查询到的 resultSet,根据 resultMap 通过反射设置 rowValue 的值
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            //如果返回对象的属性中包含懒加载,使用 javassist 代理增强,当设置属性值时被代理到 JavassistProxyFactory 的 invoke 方法
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
}

//JavassistProxyFactory 的 invoke 方法
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
    final String methodName = method.getName();
    try {
        synchronized (lazyLoader) {
            if (WRITE_REPLACE_METHOD.equals(methodName)) {
                ...
            } else {
                if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                    if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                        lazyLoader.loadAll();
                    } else if (PropertyNamer.isSetter(methodName)) {
                        final String property = PropertyNamer.methodToProperty(methodName);
                        lazyLoader.remove(property);
                    } else if (PropertyNamer.isGetter(methodName)) {
                        //测试代码中 user.getInfo() 方法的调用,在此执行懒加载查询关联 SQL 设置 info 属性
                        final String property = PropertyNamer.methodToProperty(methodName);
                        if (lazyLoader.hasLoader(property)) {
                            lazyLoader.load(property);
                        }
                    }
                }
            }
        }
        return methodProxy.invoke(enhanced, args);
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

完整 Demo:

https://javanav.com/val/973ded541e9244aa8b3169b9fb869d60.html

什么是 IoC?Spring 如何实现的?

IoC,Inversion of Control(控制反转)。

是一种设计思想,在Java开发中,将你设计好的对象交给容器控制,而不是显示地用代码进行对象的创建。

把创建和查找依赖对象的控制权交给 IoC 容器,由 IoC 容器进行注入、组合对象。这样对象与对象之间是松耦合、便于测试、功能可复用(减少对象的创建和内存消耗),使得程序的整个体系结构可维护性、灵活性、扩展性变高。

使用 IoC 的好处:

  • 资源集中管理,实现资源的可配置和易管理
  • 降低了资源的依赖程度,即松耦合
  • 便于测试
  • 功能可复用(减少对象的创建和内存消耗)
  • 使得程序的整个体系结构可维护性、灵活性、扩展性变高

DI(Dependency Injection)依赖注入,是 IoC 容器装配、注入对象的一种方式。
通过依赖注入机制,简单的配置即可注入需要的资源,完成自身的业务逻辑,不需要关心资源的出处和具体实现。

spring 提供了三种主要的方式来配置 IoC 容器中的 bean

  • 基于 XML 文件配置
  • 基于注解配置
  • 基于注解 + java 代码显式配置

Spring 中 IoC 容器的底层实现就是 BeanFactory,BeanFactory 可以通过配置文件(xml、properties)、注解的方式加载 bean;提供根据 bean 的名称或类型类型查找 bean 的能力。功能最全的一个 BeanFactory 实现就是 DefaultListableBeanFactory。

Dubbo的核心功能?

  • Remoting:网络通信框架,提供对多种 NIO 框架抽象封装,包括多种线程模型、序列化、同步转异步和请求-响应模式的信息交换方式
  • Cluster:集群容错,提供基于接口方法的透明远程过程调用,包括多协议支持、软负载均衡、失败容错、地址路由、动态配置等集群支持
  • Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器

什么是JRE?

  • Java Runtime Environment(Java运行环境)的缩写
  • 包含 JVM 标准实现及 Java 核心类库,这些是运行 Java 程序的必要组件
  • 是 Java 程序的运行环境,并不是一个开发环境,没有包含任何开发工具(如编译器和调试器)
  • 是运行基于 Java 语言编写的程序所不可缺少的运行环境,通过它,Java 程序才能正常运行

i++和++i的作用和区别

作用:都是给变量 i 加 1,相当于 i = i + 1;

区别:

  • i++ 先运算后加 1

  • ++i 先加 1 再运算

    package constxiong.interview;

    /**

    • 测试 ++i 和 i++

    • @author ConstXiong

    • @date 2019-10-17 13:44:05
      */
      public class TestAdd {

      public static void main(String[] args) {
      int a = 3;
      int b = a++;
      System.out.println(“a=” + a);
      System.out.println(“b=” + b);

       int x = 3;
       int y = ++x;
       System.out.println("x=" + x);
       System.out.println("y=" + y);
      

      }

    }

打印

a=4
b=3
x=4
y=4

过滤器的生命周期是什么样的?有什么作用?

生命周期:

//servlet 容器启动时会创建 Filter 实例
public void init(FilterConfig filterConfig) throws ServletException;

//在每次访问目标资源时执行
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

//服务器关闭时销毁Filter对象
public void destroy();

作用:

  • 验证是否来自可信网络
  • 验证用户是否可以登录
  • 验证客户的浏览器版本
  • 对提交的数据进行重新编码
  • 过滤敏感词汇
  • 记录系统日志

invokedynamic 指令是干什么的?

Java 7 开始,新引入的字节码指令,可以实现一些动态类型语言的功能。Java 8 的 Lambda 表达式就是通过 invokedynamic 指令实现,使用方法句柄实现。

遇到过元空间溢出吗?

元空间在本地内存上,默认是没有上限的,不加限制出了问题会影响整个服务器的,所以也是比较危险的。-XX:MaxMetaspaceSize 可以指定最大值。

一般使用动态代理的框架会生成很多 Java 类,如果占用空间超出了我们的设定最大值,会发生元空间溢出。

什么是递归?递归的优缺点是什么?

递归:直接或间接调用自身算法的过程

满足使用递归的条件:

  • 子问题为同类事物,且更简单
  • 必须有个出口

优点:

  • 代码简洁
  • 符合思维习惯,容易理解

缺点:

  • 效率较低
  • 递归层次太深,耗内存且容易栈溢出一定要使用的话,最好使用缓存避免相同的计算,限制递归调用的次数

synchronized锁的升级原理是什么?

锁的级别从低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁分级别原因:

没有优化以前,synchronized 是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

锁升级的目的是为了减低锁带来的性能消耗,在 Java 6 之后优化 synchronized 为此方式。

**无锁:**没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

**偏向锁:**对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。

**轻量级锁:**轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

**重量级锁:**指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

锁状态对比:

image_20191014193357.png

synchronized 锁升级的过程:

  • 在锁对象的对象头里面有一个 threadid 字段,未访问时 threadid 为空
  • 第一次访问 jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
  • 再次访问时会先判断 threadid 是否与其线程 id 一致。如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
  • 执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁

部分内容摘自:

以下代码将输出()

答案:D

分析:
创建子类对象,先执行父类的构造方法,再执行子类的构造方法

如何配置一个servlet?

web工程中的web.xml文件:

<servlet>
 <servlet-name></servlet-name>
 <servlet-class></servlet-class>
</servlet>
<servlet-mapping>
 <servlet-name></servlet-name>
 <url-pattern></url-pattern>
</servlet-mapping>

注解:

@WebServlet(name="servlet", urlPatterns={"/*"})

说说你知道的MySQL存储引擎

InnoDB

  • 默认事务型引擎,被广泛使用的存储引擎
  • 数据存储在共享表空间,即多个表和索引都存储在一个表空间中,可通过配置文件修改
  • 主键查询的性能高于其他类型的存储引擎
  • 内部做了很多优化,如:从磁盘读取数据时会自动构建hash索引,插入数据时自动构建插入缓冲区
  • 通过一些机制和工具支持真正的热备份
  • 支持崩溃后的安全恢复
  • 支持行级锁
  • 支持外键

MyISAM

  • 拥有全文索引、压缩、空间函数
  • 不支持事务和行级锁、不支持崩溃后的安全恢复
  • 表存储在两个文件:MYD 和 MYI
  • 设计简单,某些场景下性能很好,例如获取整个表有多少条数据,性能很高

其他表引擎:Archive、Blackhole、CSV、Memory

OSI的七层模型有哪些?

开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写:OSI;简称为OSI模型)是一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。

OSI模型分为七层,自下而上为 物理层(Physical Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表达层(Presentation Layer)、应用层(Application Layer)。

image_20191015135018.png

参考:

Map的遍历方式

  • Map 的 keySet() 方法,单纯拿到所有 Key 的 Set
  • Map 的 values() 方法,单纯拿到所有值的 Collection
  • keySet() 获取到 key 的 Set,遍历 Set 根据 key 找值(不推荐使用,效率比下面的方式低,原因是多出了根据 key 找值的消耗)
  • 获取所有的键值对集合,迭代器遍历
  • 获取所有的键值对集合,for 循环遍历

Redis 集群会有写操作丢失吗?

以下情况可能导致写操作丢失:

  • 过期 key 被清理
  • 最大内存不足,导致 Redis 自动清理部分 key 以节省空间
  • 主库故障后自动重启,从库自动同步
  • 单独的主备方案,网络不稳定触发哨兵的自动切换主从节点,切换期间会有数据丢失

body中的onload事件和document.ready()有什么区别?

  • onload 表示页面包含图片等文件在内的所有元素都加载完成
  • ready 表示文档结构已经加载完成,不包含图片等非文字媒体文件

抽象类能使用final修饰吗?

不能,抽象类是被用于继承的,final修饰代表不可修改、不可继承的。

什么是死锁?

线程死锁是指由于两个或者多个线程互相持有所需要的资源,导致这些线程一直处于等待其他线程释放资源的状态,无法继续执行,如果线程都不主动释放所占有的资源,将产生死锁。

当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

产生原因:

  • 持有系统不可剥夺资源,去竞争其他已被占用的系统不可剥夺资源,形成程序僵死的竞争关系。
  • 持有资源的锁,去竞争锁已被占用的其他资源,形成程序僵死的争关系。
  • 信号量使用不当。

如线程A占有资源 1 的锁,去竞争资源 2 的锁;线程 B 占有资源 2 的锁,去竞争资源1的锁。

代码表现如下

package constxiong.concurrency.a022;

/**
 * 测试死锁
 * @author ConstXiong
 * @date 2019-09-23 19:28:23
 */
public class TestDeadLock {
     
    final static Object o1 = new Object();
    
    final static Object o2 = new Object();
    
    public static void main(String[] args) {
        //先持有 o1 的锁,再去获取 o2 的锁
        Thread t1 = new Thread() {
            
            @Override
            public void run() {
                synchronized (o1) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 o1 对象的锁");
                    try {
                        System.out.println("休眠1秒");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println("线程:" + Thread.currentThread().getName() + " 去获取 o2 对象的锁");
                    synchronized (o2) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 成功获取 o2 对象的锁");
                    }
                }
            }
            
        };
        
        //先持有 o2 的锁,再去获取 o1 的锁
        Thread t2 = new Thread() {
            
            @Override
            public void run() {
                synchronized (o2) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 o2 对象的锁");
                    try {
                        System.out.println("休眠1秒");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println("线程:" + Thread.currentThread().getName() + " 去获取 o1 对象的锁");
                    synchronized (o1) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 成功获取 o1 对象的锁");
                    }
                }
            }
            
        };
        
        
        t1.start();
        t2.start();
    }

}

测试结果,发生死锁,打印如下

线程:Thread-0 获取到 o1 对象的锁
休眠1秒
线程:Thread-1 获取到 o2 对象的锁
休眠1秒
线程:Thread-1 去获取 o1 对象的锁
线程:Thread-0 去获取 o2 对象的锁

介绍下Java中垃圾回收机制

什么样的对象会被当做垃圾回收?
当一个对象的地址没有变量去引用时,该对象就会成为垃圾对象,垃圾回收器在空闲的时候会对其进行内存清理回收

如何检验对象是否被回收?
可以重写 Object 类中的 finalize 方法,这个方法在垃圾收集器执行的时候,被收集器自动调用执行的

怎样通知垃圾收集器回收对象?
可以调用 System 类的静态方法 gc(),通知垃圾收集器去清理垃圾,但不能保证收集动作立即执行,具体的执行时间取决于垃圾收集的算法

Java.util.Map的常用实现类有哪些?

  • HashMap、LinkedHashMap
  • Hashtable
  • TreeMap
  • IdentityHashMap

抽象类必须要有抽象方法吗?

不一定。如

public abstract class TestAbstractClass {
 
	public static void notAbstractMethod() {
		System.out.println("I am not a abstract method.");
	}
	
}

写一个方法实现String类的replaceAll方法

String 的 replaceAll 是基于正则表达式实现的,借助 JDK 中正则表达式实现。

package constxiong.interview;

import java.util.regex.Pattern;

/**
 * 测试实现 replaceAll 方法
 * @author ConstXiong
 */
public class TestReplaceAll {

    public static void main(String[] args) {
        String s = "01234abcd";
        System.out.println(replaceAll(s, "[a-z]", "CX"));
    }
    
    public static String replaceAll(String s, String regex, String replacement) {
        return Pattern.compile(regex).matcher(s).replaceAll(replacement);
    }
}

下面两段代码的区别是?

第一段编译报错,s1 + 1自动升级为 int 型,int 型赋值给 s1,需要手动强转

第二段隐含类型强转,不会报错

什么是 Java 内存模型?

在了解什么是 Java 内存模型之前,先了解一下为什么要提出 Java 内存模型。

之前提到过并发编程有三大问题

  • CPU 缓存,在多核 CPU 的情况下,带来了可见性问题
  • 操作系统对当前执行线程的切换,带来了原子性问题
  • 译器指令重排优化,带来了有序性问题

为了解决并发编程的三大问题,提出了 JSR-133,新的 Java 内存模型,JDK 5 开始使用。

那么什么是 Java 内存模型呢?

现在说的 Java 内存模型,一般是指 JSR-133: Java Memory Model and Thread Specification Revision 规定的 Java 内存模型。

JSR-133 具体描述:jsr133.pdf

JSR-133 在 JCP 官网的具体描述

说明下

JSR:Java Specification Requests,Java 规范提案。

JCP:Java Community Process 是一个开放的国际组织,成立于1998年,主要由 Java 开发者以及被授权者组成,是使有兴趣的各方参与定义 Java 的特征和未来版本的正式过程。

简单总结下

  • Java 内存模型是 JVM 的一种规范
  • 定义了共享内存在多线程程序中读写操作行为的规范
  • 屏蔽了各种硬件和操作系统的访问差异,保证了 Java 程序在各种平台下对内存的访问效果一致
  • 解决并发问题采用的方式:限制处理器优化和使用内存屏障
  • 增强了三个同步原语(synchronized、volatile、final)的内存语义
  • 定义了 happens-before 规则

参考:

instanceof关键字的作用是什么?

instanceof 运算符是用来在运行时判断对象是否是指定类及其父类的一个实例。
比较的是对象,不能比较基本类型

使用如下

package constxiong.interview;

/**
 * 测试 instanceof
 * @author ConstXiong
 * @date 2019-10-23 11:05:21
 */
public class TestInstanceof {

    public static void main(String[] args) {
        A a = new A();
        AA aa = new AA();
        AAA aaa = new AAA();
        System.out.println(a instanceof A);//true
        System.out.println(a instanceof AA);//false
        System.out.println(aa instanceof AAA);//false
        System.out.println(aaa instanceof A);//true
    }
    
}

class A {
}

class AA extends A {
}

class AAA extends AA {
}

ThreadLocal有什么作用?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

ThreadLocal 使用例子:

public class TestThreadLocal {
    
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
    
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i <5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
    
}

打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5

Pipeline有什么好处?

多个指令之间没有依赖关系,可以使用 pipeline 一次性执行多个指令,减少 IO,缩减时间。

Java访问修饰符有哪些?权限的区别?

Java 语言中有四种权限访问控制符,能够控制类中成员变量和方法的可见性。

  • public

    被 public 修饰的成员变量和方法可以在任何类中都能被访问到。
    被 public 修饰的类,在一个 java 源文件中只能有一个类被声明为 public ,而且一旦有一个类为 public ,那这个 java 源文件的文件名就必须要和这个被 public 所修饰的类的类名相同,否则编译不能通过。

  • protected

    被 protected 修饰的成员会被位于同一 package 中的所有类访问到,也能被该类的所有子类继承下来。

  • friendly

    默认,缺省的。在成员的前面不写访问修饰符的时候,默认就是友好的。
    同一package中的所有类都能访问。
    被 friendly 所修饰的成员只能被该类所在同一个 package 中的子类所继承下来。

  • private

    私有的。只能在当前类中被访问到。

WechatIMG520_20191013115001.png

描述一下什么情况下,对象会从年轻代进入老年代

  • 对象的年龄超过一定阀值,-XX:MaxTenuringThreshold 可以指定该阀值
  • 动态对象年龄判定,有的垃圾回收算法,比如 G1,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法
  • 大小超出某个阀值的对象将直接在老年代上分配,值默认为 0,意思是全部首选 Eden 区进行分配,-XX:PretenureSizeThreshold 可以指定该阀值,部分收集器不支持
  • 分配担保,当 Survivor 空间不够的时候,则需要依赖其他内存(指老年代)进行分配担保,这个时候,对象也会直接在老年代上分配

finally语句块一定执行吗?

答案是不一定。存在很多特殊情况导致 finally 语句块不执行。如:

  • 直接返回未执行到 try-finally 语句块
  • 抛出异常未执行到 try-finally 语句块
  • 系统退出未执行到 finally 语句块

等…

代码如下

public static String test() {
    String str = null;
    int i = 0;
    if (i == 0) {
        return str;//直接返回未执行到finally语句块
    }
    try {
        System.out.println("try...");
        return str;
    } finally {
        System.out.println("finally...");
    }
}
 
public static String test2() {
    String str = null;
    int i = 0;
    i = i / 0;//抛出异常未执行到finally语句块
    try {
        System.out.println("try...");
        return str;
    } finally {
        System.out.println("finally...");
    }
}
 
public static String test3() {
    String str = null;
    try {
        System.out.println("try...");
        System.exit(0);//系统退出未执行到finally语句块
        return str;
    } finally {
        System.out.println("finally...");
    }
}

linux指令-top

显示当前系统正在执行的进程的 ID、内存占用率、CPU 占用率等相关信息

常用参数:
-c 显示完整的进程命令
-s 保密模式
-p <进程号> 指定进程显示
-n <次数>循环显示次数

实例:
top - 00:05:02 up 204 days,  9:56,  2 users,  load average: 0.00, 0.01, 0.05
Tasks:  68 total,   1 running,  67 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.7 us,  0.7 sy,  0.0 ni, 98.3 id,  0.3 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016168 total,    65948 free,   335736 used,   614484 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   517700 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND     
 7110 root      10 -10  130476   9416   6116 S  1.3  0.9 141:26.59 AliYunDun   
15845 root      20   0   47064   4320   2180 S  0.3  0.4   2:51.16 nginx   

前五行是当前系统情况整体的统计信息区

第一行,任务队列信息,同 uptime 命令的执行结果:
00:05:02 — 当前系统时间
up 204 days,  9:56 — 系统已经连续运行了 204 天 9 小时 56 分钟未重启
2 users — 当前有 2 个用户登录系统
load average: 0.00, 0.01, 0.05 — load average 后面的三个数分别是 0 分钟、1 分钟、5分钟的负载情况,load average 数据是每隔 5 秒钟检查一次活跃的进程数,然后按特定算法计算出的数值。如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了

第二行,Tasks — 任务(进程):
系统现在共有 68 个进程,其中处于运行中的有 1 个,休眠中 67 个,停止 0 个,僵死 0个

第三行,cpu状态信息:
0.7 us — 用户空间占用 CPU 的百分比
0.7 sy — 内核空间占用 CPU 的百分比
0.0 ni — 改变过优先级的进程占用 CPU 的百分比
98.3 id — 空闲CPU百分比
0.3 wa — IO 等待占用 CPU 的百分比
0.0 hi — 硬中断(Hardware IRQ)占用 CPU 的百分比
0.0 si — 软中断(Software Interrupts)占用 CPU 的百分比
0.0 st - 虚拟机占用百分比


第四行,内存状态:
1016168 total — 物理内存总量
65948 free — 空闲内存总量
335736 used — 使用中的内存总量
614484 buff/cache — 缓存的内存量

第五行,swap交换分区信息,具体信息说明如下:
0 total — 交换区总量
0 free — 空闲交换区总量
0 used — 使用的交换区总量
517700 avail Mem - 可用内存

第七行以下:各进程(任务)的状态监控,项目列信息说明如下:
PID — 进程id
USER — 进程所有者
PR — 进程优先级
NI — nice值。负值表示高优先级,正值表示低优先级
VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
RES — 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
SHR — 共享内存大小,单位kb
S — 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程
%CPU — 上次更新到现在的CPU时间占用百分比
%MEM — 进程使用的物理内存百分比
TIME+ — 进程使用的CPU时间总计,单位1/100秒
COMMAND — 进程名称(命令名/命令行)

top 交互命令
h 显示top交互命令帮助信息
c 切换显示命令名称和完整命令行
m 以内存使用率排序
P 根据CPU使用百分比大小进行排序
T 根据时间/累计时间进行排序
W 将当前设置写入~/.toprc文件中
o或者O 改变显示项目的顺序

FactoryBean 与 BeanFactory 有什么区别?

  • BeanFactory 是 IoC 底层容器,提供了 bean 的管理
  • FactoryBean 是创建 Bean 的一种方式,帮助实现复杂的初始化逻辑

重点说下 FactoryBean,该接口包含 3 个方法 getObject、getObjectType、isSingleton,用于构建复杂的 bean。

如,MyBatis 与 Spring 集成,使用了 SqlSessionFactoryBean 继承 FactoryBean,用于构建 SqlSessionFactory bean。

得到 FactoryBean 本身这个 bean,需要在 bean name 前面加上 $

Redis如何实现消息一次生产,多次消费?

  • 使用 pub/sub 发布订阅模式,可以实现 1:N 的消息队列,即一次生产,多端消费
  • 但不在线的消费者会产生消息丢失的情况

Oracle数据库有哪些触发器?

Oracle 数据库有 4 种触发器

  • DML:当发出UPDATE、INSERT、DELETE命令就可以触发已定义好的 DML 触发器

    语法:
    create or replace trigger trigger_name
    after|before insert|update|delete
    on table_name
    for each row

  • Instead-of:向一个由多个表联接成的视图作 DML 操作时可以用 Instead-of 触发器

    语法:
    create or replace trigger trigger_name
    instead of insert|update|delete
    on view_name
    for each row

  • DDL:当发出CREATE、ALTER、DROP、TRUNCATE命令时会触发已定义好的DDL触发器,这种触发器可以用来监控某个用户或整个数据库的所有对象的结构变化

    语法:
    create or replace trigger trigger_name
    before|after create|alter|drop|truncate
    on schema|database

  • DB:当STARTUP、SHUTDOWN、LOGON、LOGOFF数据库时就会触发DB事件触发器,这种触发器可以用来监控数据库什么时候关闭/打,或者用户的LOGON/LOGOFF数据库情况

    语法:
    create or replace trigger trigger_name
    before|after startup|shutdown|logon|logoff
    on database

一般应用系统中用到 DML、Instead-of;DDL、DB 两种触发器是 DBA 管理数据库用得比较多

要创建 DDL 和 DB 这两种触发器必须有 DBA 的权限

abstract方法是否可是static的?native的?synchronized的?

都不能

  • 抽象方法需要子类重写,而静态的方法是无法被重写的
  • 本地方法是由本地动态库实现的方法,而抽象方法是没有实现的
  • 抽象方法没有方法体;synchronized 方法,需要有具体的方法体,相互矛盾

MySQL如何配置读写分离?

MySQL 最常用的集群部署方式是主从架构,可以 1 主多从,主库写,从库读,用这种方式来做读写分离。也可以主主架构,两边都可以读写,但需要业务代码控制数据冲突问题。MGR(MySQL Group Replication),是分布式架构,支持多点写入,但性能不如上述两者,且对网络要求较高。

常用的读写分离基于主从架构实现的较多

以 64 位 windows MySQL 最新版的 server,8.0.21 安装为例。

step1、安装流程:

  • 官网下载安装包
  • 解压
  • 创建安装初始化配置文件,my.ini,放在安装包与 bin 目录平级
  • 管理员 CMD 执行 mysqld --initialize --console
  • 解决报错,下载 vcruntime140_1.dll 文件放到 System32 目录
  • 继续安装,成功之后生成 root 账号的临时密码
  • 安装服务 mysqld --install
  • 启动服务 net start mysql
  • 登陆修改 root 密码

第二台电脑也是如此配置。这样两台电脑的 MySQL 服务安装就搞定了。

step2、配置主从

  • 主库在 my.ini 中新增库 id 与 log-bin 配置
  • 重启主库
  • 为从库创建同步账号、授权
  • 从库在 my.ini 中新增库 id、log-bin、relay-log 配置
  • 重启从库
  • 修改从库由主库同步的配置信息
  • 开启 slave

配置到这里,向主库新建表,增删改数据,都会自动同步到从库。

具体配置说明:

linux指令-whereis

二进制文件程序名搜索
whereis 及 locate 都是基于系统内建的数据库进行搜索,效率很高,而 find 则是遍历硬盘查找文件

常用参数:
-b   定位可执行文件
-m   定位帮助文件
-s   定位源代码文件
-u   搜索默认路径下除可执行文件、源代码文件、帮助文件以外的其它文件
whereis locale 查找 locale 程序相关文件
whereis -s locale 查找 locale 的源码文件
whereis -m locale 查找 locale 的帮助文件

基本类型byte表示的数值范围是多少?

-128 至 127

MinorGC、MajorGC、FullGC 什么时候发生?

  • MinorGC 在年轻代空间不足的时候发生
  • MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC
  • FullGC 老年代无法再分配内存;元空间不足;显示调用 System.gc;像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 时也会发生 FullGC

linux指令-rmdir

删除目录,不能删除非空目录,当子目录被删除后父目录也成为空目录的话,则一并删除

rmdir -p a/b/c 等同于 rmdir a/b/c a/b a

深拷贝和浅拷贝区别是什么?

复制一个 Java 对象

浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。

20190618153224322_20190922012225.png

深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。

![20190618154423857_20190922012336.png][]

[20190618154423857_20190922012336.png]: https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fwww.javanav.com%2F%2Faimgs%2F20190618154423857__20190922012336.png&pos_id=img-p43m8OFd-1702467223529)

Mysql中exists和in的区别

下面将主查询的表称为外表;子查询的表称为内表。exists 与 in 的主要区别如下:

  • 子查询使用 exists,会先进行主查询,将查询到的每行数据循环带入子查询校验是否存在,过滤出整体的返回数据;子查询使用 in,会先进行子查询获取结果集,然后主查询匹配子查询的结果集,返回数据
  • 外表内表相对大小情况不一样时,查询效率不一样:两表大小相当,in 和 exists 差别不大;内表大,用 exists 效率较高;内表小,用 in 效率较高。
  • 不管外表与内表的大小,not exists 的效率一般要高于 not in,跟子查询的索引访问类型有关。

详细参考:

Mysql 中 exists 和 in 的区别

以下三条sql 如何只建一条索引?

以顺序 b,a,time 建立联合索引,CREATE INDEX idx_b_a_time ON table(b,a,time)。

新 MySQL 版本会优化 WHERE 子句后面的列顺序,以匹配联合索引顺序

什么是Java的多态?

实现多态的三个条件

  • 继承的存在。继承是多态的基础,没有继承就没有多态
  • 子类重写父类的方法,JVM 会调用子类重写后的方法
  • 父类引用变量指向子类对象

向上转型:将一个父类的引用指向一个子类对象,自动进行类型转换。

  • 通过父类引用变量调用的方法是子类覆盖或继承父类的方法,而不是父类的方法。
  • 通过父类引用变量无法调用子类特有的方法。

向下转型:将一个指向子类对象的引用赋给一个子类的引用,必须进行强制类型转换。

  • 向下转型必须转换为父类引用指向的真实子类类型,不是任意的强制转换,否则会出现 ClassCastException
  • 向下转型时可以结合使用 instanceof 运算符进行判断

Dubbo有哪些集群容错方案?

  • Failover Cluster,失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。
  • Failfast Cluster,快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
  • Failsafe Cluster,失败安全,出现异常时,直接忽略。通常用于写入日志等。
  • Failback Cluster,失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知等。
  • Forking Cluster,并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks 来设置最大并行数。
  • Broadcast Cluster,广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存本地资源信息。

默认 Failover Cluster

linux指令-df

显示磁盘空间使用情况

获取硬盘被占用空间,剩余空间等信息。默认所有当前被挂载的文件系统的可用空间都会显示
默认情况下,磁盘空间以 1KB 为单位进行显示

常用参数:
-a 全部文件系统列表
-h 以方便阅读的方式显示信息
-i 显示inode信息
-k 区块为1024字节
-l 只显示本地磁盘
-T 列出文件系统类型

Mapper 接口方法如何与注解里的 SQL 进行绑定的?

  • 根据 Mapper 接口、其方法、方法上的注解,生成 mappedStatementId 与 MapperStatement,注册到 configuration 对象中
  • 根据 Mapper 接口方法查到并调用对应的 MappedStatement,执行 SQL

流程与 Mapper 接口与 xml 绑定类似。

分析

解析生成注册 MapperStatement 的代码入口在 MapperRegistry addMapper 方法

//使用 MapperProxyFactory 包装 Mapper 接口 Class 对象
knownMappers.put(type, new MapperProxyFactory<>(type));

//解析 Mapper 接口方法上的注解,生成对应的 MapperStatement
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();

获取 Mapper 接口的动态代理对象的代码入口在 MapperRegistry getMapper 方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

if-else if-else与switch的区别

if-else-if-else:

  • 适合分支较少
  • 判断条件类型不单一
  • 支持取 boolean 类型的所有运算
  • 满足条件即停止对后续分支语句的执行

switch:

  • 适合分支较多
  • 判断条件类型单一,JDK1.0-1.4 数据类型接受 byte short int char; JDK1.5 数据类型接受 byte short int char enum; JDK1.7 数据类型接受 byte short int char enum String
  • 没有 break 语句每个分支都会执行

如何让计算机最高效的算出2乘以8?

2 <<3

  • 位运算符 <<,是将一个数左移 n 位,相当于乘以了 2 的 n 次方
  • 一个数乘以 8 只要将其左移 3 位即可
  • CPU 直接支持位运算,效率最高

补充:当这个数接近Java基本整数类型的最大值时,左移位运算可能出现溢出,得出负值。

什么是JVM?

1、Java Virtual Machine(Java虚拟机)的缩写

2、实现跨平台的最核心的部分

3、.class 文件会在 JVM 上执行,JVM 会解释给操作系统执行

4、有自己的指令集,解释自己的指令集到 CPU 指令集和系统资源的调用

5、JVM 只关注被编译的 .class 文件,不关心 .java 源文件

构造方法是否可以被重载?重写?

构造方法可以被重载

构造方法不可以被重写

Dubbo有些哪些注册中心?

  • Zookeeper 注册中心: 基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更(官方推荐)

  • Multicast 注册中心: 基于网络中组播传输实现,不需要任何中心节点,只要广播地址,就能进行服务注册和发现

  • Redis 注册中心: 基于 Redis 实现,采用 key/Map 数据结构存储,主 key 存储服务名和类型,Map 中 key 存储服务 URL,Map 中 value 存储服务过期时间,基于 Redis 的发布/订阅模式通知数据变更

  • Simple 注册中心:一个普通的 Dubbo 服务,可以减少第三方依赖,使整体通讯方式一致,不支持集群

什么是assert?

  • assert:断言
  • 一种常用的调试方式,很多开发语言中都支持这种机制
  • 通常在开发和测试时开启
  • 可以用来保证程序最基本、关键的正确性
  • 为了提高性能,发布版的程序通常关闭断言
  • 断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true;如果表达式计算为 false ,会报告一个 AssertionError
  • 断言在默认情况下是禁用的,要在编译时启用断言,需使用source 1.4 标记,如 javac -source 1.4 TestAssert.java
  • 要在运行时启用断言,需加参数 -ea 或 -enableassertions
  • 要在运行时选择禁用断言,需加参数 -da 或 -disableassertions
  • 要在系统类中启用或禁用断言,需加参数 -esa 或 -dsa

Java 中断言有两种语法形式:

  • assert 表达式1;
  • assert 表达式1 : 错误表达式 ;

表达式1 是一个布尔值

错误表达式可以得出一个值,用于生成显示调试信息的字符串消息

package constxiong.interview;

public class TestAssert {

    public static void main(String[] args) {
        assert 1 > 0;
        int x = 1;
        assert x <0 : "大于0";
    }
    
}

打印:

Exception in thread "main" java.lang.AssertionError: 大于0
	at constxiong.interview.TestAssert.main(TestAssert.java:8)

基本类型和包装类对象使用 == 和 equals进行比较的结果?

1、值不同,使用 == 和 equals() 比较都返回 false

2、值相同

使用 == 比较:

  • 基本类型 - 基本类型、基本类型 - 包装对象返回 true

  • 包装对象 - 包装对象,非同一个对象(对象的内存地址不同)返回 false;对象的内存地址相同返回 true,如下值等于 100 的两个 Integer 对象(原因是 JVM 缓存部分基本类型常用的包装类对象,如 Integer -128 ~ 127 是被缓存的)

    Integer i1 = 100;
    Integer i2 = 100;
    Integer i3 = 200;
    Integer i4 = 200;

    System.out.println(i1i2); //打印true
    System.out.println(i3
    i4); //打印false

使用 equals() 比较

  • 包装对象-基本类型返回 true
  • 包装对象-包装对象返回 true

3、不同类型的对象对比,返回 false

JDK1.8,实验代码

byte b1 = 127;
Byte b2 = new Byte("127");
Byte b3 = new Byte("127");
System.out.println("Byte 基本类型和包装对象使用 == 比较 : " + (b1 == b2));
System.out.println("Byte 基本类型和包装对象使用 equals 比较 : " + b2.equals(b1));
System.out.println("Byte 包装对象和包装对象使用 == 比较 : " + (b2 == b3));
System.out.println("Byte 包装对象和包装对象使用 equals 比较 : " + b2.equals(b3));
System.out.println();

short s1 = 12;
Short s2 = new Short("12");
Short s3 = new Short("12");
System.out.println("Short 基本类型和包装对象使用 == 比较 : " + (s1 == s2));
System.out.println("Short 基本类型和包装对象使用 equals 比较 : " + s2.equals(s1));
System.out.println("Short 包装对象和包装对象使用 == 比较 : " + (s2 == s3));
System.out.println("Short 包装对象和包装对象使用 equals 比较 : " + s2.equals(s3));
System.out.println();

char c1 = 'A';
Character c2 = new Character('A');
Character c3 = new Character('A');
System.out.println("Character 基本类型和包装对象使用 == 比较 : " + (c1 == c2));
System.out.println("Character 基本类型和包装对象使用 equals 比较 : " + c2.equals(c1));
System.out.println("Character 包装对象和包装对象使用 == 比较 : " + (c2 == c3));
System.out.println("Character 包装对象和包装对象使用 equals 比较 : " + c2.equals(c3));
System.out.println();

int i1 = 10000;
Integer i2 = new Integer(10000);
Integer i3 = new Integer(10000);
System.out.println("Integer 基本类型和包装对象使用 == 比较 : " + (i1 == i2));
System.out.println("Integer 基本类型和包装对象使用 equals 比较 : " + i2.equals(i1));
System.out.println("Integer 包装对象和包装对象使用 == 比较 : " + (i2 == i3));
System.out.println("Integer 包装对象和包装对象使用 equals 比较 : " + i2.equals(i3));
System.out.println();

long l1 = 1000000000000000L;
Long l2 = new Long("1000000000000000");
Long l3 = new Long("1000000000000000");
System.out.println("Long 基本类型和包装对象使用 == 比较 : " + (l1 == l2));
System.out.println("Long 基本类型和包装对象使用 equals 比较 : " + l2.equals(l1));
System.out.println("Long 包装对象和包装对象使用 == 比较 : " + (l2 == l3));
System.out.println("Long 包装对象和包装对象使用 equals 比较 : " + l2.equals(l3));
System.out.println();

float f1 = 10000.111F;
Float f2 = new Float("10000.111");
Float f3 = new Float("10000.111");
System.out.println("Float 基本类型和包装对象使用 == 比较 : " + (f1 == f2));
System.out.println("Float 基本类型和包装对象使用 equals 比较 : " + f2.equals(f1));
System.out.println("Float 包装对象和包装对象使用 == 比较 : " + (f2 == f3));
System.out.println("Float 包装对象和包装对象使用 equals 比较 : " + f2.equals(f3));
System.out.println();

double d1 = 10000.111;
Double d2 = new Double("10000.111");
Double d3 = new Double("10000.111");
System.out.println("Double 基本类型和包装对象使用 == 比较 : " + (d1 == d2));
System.out.println("Double 基本类型和包装对象使用 equals 比较 : " + d2.equals(d1));
System.out.println("Double 包装对象和包装对象使用 == 比较 : " + (d2 == d3));
System.out.println("Double 包装对象和包装对象使用 equals 比较 : " + d2.equals(d3));
System.out.println();

boolean bl1 = true;
Boolean bl2 = new Boolean("true");
Boolean bl3 = new Boolean("true");
System.out.println("Boolean 基本类型和包装对象使用 == 比较 : " + (bl1 == bl2));
System.out.println("Boolean 基本类型和包装对象使用 equals 比较 : " + bl2.equals(bl1));
System.out.println("Boolean 包装对象和包装对象使用 == 比较 : " + (bl2 == bl3));
System.out.println("Boolean 包装对象和包装对象使用 equals 比较 : " + bl2.equals(bl3));

运行结果

Byte 基本类型和包装对象使用 == 比较 : true
Byte 基本类型和包装对象使用 equals 比较 : true
Byte 包装对象和包装对象使用 == 比较 : false
Byte 包装对象和包装对象使用 equals 比较 : true
 
Short 基本类型和包装对象使用 == 比较 : true
Short 基本类型和包装对象使用 equals 比较 : true
Short 包装对象和包装对象使用 == 比较 : false
Short 包装对象和包装对象使用 equals 比较 : true
 
Character 基本类型和包装对象使用 == 比较 : true
Character 基本类型和包装对象使用 equals 比较 : true
Character 包装对象和包装对象使用 == 比较 : false
Character 包装对象和包装对象使用 equals 比较 : true
 
Integer 基本类型和包装对象使用 == 比较 : true
Integer 基本类型和包装对象使用 equals 比较 : true
Integer 包装对象和包装对象使用 == 比较 : false
Integer 包装对象和包装对象使用 equals 比较 : true
 
Long 基本类型和包装对象使用 == 比较 : true
Long 基本类型和包装对象使用 equals 比较 : true
Long 包装对象和包装对象使用 == 比较 : false
Long 包装对象和包装对象使用 equals 比较 : true
 
Float 基本类型和包装对象使用 == 比较 : true
Float 基本类型和包装对象使用 equals 比较 : true
Float 包装对象和包装对象使用 == 比较 : false
Float 包装对象和包装对象使用 equals 比较 : true
 
Double 基本类型和包装对象使用 == 比较 : true
Double 基本类型和包装对象使用 equals 比较 : true
Double 包装对象和包装对象使用 == 比较 : false
Double 包装对象和包装对象使用 equals 比较 : true
 
Boolean 基本类型和包装对象使用 == 比较 : true
Boolean 基本类型和包装对象使用 equals 比较 : true
Boolean 包装对象和包装对象使用 == 比较 : false
Boolean 包装对象和包装对象使用 equals 比较 : true

ps:可以延伸一个问题,基本类型与包装对象的拆/装箱的过程

Oracle有哪几种索引?

  • 单列索引与复合索引

单列索引是基于单列所创建的索引,复合索引是基于两列或者多列所创建的索引

  • 唯一索引与非唯一索引

唯一索引是索引列值不能重复的索引,非唯一索引是索引列可以重复的索引。都允许取 NULL 值,默认 Oracle 创建的索引是不唯一索引

  • B 树索引

B 树索引是按 B 树算法组织并存放索引数据的,B 树索引主要依赖其组织并存放索引数据的算法来实现快速检索功能

  • 位图索引

它采用位图偏移方式来与表的行 ROWID 号对应,通过位图索引中的映射函数完成位到行的 ROWID 的转换

主要用于节省空间,减少oracle对数据块的访问

采用位图索引一般是重复值太多、只有几个枚举值的表字段

  • 函数索引

Oracle 对包含列的函数或表达式创建的索引

Spring 框架用到了哪些设计模式?

  • 工厂模式:Spring 使用工厂模式,通过 BeanFactory 来创建对象

  • 单例模式:Bean 默认就是单例模式

  • 策略模式:Resource 的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

  • 代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码提升

  • 模板方法模式:父类生成代码骨架,具体实现延迟到子类,如 JdbcTemplate、RestTemplate

  • 适配器模式:Spring AOP 中的 Advice 使用到了适配器模式,Spring MVC 中用到了适配器模式适配 Controller

  • 观察者模式:Spring 事件驱动模型就是观察者模式

volatile关键字的作用是什么?

Java 中 volatile 关键字是一个类型修饰符。JDK 1.5 之后,对其语义进行了增强。

  • 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见
  • 通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题

volatile 可见性的实现

  • 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令
  • Lock 前缀的指令会引起 CPU 缓存写回内存
  • 一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效
  • volatile 变量通过缓存一致性协议保证每个线程获得最新值
  • 缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
  • 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存

看一下我们之前的一个可见性问题的测试例子

package constxiong.concurrency.a014;

/**
 * 测试可见性问题
 * @author ConstXiong
 */
public class TestVisibility {

    //是否停止 变量
    private static boolean stop = false;
    
    public static void main(String[] args) throws InterruptedException {
        //启动线程 1,当 stop 为 true,结束循环
        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();
        
        //休眠 10 毫秒
        Thread.sleep(10);
        
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
    
}

程序会一直循环运行下去

这个就是因为 CPU 缓存导致的可见性导致的问题。

线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。

示意如图:

image_20190914114150.png

给 stop 变量加上 valatile 关键字修饰就可以解决这个问题。

volatile 有序性的实现

  • 3 个 happens-before 规则实现:
  1. 对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读

  2. 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作

  3. happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C

  • 内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令)禁止重排序
  1. 在程序运行时,为了提高执行性能,在不改变正确语义的前提下,编译器和 CPU 会对指令序列进行重排序。

  2. Java 编译器会在生成指令时,为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的指令重排序

  3. 编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令

  4. 内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序

内存屏障

  • 为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的 CPU 重排序。
  • 对于编译器,内存屏障将限制它所能做的重排序优化;对于 CPU,内存屏障将会导致缓存的刷新操作
  • volatile 变量的写操作,在变量的前面和后面分别插入内存屏障;volatile 变量的读操作是在后面插入两个内存屏障
  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障
  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
  3. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
  4. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障
  • 屏障说明
  1. StoreStore:禁止之前的普通写和之后的 volatile 写重排序;
  2. StoreLoad:禁止之前的 volatile 写与之后的 volatile 读/写重排序
  3. LoadLoad:禁止之后所有的普通读操作和之前的 volatile 读重排序
  4. LoadStore:禁止之后所有的普通写操作和之前的 volatile 读重排序

我觉得,有序性最经典的例子就是 JDK 并发包中的显式锁 java.util.concurrent.locks.Lock 的实现类对有序性的保障。

以下摘自:http://ifeve.com/java锁是如何保证数据可见性的/

实现 Lock 的代码思路简化为

private volatile int state;

void lock() {
    read state
    if (can get lock)
        write state
}

void unlock() {
    write state
}
  • 假设线程 a 通过调用lock方法获取到锁,此时线程 b 也调用了 lock() 方法,因为 a 尚未释放锁,b 只能等待。
  • a 在获取锁的过程中会先读 state,再写 state。
  • 当 a 释放掉锁并唤醒 b,b 会尝试获取锁,也会先读 state,再写 state。

Happens-before 规则:一个 volatile 变量的写操作发生在这个 volatile 变量随后的读操作之前。

linux指令-locate

搜索文档数据库命令
locate 通过搜寻系统内建文档数据库达到快速找到档案,数据库由 updatedb 程序来更新,updatedb 由 cron daemon 周期性调用
locate 命令在搜寻较快,但最近才建立或刚更名的,可能会找不到
locate 与 find 命令相似,可以使用正则匹配查找

常用参数:
-l num 要显示的行数
-f     将特定的档案系统排除在外
-r     使用正则运算式做为寻找条件

locate pwd 查找文件名中包含 pwd 的所有文件
locate /etc/sh 搜索 etc 目录下所有以 sh 开头的文件
locate -r '^/var.*txt$' 查找 /var 目录下,以 txt 结尾的文件

Servlet中的doPost和doGet方法有什么区别?传递和获取参数上有什么区别?

区别:doPost 用来处理 post 请求,doGet 用来处理 get 请求

参数:传递的参数相同的都是 HttpServletRequest 和 HttpServletResponse

锁如何使用?有什么注意事项?

Java 中常见的锁有

  • synchronized
  • 可重入锁 java.util.concurrent.lock.ReentrantLock
  • 可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock

synchronized 有 3种用法

  • 修饰普通方法,执行方法代码,需要获取对象本身 this 的锁

    package constxiong.concurrency.a18;

    import java.util.ArrayList;
    import java.util.List;

    /**

    • 测试 synchronized 普通方法

    • @author ConstXiong

    • @date 2019-09-19 10:49:46
      */
      public class TestSynchronizedNormalMethod {

      private int count = 0;

    // private void add1000() {
    private synchronized void add1000() { //使用 synchronized 修饰 add100 方法,即可获得正确的值 30000
    for (int i = 0; i <1000; i++) {
    count++;
    }
    }

    //启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
    private void test() throws InterruptedException {
        List<Thread> threads = new ArrayList<Thread>(10);
        
        for (int i = 0; i <30; i++) {
            Thread t =  new Thread(() -> {
                add1000();
            });
            t.start();
            threads.add(t);
        }
        
        //等待所有线程执行完毕
        for (Thread t : threads) {
            t.join();
        }
        
        //打印 count 的值
        System.out.println(count);
    }
    
    public static void main(String[] args) throws InterruptedException {
        //创建 TestSynchronizedNormalMethod 对象,调用 test 方法
        new TestSynchronizedNormalMethod().test();
    }
    

    }

  • 修饰静态方法,执行方法代码,需要获取 class 对象的锁

    package constxiong.concurrency.a18;

    import java.util.ArrayList;
    import java.util.List;

    /**

    • 测试 synchronized 静态方法

    • @author ConstXiong

    • @date 2019-09-19 10:49:46
      */
      public class TestSynchronizedStaticMethod {

      private static int count = 0;

      private static void add1000() {
      // private synchronized static void add1000() { //使用 synchronized 修饰 add100 方法,即可获得正确的值 30000
      for (int i = 0; i <1000; i++) {
      count++;
      }
      }

      public static void main(String[] args) throws InterruptedException {

       //启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
       List<Thread> threads = new ArrayList<Thread>(10);
       
       for (int i = 0; i <30; i++) {
           Thread t =  new Thread(() -> {
               add1000();
           });
           t.start();
           threads.add(t);
       }
       
       //等待所有线程执行完毕
       for (Thread t : threads) {
           t.join();
       }
       
       //打印 count 的值
       System.out.println(count);
      

      }
      }

  • 锁定 Java 对象,修饰代码块,显示指定需要获取的 Java 对象锁

    package constxiong.concurrency.a18;

    import java.util.ArrayList;
    import java.util.List;

    /**

    • 测试 synchronized 代码块

    • @author ConstXiong

    • @date 2019-09-19 10:49:46
      */
      public class TestSynchronizedCodeBlock {

      private int count = 0;

      //锁定的对象
      private final Object obj = new Object();

      private void add1000() {

       //执行下面的加 1000 的操作,都需要获取 obj 这个对象的锁
       synchronized (obj) {
           for (int i = 0; i <1000; i++) {
               count++;
           }
       }
      

      }

      //启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
      private void test() throws InterruptedException {
      List threads = new ArrayList(10);

       for (int i = 0; i <30; i++) {
           Thread t =  new Thread(() -> {
               add1000();
           });
           t.start();
           threads.add(t);
       }
       
       //等待所有线程执行完毕
       for (Thread t : threads) {
           t.join();
       }
       
       //打印 count 的值
       System.out.println(count);
      

      }

      public static void main(String[] args) throws InterruptedException {
      //创建 TestSynchronizedNormalMethod 对象,调用 test 方法
      new TestSynchronizedCodeBlock().test();
      }
      }

可重入锁 java.util.concurrent.lock.ReentrantLock 的使用示例

package constxiong.concurrency.a18;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 测试 ReentrantLock 
 * @author ConstXiong
 * @date 2019-09-19 11:26:50
 */
public class TestReentrantLock {

    private int count = 0;
    
    private final Lock lock = new ReentrantLock();
    
    private void add1000() {
        lock.lock();
        try {
            for (int i = 0; i <1000; i++) {
                count++;
            }
        } finally {
            lock.unlock();
        }
    }
    
    //启动 30 个线程,每个线程 对 TestSynchronized 对象的 count 属性加 1000
    private void test() throws InterruptedException {
        List<Thread> threads = new ArrayList<Thread>(10);
        
        for (int i = 0; i <30; i++) {
            Thread t =  new Thread(() -> {
                add1000();
            });
            t.start();
            threads.add(t);
        }
        
        //等待所有线程执行完毕
        for (Thread t : threads) {
            t.join();
        }
        
        //打印 count 的值
        System.out.println(count);
    }
    
    public static void main(String[] args) throws InterruptedException {
        //创建 TestReentrantLock 对象,调用 test 方法
        new TestReentrantLock().test();
    }
    
}

可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock 的使用示例

package constxiong.concurrency.a18;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * 测试可重入读写锁 ReentrantReadWriteLock
 * @author ConstXiong
 * @date 2019-09-19 11:36:19
 */
public class TestReentrantReadWriteLock {
    
    //存储 key value 的 map
    private Map<String, Object> map = new HashMap<String, Object>();
    
    //读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    /**
     * 根据 key 获取 value
     * @param key
     */
    public Object get(String key) {
        Object value = null;
        lock.readLock().lock();
        try {
            Thread.sleep(50L);
            value = map.get(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
        return value; 
    }
    
    /**
     * 设置key-value
     * @param key
     */
    public void set(String key, Object value) {
        lock.writeLock().lock();
        try {
            Thread.sleep(50L);
            map.put(key, value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
 
    //测试5个线程读数据,5个线程写数据
    public static void main(String[] args) {
        //创建测试可重入读写锁 TestReentrantReadWriteLock 对象
        TestReentrantReadWriteLock test = new TestReentrantReadWriteLock();
        
        String key = "lock";//存入 map 中的 key
        Random r = new Random();//生成随机数作为 value
        
        for (int i = 0; i <5; i++) {
            //5 个线程读 map 中 key 的 value
            new Thread(() -> {
                for (int j = 0; j <10; j++) {
                    System.out.println(Thread.currentThread().getName() + " read value=" + test.get(key));
                }
            }).start();
            
            //5 个线程写 map 中 key 的 value
            new Thread(() -> {
                for (int j = 0; j <10; j++) {
                    int value = r.nextInt(1000);
                    test.set(key, value);
                    System.out.println(Thread.currentThread().getName() + " write value=" + value);
                }
            }).start();
        }
    }
    
}

锁的使用注意事项

  • synchronized 修饰代码块时,最好不要锁定基本类型的包装类,如 jvm 会缓存 -128 ~ 127 Integer 对象,每次向如下方式定义 Integer 对象,会获得同一个 Integer,如果不同地方锁定,可能会导致诡异的性能问题或者死锁

    Integer i = 100;

  • synchronized 修饰代码块时,要线程互斥地执行代码块,需要确保锁定的是同一个对象,这点往往在实际编程中会被忽视

  • synchronized 不支持尝试获取锁、锁超时和公平锁

  • ReentrantLock 一定要记得在 finally{} 语句块中调用 unlock() 方法释放锁,不然可能导致死锁

  • ReentrantLock 在并发量很高的情况,由于自旋很消耗 CPU 资源

  • ReentrantReadWriteLock 适合对共享资源写操作很少,读操作频繁的场景;可以从写锁降级到读锁,无法从读锁升级到写锁

什么是UML?

  • UML是统一建模语言,Unified Modeling Language的缩写
  • 综合了面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持
  • 可以帮助沟通与交流、辅助应用设计、文档的生成、阐释系统的结构和行为
  • 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构
  • 包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)

常见的HTTP协议状态码?

2xx:表示请求已被成功接收、理解、接受

  • 200(成功) 服务器已成功处理了请求。这个状态码对servlet是缺省的,如果没有调用setStatus方法的话,就会得到 200
  • 204(无内容) 服务器成功处理了请求,未返回任何内容
  • 205(重置内容) 服务器成功处理了请求,未返回任何内容,重置文档视图,如清除表单内容
  • 206(部分内容) 服务器成功处理了部分 GET 请求

3xx:重定向

  • 300(多种选择) 服务器根据请求可执行多种操作。服务器可根据请求者 来选择一项操作,或提供操作列表供其选择
  • 301(永久移动) 请求的网页已被永久移动到新位置。服务器返回此响应时,会自动将请求者转到新位置
  • 302(临时移动) 服务器目前正从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。会自动将请求者转到新位置
  • 304(未修改) 自从上次请求后,请求的网页未被修改过,不会返回网页内容
  • 305(使用代理) 请求者只能使用指定的代理访问请求的网页

4xx:客户端错误,请求有语法错误或请求无法实现

  • 400(错误请求) 服务器不理解请求的语法
  • 401(身份验证错误) 此页要求授权
  • 403(禁止) 服务器拒绝请求
  • 404(未找到) 服务器找不到请求的网页
  • 406(不接受) 无法使用请求的内容特性响应请求的网页
  • 408(请求超时) 服务器等候请求时发生超时
  • 414(请求的 URI 过长) 请求的 URI 过长,服务器无法处理

5xx:服务器端错误,无法处理请求

  • 500(服务器内部错误) 服务器遇到错误,无法完成请求。
  • 503(服务不可用) 目前无法使用服务器(由于超载或进行停机维护)。通常,这只是一种暂时的状态。
  • 504(网关超时) 服务器作为网关或代理,未及时从上游服务器接收请求。
  • 505(HTTP 版本不受支持) 服务器不支持请求中所使用的 HTTP 协议版本

面向字符的输入流是()

答案:C

分析:

  • FileInputStream 文件输入流
  • BufferedWriter 带缓冲的字符输出流
  • ObjectInputStream 对象输入流

说说你对面向对象的理解

对 Java 语言来说,一切皆是对象。

对象有以下特点:

  • 对象具有属性和行为
  • 对象具有变化的状态
  • 对象具有唯一性
  • 对象都是某个类别的实例
  • 一切皆为对象,真实世界中的所有事物都可以视为对象

面向对象的特性:

  • 抽象性:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
  • 继承性:指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承。
  • 封装性:封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息。
  • 多态性:多态性体现在父类的属性和方法被子类继承后或接口被实现类实现后,可以具有不同的属性或表现方式。

notify()和notifyAll()有什么区别?

先解释两个概念。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池,等待池中的线程不会去竞争该对象的锁。
  • 锁池:只有获取了对象的锁,线程才能执行对象的 synchronized 代码,对象的锁每次只有一个线程可以获得,其他线程只能在锁池中等待

区别:

notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。

测试代码

public class TestNotifyNotifyAll {
 
    private static Object obj = new Object();
    
    public static void main(String[] args) {
        
        //测试 RunnableImplA wait()        
        Thread t1 = new Thread(new RunnableImplA(obj));
        Thread t2 = new Thread(new RunnableImplA(obj));
        t1.start();
        t2.start();
        
        //RunnableImplB notify()
        Thread t3 = new Thread(new RunnableImplB(obj));
        t3.start();
        
        
//        //RunnableImplC notifyAll()
//        Thread t4 = new Thread(new RunnableImplC(obj));
//        t4.start();
    }
    
}
 
 
class RunnableImplA implements Runnable {
 
    private Object obj;
    
    public RunnableImplA(Object obj) {
        this.obj = obj;
    }
    
    public void run() {
        System.out.println("run on RunnableImplA");
        synchronized (obj) {
            System.out.println("obj to wait on RunnableImplA");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("obj continue to run on RunnableImplA");
        }
    }
}
 
class RunnableImplB implements Runnable {
 
    private Object obj;
    
    public RunnableImplB(Object obj) {
        this.obj = obj;
    }
    
    public void run() {
        System.out.println("run on RunnableImplB");
        System.out.println("睡眠3秒...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (obj) {
            System.out.println("notify obj on RunnableImplB");
            obj.notify();
        }
    }
}
 
class RunnableImplC implements Runnable {
 
    private Object obj;
    
    public RunnableImplC(Object obj) {
        this.obj = obj;
    }
    
    public void run() {
        System.out.println("run on RunnableImplC");
        System.out.println("睡眠3秒...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (obj) {
            System.out.println("notifyAll obj on RunnableImplC");
            obj.notifyAll();
        }
    }
}

结果:仅调用一次 obj.notify(),线程 t1 或 t2 中的一个始终在等待被唤醒,程序不终止

run on RunnableImplA
obj to wait on RunnableImplA
run on RunnableImplA
obj to wait on RunnableImplA
run on RunnableImplB
睡眠3秒...
notify obj on RunnableImplB
obj continue to run on RunnableImplA

把 t3 注掉,启动 t4 线程。调用 obj.notifyAll() 方法

public class TestNotifyNotifyAll {
 
    private static Object obj = new Object();
    
    public static void main(String[] args) {
        
        //测试 RunnableImplA wait()        
        Thread t1 = new Thread(new RunnableImplA(obj));
        Thread t2 = new Thread(new RunnableImplA(obj));
        t1.start();
        t2.start();
        
//        //RunnableImplB notify()
//        Thread t3 = new Thread(new RunnableImplB(obj));
//        t3.start();
        
        
        //RunnableImplC notifyAll()
        Thread t4 = new Thread(new RunnableImplC(obj));
        t4.start();
    }

}

结果:t1、t2线程均可以执行完毕

run on RunnableImplA
obj to wait on RunnableImplA
run on RunnableImplA
obj to wait on RunnableImplA
run on RunnableImplC
睡眠3秒...
notifyAll obj on RunnableImplC
obj continue to run on RunnableImplA
obj continue to run on RunnableImplA

Java属于编译型还是解释型语言?

计算机不能直接理解高级语言,只能理解和运行机器语言。必须要把高级语言翻译成机器语言,计算机才能运行高级语言所编写的程序。
翻译的方式有两种,一个是编译,一个是解释。

用编译型语言写的程序执行之前,需要一个专门的编译过程,通过编译系统把高级语言翻译成机器语言,把源高级程序编译成为机器语言文件,以后直接运行而不需要再编译了,所以一般编译型语言的程序执行效率高。

解释型语言在运行的时候才解释成机器语言,每个语句都是执行时才翻译。每执行一次就要翻译一次,效率较低。

Java 是一种兼具编译和解释特性的语言,.java 文件会被编译成与平台无关的 .class 文件,但是 .class 字节码文件无法被计算机直接,仍然需要 JVM 进行翻译成机器语言。
所以严格意义上来说,Java 是一种解释型语言。

下面代码的输出是?

输出

1 dbc

分析:

  • te 对象的 str 属性被 exchange 方法处理过之后,仍然指向字符串常量缓冲区 “1”
  • arr 属性是个数组,在 exchange 方法中,数组的内部第一个位置的值被修改,是生效的。如果加上 arr = new char[0]; 这段,输出值不会改变,原理同上。

介绍一下 MyBatis

MyBatis 是一款优秀的持久层框架。

  • 支持自定义 SQL、存储过程以及高级映射
  • 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作
  • 通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录

ps: 摘自官网
https://mybatis.org/mybatis-3/zh/index.html

MySQL有哪些常用函数?

数值型函数

  • ABS:计算绝对值
  • SQRT:计算二次方根
  • MOD:计算余数
  • CEIL、CEILING:返回不小于参数的最小整数,即向上取整
  • FLOOR:向下取整,返回值转化为一个 BIGINT
  • RAND:生成一个 0~1 之间的随机数
  • ROUND:四舍五入
  • SIGN:返回参数的符号
  • POW、POWER:参数次方的值
  • SIN:计算正弦值
  • ASIN:计算反正弦值
  • COS:计算余弦值
  • ACOS:计算反余弦值
  • TAN:计算正切值
  • ATAN:计算反正切值
  • COT:计算余切值

字符串函数

  • LENGTH:返回字符串的字节长度
  • CONCAT:合并字符串,返回结果为连接参数产生的字符串,参数可以使一个或多个
  • INSERT:替换字符串
  • LOWER:将字符串中的字母转换为小写
  • UPPER:将字符串中的字母转换为大写
  • LEFT:从左侧字截取符串,返回字符串左边的若干个字符
  • RIGHT:从右侧字截取符串,返回字符串右边的若干个字符
  • TRIM:删除字符串左右两侧的空格
  • REPLACE:字符串替换,返回替换后的新字符串
  • SUBSTRING:截取字符串,返回从指定位置开始的指定长度的字符换
  • REVERSE:字符串反转,返回与原始字符串顺序相反的字符串

日期和时间函数

  • CURDATE、CURRENT_DATE:返回当前系统的日期值
  • CURTIME、CURRENT_TIME:返回当前系统的时间值
  • NOW、SYSDATE:返回当前系统的日期和时间值
  • UNIX_TIMESTAMP:获取 UNIX 时间戳函数,返回一个以 UNIX 时间戳为基础的无符号整数
  • FROM_UNIXTIME:将 UNIX 时间戳转换为时间格式
  • MONTH:获取指定日期中的月份
  • MONTHNAME:获取指定日期中的月份英文名称
  • DAYNAME:获取指定曰期对应的星期几的英文名称
  • DAYOFWEEK:获取指定日期对应的一周的索引位置值
  • WEEK:获取指定日期是一年中的第几周
  • DAYOFYEAR:获取指定曰期是一年中的第几天,返回值 1~366
  • DAYOFMONTH:获取指定日期是一个月中是第几天,返回值 1~31
  • YEAR:获取年份
  • TIME_TO_SEC:将时间参数转换为秒数
  • SEC_TO_TIME:将秒数转换为时间
  • DATE_ADD、ADDDATE:向日期添加指定的时间间隔
  • DATE_SUB、SUBDATE:向日期减去指定的时间间隔
  • ADDTIME:时间加法运算,在原始时间上添加指定的时间
  • SUBTIME:时间减法运算,在原始时间上减去指定的时间
  • DATEDIFF:获取两个日期之间间隔,返回参数 1 减去参数 2 的值
  • DATE_FORMAT:格式化指定的日期,根据参数返回指定格式的值
  • WEEKDAY:获取指定日期在一周内的对应的工作日索引

聚合函数

  • MAX:查询指定列的最大值
  • MIN:查询指定列的最小值
  • COUNT:统计查询结果的行数
  • SUM:求和,返回指定列的总和
  • AVG:求平均值,返回指定列数据的平均值

流程控制函数

  • IF:判断是否为 true
  • IFNULL:判断是否为空
  • CASE:分支判断

什么是并发编程?

并发:

  • 在程序设计的角度,希望通过某些机制让计算机可以在一个时间段内,执行多个任务。
  • 一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。
  • 任务数多余 CPU 的核数,通过操作系统的任务调度算法,实现多个任务一起执行。
  • 有多个线程在执行,计算机只有一个 CPU,不可能真正同时运行多个线程,操作系统只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。

并发编程:

  • 用编程语言编写让计算机可以在一个时间段内执行多个任务的程序。

1566572348851_20190823231153.jpg

写一些常见的Javascript正则表达式?

  • 身份证:/(\d{15}$)|(^\d{18}$)|(\d{17}(\d|X|x)$)/
  • 数字:/[0-9]/
  • 英文:/^[a-z]+$/i
  • 中文:/[\u4e00-\u9fa5]/gm
  • 数字逗号:/^[\d,]*$/
  • 手机号:/^[1][3,4,5,7,8][0-9]{9}$/
  • 新能源车牌(长度为8):/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]KaTeX parse error: Undefined control sequence: \[ at position 4: )|(\̲[̲DF\]\[A-HJ-NP-Z…))/
  • 常规车牌(长度为7):/^[京津沪渝冀豫云
  • 辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/
  • 英文特殊字符:/[`~!@#$%^&*()_+<>?:"{},\\.\/;'[\]]/im
  • 中文特殊字符:/[·!#¥(——):;“”‘、,|《。》?、【】[\]]/im

BIO、NIO、AIO有什么区别?

  • BIO:线程发起 IO 请求,不管内核是否准备好 IO 操作,从发起请求起,线程一直阻塞,直到操作完成。

  • NIO:客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。

  • AIO:线程发起 IO 请求,立即返回;内存做好 IO 操作的准备之后,做 IO 操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做 IO 操作完成或者失败。

  • BIO 是一个连接一个线程。

  • NIO 是一个请求一个线程。

  • AIO 是一个有效请求一个线程。

  • BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

  • AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 IO 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。

适用场景分析

  • BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
  • NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
  • AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

举个例子

  • 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,餐没做好,你就必须等着!
  • 同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没?
  • 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于外卖。

内容摘自:https://blog.csdn.net/u013068377/article/details/70312551
结合代码可参考:https://www.cnblogs.com/barrywxx/p/8430790.html

MySQL的数据类型有哪些?

1、整数类型: TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT

分别占用 1 字节、2 字节、3 字节、4 字节、8 字节;任何整数类型都可以加上 UNSIGNED 属性,表示数据是无符号的,即非负整数;整数类型可以被指定长度,即为显示长度,不影响存储占用空间

2、实数类型: FLOAT、DOUBLE、DECIMAL

DECIMAL 可以用于存储比 BIGINT 还大的整型,能存储精确的小数;FLOAT 和 DOUBLE 有取值范围,支持使用标准的浮点进行近似计算

3、字符串类型: CHAR、VARCHAR、TEXT、BLOB

CHAR 是定长的,根据定义的字符串长度分配足够的空间;VARCHAR 用于存储可变长字符串;TEXT 存大文本;BLOB 存二进制数据

4、枚举类型:ENUM

把不重复的数据存储为一个预定义的集合,可以替代常用的字符串类型

5、日期和时间类型:YEAR、TIME、DATE、TIMESTAMP、DATETIME

分别占用 1 byte、3 bytes、4 bytes、4 bytes、8 bytes

打印值是多少?

答案:A

分析:

  • do while 循环是先执行后判断
  • 代码先执行 --b 操作,b = -1
  • 之后执行 a=a-1,a 为 -1
  • 然后判断 b 是否大于 0 ,条件不成立,退出循环
  • b 输出 -1

怎么实现动态代理?

  • JDK 动态代理
  • CGLib 动态代理
  • 使用 Spring aop 模块完成动态代理功能

项目中如何用 Spring 和 Spring MVC 框架的?

如果你在企业的项目中用过 Struts2 框架,那说明你搞 Java 可能在 5 年以上了。

在 Spring MVC 火之前,Struts2 + Spring + Hibernate 就是传说中的 SSH 框架,也有 Struts2 + Spring + MyBatis 即 SSM。后来渐渐就演化到 Spring + SpringMVC + MyBatis 成为了主流。再后来大家就都知道了。

Spring 成为后端开发框架的标准早已是事实。使用 Spring 最大的好处它的 IoC 和 AOP 功能,项目中一般通过 xml 配置文件 + 注解的方式,把 Bean 的管理交给 Spring 的 IoC 容器;日志、统计耗时次数、事务管理都交由 AOP 实现,xml 和 注解申明的方式都会使用到。

Spring MVC 也基本是必用的,通过 web.xml 的配置、@Controller、@Service、@Repository,完成 http 请求到数据库的 crud 再到 view 层展示,整个调用链。其中还要配置对象转 json 的 Converter、登录拦截器、文件上传大小限制、数据源及连接池相关等等…

Spring Boot、Spring Cloud 都是基于 Spring Framework 和 Spring MVC 进一步衍生出来的。

final修饰变量,是引用不能变?还是引用的对象不能变?

  • final 修饰基本类型变量,值不能改变
  • final 修饰引用类型变量,栈内存中的引用不能改变,所指向的堆内存中的对象的属性值可能可以改变

Redis持久化机制有哪些?各有什么优缺点?

Redis 提供两种持久化机制: RDB 和 AOF

RDBRedis DataBase:

指用数据集快照的方式半持久化模式,记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,可恢复数据

优点:

  • 只有一个文件 dump.rdb,恢复操作简单,容灾性好
  • 性能较高,fork 子进程进行写操作,主进程继续处理命令
  • 大数据集比 AOF 的恢复效率高

缺点:

  • 数据安全性低,RDB 是每间隔一段时间进行持久化,若期间 redis 发生故障,可能会发生数据丢失

AOFAppend-only file

指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储,保存为 aof 文件

优点:

  • 数据安全,aof 持久化可以配置 appendfsync 属性为 always,记录每个命令操作到 aof 文件中一次;通过 append 模式写文件,即使中途服务器宕机,也可以通过 redis-check-aof 工具解决数据一致性问题
  • AOF 机制的 rewrite 模式,AOF 文件没被 rewrite 之前可以进行处理,如删除文件中的 flushall 命令

缺点:

  • AOF 的持久化文件比 RDB 大,恢复速度慢

类的实例化方法调用顺序

类加载器实例化时进行的操作步骤:

加载 -> 连接 -> 初始化

  • 代码书写顺序加载父类静态变量和父类静态代码块
  • 代码书写顺序加载子类静态变量和子类静态代码块
  • 父类非静态变量(父类实例成员变量)
  • 父类非静态代码块
  • 父类构造函数
  • 子类非静态变量(子类实例成员变量)
  • 子类非静态代码块
  • 子类构造函数

动态代理是什么?应用场景?

动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。

Java 中实现动态的方式:

  • JDK 中的动态代理
  • Java类库 CGLib

应用场景:

  • 统计每个 api 的请求耗时
  • 统一的日志输出
  • 校验被调用的 api 是否已经登录和权限鉴定
  • Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程

Mapper 接口如何与写 SQL 的 XML 文件进行绑定的?

  • Mapper 接口与 XML 文件的绑定是通过 XML 里 mapper 标签的 namespace 值与 Mapper 接口的 包路径.接口名 进行绑定
  • Mapper 接口的方法名与 XML 文件中的 sql、select、insert、update、delete 标签的 id 参数值进行绑定
  • 其中涉及到了 MappedStatement 的 id、SqlCommand 的 name 的值为 Mapper 接口的 包路径.接口名.方法名

源码分析

要点 1、Mapper 接口与 XML 文件的绑定是通过 XML 里 mapper 标签的 namespace 值与 Mapper 接口的 包路径.接口名 进行绑定

源码体现在 XMLMapperBuilder 的 bindMapperForNamespace 方法

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
        }
        if (boundType != null && !configuration.hasMapper(boundType)) {
            // Spring may not know the real resource name so we set a flag
            // to prevent loading again this resource from the mapper interface
            // look at MapperAnnotationBuilder#loadXmlResource
            configuration.addLoadedResource("namespace:" + namespace);
            configuration.addMapper(boundType);
        }
    }
}

要点 2、Mapper 接口的方法名与 XML 文件中的 sql、select、insert、update、delete 标签的 id 参数值进行绑定

源码体现在两个部分

1)生成 id 与 MappedStatement 对象注册到 configuration

XMLMapperBuilder configurationElement 方法中

//sql标签
sqlElement(context.evalNodes("/mapper/sql"));
//select、insert、update、delete标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

XMLMapperBuilder sqlElement 方法中

String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
    sqlFragments.put(id, context);
}

XMLStatementBuilder parseStatementNode 方法中

//获取 Mapper xml 中标签 id
String id = context.getStringAttribute("id");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    resultSetTypeEnum, flushCache, useCache, resultOrdered,
    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

MapperBuilderAssistant addMappedStatement 方法中

id = applyCurrentNamespace(id, false);

MapperBuilderAssistant applyCurrentNamespace 方法中

return currentNamespace + "." + base;

MapperBuilderAssistant addMappedStatement 方法中,最后把 MappedStatement 注册到 configuration 对象中

configuration.addMappedStatement(statement);

2)根据 Mapper 接口方法查到并调用对应的 MappedStatement,完成绑定

MapperProxy cachedInvoker 方法创建 PlainMethodInvoker 对象,创建了 MapperMethod 对象

return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));

MapperMethod 对象的 SqlCommand 中的 name 属性根据解析设置为对应的 MappedStatement 的 id

MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);
name = ms.getId();

MapperMethod execute 方法 SqlCommand 类型,通过 sqlSession 根据 SqlCommand 的 name(上一步被设置为 对应的 MappedStatement 的 id) 找到 MappedStatement 执行 select、insert、update、delete

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        .
        .
        .
    return result;
  }
}

JIT 是什么?

Just In Time Compiler 的简称,即时编译器。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是 JIT。

MyISAM索引与InnoDB索引的区别?

  • InnoDB 索引是聚簇索引,MyISAM 索引是非聚簇索引
  • InnoDB 的主键索引的叶子节点存储着行数据,主键索引非常高效
  • MyISAM 索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据
  • InnoDB 非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效

MyBatis 中注册 Mapper 有哪些方式?

方式一:在配置文件 mybatis-config.xml 中添加及其子标签,编写对应的 Mapper 接口与 XML

<mappers>
    <mapper resource="constxiong/mapper/UserMapper.xml"/>
</mappers>

方式二、硬编码方式在 configuration 对象中注册 Mapper 接口

//配置
Configuration configuration = new Configuration(environment);
//注册
configuration.addMapper(UserMapper.class);

HashMap和Hashtable 有什么区别?

JDK 1.8 中 HashMap 和 Hashtable 主要区别如下:

  • 线程安全性不同。HashMap 线程不安全;Hashtable 中的方法是 synchronized 的。
  • key、value 是否允许 null。HashMap 的 key 和 value 都是可以是 null,key 只允许一个 null;Hashtable 的 key 和 value 都不可为 null。
  • 迭代器不同。HashMap 的 Iterator 是 fail-fast 迭代器;Hashtable 还使用了 enumerator 迭代器。
  • hash的计算方式不同。HashMap 计算了 hash值;Hashtable 使用了 key 的 hashCode方法。
  • 默认初始大小和扩容方式不同。HashMap 默认初始大小 16,容量必须是 2 的整数次幂,扩容时将容量变为原来的2倍;Hashtable 默认初始大小 11,扩容时将容量变为原来的 2 倍加 1。
  • 是否有 contains 方法。HashMap 没有 contains 方法;Hashtable 包含 contains 方法,类似于 containsValue。
  • 父类不同。HashMap 继承自 AbstractMap;Hashtable 继承自 Dictionary。

深入的细节,可以参考:

Redis过期键的删除策略有哪些?

  • 定时删除:在设置键的过期时间的同时,创建一个定时器,达到过期时间,执行键的删除操作
  • 惰性删除:不主动删除过期键,从键空间中获取键时,都检查取得的键是否过期,过期则删除;没过期则返回
  • 定期删除:每隔一段时间对数据库进行一次检查,删除里面的过期键。删除多少过期键、检查多少个数据库,由算法决定。

Redis如何实现分布式锁?

实现思路与注意事项:

  • 设置合理的过期时间,解决忘记释放锁、甚至服务器宕机未释放锁的问题

  • 获取锁和设置过期时间,需要具有原子性,使用指令

    SET key value NX PX milliseconds
    NX 代表只有当键key不存在的时候才会设置key的值
    PX 表示设置键 key 的过期时间,单位是毫秒

  • value 值随机设置,删除 value 前判断是否相等,解决当前线程可能释放其他线程加的锁的问题

  • lua 脚本可以解决,删除 value 时判断-删除,非原子操作的问题

如何避免死锁?

并发程序一旦死锁,往往我们只能重启应用。解决死锁问题最好的办法就是避免死锁。

死锁发生的条件

  • 互斥,共享资源只能被一个线程占用
  • 占有且等待,线程 t1 已经取得共享资源 s1,尝试获取共享资源 s2 的时候,不释放共享资源 s1
  • 不可抢占,其他线程不能强行抢占线程 t1 占有的资源 s1
  • 循环等待,线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源

避免死锁的方法

对于以上 4 个条件,只要破坏其中一个条件,就可以避免死锁的发生。

对于第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥。

其他三个条件,我们可以尝试

  • 一次性申请所有的资源,破坏 “占有且等待” 条件
  • 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件
  • 按序申请资源,破坏 “循环等待” 条件

编程中的最佳实践:

  • 使用 Lock 的 tryLock(long timeout, TimeUnit unit)的方法,设置超时时间,超时可以退出防止死锁
  • 尽量使用并发工具类代替加锁
  • 尽量降低锁的使用粒度
  • 尽量减少同步的代码块

示例

使用管理类一次性申请所有的资源,破坏 “占有且等待” 条件示例

package constxiong.concurrency.a023;

import java.util.HashSet;
import java.util.Set;

/**
 * 测试 一次性申请所有的资源,破坏 "占有且等待" 条件示例
 * @author ConstXiong
 * @date 2019-09-24 14:04:12
 */
public class TestBreakLockAndWait {

    //单例的资源管理类
    private final static Manger manager = new Manger();
    
    //资源1
    private static Object res1 = new Object();
    
    //资源2
    private static Object res2 = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            boolean applySuccess = false;
            while (!applySuccess) {
                //向管理类,申请res1和res2,申请失败,重试
                applySuccess = manager.applyResources(res1, res2);
                if (applySuccess) {
                    try {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源成功");
                        synchronized (res1) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res1 资源的锁");
                            //休眠 1秒
                            try {
                                Thread.sleep(1000);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            synchronized (res2) {
                                System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res2 资源的锁");
                            }
                        }
                    } finally {
                        manager.returnResources(res1, res2);//归还资源
                    }
                } else {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源失败");
                    //申请失败休眠 200 毫秒后重试
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        
        new Thread(() -> {
            boolean applySuccess = false;
            while (!applySuccess) {
                //向管理类,申请res1和res2,申请失败,重试
                applySuccess = manager.applyResources(res1, res2);
                if (applySuccess) {
                    try {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源成功");
                        synchronized (res2) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res1 资源的锁");
                            //休眠 1秒
                            try {
                                Thread.sleep(1000);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            synchronized (res1) {
                                System.out.println("线程:" + Thread.currentThread().getName() + " 获取到 res2 资源的锁");
                            }
                        }
                    } finally {
                        manager.returnResources(res1, res2);//归还资源
                    }
                } else {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 申请 res1、res2 资源失败");
                    //申请失败休眠 200 毫秒后重试
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        
    }
    
}

/**
 * 资源申请、归还管理类
 * @author ConstXiong
 * @date 2019-09-24 14:10:57
 */
class Manger {
    
    //资源存放集合
    private Set<Object> resources = new HashSet<Object>();
    
    /**
     * 申请资源
     * @param res1
     * @param res2
     * @return
     */
    synchronized boolean applyResources(Object res1, Object res2) {
        if (resources.contains(res1) || resources.contains(res1)) {
            return false;
        } else {
            resources.add(res1);
            resources.add(res2);
            return true;
        }
    }
    
    /**
     * 归还资源
     * @param res1
     * @param res2
     */
    synchronized void returnResources(Object res1, Object res2) {
        resources.remove(res1);
        resources.remove(res2);
    }
    
}

打印结果如下,线程-1 在线程-0 释放完资源后才能成功申请 res1 和 res2 的锁

线程:Thread-0 申请 res1、res2 资源成功
线程:Thread-0 获取到 res1 资源的锁
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-0 获取到 res2 资源的锁
线程:Thread-1 申请 res1、res2 资源失败
线程:Thread-1 申请 res1、res2 资源成功
线程:Thread-1 获取到 res1 资源的锁
线程:Thread-1 获取到 res2 资源的锁

使用 Lock 的 tryLock() 方法,获取锁失败释放所有资源,破坏 “不可抢占” 条件示例

package constxiong.concurrency.a023;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 测试 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 "不可抢占" 条件
 * @author ConstXiong
 * @date 2019-09-24 14:50:51
 */
public class TestBreakLockOccupation {
    
    private static Random r = new Random(); 

    private static Lock lock1 = new ReentrantLock();
    
    private static Lock lock2 = new ReentrantLock();
    
    public static void main(String[] args) {
        new Thread(() -> {
            //标识任务是否完成
            boolean taskComplete = false;
            while (!taskComplete) {
                lock1.lock();
                System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 成功");
                try {
                    //随机休眠,帮助造成死锁环境
                    try {
                        Thread.sleep(r.nextInt(30));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    
                    //线程 0 尝试获取 lock2
                    if (lock2.tryLock()) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 成功");
                        try {
                            taskComplete = true;
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 失败");
                    }
                } finally {
                    lock1.unlock();
                }
                
                //随机休眠,避免出现活锁
                try {
                    Thread.sleep(r.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        new Thread(() -> {
            //标识任务是否完成
            boolean taskComplete = false;
            while (!taskComplete) {
                lock2.lock();
                System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock2 成功");
                try {
                    //随机休眠,帮助造成死锁环境
                    try {
                        Thread.sleep(r.nextInt(30));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    
                    //线程2 尝试获取锁 lock1
                    if (lock1.tryLock()) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 成功");
                        try {
                            taskComplete = true;
                        } finally {
                            lock1.unlock();
                        }
                    } else {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 获取锁 lock1 失败");
                    }
                } finally {
                    lock2.unlock();
                }
                
                //随机休眠,避免出现活锁
                try {
                    Thread.sleep(r.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    
}

打印结果如下

线程:Thread-0 获取锁 lock1 成功
线程:Thread-1 获取锁 lock2 成功
线程:Thread-1 获取锁 lock1 失败
线程:Thread-1 获取锁 lock2 成功
线程:Thread-0 获取锁 lock2 失败
线程:Thread-1 获取锁 lock1 成功
线程:Thread-0 获取锁 lock1 成功
线程:Thread-0 获取锁 lock2 成功

按照一定的顺序加锁,破坏 “循环等待” 条件示例

package constxiong.concurrency.a023;

/**
 * 测试 按序申请资源,破坏 "循环等待" 条件
 * @author ConstXiong
 * @date 2019-09-24 15:26:23
 */
public class TestBreakLockCircleWait {

    private static Object res1 = new Object();
    
    private static Object res2 = new Object();
    
    
    public static void main(String[] args) {
        new Thread(() -> {
            Object first = res1;
            Object second = res2;
            //比较 res1 和 res2 的 hashCode,如果 res1 的 hashcode > res2,交换 first 和 second。保证 hashCode 小的对象先加锁
            if (res1.hashCode() > res2.hashCode()) {
                first = res2;
                second = res1;
            }
            synchronized (first) {
                System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + first + " 锁成功");
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized(second) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + second + " 锁成功");
                }
            }
        }).start();
        
        new Thread(() -> {
            Object first = res1;
            Object second = res2;
            //比较 res1 和 res2 的 hashCode,如果 res1 的 hashcode > res2,交换 first 和 second。保证 hashCode 小的对象先加锁
            if (res1.hashCode() > res2.hashCode()) {
                first = res2;
                second = res1;
            }
            synchronized (first) {
                System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + first + " 锁成功");
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized(second) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "获取资源 " + second + " 锁成功");
                }
            }
        }).start();
    }
    
}

打印结果如下

线程:Thread-0获取资源 java.lang.Object@7447157c 锁成功
线程:Thread-0获取资源 java.lang.Object@7a80f45c 锁成功
线程:Thread-1获取资源 java.lang.Object@7447157c 锁成功
线程:Thread-1获取资源 java.lang.Object@7a80f45c 锁成功

MyBatis 中实体类的属性名与表中的字段名不一致怎么处理?

1、修改 SQL,给查询字段重命名,如 将 user_id 重命名为 userId

select user_id as userId from table

2、MyBatis 的 XML 映射文件中,使用 标签,定义数据库字段名与实体 bean 的属性字段名的映射关系

<select id="getUser" parameterType="int" resultMap="”UserMap”">
    select * from user where user_id=#{id}
</select>
 
<resultMap type=”User” id=”UserMap”>
    <!–- id 标签映射主键字段 -–>
    <id property=”id” column=user_id>

    <!–- result 标签映射非主键字段,property 为实体 bean 属性名,column 为数据库表中字段名 -–>
    <result property=“userName” column =”user_name”/>
</reslutMap>

linux指令-more

阅读命令,与 cat 类似, more 会以一页一页的显示方便逐页阅读,按空格键(space)就往下一页显示,按 b 键就会往回(back)一页显示

命令参数:
+n        从笫 n 行开始显示
-n        定义屏幕大小为n行
+/pattern 在每个档案显示前搜寻该字串(pattern),然后从该字串前两行之后开始显示 
-c        从顶部清屏,然后显示
-d        提示“Press space to continue,’q’ to quit(按空格键继续,按q键退出)”,禁用响铃功能
-l        忽略Ctrl+l(换页)字符
-p        通过清除窗口而不是滚屏来对文件进行换页,与-c选项相似
-s        把连续的多个空行显示为一行
-u        把文件内容中的下画线去掉

常用操作命令:
Enter    向下 n 行,需要定义。默认为 1 行
Ctrl+F   向下滚动一屏
空格键   向下滚动一屏
Ctrl+B   返回上一屏
=        输出当前行的行号
:f       输出文件名和当前行的行号
V        调用vi编辑器
!命令    调用Shell,并执行命令
q        退出more

more +3 text.txt 显示文件中从第3行起的内容
ls -l | more -5 在所列出文件目录详细信息,每次显示 5 行

Spring中BeanFactory.getBean是否线程安全?

getBean 的默认实现的入口是在 AbstractBeanFactory#doGetBean 方法
结合源码来看,创建 bean 的线程安全是通过可并发容器 + 加锁 synchronized 保证的
比如列举几个可说的点:

  • 根据 beanName 获取是否存在早期已缓存的单例 bean,存在 get、判空、put、remove 操作,所以加了锁。如源码1
  • 合并 BeanDefinition 成 RootDefinition 时,AbstractBeanFactory#getMergedBeanDefinition 方法也加了锁。如源码 2

类似之处还有很多,可结合源码进行查看

源码1:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return s



源码2:
protected RootBeanDefinition getMergedBeanDefinition(
            String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
            throws BeanDefinitionStoreException {

        synchronized (this.mergedBeanDefinitions) {
            RootBeanDefinition mbd = null;
            RootBeanDefinition previous = null;

并行是什么意思?与并发的区别是什么?

并行:指两个或两个以上事件或活动在同一时刻发生。如多个任务在多个 CPU 或 CPU 的多个核上同时执行,不存在 CPU 资源的竞争、等待行为。

image_20190826194942.png

并行与并发的区别

  • 并行指多个事件在同一个时刻发生;并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。
  • 并行没有对 CPU 资源的抢占;并发执行的线程需要对 CPU 资源进行抢占。
  • 并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。

Java 中的多线程

  • 通过 JDK 中的 java.lang.Thread 可以实现多线程。
  • Java 中多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源( CPU 的个数和 CPU 的核数)。
  • CPU 资源比较充足时,多线程被分配到不同的 CPU 资源上,即并行;CPU 资源比较紧缺时,多线程可能被分配到同个 CPU 的某个核上去执行,即并发。
  • 不管多线程是并行还是并发,都是为了提高程序的性能。

说说Redis的同步机制?

2.8 版以前

Redis 通过同步(sync)和指令传播(command propagate)两个操作完成同步

同步(sync):将从节点的数据库状态更新至与主节点的数据库状态一致

  • 从节点向主节点发送 SYNC 指令
  • 收到 SYNC 指令,主节点执行 BGSAVE 指令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从现在开始执行的所有写指令
  • 主节点 BGSAVE 指令执行后,会将生成的 RDB 文件发送给从节点
  • 从节点接收、载入 RDB 文件,将数据库状态更新至主节点执行 BGSAVE 指令时的数据库状态
  • 从节点加载完 RDB 文件,通知主节点将记录在缓冲区里面的所有写指令发送给从节点,从节点执行这些写指令,将数据库状态更新至主节点当前数据库状态

指令传播(command propagate):主节点数据被修改,会主动向从节点发送执行的写指令,从节点执行之后,两个节点数据状态又保持一致

为了解决主从节点断线复制低效的问题(SYNC过程中生成、传输、载入 RDB 文件耗费大量 CPU、内存、磁盘 IO 资源),2.8 版开始新增 PSYNC 指令。

PSYNC 具有两种模式

  • 完整重同步(full resynchronization),与SYNC过程基本一致
  • 部分重同步(partial resynchronization),借助复制偏移量、复制积压缓冲区、服务器运行 ID ,完成主从节点断开连接后,从节点重连主节点后,条件允许,主节点将连接断开期间执行的写指令发送给从节点,从节点接收并执行写指令,将数据库更新至主节点当前状态

Mapper 接口中能不能根据参数不同进行重载?

不能

  • MapperedStatement 的 id 属性值等于 Mapper 接口的 包名.接口名.方法名 作为 key 添加到 Configuration 对象的 Map 结构的 mappedStatements 属性里
  • 查找 MapperedStatement 执行 SQL 时,也是根据 Mapper 接口的 包名.接口名.方法名 作为 SqlCommand 的 name 属性值,在 Configuration 对象的 mappedStatements 找到对应的 MapperedStatement 对象
  • 即接口中方法名相同 key 就相同,只能获取一个 MapperedStatement 对象,无法重载

脏读、幻读、不可重复读指什么?

  • 脏读:一个事务读取另外一个事务还没有提交的数据。

  • sql 1992 标准

    http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

    1. P2 (“Non-repeatable read”): SQL-transaction T1 reads a row. SQL-

      transaction T2 then modifies or deletes that row and performs
      a COMMIT. If T1 then attempts to reread the row, it may receive
      the modified value or discover that the row has been deleted.

    2. P3 (“Phantom”): SQL-transaction T1 reads the set of rows N
      that satisfy some. SQL-transaction T2 then
      executes SQL-statements that generate one or more rows that
      satisfy theused by SQL-transaction T1. If
      SQL-transaction T1 then repeats the initial read with the same
      , it obtains a different collection of rows.

    **不可重复读:**事务 T1 读到某行;事务 T2 修改或删除这行,提交事务;T1 重新读取发现这行数据已经被修改或删除。

    **幻读:**事务 T1 读取了 N 行;事务 T2 在事务 T1 读取的条件范围内生成了一行或多行数据;T1 重新读取获得与之前不同集合的行数据。

    mysql 官网的术语解释,8.0 最新版

    https://dev.mysql.com/doc/refman/8.0/en/glossary.html

    non-repeatable read
    The situation when a query retrieves data, and a later query within the same transaction retrieves what should be the same data, but the queries return different results (changed by another transaction committing in the meantime).

    phantom
    A row that appears in the result set of a query, but not in the result set of an earlier query. For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the WHERE clause of the query.

    This occurrence is known as a phantom read. It is harder to guard against than a non-repeatable read, because locking all the rows from the first query result set does not prevent the changes that cause the phantom to appear.

    **不可重复读:**一个事务内,两次相同条件的查询返回了不同的结果。

    **幻读:**同一个事务中,一条数据出现在这次查询的结果集里,却没有出现在之前的查询结果集中。例如,在一个事务中进行了同一个查询运行了两次,期间被另外一个事务提交插入一行或修改查询条件匹配的一行。它比不可重复读更难防范,因为锁定第一个查询结果集的所有行并不能阻止导致幻象出现的更改。

    从以上两处的定义可以看出

     影响因素
     sql 1992 标准mysql 术语解释
    不可重复读其他事务修改或删除未明确不同结果的原因
    幻读新增一行或多行新增或修改

    不同出处的规定存在细微差别,并非完全统一。

#{} 和 ${} 的区别

  • MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ?,预编译 SQL,通过 PreparedStatement 的 setXxxx 的方法进行参数赋值。使用 #{} 可以有效地防止 SQL 注入。
  • MyBatis 在处理 ${} 时,会直接把 ${} 替换为参数值,存在 SQL 注入的风险。
  • #{} 比 ${} 安全,但还是提供了 ${} 这种动态替换参数的方式,是因为有些复杂的 SQL 使用场景通过预编译的方式比较麻烦,且在代码中完全可以做到控制非法参数,有些参数可能是一些常量或字段值。

PS:

SQL 注入是在编译的过程中,注入了某些特殊的恶意 SQL 片段,被编译成了恶意的 SQL 执行操作。

预编译是提前对 SQL 进行编译,后面注入的参数不会对 SQL 的结构产生影响,从而避免安全风险。

synchronized和Lock有什么区别?

  • 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
  • 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
  • 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
  • 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
  • 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率

linux指令-chmod

用于改变 linux 系统文件或目录的访问权限
该命令有两种用法:一种是包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法
每一文件或目录的访问权限都有三组,每组用三位代号表示:

  • 文件属主的读、写和执行权限

  • 与属主同组的用户的读、写和执行权限

  • 系统中其他用户的读、写和执行权限

    常用参数:
    -c 当发生改变时,报告处理信息
    -R 处理指定目录以及其子目录下所有文件

    权限范围:
    u:目录或者文件的当前的用户
    g:目录或者文件的当前的群组
    o:除了目录或者文件的当前用户或群组之外的用户或者群组
    a:所有的用户及群组

    权限代号:
    r:读权限,用数字4表示
    w:写权限,用数字2表示
    x:执行权限,用数字1表示
    -:删除权限,用数字0表示
    s:特殊权限

Java中实现线程通信方式有哪些?

  • 对象的 wait(long timeout)、wait(long timeout, int nanos)、wait() 方法,组合对象的 notify()、notifyAll()
  • 显示锁:Lock.newCondition()、Condition await 系列方法、Condition signal()、signalAll()
  • 信号量:Semaphore acquire 系列方法、release()系列方法

解析xml的方式有哪些?如何选择?

常用的两种方式:DOM 和 SAX

  • DOM 建立树形结构的方式解析 XML 文档,DOM 解析器把 XML 文档转化为一个包含节点信息的树,可以对树的访问与修改,读取和修改 XML。
  • SAX 采用事件模型,解析 XML 文档时可以触发一系列事件,解析到期望的节点,可以激活一个回调方法,处理该节点上的数据

选择:

  • 大文档解析,使用 SAX 方式
  • 仅需要解析出 XML 文档中的特定信息,使用 SAX 方式
  • 希望速度快、占用内存少,使用 SAX 方式
  • 频繁对 XML 修改,使用 DOM 方式
  • 需要快速定位 XML 不同层次节点,使用 DOM 方式

简述oracle中 dml、ddl、dcl

  • dml 数据操纵语言,如 select、update、delete、insert
  • ddl 数据定义语言,如 create table 、drop table
  • dcl 数据控制语言,如 commit、rollback、grant、invoke

session的工作原理?

1、什么是 session
session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息。

由于 http 协议是无状态的,即 http 请求一次连接一次,数据传输完毕,连接就断开了,下次访问需要重新连接。

通过 cookie 中的 sessionid 字段和服务器端的 session 关联,可以确定会话的身份信息。

2、session 比 cookie 更安全
用户信息可以通过加密存储到 cookie,但是这样做的安全性很差,浏览器的 cookie 的容易被其他程序获取和篡改。使用 session 的意义在于 session 存储在服务器,相对安全性更高。

3、session 的生命周期

  • 创建

浏览器访问服务器的 servlet(jsp)时,服务器会自动创建 session,并把 sessionid 通过 cookie 返回到浏览器。

servlet 规范中,通过 request.getSession(true) 可以强制创建 session。

  • 销毁

服务器会默认给 session 一个过期时间,即从该 session 的会话在有效时间内没有再被访问就会被设置过超时,需要重新建立会话。

如 tomcat 的默认会话超时时间为30分钟。

会话超时时间是可以通过配置文件设置,如修改 web.xml 、server.xml 文件

1、web.xml 文件
 
<session-config>
    <session-timeout>30</session-timeout>
</session-config>
 
 
 
2、server.xml 文件
 
<Context path="/demo" docBase="/demo" defaultSessionTimeOut="3600" 
isWARExpanded="true" isWARValidated="false" isInvokerEnabled="true" isWorkDirPersistent="false"/>

调用 servlet api 手动设置 session 超时时间

request.getSession().setMaxInactiveInterval(60 * 30);//session 30分钟失效

调用 servlet api 手动销毁 session

request.getSession().invalidate();

4、注意事项
如果浏览器禁用 cookie,默认情况下 session 无法生效。可以通过url重载携带 sessionid 参数、把 sessionid 设置为 http 协议 header 设为其他自定义字段中,请求中始终携带。
当用户量很大、 session 的失效时间很长,需要注意 session 的查找和存储对服务器性能的影响。
web 容器可以设置 session 的钝化(从内存持久化到文件) 和 活化(从文件读到内存),提高性能。

linux指令-cp

复制,将多文件或目录复制至目标目录(shell 脚本中不加 -i 参数会直接覆盖不会提示)

常用命令:
-i 提示
-r 复制目录及目录内所有项目
-a 复制的文件与原文件时间一样

cp -ai a.txt test  复制 a.txt 到 test 目录下,保持原文件时间,如果原文件存在提示是否覆盖。 
cp -s a.txt a_link.txt  为 a.txt 文件创建一个链接

静态方法能直接调用非静态方法吗?

不能

  • 静态方法只能访问静态成员
  • 调用静态方法时可能对象并没有被初始化,此时非静态变量还未初始化
  • 非静态方法的调用和非静态成员变量的访问要先创建对象

linux指令-find

查找文件树命令,用于在文件树中查找文件,并作出相应的处理。

命令格式:find pathname -options [-print -exec -ok ...]
命令参数:
pathname: 查找的目录路径
-print: 匹配的文件输出到标准输出
-exec: 对匹配的文件执行该参数所给出的 shell 命令
-ok: 和-exec的作用相同,只不过以一种更为安全的模式来执行该参数所给出的 shell 命令,在执行每一个命令之前,都会给出提示,让用户来确定是否执行

命令选项:
-name 按照文件名查找文件
-perm 按文件权限查找文件
-user 按文件属主查找文件
-group  按照文件所属的组来查找文件。
-type  查找某一类型的文件
    b - 块设备文件
    d - 目录
    c - 字符设备文件
    l - 符号链接文件
    p - 管道文件
    f - 普通文件

-size n :[c] 查找文件长度为n块文件,带有 c 时表文件字节大小
-amin n   查找系统中最后 n 分钟访问的文件
-atime n  查找系统中最后 n*24小时访问的文件
-cmin n   查找系统中最后 n 分钟被改变文件状态的文件
-ctime n  查找系统中最后 n*24小时被改变文件状态的文件
-mmin n   查找系统中最后 n 分钟被改变文件数据的文件
-mtime n  查找系统中最后 n*24 小时被改变文件数据的文件,用减号 - 来限定更改时间在距今 n 日以内的文件,而用加号 + 来限定更改时间在距今n日以前的文件
-maxdepth n 最大查找目录深度
-prune 选项来指出需要忽略的目录
-newer 查找更改时间比某个文件新但比另一个文件旧的所有文件

MyBatis 的适用场景

  • 直接编写 SQL,对应多变的需求改动较小
  • 对性能的要求很高,做 SQL 的性能优化相对简单、直接

==和equals的区别是什么?

  • == 是关系运算符,equals() 是方法,结果都返回布尔值
  • Object 的 == 和 equals() 比较的都是地址,作用相同

== 作用:

  • 基本类型,比较值是否相等
  • 引用类型,比较内存地址值是否相等
  • 不能比较没有父子关系的两个对象

equals()方法的作用:

  • JDK 中的类一般已经重写了 equals(),比较的是内容
  • 自定义类如果没有重写 equals(),将调用父类(默认 Object 类)的 equals() 方法,Object 的 equals() 比较使用了 this == obj
  • 可以按照需求逻辑,重写对象的 equals() 方法(重写 equals 方法,一般须重写 hashCode 方法)

Java中如何进行异常处理?throws、throw、try、catch、finally分别如何使用?

  • 把各种不同的异常进行分类

  • 每个异常都是一个对象,是 Throwable 或其子类的实例

  • 一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用对象的方法可以捕获到这个异常并进行处理

  • Java 中的异常处理通过 5 个关键词实现:throw、throws、try、catch 和 finally

    定义方法时,可以使用 throws 关键字抛出异常
    方法体内使用 throw 抛出异常
    使用 try 执行一段代码,当出现异常后,停止后续代码的执行,跳至 catch 语句块
    使用 catch 来捕获指定的异常,并进行处理
    finally 语句块表示的语义是在 try、catch 语句块执行结束后,最后一定会被执行

truncate和delete的异同?

  • truncate 命令永久地从表中删除所有数据;delete 命令从一个表中删除某一行或多行数据
  • truncate 和 delete 都可以将数据实体删掉,truncate 的操作并不记录到 rollback 日志,操作速度较快,删除数据不能恢复
  • delete 操作不释放表空间
  • truncate 不能对视图等进行删除;delete 可以删除单表的视图数据(本质是对表数据的删除)
  • truncate 是数据定义语言(DDL);delete 是数据操纵语言(DML)

你有哪些手段来排查 OOM 的问题?

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录
  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域
  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用

什么是单例模式?有什么作用和特点?可以解决哪些问题?懒汉式和饿汉式的区别?如何保证线程安全?

单例模式:

一个类只允许创建一个实例对象,并提供访问其唯一的对象的方式。这个类就是一个单例类,这种设计模式叫作单例模式。

作用:

避免频繁创建和销毁系统全局使用的对象。

单例模式的特点:

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例的访问

应用场景:

  • 全局唯一类,如 系统配置类、系统硬件资源访问类
  • 序列号生成器
  • Web 计数器

饿汉式与懒汉式的区别:

  • 饿汉式是类一旦加载,就把单例初始化完成,保证 getInstance() 方法被调用时的时候,单例已经初始化完成,可以直接使用。
  • 懒汉式比较懒,只有当被调用 getInstance() 方法时,才会去初始化这个单例。

线程安全性问题:

饿汉式,在被调用 getInstance() 方法时,单例已经由 jvm 加载初始化完成,所以并发访问 getInstance() 方法返回的都是同一实例对象,线程安全。

懒汉式,要保证线程安全,可以有以下几种方式:

  • 给静态 getInstance() 方法加锁,性能差
  • getInstance() 方法双重检查给类加锁后创建对象(以上两种低版本 JDK,由于指令重排,需要加 volatile 关键字,否则创建出多个对象;JDK 1.5 内存模型加强后解决了对象 new 操作和初始化操作的原子性问题)
  • 通过静态内部类实现
  • 通过枚举实现

示例代码:
1、饿汉式

package constxiong.interview;

/**
 * 单例模式 饿汉式
 * @author ConstXiong
 */
public class TestSingleton {

    private static final TestSingleton instance = new TestSingleton();
    
    private TestSingleton() {
    }
    
    public static TestSingleton getInstance() {
        return instance;
    }
    
}

2、懒汉式:线程不安全

package constxiong.interview;

/**
 * 单例模式 懒汉式-线程不安全
 * @author ConstXiong
 */
public class TestSingleton {

    private static TestSingleton instance;
    
    private TestSingleton() {
    }
    
    public static TestSingleton getInstance() {
        if (instance == null) {
            instance = new TestSingleton();
        }
        return instance;
    }
    
}

3、懒汉式:getInstance() 方法加锁,线程安全,性能差

package constxiong.interview;

/**
 * 单例模式 懒汉式-加锁
 * @author ConstXiong
 */
public class TestSingleton {

    private static volatile TestSingleton instance;
    
    private TestSingleton() {
    }
    
    public static synchronized TestSingleton getInstance() {
        if (instance == null) {
            instance = new TestSingleton();
        }
        return instance;
    }
    
}

4、懒汉式:双重检查 + 对类加锁

package constxiong.interview;

/**
 * 单例模式 懒汉式-双重检查 + 对类加锁
 * @author ConstXiong
 */
public class TestSingleton {

    private static volatile TestSingleton instance;
    
    private TestSingleton() {
    }
    
    public static TestSingleton getInstance() {
        if (instance == null) {
            synchronized (TestSingleton.class) {
                if (instance == null) {
                    instance = new TestSingleton();
                }
            }
        }
        return instance;
    }
    
}

5、懒汉式:静态内部类

package constxiong.interview;

/**
 * 单例模式 懒汉式-静态内部类
 * @author ConstXiong
 */
public class TestSingleton {

    private static class SingletonHolder {
        private static final TestSingleton instance = new TestSingleton();
    }
    
    private TestSingleton() {
    }
    
    public static TestSingleton getInstance() {
        return SingletonHolder.instance;
    }
    
}

6、懒汉式:枚举

package constxiong.interview;

import java.util.concurrent.atomic.AtomicLong;

/**
 * 单例模式 懒汉式-枚举,id生成器
 * @author ConstXiong
 */
public enum TestSingleton {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);
    public long getId() {
        return id.incrementAndGet();
    }
}

实现方式的选择建议:

  • 没有特殊要求,建议使用 1、饿汉式,提前初始化好对象,虽然提前占用内存资源和提前了初始化的时间,但避免了懒加载过程中程序出现内存不够、超时等问题,符合 fail-fast 原则。
  • 明确要求懒加载,可以使用 5、静态内部类的方式
  • 有其他特殊要求,使用 4、双重检查 + 对类加锁的方法

MyBatis 中的本地缓存和二级缓存的作用是什么?怎么实现的?

本地缓存

作用:

SqlSession 级别的缓存,默认开启,在 MyBatis 配置文件中可以修改 MyBatis 文件中 标签 localCacheScope 参数值改变缓存的作用域。statementId、boundSql.getSql() 执行 sql、查询参数、RowBounds 都相同,即认为是同一次查询,返回缓存值。

实现原理:

每个 SqlSession 对象包含一个 Executor 对象,Executor 对象中 localCache 属性使用 PerpetualCache 对象缓存查询数据;从源码中看 DefaultSqlSession 的 close、commit、rollback、insert、delete、update 相关的方法都会触发 BaseExecutor 对象清掉缓存。

二级缓存

作用:
MappedStatement 级别的缓存,默认不开启,可以在 Mapper xml 中通过 标签开启 或者 MyBatis 文件中 标签设置 cacheEnabled 参数为 true 全局开启 或者 mapper xml 配置文件中的 select 节点需要加上属性 useCache,在 SqlSession 关闭或提交之后才会生效。

开启二级缓存的默认作用摘自官网

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

实现原理:

  • XMLMappedBuilder 解析 Mapper xml 中的 、 标签
  • 通过 builderAssistant 对象 addMappedStatement 方法,设置 cache 信息到 MappedStatement 对象内
  • CachingExecutor 对象的 query 方法先 MappedStatement 对象中 getCache() 获取缓存 Cache 对象,如果没有查到则到 BaseExecutor 中查询,走本地缓存逻辑

Redis的内存用完了会发生什么?

这个跟 Redis 的内存回收策略有关。

Redis 的默认回收策略是 noenviction,当内存用完之后,写数据会报错。

Redis 的其他内存回收策略含义:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最近最少使用的数据
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最早会过期的数据
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中,随机淘汰数据
  • allkeys-lru:从数据集(server.db[i].dict)中,淘汰最近最少使用的数据
  • allkeys-random:从数据集(server.db[i].dict)中,随机淘汰数据

this和super关键字的作用

this:

  • 对象内部指代自身的引用
  • 解决成员变量和局部变量同名问题
  • 可以调用成员变量
  • 不能调用局部变量
  • 可以调用成员方法
  • 在普通方法中可以省略 this
  • 在静态方法当中不允许出现 this 关键字

super:

  • 代表对当前对象的直接父类对象的引用
  • 可以调用父类的非 private 成员变量和方法
  • super(); 可以调用父类的构造方法,只限构造方法中使用,且必须是第一条语句

java 有哪些常用容器(集合)?

Java 容器分为 Collection 和 Map 两大类,各自都有很多子类。

Collection

|  ├AbstractCollection 对Collection接口的最小化抽象实现

|  │

|  ├List 有序集合

|  │-├AbstractList 有序集合的最小化抽象实现

|  │-├ArrayList 基于数组实现的有序集合

|  │-├LinkedList 基于链表实现的有序集合

|  │-└Vector 矢量队列

|  │ └Stack 栈,先进后出

|  │

|  ├Set 不重复集合

|  │├AbstractSet 不重复集合的最小化抽象实现

|  │├HashSet 基于hash实现的不重复集合,无序

|  │├LinkedHashSet 基于hash实现的不重复集合,有序

|  │└SortedSet 可排序不重复集合

|  │ └NavigableSet 可导航搜索的不重复集合

|  │ └TreeSet 基于红黑树实现的可排序不重复集合

|  │

|  ├Queue 队列

|  │├AbstractQueue 队列的核心实现

|  │├BlockingQueue 阻塞队列

|  │└Deque 可两端操作线性集合

|

Map 键值映射集合

|  ├AbstractMap 键值映射集合最小化抽象实现

|  ├Hashtable 基于哈希表实现的键值映射集合,key、value均不可为null

|  ├HashMap 类似Hashtable,但方法不同步,key、value可为null

|   └LinkedHashMap 根据插入顺序实现的键值映射集合

|  ├IdentityHashMap 基于哈希表实现的键值映射集合,两个key引用相等==,认为是同一个key

|  ├SortedMap 可排序键值映射集合

|   └NavigableMap 可导航搜索的键值映射集合

|  └WeakHashMap 弱引用建,不阻塞被垃圾回收器回收,key回收后自动移除键值对

20191002072124515_20191013143527.jpg

可以比较的点:

  • 有序、无序
  • 可重复、不可重复
  • 键、值是否可为null
  • 底层实现的数据结构(数组、链表、哈希…)
  • 线程安全性

Dubbo的注册中心挂掉,Consumer和Provider之间还能通讯吗?

可以通讯

  • 注册中心集群,发生宕机会自动切换
  • 启动 Dubbo 时,Consumer 会从 zookeeper 拉取 Provider 注册的地址、接口等数据,缓存在本地
  • Consumer 每次调用时,按照本地存储的 Provider 地址进行调用
  • Provider 全部宕机,Consumer 会无法使用,并无限次重连等待 Provider 恢复
  • 无法增加和调用新服务

声明合法的是()

答案:AD

分析:

  • int 类型申明不需要在值后面加字母,如 int = 4
  • float 类型申明需要在值后面加字母 f 或 F,如 float f = 12.34f

linux指令-ps

ps(process status),用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用 top 指令

linux 系统中进程有5种状态:

  • 运行(正在运行或在运行队列中等待)
  • 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
  • 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
  • 僵死(进程已终止, 但进程描述符存在, 直到父进程调用 wait4() 系统调用后释放)
  • 停止(进程收到 SIGSTOP, SIGSTP, SIGTIN, SIGTOU 信号后停止运行)

ps 工具标识进程的5种状态码:

  • R 运行 runnable

  • S 中断 sleeping

  • D 不可中断 uninterruptible sleep

  • Z 僵死 a defunct process

  • T 停止 traced or stopped

    常用参数:
    -A 显示所有进程
    -a 显示同一终端下所有进程
    -f: full 展示进程详细信息
    -e: every 展示所有进程信息
    -ax: all 与 -e 同,展示所有进程信息
    -o: 设置输出格式, 可以指定需要输出的进程信息列
    -L: 展示线程信息
    -C: 获取指定命令名的进程信息
    -t: tty 展示关联指定 tty 的进程
    –forest: 展示进程数
    –sort: 按照某个或者某些进程信息列排序展示
    a 显示所有进程
    c 显示进程真实名称
    e 显示环境变量
    f 显示进程间的关系
    r 显示当前终端运行的进程

    -aux 显示所有包含其它使用的进程
    -ef 显示所有当前进程信息
    ps -C bash 显示指定名称的进程信息
    ps -eLf 显示当前系统中的线程信息
    ps -ef --forest 显示进程树

Dubbo框架分了哪些层?

Dubbo 框架设计一共划分了 10 层:

  • 服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现
  • 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心
  • 服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton
  • 服务注册层(Registry):封装服务地址的注册与发现,以服务 URL 为中心
  • 集群层(Cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心
  • 监控层(Monitor):RPC 调用次数和调用时间监控
  • 远程调用层(Protocol):封将 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocol、Invoker、Exporter
  • 信息交换层(Exchange):封装请求响应模式,同步转异步,以 Request 和 Response 为中心
  • 网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心
  • 数据序列化层(Serialize):序列化的一些工具

linux指令-chown

将指定文件的拥有者改为指定的用户或组

用户可以是用户名或者用户 ID
组可以是组名或者组 ID
文件是以空格分开的要改变权限的文件列表,支持通配符

常用参数:
-c 显示更改的部分的信息
-R 处理指定目录及子目录

示例
chown -c log:log log.log  改变文件 log.log 的拥有者和群组都为 log 并显示改变信息 
chown -c :log log.log  改变文件 log.log 的群组为 log
chown -cR log: log/  改变文件夹 log 及子文件、目录属主 log 

jsp有哪些内置对象?作用分别是什么?

  1. request:对应 Java 类 javax.servlet.http.HttpServletRequest;客户端的请求信息:Http协议头信息、Cookie、请求参数等
  2. response:对应 Java 类 javax.servlet.http.HttpServletRespons;用于服务端响应客户端请求,返回信息
  3. pageContext:对应 Java 类 javax.servlet.jsp.PageContext;页面的上下文
  4. session:对应 Java 类 javax.servlet.http.HttpSession;客户端与服务端之间的会话
  5. application:对应 Java 类 javax.servlet.ServletContext;用于获取服务端应用生命周期的信息
  6. out:对应 Java 类 javax.servlet.jsp.JspWriter;用于服务端传输内容到客户端的输出流
  7. config:对应 Java 类 javax.servlet.ServletConfig;初始化时,Jsp 引擎向 Jsp 页面传递的信息
  8. page:对应 Java 类 java.lang.Object;指向 Jsp 页面本身
  9. exception:对应 Java 类 java.lang.Throwabl;页面发生异常,产生的异常对象

Java针对不同的应用场景提供了哪些版本?

J2SE:Standard Edition(标准版) ,包含 Java 语言的核心类。如IO、JDBC、工具类、网络编程相关类等。从JDK 5.0开始,改名为Java SE。

J2EE:Enterprise Edition(企业版),包含 J2SE 中的类和企业级应用开发的类。如web相关的servlet类、JSP、xml生成与解析的类等。从JDK 5.0开始,改名为Java EE。

J2ME:Micro Edition(微型版),包含 J2SE 中的部分类,新添加了一些专有类。一般用设备的嵌入式开发,如手机、机顶盒等。从JDK 5.0开始,改名为Java ME。

如何保证Redis中存的都是热点数据?

Redis存储在内存中的数据升到配置大小时,就进行数据淘汰

使用 allkeys-lru 策略,从数据集(server.db[i].dict)中挑选最近最少使用的数据优先淘汰,即可满足保存热点数据

linux指令-cd

切换目录,changeDirectory 的缩写
命令语法:cd [目录名]

cd / 进入要目录
cd ~ 进入 "home" 目录
cd - 进入上一次工作路径
cd !$ 把上个命令的参数作为cd参数使用

Oracle中字符串链接符是什么?

Oracle中使用 || 这个符号连接字符串

如 ‘Const’ || ‘Xiong’

说说Redis的回收策略

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最近最少使用的数据
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最早会过期的数据
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中,随机淘汰数据
  • allkeys-lru:从数据集(server.db[i].dict)中,淘汰最近最少使用的数据
  • allkeys-random:从数据集(server.db[i].dict)中,随机淘汰数据
  • noenviction:Redis 的默认策略,不回收数据,当达到最大内存时,新增数据返回 error

注:

  • volatile 是对已设置过期时间的数据集淘汰数据
  • allkeys 是从全部数据集淘汰数据
  • lru 是 Least Recently Used 的缩写,即最近最少使用
  • ttl 指令可以获取键到期的剩余时间(秒),这里的意思是淘汰最早会过期的数据

Queue的add()和offer()方法有什么区别?

  • Queue 中 add() 和 offer() 都是用来向队列添加一个元素。
  • 在容量已满的情况下,add() 方法会抛出IllegalStateException异常,offer() 方法只会返回 false 。

JDK1.8 源码中的解释

/**
 * Inserts the specified element into this queue if it is possible to do so
 * immediately without violating capacity restrictions, returning
 * {@code true} upon success and throwing an {@code IllegalStateException}
 * if no space is currently available.
 *
 * @param e the element to add
 * @return {@code true} (as specified by {@link Collection#add})
 * @throws IllegalStateException if the element cannot be added at this
 *         time due to capacity restrictions
 * @throws ClassCastException if the class of the specified element
 *         prevents it from being added to this queue
 * @throws NullPointerException if the specified element is null and
 *         this queue does not permit null elements
 * @throws IllegalArgumentException if some property of this element
 *         prevents it from being added to this queue
 */
boolean add(E e);
 
/**
 * Inserts the specified element into this queue if it is possible to do
 * so immediately without violating capacity restrictions.
 * When using a capacity-restricted queue, this method is generally
 * preferable to {@link #add}, which can fail to insert an element only
 * by throwing an exception.
 *
 * @param e the element to add
 * @return {@code true} if the element was added to this queue, else
 *         {@code false}
 * @throws ClassCastException if the class of the specified element
 *         prevents it from being added to this queue
 * @throws NullPointerException if the specified element is null and
 *         this queue does not permit null elements
 * @throws IllegalArgumentException if some property of this element
 *         prevents it from being added to this queue
 */
boolean offer(E e);

创建进程需要()

答案:BC

分析:

  • 任务调度的单位是线程
  • 如果未涉及对文件的操作,可能不会分配文件描述符

什么是Java的垃圾回收机制?

垃圾回收机制,简称 GC

  • Java 语言不需要程序员直接控制内存回收,由 JVM 在后台自动回收不再使用的内存
  • 提高编程效率
  • 保护程序的完整性
  • JVM 需要跟踪程序中有用的对象,确定哪些是无用的,影响性能

特点

  • 回收 JVM 堆内存里的对象空间,不负责回收栈内存数据
  • 无法处理一些操作系统资源的释放,如数据库连接、输入流输出流、Socket 连接
  • 垃圾回收发生具有不可预知性,程序无法精确控制垃圾回收机制执行
  • 可以将对象的引用变量设置为 null,垃圾回收机制可以在下次执行时回收该对象。
  • JVM 有多种垃圾回收 实现算法,表现各异
  • 垃圾回收机制回收任何对象之前,会先调用对象的 finalize() 方法
  • 可以通过 System.gc() 或 Runtime.getRuntime().gc() 通知系统进行垃圾回收,会有一些效果,但系统是否进行垃圾回收依然不确定
  • 不要主动调用对象的 finalize() 方法,应该交给垃圾回收机制调用

静态与非静态成员变量区别?

  • 生命周期不同:非静态成员变量随着对象的创建而存在;静态成员变量随着类的加载而存在
  • 调用方式不同:非静态成员变量用 对象名.变量名 调用;静态成员变量用 类名.变量名,JDK1.7 以后也能用对象名.变量名调用
  • 别名不同:非静态成员变量也称为实例变量;静态变量称为类变量
  • 数据存储位置不同:成员变量数据存储在堆内存的对象中,对象的特有数据;JDK1.6 静态变量数据存储在方法区(共享数据区)的静态区,对象的共享数据,JDK1.7 静态变量移到堆中存储

关于抽象,正确的是()

答案:B

分析:

  • abstract 只能修饰方法和类,不能修饰字段
  • 抽象方法不能有方法体,即没有括号

java.sql.Date和java.util.Date的区别

  • java.sql.Date 是 java.util.Date 的子类
  • java.util.Date 是 JDK 中的日期类,精确到时、分、秒、毫秒
  • java.sql.Date 与数据库 Date 相对应的一个类型,只有日期部分,时分秒都会设置为 0,如:2019-10-23 00:00:00
  • 要从数据库时间字段取 时、分、秒、毫秒数据,可以使用 java.sql.Timestamp

简单工厂和抽象工厂有什么区别?

  • 简单工厂模式

是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例
可以生产结构中的任意产品,不能增加新的产品

  • 抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
生产不同产品族的全部产品,不能新增产品,可以新增产品族

Spring Advice 有哪些类型?

  • Before Advice:在连接点(Join point)之前执行
  • After Advice:当连接点退出的时候执行
  • Around Advice:环绕一个连接点的增强,这是最强大的一种增强类型。可以在方法调用前、后完成自定义的行为、是否继续执行连接点、是否进行原逻辑、是否抛出异常来结束执行
  • AfterReturning Advice:在连接点正常完成后执行的增强,如果连接点抛出异常,则不执行
  • AfterThrowing Advice:在方法抛出异常退出时执行的增强

Advice 的 执行顺序:

  • Around Before Advice
  • Before Advice
  • target method 执行
  • Around After Advice
  • After Advice
  • AfterReturning | AfterThrowing & Exception

Inner Class和Static Nested Class的区别?

Inner Class:内部类

  • 内部类就是在一个类的内部定义的类

  • 内部类中不能定义静态成员

  • 内部类可以直接访问外部类中的成员变量

  • 内部类可以定义在外部类的方法外面,也可以定义在外部类的方法体中

  • 在方法体外面定义的内部类的访问类型可以是 public, protected , 默认的,private 等 4 种类型

  • 方法内部定义的内部类前面不能有访问类型修饰符,可以使用 final 或 abstract 修饰符

  • 创建内部类的实例对象时,一定要先创建外部类的实例对象,然后用这个外部类的实例对象去创建内部类的实例对象

  • 内部类里还包含匿名内部类,即直接对类或接口的方法进行实现,不用单独去定义内部类

    //内部类的创建语法
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Innner();

Static Nested Class:静态嵌套类

  • 不依赖于外部类的实例对象

  • 不能直接访问外部类的非 static 成员变量

  • 可以直接引用外部类的static的成员变量,不需要加上外部类的名字

  • 在静态方法中定义的内部类也是Static Nested Class

    //静态内部类创建语法
    Outter.Inner inner = new Outter.Inner();

JavaScript中null、undefined有什么区别?

  • 赋值:null 表示定义了赋值为空值 null,undefined 表示未定义或者定义了未赋值
  • 数据转换:null 在做数值转换时会被转换为 0,undefined 会被转换为 NaN

单个Redis实例最多能存放多少个key?

官方给出,理论值是 2 的 32 次方个

实际使用中单个 Redis 实例最小储存 2.5 亿个 key

对比一下Java和JavaScriprt

JavaScript 与 Java 是两个公司开发的不同的两个产品。

  • Java 是 Sun 公司推出的面向对象的编程语言,现在多用于于互联网服务端开发,前身是 Oak
  • JavaScript 是 Netscape 公司推出的,为了扩展 Netscape 浏览器功能而开发的一种可以嵌入Web 页面中运行的基于对象和事件驱动的解释性语言,前身是 LiveScript

区别:

  • 面向对象和基于对象:Java是一种面向对象的语言;JavaScript 是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,提供了丰富的内部对象供开发者使用
  • 编译和解释:Java 的源代码在执行之前,必须经过编译;JavaScript 是一种解释型编程语言,其源代码不需经过编译,由浏览器直接解释执行
  • 静态与动态语言:Java 是静态语言(编译时变量的数据类型即可确定的语言);JavaScript 是动态语言(运行时确定数据类型的语言)
  • 强类型变量和类型弱变量:Java 采用强类型变量检查,所有变量在编译之前必须声明类型;JavaScript 中变量声明,采用弱类型,即变量在使用前不需作声明类型,解释器在运行时检查其数据类型

怎么测试Redis的连通性?

使用 ping 指令,如:

redis-cli -h host -p port -a password
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

Java 代码对 Redis 连通性测试,可以使用 Redis 客户端类库包里的 api 发送 ping 指令

//连接redis
Jedis jedis=new Jedis("127.0.0.1",6379);
//查看服务器是否运行,打出 pong 表示OK
System.out.println("ping redis:" + jedis.ping());

如果有两个类A、B(注意不是接口),如何编写C类同时使用这两个类的功能?

让A、B成为父子类,C继承子类即可。

运行时栈帧包含哪些结构?

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 返回地址
  • 附加信息

节点流和处理流区别

按流的处理位置分类

  • 节点流:可以从某节点读数据或向某节点写数据的流。如 FileInputStream
  • 处理流:对已存在的流的连接和封装,实现更为丰富的流数据处理,处理流的构造方法必需其他的流对象参数。如 BufferedReader

字节流和字符流区别与适用场景

  • Java 中的字节流处理的最基本单位为 1 个字节,通常用来处理二进制数据。字节流类 InputStream 和 OutputStream 类均为抽象类,代表了基本的输入字节流和输出字节流。
  • Java 中的字符流处理的最基本的单元是 Unicode 代码单元(大小2字节),通常用来处理文本数据。

区别:

  • 字节流操作的基本单元是字节;字符流操作的基本单元是字符
  • 字节流默认不使用缓冲区;字符流使用缓冲区
  • 字节流通常用于处理二进制数据,不支持直接读写字符;字符流通常用于处理文本数据
  • 在读写文件需要对文本内容进行处理:按行处理、比较特定字符的时候一般会选择字符流;仅仅读写文件,不处理内容,一般选择字节流

特征:

  • 以 stream 结尾都是字节流,reader 和 writer 结尾是字符流
  • InputStream 是所有字节输入流的父类,OutputStream 是所有字节输出流的父类
  • Reader 是字符输入流的父类,Writer 是字符输出流的父类

常见的字节流:

  • 文件流:FileOutputStream 和 FileInputStream
  • 缓冲流:BufferedOutputStream 和 BufferedInputStream
  • 对象流:ObjectOutputStream 和 ObjectInputStream

常见的字符流:

  • 字节转字符流:InputStreamReader 和 OutputStreamWriter
  • 缓冲字符流:PrintWriter 和 BufferedReader

强引用、软引用、弱引用、虚引用是什么,有什么区别?

  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)
  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现
  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现
  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现

MQ 是什么?为什么使用?

MQ(Message Queue)消息队列,是 “先进先出” 的一种数据结构。

MQ 的作用:一般用来解决应用解耦,异步处理,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。

  • 应用解耦:

当 A 系统生产关键数据,发送数据给多个其他系统消费,此时 A 系统和其他系统产生了严重的耦合,如果将 A 系统产生的数据放到 MQ 当中,其他系统去 MQ 获取消费数据,此时各系统独立运行只与 MQ 交互,添加新系统消费 A 系统的数据也不需要去修改 A 系统的代码,达到了解耦的效果。

  • 异步处理:

互联网类企业对用户的直接操作,一般要求每个请求在 200ms 以内完成。对于一个系统调用多个系统,不使用 MQ 的情况下,它执行完返回的耗时是调用完所有系统所需时间的总和;使用 MQ 进行优化后,执行的耗时则是执行主系统的耗时加上发送数据到消息队列的耗时,大幅度提升系统性能和用户体验。

  • 流量削峰:

MySQL 每秒最高并发请求在 2000 左右,用户访问量高峰期的时候涌入的大量请求,会将 MySQL 打死,然后系统就挂掉,但过了高峰期,请求量可能远低于 2000,这种情况去增加服务器就不值得,如果使用 MQ 的情况,将用户的请求全部放到 MQ 中,让系统去消费用户的请求,不要超过系统所能承受的最大请求数量,保证系统不会再高峰期挂掉,高峰期过后系统还是按照最大请求数量处理完请求。

如何实现对象克隆与深拷贝?

1、实现 Cloneable 接口,重写 clone() 方法。

2、不实现 Cloneable 接口,会报 CloneNotSupportedException 异常。

package constxiong.interview;
 
/**
 * 测试克隆
 * @author ConstXiong
 * @date 2019-06-18 11:21:21
 */
public class TestClone {
 
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person(1, "ConstXiong");//创建对象 Person p1
        Person p2 = (Person)p1.clone();//克隆对象 p1
        p2.setName("其不答");//修改 p2的name属性,p1的name未变
        System.out.println(p1);
        System.out.println(p2);
    }
    
}
 
/**
 * 人
 * @author ConstXiong
 * @date 2019-06-18 11:54:35
 */
class Person implements Cloneable {
    
    private int pid;
    
    private String name;
    
    public Person(int pid, String name) {
        this.pid = pid;
        this.name = name;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+"]";
    }
    
}

打印结果

Person constructor call
Person [pid:1, name:ConstXiong]
Person [pid:1, name:其不答]

3、Object 的 clone() 方法是浅拷贝,即如果类中属性有自定义引用类型,只拷贝引用,不拷贝引用指向的对象。

可以使用下面的两种方法,完成 Person 对象的深拷贝。

方法1、对象的属性的Class 也实现 Cloneable 接口,在克隆对象时也手动克隆属性。

@Override
public Object clone() throws CloneNotSupportedException {
    DPerson p = (DPerson)super.clone();
    p.setFood((DFood)p.getFood().clone());
    return p;
}

完整代码

package constxiong.interview;
 
/**
 * 测试克隆
 * @author ConstXiong
 * @date 2019-06-18 11:21:21
 */
public class TestManalDeepClone {
 
    public static void main(String[] args) throws Exception {
        DPerson p1 = new DPerson(1, "ConstXiong", new DFood("米饭"));//创建Person 对象 p1
        DPerson p2 = (DPerson)p1.clone();//克隆p1
        p2.setName("其不答");//修改p2的name属性
        p2.getFood().setName("面条");//修改p2的自定义引用类型 food 属性
        System.out.println(p1);//修改p2的自定义引用类型 food 属性被改变,p1的自定义引用类型 food 属性也随之改变,说明p2的food属性,只拷贝了引用,没有拷贝food对象
        System.out.println(p2);
    }
    
}
 
class DPerson implements Cloneable {
    
    private int pid;
    
    private String name;
    
    private DFood food;
    
    public DPerson(int pid, String name, DFood food) {
        this.pid = pid;
        this.name = name;
        this.food = food;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public Object clone() throws CloneNotSupportedException {
        DPerson p = (DPerson)super.clone();
        p.setFood((DFood)p.getFood().clone());
        return p;
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+", food:"+food.getName()+"]";
    }
 
    public DFood getFood() {
        return food;
    }
 
    public void setFood(DFood food) {
        this.food = food;
    }
    
}
 
class DFood implements Cloneable{
    
    private String name;
    
    public DFood(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
}

打印结果

Person constructor call
Person [pid:1, name:ConstXiong, food:米饭]
Person [pid:1, name:其不答, food:面条]

方法2、结合序列化(JDK java.io.Serializable 接口、JSON格式、XML格式等),完成深拷贝

结合 java.io.Serializable 接口,完成深拷贝

package constxiong.interview;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
 
public class TestSeriazableClone {
 
    public static void main(String[] args) {
        SPerson p1 = new SPerson(1, "ConstXiong", new SFood("米饭"));//创建 SPerson 对象 p1
        SPerson p2 = (SPerson)p1.cloneBySerializable();//克隆 p1
        p2.setName("其不答");//修改 p2 的 name 属性
        p2.getFood().setName("面条");//修改 p2 的自定义引用类型 food 属性
        System.out.println(p1);//修改 p2 的自定义引用类型 food 属性被改变,p1的自定义引用类型 food 属性未随之改变,说明p2的food属性,只拷贝了引用和 food 对象
        System.out.println(p2);
    }
    
}
 
class SPerson implements Cloneable, Serializable {
    
    private static final long serialVersionUID = -7710144514831611031L;
 
    private int pid;
    
    private String name;
    
    private SFood food;
    
    public SPerson(int pid, String name, SFood food) {
        this.pid = pid;
        this.name = name;
        this.food = food;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    /**
     * 通过序列化完成克隆
     * @return
     */
    public Object cloneBySerializable() {
        Object obj = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            obj = ois.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return obj;
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+", food:"+food.getName()+"]";
    }
 
    public SFood getFood() {
        return food;
    }
 
    public void setFood(SFood food) {
        this.food = food;
    }
    
}
 
class SFood implements Serializable {
    
    private static final long serialVersionUID = -3443815804346831432L;
    
    private String name;
    
    public SFood(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
}

打印结果

Person constructor call
Person [pid:1, name:ConstXiong, food:米饭]
Person [pid:1, name:其不答, food:面条]

Redis各数据类型最大容量是多少?

  • Strings:一个 String 类型的 value 最大可以存储512M
  • Lists:元素个数最多为 2^32-1 个,即 4294967295 个
  • Sets:元素个数最多为 2^32-1 个,即 4294967295 个
  • Hashes:键值对个数最多为 2^32-1 个,即 4294967295 个
  • Sorted sets类型:同 Sets

XML文档定义有几种形式?有何区别?

XML文档定义分为 Schema 和 DTD 两种形式

  • Schema 是对XML文档结构的定义和描述,其主要的作用是用来约束XML文件,并验证XML文件有效性。
  • DTD 的作用是定义XML的合法构建模块,它使用一系列的合法元素来定义文档结构。

区别:

  • Schema 本身也是 XML 文档,DTD 定义跟 XML 无关
  • Schema 文档结构性强,各元素之间的嵌套关系非常直观;DTD 文档的结构是"平铺型"的,如果定义复杂的XML文档,很难把握各元素之间的嵌套关系
  • Schema 可以定义元素文本的具体类型; TD 只能指定元素含有文本
  • Schema 支持元素节点顺序的描述;DTD 没有提供无序情况的描述
  • Schema 可以很好满足命名空间;DTD 不可以

什么是游标?在oracle中如何使用?

游标:

  • SQL 的一个内存工作区存放查询出来的记录,由系统或用户以变量的形式定义
  • 为了查看或处理查询结果集中的数据,游标提供了在结果集中一次一行或者多行前进或向后浏览数据的能力
  • 可以把游标当作一个指针,它可以指定结果中的任何位置,然后允许用户对指定位置的数据进行处理
  • 游标一旦打开,数据就从数据库中传送到游标变量中,然后应用程序再从游标变量中分解出需要的数据,并进行处理

oracle中的游标分为隐式游标和显示游标

DML 操作和单行 SELECT 语句会使用隐式游标,如:

  • 插入操作:INSERT
  • 更新操作:UPDATE
  • 删除操作:DELETE
  • 单行查询操作:SELECT … INTO …

可以通过隐式游标的属性来了解操作的状态和结果,进而控制程序的流程。隐式游标的属性如下:

  • %ROWCOUNT – 整型,代表 DML 语句成功执行的数据行数
  • %FOUND – 布尔型,值为 TRUE 代表插入、删除、更新或单行查询操作成功
  • %NOTFOUND – 布尔型,与 %FOUND 属性值相反
  • %ISOPEN – 布尔型,DML 执行过程中为真,执行结束后为假

显示游标可以对查询语句(select)返回的多条记录进行处理

游标的使用步骤:

  • 声明一个游标:cursor cursor_name[ 参数1 参数类型,参数2,参数类型…] is select 语句
  • 打开游标 open 游标名(参数1,参数2…)
  • 使用循环遍历游标,从游标中取值。fetch 游标名 into 变量名,循环的退出条件是 游标名 %notfound
  • 关闭游标 close 游标名

导致并发程序出问题的根本原因是什么?

CPU、内存、IO 设备的读写速度差异巨大,表现为 CPU 的速度 > 内存的速度 > IO 设备的速度。

程序的性能瓶颈在于速度最慢的 IO 设备的读写,也就是说当涉及到 IO 设备的读写,再怎么提升 CPU 和内存的速度也是起不到提升性能的作用。

为了更好地利用 CPU 的高性能

  • 计算机体系结构,给 CPU 增加了缓存,均衡 CPU 和内存的速度差异
  • 操作系统,增加了进程与线程,分时复用 CPU,均衡 CPU 和 IO 设备的速度差异
  • 编译器,增加了指令执行重排序,更好地利用缓存,提高程序的执行速度

基于以上优化,给并发编程带来了三大问题。

1、 CPU 缓存,在多核 CPU 的情况下,带来了可见性问题

可见性:一个线程对共享变量的修改,另一个线程能够立刻看到修改后的值

看下面代码,启动两个线程,一个线程当 stop 变量为 true 时,停止循环,一个线程启动就设置 stop 变量为 true。

package constxiong.concurrency.a014;

/**
 * 测试可见性问题
 * @author ConstXiong
 */
public class TestVisibility {

    //是否停止 变量
    private static boolean stop = false;
    
    public static void main(String[] args) throws InterruptedException {
        //启动线程 1,当 stop 为 true,结束循环
        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();
        
        //休眠 10 毫秒
        Thread.sleep(10);
        
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
    
}

这个就是因为 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。

示意如图:

image_20190914114150.png

可以通过 volatile、synchronized、Lock接口、Atomic 类型保障可见性。

2、操作系统对当前执行线程的切换,带来了原子性问题

原子性:一个或多个指令在 CPU 执行的过程中不被中断的特性

看下面的一段代码,线程 1 和线程 2 分别对变量 count 增加 10000,但是结果 count 的输出却不是 20000

package constxiong.concurrency.a014;

/**
 * 测试原子性问题
 * @author ConstXiong
 */
public class TestAtomic {
    
    //计数变量
    static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 结束");
        });
        
        //线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 结束");
        });
        
        //启动线程 1
        t1.start();
        //启动线程 2
        t2.start();
        
        //等待线程 1 执行完成
        t1.join();
        //等待线程 2 执行完成
        t2.join();
        
        //打印 count 变量
        System.out.println(count);
    }
    
}

打印结果:

thread t2 count 加 10000 结束
thread t1 count 加 10000 结束
11377

这个就是因为线程切换导致的原子性问题。

Java 代码中 的 count++ ,至少需要三条 CPU 指令:

  • 指令 1:把变量 count 从内存加载到 CPU 的寄存器
  • 指令 2:在寄存器中执行 count + 1 操作
  • 指令 3:+1 后的结果写入 CPU 缓存 或 内存

即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。

指令执行与线程切换

image_20190914175932.png

3、编译器指令重排优化,带来了有序性问题

有序性:程序按照代码执行的先后顺序

看下面这段代码,复现指令重排带来的有序性问题。

package constxiong.concurrency.a014;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 测试有序性问题
 * @author ConstXiong
 */
public class TestOrderliness {
    
    static int x;//静态变量 x
    static int y;//静态变量 y

    public static void main(String[] args) throws InterruptedException {
        Set<String> valueSet = new HashSet<String>();//记录出现的结果的情况
        Map<String, Integer> valueMap = new HashMap<String, Integer>();//存储结果的键值对
        
        //循环 1000 万次,记录可能出现的 v1 和 v2 的情况
        for (int i = 0; i <10000000; i++) {
            //给 x y 赋值为 0
            x = 0; 
            y = 0; 
            valueMap.clear();//清除之前记录的键值对
            Thread t1 = new Thread(() -> {
                int v1 = y;//将 y 赋值给 v1 ----> Step1
                x = 1;//设置 x 为 1  ----> Step2
                valueMap.put("v1", v1);//v1 值存入 valueMap 中  ----> Step3
            }) ;
            
            Thread t2 = new Thread(() -> {
                int v2 = x;//将 x 赋值给 v2  ----> Step4
                y = 1;//设置 y 为 1  ----> Step5
                valueMap.put("v2", v2);//v2 值存入 valueMap 中  ----> Step6
            });
            
            //启动线程 t1 t2
            t1.start();
            t2.start();
            //等待线程 t1 t2 执行完成
            t1.join();
            t2.join();
            
            //利用 Set 记录并打印 v1 和 v2 可能出现的不同结果
            valueSet.add("(v1=" + valueMap.get("v1") + ",v2=" + valueMap.get("v2") + ")");
            System.out.println(valueSet);
        }
    }
    
}

打印结果出现四种情况:

image_20190914220211.png

v1=0,v2=0 的执行顺序是 Step1 和 Step 4 先执行

v1=1,v2=0 的执行顺序是 Step5 先于 Step1 执行

v1=0,v2=1 的执行顺序是 Step2 先于 Step4 执行

v1=1,v2=1 出现的概率极低,就是因为 CPU 指令重排序造成的。Step2 被优化到 Step1 前,Step5 被优化到 Step4 前,至少需要成立一个。

指令重排,可能会发生在两个没有相互依赖关系之间的指令。

jsp和servlet有什么区别?

Servlet

  • 一种服务器端的Java应用程序
  • 由 Web 容器加载和管理
  • 用于生成动态 Web 内容
  • 负责处理客户端请求

Jsp

  • 是 Servlet 的扩展,本质上还是 Servlet
  • 每个 Jsp 页面就是一个 Servlet 实例
  • Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求

区别

  • Servlet 适合动态输出 Web 数据和业务逻辑处理,对于 html 页面内容的修改非常不方便;Jsp 是在 Html 代码中嵌入 Java 代码,适合页面的显示
  • 内置对象不同,获取内置对象的方式不同

子类构造方法的执行过程是什么样的?

子类构造方法的调用规则:

  • 如果子类的构造方法中没有通过 super 显式调用父类的有参构造方法,也没有通过 this 显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。这种情况下,写不写 super(); 语句,效果是一样的
  • 如果子类的构造方法中通过 super 显式调用父类的有参构造方法,将执行父类相应的构造方法,不执行父类无参构造方法
  • 如果子类的构造方法中通过 this 显式调用自身的其他构造方法,将执行类中相应的构造方法
  • 如果存在多级继承关系,在创建一个子类对象时,以上规则会多次向更高一级父类应用,一直到执行顶级父类 Object 类的无参构造方法为止

可序列化对象为什么要定义serialversionUID值?

  • SerialVersionUid 是为了序列化对象版本控制,告诉 JVM 各版本反序列化时是否兼容
  • 如果在新版本中这个值修改了,新版本就不兼容旧版本,反序列化时会抛出InvalidClassException异常
  • 仅增加了一个属性,希望向下兼容,老版本的数据都保留,就不用修改
  • 删除了一个属性,或更改了类的继承关系,就不能不兼容旧数据,这时应该手动更新 SerialVersionUid

如何保证消息不丢失?

  • 生产者丢失消息:如网络传输中丢失消息、MQ 发生异常未成功接收消息等情况。 解决办法:主流的 MQ 都有确认或事务机制,可以保证生产者将消息送达到 MQ。如 RabbitMQ 就有事务模式和 confirm 模式。
  • MQ 丢失消息:MQ 成功接收消息内部处理出错、宕机等情况。 解决办法:开启 MQ 的持久化配置。
  • 消费者丢失消息:采用消息自动确认模式,消费者取到消息未处理挂掉了。 解决办法:改为手动确认模式,消费者成功消费消息再确认。

String类是否可以继承?

不可以

String 类在 JDK 中被广泛使用,为了保证正确性、安全性,String 类是用 final 修饰,不能被继承,方法不可以被重写。

谈谈遇到的Dubbo超时问题

dubbo 调用服务超时,默认是会重试两次的,但可能两次请求都是成功的。如果没有幂等性处理,就会产生重复数据。

  • 可以考虑去除 dubbo 超时重试机制,重新评估设置超时时间
  • dubbo 的重试在集群环境下,会把超时的请求发到其他服务
  • 引起超时的原因可能出在消费端,也可能出现在服务端,服务器的网络、内存、CPU、存储空间都可能引起超时问题
  • 超时时间设置过小也会导致超时问题

Java实现文件夹复制

使用递归复制文件夹和文件

package constxiong.interview;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;


/**
 * 复制文件夹
 * @author ConstXiong
 * @date 2019-11-13 13:38:19
 */
public class TestCopyDir {

    public static void main(String[] args) {
        String srcPath = "E:/a";
        String destPath = "E:/a_";
        copyDir(srcPath, destPath);
    }
    
    /**
     * 复制文件夹
     * @param srcFile
     * @param destFile
     */
    public static void copyDir(String srcDirPath, String destDirPath) {
        File srcDir = new File(srcDirPath);
        if (!srcDir.exists() || !srcDir.isDirectory()) {
            throw new IllegalArgumentException("参数错误");
        }
        File destDir = new File(destDirPath);
        if (!destDir.exists()) {
            destDir.mkdirs();
        }
        File[] files = srcDir.listFiles();
        for (File f : files) {
            if (f.isFile()) {
                copyFile(f, new File(destDirPath, f.getName()));
            } else if (f.isDirectory()) {
                copyDir(srcDirPath + File.separator + f.getName(),
                        destDirPath + File.separator + f.getName());
            }
        }
    }
    
    /**
     * 复制文件
     * @param srcFile
     * @param destFile
     */
    public static void copyFile(File srcFile, File destFile) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        byte[] b = new byte[1024];
        
        try {
            bis = new BufferedInputStream(new FileInputStream(srcFile));
            bos = new BufferedOutputStream(new FileOutputStream(destFile));
            int len;
            while ((len = bis.read(b)) > -1) {
                bos.write(b, 0, len);
            }
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

Java中有哪些无锁技术来解决并发问题?如何使用?

除了使用 synchronized、Lock 加锁之外,Java 中还有很多不需要加锁就可以解决并发问题的工具类

1、原子工具类

JDK 1.8 中,java.util.concurrent.atomic 包下类都是原子类,原子类都是基于 sun.misc.Unsafe 实现的。

  • CPU 为了解决并发问题,提供了 CAS 指令,全称 Compare And Swap,即比较并交互
  • CAS 指令需要 3 个参数,变量、比较值、新值。当变量的当前值与比较值相等时,才把变量更新为新值
  • CAS 是一条 CPU 指令,由 CPU 硬件级别上保证原子性
  • java.util.concurrent.atomic 包中的原子分为:原子性基本数据类型、原子性对象引用类型、原子性数组、原子性对象属性更新器和原子性累加器

原子性基本数据类型:AtomicBoolean、AtomicInteger、AtomicLong

原子性对象引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference

原子性数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

原子性对象属性更新:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

原子性累加器:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder

修改我们之前测试原子性问题的类,使用 AtomicInteger 的简单例子

package constxiong.concurrency.a026;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 测试 原子类 AtomicInteger
 * 
 * @author ConstXiong
 */
public class TestAtomicInteger {

    // 计数变量
    static volatile AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count.incrementAndGet();
            }
            System.out.println("thread t1 count 加 10000 结束");
        });

        // 线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count.incrementAndGet();
            }
            System.out.println("thread t2 count 加 10000 结束");
        });

        // 启动线程 1
        t1.start();
        // 启动线程 2
        t2.start();

        // 等待线程 1 执行完成
        t1.join();
        // 等待线程 2 执行完成
        t2.join();

        // 打印 count 变量
        System.out.println(count.get());
    }

}

打印结果如预期

thread t2 count 加 10000 结束
thread t1 count 加 10000 结束
20000

2、线程本地存储

  • java.lang.ThreadLocal 类用于线程本地化存储。
  • 线程本地化存储,就是为每一个线程创建一个变量,只有本线程可以在该变量中查看和修改值。
  • 典型的使用例子就是,spring 在处理数据库事务问题的时候,就用了 ThreadLocal 为每个线程存储了各自的数据库连接 Connection。
  • 使用 ThreadLocal 要注意,在不使用该变量的时候,一定要调用 remove() 方法移除变量,否则可能造成内存泄漏的问题。

示例

package constxiong.concurrency.a026;

/**
 * 测试 原子类 AtomicInteger
 * 
 * @author ConstXiong
 */
public class TestThreadLocal {

    // 线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {//初始值
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {// 启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        try {
            for (int i = 0; i <5; i++) {
                Integer n = THREAD_LOCAL_NUM.get();
                n += 1;
                THREAD_LOCAL_NUM.set(n);
                System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
            }
        } finally {
            THREAD_LOCAL_NUM.remove();// 将变量移除
        }
    }
}

每个线程最后一个值都打印到了 5

Thread-0 : ThreadLocal num=1
Thread-2 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-2 : ThreadLocal num=2
Thread-0 : ThreadLocal num=2
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=4
Thread-0 : ThreadLocal num=5
Thread-1 : ThreadLocal num=3
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=4
Thread-1 : ThreadLocal num=5

3、copy-on-write

  • 根据英文名称可以看出,需要写时复制,体现的是一种延时策略。
  • Java 中的 copy-on-write 容器包括:CopyOnWriteArrayList、CopyOnWriteArraySet。
  • 涉及到数组的全量复制,所以也比较耗内存,在写少的情况下使用比较适合。

简单的 CopyOnWriteArrayList 的示例,这里只是说明 CopyOnWriteArrayList 怎么用,并且是线程安全的。这个场景并不适合使用 CopyOnWriteArrayList,因为写多读少。

package constxiong.concurrency.a026;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 测试 copy-on-write
 * @author ConstXiong
 */
public class TestCopyOnWrite {

    private static final Random R = new Random();
    
    private static CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>();
//    private static ArrayList<Integer> cowList = new ArrayList<Integer>();
    
    public static void main(String[] args) throws InterruptedException {
        List<Thread> threadList = new ArrayList<Thread>();
        //启动 1000 个线程,向 cowList 添加 5 个随机整数
        for (int i = 0; i <1000; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j <5; j++) {
                    //休眠 10 毫秒,让线程同时向 cowList 添加整数,引出并发问题
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cowList.add(R.nextInt(100));
                }
            }) ;
            t.start();
            threadList.add(t);
        }
        
        for (Thread t : threadList) {
            t.join();
        }
        System.out.println(cowList.size());
    }
}

打印结果

5000

如果把

private static CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>();

改为

private static ArrayList<Integer> cowList = new ArrayList<Integer>();

打印结果就是小于 5000 的整数了

4、其他 “Concurrent” 开头的并发工具类,如:ConcurrentHashMap、ConcurrentLinkedDeque、ConcurrentLinkedQueue…

volatile关键字能否保证线程安全?

单纯使用 volatile 关键字是不能保证线程安全的

  • volatile 只提供了一种弱的同步机制,用来确保将变量的更新操作通知到其他线程
  • volatile 语义是禁用 CPU 缓存,直接从主内存读、写变量。表现为:更新 volatile 变量时,JMM 会把线程对应的本地内存中的共享变量值刷新到主内存中;读 volatile 变量时,JMM 会把线程对应的本地内存设置为无效,直接从主内存中读取共享变量
  • 当把变量声明为 volatile 类型后,JVM 增加内存屏障,禁止 CPU 进行指令重排

Dubbo与Spring如何集成?

Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。

Dobbo扩展的 spring xml配置文件节点说明如下:

  • dubbo:service/ 服务配置,用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
  • dubbo:reference/ 引用服务配置,用于创建一个远程服务代理,一个引用可以指向多个注册中心
  • dubbo:protocol/ 协议配置,用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
  • dubbo:application/ 应用配置,用于配置当前应用信息,不管该应用是提供者还是消费者
  • dubbo:module/ 模块配置,用于配置当前模块信息
  • dubbo:registry/ 注册中心配置,用于配置连接注册中心相关信息
  • dubbo:monitor/ 监控中心配置,用于配置连接监控中心相关信息
  • dubbo:provider/ 提供方的缺省值,当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值
  • dubbo:consumer/ 消费方缺省配置,当 ReferenceConfig 某属性没有配置时,采用此缺省值
  • dubbo:method/ 方法配置,用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息
  • dubbo:argument/ 用于指定方法参数配置

项目中如何实现读写分离?怎么配置?

主从复制的原理思想也很简单,就是从库不断地同步主库的改动,保持与主库数据一致;应用仅在从库中读数据。

在项目中,使用读写分离本质上就是,增加数据库服务器资源 + 网络带宽,来分摊对数据库的读写请求,从而提高了性能和可用性。主从复制实现读写分离最大的缺点就是从库同步到主库的数据存在延迟,网络不好的时候,延迟比较严重。

如何实现读写分离?

在我们平时开发中,一般不会自己去控制 select 请求从从库拿 Connection,insert、delete、update 请求从主库拿 Connection。当然也有这么干,就是把读写请求按规则命名方法,然后根据方法名通过反射统一处理请求不同的库。

大部分企业在项目中是使用中间件去实现读写分离的,如 mycat、atlas、dbproxy、cetus、Sharding-JDBC…,每种中间件各有优缺点。

Sharding-JDBC 是 apache 旗下的 ShardingSphere 中的一款产品,轻量,引入 jar 即可完成读写分离的需求,可以理解为增强版的 JDBC,现在被使用的较多。

搭建项目

maven 依赖的库

<!-- 当前最新版 sharding-jdbc -->
<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>sharding-jdbc-core</artifactId>
  <version>4.1.1</version>
</dependency>
<!-- 结合官方文档使用了 HikariCP 数据库连接池 -->
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
  <version>3.4.5</version>
</dependency>
<!-- MySQL 8.0.21 驱动 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
</dependency>

获取数据源的工具类

package constxiong;

import com.zaxxer.hikari.HikariDataSource;

/**
 * 获取 DataSource 工具类,使用了 Hikari 数据库连接池
 */
import javax.sql.DataSource;

public final class DataSourceUtil {

    private static final int PORT = 3306;

    /**
     * 通过 Hikari 数据库连接池创建 DataSource
     * @param ip
     * @param username
     * @param password
     * @param dataSourceName
     * @return
     */
    public static DataSource createDataSource(String ip, String username, String password, String dataSourceName) {
        HikariDataSource result = new HikariDataSource();
        result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
        result.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8", ip, PORT, dataSourceName));
        result.setUsername(username);
        result.setPassword(password);
        return result;
    }
}

测试 Sharding-JDBC 读写分离

主库:172.31.32.184

从库:172.31.32.234

观察通过 Sharding-JDBC 获取的 DataSource 是否会自动写入到主库,从库是否主动同步,从库同步数据的延迟时间

测试代码

package constxiong;

import org.apache.shardingsphere.api.config.masterslave.MasterSlaveRuleConfiguration;
import org.apache.shardingsphere.shardingjdbc.api.MasterSlaveDataSourceFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalTime;
import java.util.*;

/**
 * 测试 ShardingSphere 读写分离
 * 主库:172.31.32.184
 * 从库:172.31.32.234
 *
 * 观察通过 ShardingSphere 获取的 DataSource 是否会自动写入到主库,从库是否主动同步,从库同步数据的延迟时间
 */
public class Test {

    //主库 DataSource
    private static DataSource dsSlave = DataSourceUtil.createDataSource("172.31.32.234", "root", "constxiong@123", "constxiong");
    //从库 DataSource
    private static DataSource dsMaster = DataSourceUtil.createDataSource("172.31.32.184", "root", "constxiong@123", "constxiong");

    public static void main(String[] args) throws SQLException {
        //启动线程打印主库与从库当前 cuser 数据量与时间,观察从库同步数据延迟
        printMasterAndSlaveData();

        //从 ShardingSphere 获取 DataSource,出入数据,观察插入数据的库是否为主库
        DataSource ds = getMasterSlaveDataSource();
        Connection conn = ds.getConnection();
        Statement stt = conn.createStatement();
        stt.execute("insert into cuser values(2, 'fj')");
    }

    /**
     * 启动线程打印,两个主从库 cuser 表的信息、数据量、当前时间
     * @throws SQLException
     */
    private static void printMasterAndSlaveData() throws SQLException {
        Connection masterConn = dsMaster.getConnection();
        Connection slaveConn = dsSlave.getConnection();
        new Thread(() -> {
            while (true) {
                try {
                    System.out.println("------master------" + LocalTime.now());
                    print(masterConn);
                    System.out.println("------slave------" + LocalTime.now());
                    print(slaveConn);
                } catch (SQLException e) {
                }
            }
        }).start();
    }

    private static void print(Connection conn) throws SQLException {
        Statement statement = conn.createStatement();
        statement.execute("select * from cuser");
        ResultSet rs = statement.getResultSet();
        int count = 0;
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");
            System.out.println(id + "-" + name);
            count++;
        }
        System.out.println("total: " + count);
    }

    /**
     * 设置 ShardingSphere 的主从库
     * @return
     * @throws SQLException
     */
    private static DataSource getMasterSlaveDataSource() throws SQLException {
        MasterSlaveRuleConfiguration masterSlaveRuleConfig = new MasterSlaveRuleConfiguration("ds_master_slave", "ds_master", Arrays.asList("ds_slave"));
        return MasterSlaveDataSourceFactory.createDataSource(createDataSourceMap(), masterSlaveRuleConfig, new Properties());
    }

    /**
     * 用 主从库的 DataSource 构造 map
     * @return
     */
    private static Map<String, DataSource> createDataSourceMap() {
        Map<String, DataSource> result = new HashMap<>();
        result.put("ds_master", dsMaster);
        result.put("ds_slave", dsSlave);
        return result;
    }
}

分析延迟信息

数据默认配置的情况,在内网从库同步的时间延迟,在 200ms 左右,当然这个统计是不精确的,只是看个大概情况,理论值应该是可以做毫秒级

image_20200919102911.png

参考文档:

代码上传至:

java.lang.Object的常用方法?

  • public final native Class<?> getClass(); 获取类结构信息
  • public native int hashCode() 获取哈希码
  • public boolean equals(Object) 默认比较对象的地址值是否相等,子类可以重写比较规则
  • protected native Object clone() throws CloneNotSupportedException 用于对象克隆
  • public String toString() 把对象转变成字符串
  • public final native void notify() 多线程中唤醒功能
  • public final native void notifyAll() 多线程中唤醒所有等待线程的功能
  • public final void wait() throws InterruptedException 让持有对象锁的线程进入等待
  • public final native void wait(long timeout) throws InterruptedException 让持有对象锁的线程进入等待,设置超时毫秒数时间
  • public final void wait(long timeout, int nanos) throws InterruptedException 让持有对象锁的线程进入等待,设置超时纳秒数时间
  • protected void finalize() throws Throwable 垃圾回收前执行的方法

为什么基本类型不能做为HashMap的键值?

  • Java中是使用泛型来约束 HashMap 中的key和value的类型的,HashMap<K, V>
  • 泛型在Java的规定中必须是对象Object类型的,基本数据类型不是Object类型,不能作为键值
  • map.put(0, “ConstXiong”)中编译器已将 key 值 0 进行了自动装箱,变为了 Integer 类型

Redis与Memcached的区别

  • 数据结构:Redis 支持 5 种数据结构;Memcached 只支持字符串
  • 性能对比:单核小数据量存储 Redis 比 Memcached 快;大数据存储 Redis 稍逊
  • 持久化:Redis 支持持久化;Memecached 数据都在内存之中
  • 线程模型:Redis 使用单线程模型,基于非阻塞的 IO 多路复用机制,无线程切换;Memecached 使用多线程模型,一个 master 线程,多个 worker 线程
  • 灾难恢复:Redis数据丢失后可以通过 aof 恢复;Memecached 挂掉后数据不可恢复
  • 集群模式:Redis 原生支持cluster模式;Memcached 没有原生的集群模式

如何用 Spring 实现事件驱动编程?

JDK 中事件编程标准接口

  • 事件对象 java.util.EventObject
  • 事件监听器 java.util.EventListener

Spring 中的事件对应的类是 ApplicationEvent,事件的处理方式如下:

1、实现 ApplicationListener 接口,可以在 onApplicationEvent 方法上处理 ApplicationEvent

2、@EventListener 注解加载事件处理的方法上

需要将 ApplicationListener 注册为 Bean 或者

通过 ConfigurableApplicationContext#addApplicationListener 方法添加

事件的发布,可以通过 ApplicationEventPublisher 发布,也可以通过

ApplicationEventMulticaster 进行广播

MyBatis 有哪些分页的方式?分页插件的原理是什么?

  • 使用 RowBounds 对象进行分页,它是对 ResultSet 结果集进行内存分页
  • 在 xml 或者 注解的 SQL 中传递分页参数
  • 使用分页插件 Mybatis-PageHelper

其中分页插件的原理是,使用 MyBatis 提供的插件接口,拦截待执行的 SQL,根据数据库种类的配置与分页参数,生成带分页 SQL 语句,执行。

Oracle数据库如何迁移?

  • 使用 imp/exp 将老库中的数据导入到新的库中。可以跨平台使用,但停机时间长
  • 如果是存储迁移直接将存储设备挂到新机器上,在新机器上启动数据库。这种方式操作简单,但要求新老库版本一致
  • 使用 rman,适合跨文件系统的迁移
  • 使用 dataguard 迁移
  • 借助工具,如 pl/sql

生产环境服务器变慢,如何诊断处理?

使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。

如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。

如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。

如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析。

什么是泛型?为什么要使用泛型?

泛型:

  • “参数化类型”,将类型由具体的类型参数化,把类型也定义成参数形式(称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
  • 是 JDK 5 中引入的一个新特性,提供了编译时类型安全监测机制,该机制允许程序员在编译时监测非法的类型。
  • 泛型的本质是把参数的类型参数化,也就是所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中。

为什么要用泛型?

  • 使用泛型编写的程序代码,要比使用 Object 变量再进行强制类型转换的代码,具有更好的安全性和可读性。
  • 多种数据类型执行相同的代码使用泛型可以复用代码。

比如集合类使用泛型,取出和操作元素时无需进行类型转换,避免出现 java.lang.ClassCastException 异常

匿名内部类可以继承类或实现接口吗?为什么?

  • 匿名内部类本质上是对父类方法的重写 或 接口的方法的实现
  • 从语法角度看,匿名内部类创建处是无法使用关键字继承类 或 实现接口

原因:

  • 匿名内部类没有名字,所以它没有构造函数。因为没有构造函数,所以它必须通过父类的构造函数来实例化。即匿名内部类完全把创建对象的任务交给了父类去完成。
  • 匿名内部类里创建新的方法没有太大意义,新方法无法被调用。
  • 匿名内部类一般是用来覆盖父类的方法。
  • 匿名内部类没有名字,所以无法进行向下的强制类型转换,只能持有匿名内部类对象引用的变量类型的直接或间接父类。

Redis有哪些优缺点?

优点:

  • 性能极高,能到 100000 次/s 读写速度
  • 支持数据的持久化,对数据的更新采用Copy-on-write技术,可以异步地保存到磁盘上
  • 丰富的数据类型,String(字符串)、List(列表)、Hash(字典)、Set(集合)、Sorted Set(有序集合)
  • 原子性:Redis 的所有操作都是原子性的,多个操作通过 MULTI 和 EXEC 指令支持事务
  • 丰富的特性:key 过期、publish/subscribe、notify
  • 支持数据的备份,快速的主从复制
  • 节点集群,很容易将数据分布到多个Redis实例中

缺点:

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
  • 适合的场景主要局限在较小数据量的高性能操作和运算上

谈谈你知道的垃圾回收算法

判断对象是否可回收的算法有两种:

  • Reference Counting GC,引用计数算法
  • Tracing GC,可达性分析算法

JVM 各厂商基本都是用的 Tracing GC 实现

大部分垃圾收集器遵从了分代收集(Generational Collection)理论。
针对新生代与老年代回收垃圾内存的特点,提出了 3 种不同的算法:

1、标记-清除算法(Mark-Sweep)

标记需回收对象,统一回收;或标记存活对象,回收未标记对象。
缺点:

  • 大量对象需要标记与清除时,效率不高
  • 标记、清除产生的大量不连续内存碎片,导致无法分配大对象

2、标记-复制算法(Mark-Copy)

可用内存等分两块,使用其中一块 A,用完将存活的对象复制到另外一块 B,一次性清空 A,然后改分配新对象到 B,如此循环。
缺点:

  • 不适合大量对象不可回收的情况,换句话说就是仅适合大量对象可回收,少量对象需复制的区域
  • 只能使用内存容量的一半,浪费较多内存空间

3、标记-整理算法(Mark-Compact)

标记存活的对象,统一移到内存区域的一边,清空占用内存边界以外的内存。
缺点:

  • 移动大量存活对象并更新引用,需暂停程序运行

String、StringBuilder、StringBuffer的区别?

相同点:

  • 都可以储存和操作字符串
  • 都使用 final 修饰,不能被继承
  • 提供的 API 相似

区别:

  • String 是只读字符串,String 对象内容是不能被改变的
  • StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
  • StringBuilder 线程不安全;StringBuffer 线程安全

方法体内没有对字符串的并发操作,且存在大量字符串拼接操作,建议使用 StringBuilder,效率较高。

final与static的区别

  • 都可以修饰类、方法、成员变量。
  • 都不能用于修饰构造方法。
  • static 可以修饰类的代码块,final 不可以。
  • static 不可以修饰方法内的局部变量,final 可以。

static:

  • static 修饰表示静态或全局,被修饰的属性和方法属于类,可以用类名.静态属性 / 方法名 访问
  • static 修饰的代码块表示静态代码块,当 Java 虚拟机(JVM)加载类时,就会执行该代码块,只会被执行一次
  • static 修饰的属性,也就是类变量,是在类加载时被创建并进行初始化,只会被创建一次
  • static 修饰的变量可以重新赋值
  • static 方法中不能用 this 和 super 关键字
  • static 方法必须被实现,而不能是抽象的abstract
  • static 方法不能被重写

final:

  • final 修饰表示常量、一旦创建不可改变
  • final 标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值
  • final 方法不能被子类重写
  • final 类不能被继承,没有子类,final 类中的方法默认是 final 的

final 和 static 修饰成员变量加载过程例子

import java.util.Random;
 
public class TestStaticFinal {
 
	public static void main(String[] args) {
		StaticFinal sf1 = new StaticFinal();
		StaticFinal sf2 = new StaticFinal();
		
		System.out.println(sf1.fValue == sf2.fValue);//打印false
		System.out.println(sf1.sValue == sf2.sValue);//打印true
	}
}
 
class StaticFinal {
	
	final int fValue = new Random().nextInt();
	static int sValue = new Random().nextInt();
	
}

左连接、右连接、内连接和全外连接的区别

  • 左连接(left join):返回包括左表中的所有记录和右表中连接字段相等的记录。
  • 右连接(right join):返回包括右表中的所有记录和左表中连接字段相等的记录。
  • 内连接(inner join):只返回两个表中连接字段相等的记录。
  • 全外连接(full join):返回左右表中连接字段相等的记录和剩余所有记录。

int(10)中10指什么?

 INT[(M)] [UNSIGNED] [ZEROFILL]   M 默认为11

10 就是上述语句里的 M,指最大显示宽度,最大值为 255

最大显示宽度意思是,如果是 int(10),字段存的值是 10,则,显示会自动在之前补 8 个 0,显示为 0000000010

int 类型在数据库里面存储占 4 个字节的长度

  • 有符号的整型范围是 -2147483648 ~ 2147483647
  • 无符号的整型范围是 0 ~ 4294967295

如何进行复杂度分析?

时间复杂度分析:

1、大 O 复杂度表示法:T(n) = O(f(n)),公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比

  • 只关注循环执行次数最多的一段代码
  • 总复杂度等于量级最大的那段代码的复杂度
  • 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

2、最好情况时间复杂度:代码在最理想情况下执行的时间复杂度
3、最坏情况时间复杂度:代码在最坏情况下执行的时间复杂度
4、平均时间复杂度:代码在所有情况下执行的次数的加权平均值
5、均摊时间复杂度:极少数高级别复杂度且发生具有时序关系时,可以将这几个高级别的复杂度均摊到低级别复杂度上,一般均摊结果就等于低级别复杂度

空间复杂度分析:

  • 与时间复杂度分析类似,关注算法的存储空间与数据规模之间的增长关系

常见的复杂度:

  • 常见的复杂度并不多,从低到高阶:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)

索引对性能有哪些影响?

优点:

  • 减少数据库服务器需要扫描的数据量
  • 帮助数据库服务器避免排序和临时表
  • 将随机 I/O 变顺序I/O
  • 提高查询速度
  • 唯一索引,能保证数据的唯一性

缺点:

  • 索引的创建和维护耗时随着数据量的增加而增加
  • 对表中数据进行增删改时,索引也要动态维护,降低了数据的维护速度
  • 增大磁盘占用

Java中数组有什么特征?

  • 在内存中申请一块连续的空间
  • 数组下标从 0 开始
  • 每个数组元素都有默认值,基本类型的默认值为 0、0.0、false,引用类型的默认值为 null
  • 数组的类型只能是一个,且固定,在申明时确定
  • 数组的长度一经确定,无法改变,即定长。要改变长度,只能重新申明一个

List、Map、Set 三个接口,存取元素时,各有什么特点?

  • List 以索引来存取元素,元素可重复
  • Set 不能存放重复元素
  • Map 保存键值对映射,映射关系可以一对一、多对一
  • List 有基于数组和链表实现两种方式
  • Set、Map 容器有基于哈希存储和红黑树两种方式实现
  • Set 基于 Map 实现,Set 里的元素值就是 Map 里 key

linux指令-ln

为文件在另外一个位置建立一个同步的链接

链接分为:
1、软链接

  • 软链接,以路径的形式存在。类似于 Windows 操作系统中的快捷方式
  • 软链接可以跨文件系统 ,硬链接不可以
  • 软链接可以对一个不存在的文件名进行链接
  • 软链接可以对目录进行链接

2、硬链接

  • 硬链接,以文件副本的形式存在。但不占用实际空间。
  • 不允许给目录创建硬链接
  • 硬链接只有在同一个文件系统中才能创建

需要注意:

  • ln 命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化

  • ln 的链接又分软链接和硬链接两种,软链接就是ln –s 源文件 目标文件,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间;硬链接 ln 源文件 目标文件,没有参数 -s, 在指定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化

  • ln 指令用在链接文件或目录,如同时指定两个以上的文件或目录,且目标目录已经,则会把前面指定的所有文件或目录复制到该目录中。若同时指定多个文件或目录,且目标目录不存在,则会出现错误信息

    常用参数:
    -b 删除,覆盖之前建立的链接
    -s 软链接
    -v 显示详细处理过程

Collections工具类中的sort方法如何比较元素?

Collections 工具类的 sort() 方法有两种方式

  • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较
  • 第二种不强制性的要求容器中的元素必须可比较,但要求传入参数 Comparator 接口的子类,需要重写 compare() 方法实现元素的比较规则,其实就是通过接口注入比较元素大小的算法,这就是回调模式的应用

为什么要使用spring框架?

spring 是一个开源的轻量级 JavaBean 容器框架。使用 JavaBean 代替 EJB ,并提供了丰富的企业应用功能,降低应用开发的复杂性。

  • 轻量:非入侵性的、所依赖的东西少、资源占用少、部署简单,不同功能选择不同的 jar 组合
  • 容器:工厂模式实现对 JavaBean 进行管理,通过控制反转(IOC)将应用程序的配置和依赖性与应用代码分开
  • 松耦合:通过 xml 配置或注解即可完成 bean 的依赖注入
  • AOP:通过 xml 配置 或注解即可加入面向切面编程的能力,完成切面功能,如:日志,事务…的统一处理
  • 方便集成:通过配置和简单的对象注入即可集成其他框架,如 Mybatis、Hibernate、Shiro…
  • 丰富的功能:JDBC 层抽象、事务管理、MVC、Java Mail、任务调度、JMX、JMS、JNDI、EJB、动态语言、远程访问、Web Service…

被引用的对象就一定能存活吗?

不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前会被回收,但如果没有在 Reference Chain 中的对象就一定会被回收。

Redis如何实现消息延迟?

  • 使用 sorted set 集合,zadd 指令添加消息,用时间戳作为 score,消息内容作为 key
  • zrangebyscore 指令可以获取指定区间内的元素,调整区间参数即可实现消息延迟

linux指令-rm

删除一个目录中的一个或多个文件或目录。如果没有使用 -r 选项,则 rm 不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状。

命令语法:rm [选项] 文件…
rm -i *.log 删除任何 .log 文件,删除前逐一询问确认
rm -rf test 强制删除 test 目录或文件,无需确认

关于构造方法,下列说法正确的是()

答案:C

分析:

Java 类中不写构造方法,编译器会默认提供一个无参构造

方法名可以与类名相同,但不符合命名规范,类名首字母建议大写,方法名建议首字母小写

一个类中可以定义多个构造方法,这就是构造方法的重载

不通过构造方法能创建对象吗?

Java 创建对象的方式:

  1. 用 new 语句创建对象
  2. 运用反射,调用 java.lang.Class 或 java.lang.reflect.Constructor 类的 newInstance() 方法
  3. 调用对象的 clone() 方法
  4. 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法

上述 1、2 会调用构造函数
上述 3、4 不会调用构造函数

package constxiong.interview;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 测试创建对象
 * @author ConstXiong
 * @date 2019-10-31 11:53:31
 */
public class TestNewObject implements Cloneable, Serializable{
    
    private static final long serialVersionUID = 1L;

    public TestNewObject() {
        System.out.println("Constructor init");
    }
    
    public static void main(String[] args) throws Exception {
        TestNewObject o1 = new TestNewObject();
        TestNewObject o2 = TestNewObject.class.newInstance();
        TestNewObject o3 = TestNewObject.class.getConstructor().newInstance();
        
        TestNewObject o4 = (TestNewObject)o1.clone();
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o1);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        TestNewObject o5 = (TestNewObject)ois.readObject();
    }

}

打印

Constructor init
Constructor init
Constructor init

用 MyBatis 如何使用模糊查询?

1、XML 中使用 #{},Java 代码中传入 “%参数值%”

Java:
    list<User> users = mapper.select(Collections.singleMap("name", "%constxiong%"));
 
XML:
    <select id=”select” resultMap="User" parameterType="String">
      select * from user where name like #{name}
    </select>

2、XML 中使用 ${},Java 代码中传入

Java:
    list<User> users = mapper.select(Collections.singleMap("name", "constxiong"));
 
XML:
    <select id=”select” resultMap="User" parameterType="String">
        select * from user where name like '%${name}%'
    </select>

Oracle中function和procedure有什么区别?

存储过程:

  • 一般用于在数据库中完成特定的业务或任务
  • 可以定义返回类型,也可以不定义返回类型
  • 可返回多个参数
  • dml 数据操纵语句不可以调用

函数:

  • 一般用于特定的数据查询或数据转转换处理
  • 申请时必须要定义返回类型,且程序体中必须定义 return 语句
  • 最多返回一个值
  • 不能独立执行,必须作为表达式的一部分调用
  • dml 数据操纵语句可以调用

说一说你的对面向过程和面向对象的理解

  • 软件开发思想,先有面向过程,后有面向对象
  • 在大型软件系统中,面向过程的做法不足,从而推出了面向对象
  • 都是解决实际问题的思维方式
  • 两者相辅相成,宏观上面向对象把握复杂事物的关系;微观上面向过程去处理
  • 面向过程以实现功能的函数开发为主;面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能
  • 面向过程是封装的是功能;面向对象封装的是数据和功能
  • 面向对象具有继承性和多态性;面向过程则没有

linux指令-ls

list 的缩写,通过 ls 命令不仅可以查看 linux 文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限)、查看目录信息等等。

常用命令:

ls -a 列出目录所有文件,包含以.开始的隐藏文件
ls -A 列出除.及..的其它文件
ls -r 反序排列
ls -t 以文件修改时间排序
ls -S 以文件大小排序
ls -h 以易读大小显示
ls -l 除了文件名之外,还将文件的权限、所有者、文件大小等信息详细列出来
ls -lhrt 按易读方式按时间反序排序,并显示文件详细信息
ls -lrS 按大小反序显示文件详细信息
ls -l t* 列出当前目录中所有以"t"开头的目录的详细内容
ls | sed "s:^:`pwd`/:" 列出文件绝对路径(不包含隐藏文件)
find $pwd -maxdepth 1 | xargs ls -ld 列出文件绝对路径(包含隐藏文件)

统计一段长字符串中某字符串的出现次数

  • 截取字符串统计字符串出现次数

  • 通过替换字符串,统计字符串出现次数

  • 通过正则表达式,统计字符串出现次数

    package constxiong.interview;

    import java.util.regex.Matcher;
    import java.util.regex.Pattern;

    /**

    • 统计一段长字符串中某字符串的出现次数

    • @author ConstXiong

    • @date 2019-11-13 11:01:22
      */
      public class TestCountWordTimesInText {

      public static void main(String[] args) {
      String text = “统计一CX段长CX字符串中某字符串的出C现X次cx数”;
      String word = “CX”;
      System.out.println(countWordTimesByCutString(text, word));
      System.out.println(countWordTimesByReplace(text, word));
      System.out.println(countWordTimesByRegex(text, word));//正则匹配,需要注通配符的使用对结果的影响
      }

      /**

      • 截取字符串统计字符串出现次数
      • @param text
      • @param word
      • @return
        */
        public static int countWordTimesByCutString(String text, String word) {
        int times = 0;
        if (!isEmpty(text) && !isEmpty(word)) {
        String subText = text;
        int index = -1;
        int wordLength = word.length();
        while (subText.length() >= wordLength && (index = subText.indexOf(word)) > -1) {
        subText = subText.substring(index + wordLength);
        times++;
        }
        }
        return times;
        }

      /**

      • 通过替换字符串,统计字符串出现次数
      • @param text
      • @param word
      • @return
        */
        public static int countWordTimesByReplace(String text, String word) {
        int times = 0;
        if (!isEmpty(text) && !isEmpty(word)) {
        times = (text.length() - text.replace(word, “”).length()) / word.length();
        }
        return times;
        }

      /**

      • 通过正则表达式,统计字符串出现次数
      • @param text
      • @param word
      • @return
        */
        public static int countWordTimesByRegex(String text, String word) {
        int times = 0;
        if (!isEmpty(text) && !isEmpty(word)) {
        Pattern p = Pattern.compile(word);
        Matcher m = p.matcher(text);
        while (m.find()) {
        times++;
        }
        }
        return times;
        }

      /**

      • 字符串是否为空
      • @param str
      • @return
        */
        private static boolean isEmpty(String str) {
        return str == null || str.length() == 0;
        }

    }

静态内部类和非静态内部类有什么区别?

  • 静态内部类不需要有指向外部类的引用;非静态内部类需要持有对外部类的引用
  • 静态内部类可以有静态方法、属性;非静态内部类则不能有静态方法、属性
  • 静态内部类只能访问外部类的静态成员,不能访问外部类的非静态成员;非静态内部类能够访问外部类的静态和非静态成员
  • 静态内部类不依赖于外部类的实例,直接实例化内部类对象;非静态内部类通过外部类的对象实例生成内部类对象

return与finally的执行顺序对返回值的影响

对于 try 和 finally 至少一个语句块包含 return 语句的情况:

  • finally 语句块会执行
  • finally 没有 return,finally 对 return 变量的重新赋值修改无效
  • try 和 finally 都包含 return,return 值会以 finally 语句块 return 值为准

如下面的例子

public static void main(String[] args) {
    System.out.println(getString());
}
	
public static String getString() {
    String str = "A";
    try {
        str = "B";
        return str;
    } finally {
        System.out.println("finally change return string to C");
        str = "C";
    }
}

打印

finally change return string to C
B

finally 语句块中新增 return 语句

public static void main(String[] args) {
    System.out.println(getString());
}

public static String getString() {
    String str = "A";
    try {
        str = "B";
        return str;
    } finally {
        System.out.println("finally change return string to C");
        str = "C";
        return str;
    }
}

打印结果

finally change return string to C
C

Redis如何设置过期时间?

 redis.expire(key, expiration)
  • 低于 2.1.3 版,只能对 key 设置一次过期时间
  • 2.1.3 版开始,可以更新 key 的过期时间
  • set、del 命令会移除 key 的过期时间设置

过期处理策略:

  • 定时删除:在设置 key 的过期时间时,创建一个定时器,当过期时间到的时候立马执行删除操作
  • 惰性删除:不会在 key 过期时立马删除,而是当外部指令获取这个 key 的时候才会主动删除
  • 定期删除:设置一个时间间隔,每个时间段都会检测是否有过期键,如果有执行删除操作

JavaScript如何定义含有数值1至3的数组?

 var arr = [1,2,3];
var arr = new Array(1, 2, 3);

var arr = new Array();
arr[0]=1;
arr[1]=2;
arr[2]=3;

介绍一下 Spring bean 的生命周期

Bean 的生命周期按最详细的来说如下(参照小马哥的 Spring 专栏课件),其实细节还远不止如此,都在代码 AbstractBeanFactory#doGetBean 里,可以自己走起!

1、Spring Bean 元信息配置阶段。xml、properties 配置文件中配置 bean 的信息;代码中使用注解标识 bean;代码中使用 api 设置 BeanDefinition 的属性值或构造方法参数。
2、Spring Bean 元信息解析阶段。BeanDefinitionReader 的三种实现类(XmlBeanDefinitionReader、PropertiesBeanDefinitionReader、GroovyBeanDefinitionReader),将配置信息解析为 BeanDefinition;AnnotatedBeanDefinitionReader 将注解标识的类或方法,解析成 BeanDefinition。
3、Spring Bean 注册阶段。将 BeanDefinition 注册到 BeanDefinitionRegistry 中。
4、Spring BeanDefinition 合并阶段。AbstractBeanFactory#getMergedBeanDefinition 方法,将有父子层次关系的 BeanDefinition 合并成 RootBeanDefinition。
5、Spring Bean Class 加载阶段。AbstractBeanFactory#resolveBeanClass 方法,若 BeanDefinition 中的 beanClass 不存在,获取类加载器根据包路径+类名加载其 Class 对象,用于后面的实例化。
6、Spring Bean 实例化前阶段。AbstractAutowireCapableBeanFactory#resolveBeforeInstantiation,执行 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
7、Spring Bean 实例化阶段。AbstractAutowireCapableBeanFactory#instantiateBean,执行 InstantiationStrategy#instantiate 方法实例化 bean
8、Spring Bean 实例化后阶段。AbstractAutowireCapableBeanFactory#populateBean,执行 InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation
9、Spring Bean 属性赋值前阶段。AbstractAutowireCapableBeanFactory#populateBean 执行设置属性值,InstantiationAwareBeanPostProcessor#postProcessProperties
10、Spring Bean Aware接口回调阶段。AbstractAutowireCapableBeanFactory#initializeBean -> #invokeAwareMethods 方法中执行
11、Spring Bean 初始化前阶段。AbstractAutowireCapableBeanFactory#initializeBean -> #applyBeanPostProcessorsBeforeInitialization 方法执行 BeanPostProcessor#postProcessBeforeInitialization
12、Spring Bean 初始化阶段。AbstractAutowireCapableBeanFactory#initializeBean -> #invokeInitMethods 方法执行
13、Spring Bean 初始化后阶段。AbstractAutowireCapableBeanFactory#initializeBean -> #applyBeanPostProcessorsAfterInitialization 方法执行
14、Spring Bean 初始化完成阶段。在 ApplicationContext 的生命周期里,AbstractApplicationContext#finishBeanFactoryInitialization -> DefaultListableBeanFactory#preInstantiateSingletons -> SmartInitializingSingleton#afterSingletonsInstantiated
15、Spring Bean 销毁前阶段。AbstractBeanFactory#destroyBean -> DisposableBeanAdapter#destroy -> DestructionAwareBeanPostProcessor#postProcessBeforeDestruction
16、Spring Bean 销毁阶段。AbstractBeanFactory#destroyBean 执行 @PreDestroy 标注方法、实现DisposableBean 接口的destroy() 方法、自定义销毁方法
17、Spring Bean 垃圾收集。BeanFactory 被置空,所缓存的 bean 可被 gc

JDK中什么类可以通过流写入数据到内存?

java.io.ByteArrayOutputStream

接口可否继承接口?抽象类是否可实现接口?抽象类是否可继承实体类?

都可以

打印结果是什么

打印:132424

创建对象时构造器的调用顺序

  • 递归初始化父类静态成员和静态代码块,上层优先
  • 初始化本类静态成员和静态代码块
  • 递归父类构造器,上层优先
  • 调用自身构造器

Mapper 接口并没有实现类,它是如何工作的?

  • Mapper 接口的 Class 对象,被解析包装成 MapperProxyFactory 对象
  • SqlSession 获取 Mapper 接口时,通过 MapperProxyFactory 对象实例化 MapperProxy 动态代理 Mapper 接口
  • 执行 Mapper 接口的方法时,动态代理反射调用 MapperProxy 的 invoke 方法,根据接口与方法找到对应 MappedStatement 执行 SQL

源码入口与上题同。

哪些不能修饰 interface

答案:BCD

分析:
只有 public、abstract和默认的 3 种修饰符能够修饰 interface

Java中有哪些解析XML的类库?有什么特点?

1. DOM(Document Object Model)

  • 符合官方 W3C 标准
  • 是以层次结构组织的节点
  • 可以在层次结构中寻找特定信息
  • 需要加载整个文档、构造层次结构

**优点:**可获取和操作 xml 任意部分的结构和数据
**缺点:**需加载整个 XML 文档,消耗资源大

2. SAX(Simple API for XML)
SAX 解析器基于事件的模型,解析 XML 文档时可以触发一系列事件,解析到期望的节点,可以激活一个回调方法,处理该节点上的数据

优点:

  • 不需要全部加载完 XML 文档,可以在达到期望条件时停止解析
  • 对内存的要求较低,能解析占用存储较大的文档
  • 可以一边加载 XML,一边解析
  • 解析 XML 效率和性能较高

缺点:

  • 需要开发者负责节点的处理逻辑,文档越复杂程序就越复杂
  • 按照 XML 内容单向解析,无法直接定位文档层次,很难同时访问同一个文档中的多处不同数据

3. JDOM(Java-based Document Object Model)
JDOM 自身不包含解析器,使用 SAX2 解析器来解析和验证输入XML文档
包含一些转换器以将 JDOM 表示输出成 SAX2 事件流、DOM 模型、XML 文本文档

**优点:**API 简单,方便开发
**缺点:**灵活性较差;性能较差

4. dom4j(Document Object Model for Java)

  • dom4j 包含了超出 XML 文档表示的功能
  • 支持 XML Schema
  • 支持大文档或流化文档的基于事件的处理
  • 可以选择按照 DOM 或 SAX 方式解析 XML 文档

优点:

  • API丰富易用,较直观,方便开发
  • 支持 XPath
  • 性能较好

缺点:

  • 接口较多,API 较为复杂

总结

  • dom4j 性能最佳
  • JDOM 和 DOM 性能不佳,大文档易内存溢出,但可移植。小文档可考虑使用 DOM 和 JDOM
  • XML 文档较大且不考虑移植性问题可用 dom4j
  • 无需解析全部 XML 文档,可用 SAX

Unsupported major.minor version 52是什么造成的,如何解决?

造成的原因是工程中存在 jar 包编译时所用的 JDK 版本高于工程 build path 中 JDK 的版本。
这里的 version 52 对应 JDK 版本是 1.8,将项目的 build path 中 JDK 的版本调整为高于或等于 1.8 即可。

java中IO流有哪些?

  • 按数据流向:输入流和输出流

输入和输出都是从程序的角度来说的。

输入流:数据流向程序

输出流:数据从程序流出。

  • 按处理单位:字节流和字符流

字节流:一次读入或读出是8位二进制

字符流:一次读入或读出是16位二进制

JDK 中后缀是 Stream 是字节流;后缀是 Reader,Writer 是字符流

  • 按功能功能:节点流和处理流

节点流:直接与数据源相连,读入或写出

处理流:与节点流一块使用,在节点流的基础上,再套接一层

最根本的四大类:InputStream(字节输入流),OutputStream(字节输出流),Reader(字符输入流),Writer(字符输出流)

四大类的扩展,按处理单位区分

  1. InputStream:FileInputStream、PipedInputStream、ByteArrayInputStream、BufferedInputstream、SequenceInputStream、DataInputStream、ObjectInputStream
  2. OutputStream:FileOutputStream、PipedOutputStream、ByteArrayOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream、PrintStream
  3. Reader:FileReader、PipedReader、CharArrayReader、BufferedReader、InputStreamReader
  4. Writer:FileWriter、PipedWriter、CharArrayWriter、BufferedWriter、InputStreamWriter、PrintWriter

常用的流

  1. 对文件进行操作:FileInputStream(字节输入流)、FileOutputStream(字节输出流)、FileReader(字符输入流)、FileWriter(字符输出流)
  2. 对管道进行操作:PipedInputStream(字节输入流)、PipedOutStream(字节输出流)、PipedReader(字符输入流)、PipedWriter(字符输出流)
  3. 字节/字符数组:ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
  4. Buffered 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
  5. 字节转化成字符流:InputStreamReader、OutputStreamWriter
  6. 数据流:DataInputStream、DataOutputStream
  7. 打印流:PrintStream、PrintWriter
  8. 对象流:ObjectInputStream、ObjectOutputStream
  9. 序列化流:SequenceInputStream

Redisson、Jedis、Lettuce各有什么优缺点?

Redisson

优点:

  • 实现了分布式特性和可扩展的 Java 数据结构,适合分布式开发
  • API 线程安全
  • 基于 Netty 框架的事件驱动的通信,可异步调用

缺点:

  • API 更抽象,学习使用成本高

Jedis

优点:

  • 提供了比较全面的 Redis 操作特性的 API
  • API 基本与 Redis 的指令一一对应,使用简单易理解

缺点:

  • 同步阻塞 IO
  • 不支持异步
  • 线程不安全

Lettuce

优点:

  • 线程安全
  • 基于 Netty 框架的事件驱动的通信,可异步调用
  • 适用于分布式缓存

缺点:

  • API 更抽象,学习使用成本高

哪些因素影响oracle查询性能?

  • 硬件:处理器速度,内存大小,磁盘读写速度,网络传输速度等
  • 索引:是否建立了索引,索引是否合理
  • 碎片:表碎片和索引碎片,生产库长时间运营,碎片可能导致查询使用错误的执行计划,导致查询速度变慢
  • initial 参数:表或索引的 initial 参数配置不同,导致数据扩展区大小不一,也可能导致查询速度降低
  • 慢 SQL:编写的 SQL 执行效率低,查询速度慢
  • 负载:数据库负载过大

List、Set、Map哪个继承自Collection接口?

  • List 和 Set 继承自 Collection 接口
  • Map 是一个接口,未继承其他接口,仅仅默认继承了 Object 类

oracle中常用的函数

  • length 长度
  • lower 小写
  • upper 大写
  • to_date 转化日期
  • to_char 转化字符
  • to_number 转变为数字
  • ltrim 去左边空格
  • rtrim 去右边空格
  • substr 取字串

解释以下正则表达式的含义

  • \d 匹配一个数字字符,等价于[0-9]
  • \D 匹配一个非数字字符,等价于[^0-9]
  • \s 匹配任何空白字符,包括空格、制表符、换页符等,等价于 [ \f\n\r\t\v]
  • . 匹配除换行符 \n 之外的任何单字符,匹配 . 字符需要转译,使用 \.
  • * 匹配前面的子表达式零或多次,匹配 * 字符,需要转译使用 \*
  • ? 匹配前面子表达式零或一次,或表示指明表达式为非贪婪模式的限定符。匹配 ? 字符,需要转译使用 \?
  • | 将两个匹配条件进行逻辑 或 运算
  • + 匹配前面的子表达式一次或多次,要匹配 + 字符需要转译,使用 \+
  • [0-9]{6} 匹配连续6个0-9之间的数字

谈谈你知道的垃圾收集器

Serial
特点:

  • JDK 1.3 开始提供
  • 新生代收集器
  • 无线程交互开销,单线程收集效率最高
  • 进行垃圾收集时需要暂停用户线程
  • 适用于客户端,小内存堆的回收

ParNew
特点:

  • 是 Serial 收集器的多线程并行版
  • JDK 7 之前首选的新生代收集器
  • 第一款支持并发的收集器,首次实现垃圾收集线程与用户线程基本上同时工作
  • 除 Serial 外,只有它能与 CMS 配合

Parallel Scavenge
特点:

  • 新生代收集器
  • 标记-复制算法
  • 多线程并行收集器
  • 追求高吞吐量,即最小的垃圾收集时间
  • 可以配置最大停顿时间、垃圾收集时间占比
  • 支持开启垃圾收集自适应调节策略,追求适合的停顿时间或最大的吞吐量

Serial Old
特点:

  • 与 Serial 类似,是 Serial 收集器的老年代版本
  • 使用标记-整理算法

Parallel Old
特点:

  • JDK 6 开始提供
  • Parallel Scavenge 的老年代版
  • 支持多线程并发收集
  • 标记-整理算法
  • Parallel Scavenge + Parallel Old 是一个追求高吞吐量的组合

CMS
特点:

  • 标记-清除算法
  • 追求最短回收停顿时间
  • 多应用于关注响应时间的 B/S 架构的服务端
  • 并发收集、低停顿
  • 占用一部分线程资源,应用程序变慢,吞吐量下降
  • 无法处理浮动垃圾,可能导致 Full GC
  • 内存碎片化问题

G1
特点:

  • JDK 6 开始实验,JDK 7 商用
  • 面向服务端,JDK 9 取代 Parallel Scavenge + Parallel Old
  • 结合标记-整理、标记-复制算法
  • 首创局部内存回收设计思路
  • 基于 Region 内存布局,采用不同策略实现分代
  • 不再使用固定大小、固定数量的堆内存分代区域划分
  • 优先回收价收益最大的 Region
  • 单个或多个 Humongous 区域存放大对象
  • 使用记忆集解决跨 Region 引用问题
  • 复杂的卡表实现,导致更高的内存占用,堆的 10%~20%
  • 全功能垃圾收集器
  • 追求有限的时间内最高收集效率、延迟可控的情况下最高吞吐量
  • 追求应付内存分配速率,而非一次性清掉所有垃圾内存
  • 适用于大内存堆

Shenandoah
特点:

  • 追求低延迟,停顿 10 毫秒以内
  • OpenJDK 12 新特性,RedHat 提供
  • 连接矩阵代替记忆集,降低内存使用与伪共享问题出现概率

ZGC
特点:

  • JDK 11 新加的实验性质的收集器
  • 追求低延迟,停顿 10 毫秒以内
  • 基于 Region 内存布局
  • 未设分代
  • 读屏障、染色指针、内存多重映射实现可并发的标记-整理算法
  • 染色指针和内存多重映射设计精巧,解决部分性能问题,但降低了可用最大内存、操作系统受限、只支持 32 位、不支持压缩指针等
  • 成绩亮眼、性能彪悍

linux指令-du

是查看目录使用空间情况,与 df 命令不同的是 du 命令是对文件和目录磁盘使用的空间的查看

命令格式:du [选项] [文件]
常用参数: 
-a 显示目录中所有文件大小
-k 以KB为单位显示文件大小
-m 以MB为单位显示文件大小
-g 以GB为单位显示文件大小
-h 以易读方式显示文件大小
-s 仅显示总计
-c或--total  除了显示个别目录或文件的大小外,同时也显示所有目录或文件的总和

char与varchar的区别

  • char 是一种固定长度的字符串类型
  • varchar 是一种可变长度的字符串类型

什么是CSRF攻击,如何避免?

CSRF:Cross Site Request Forgery(跨站点请求伪造)。
CSRF 攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任,以用户身份在攻击页面对目标网站发起伪造用户操作的请求,达到攻击目的。

避免方法:

  • CSRF 漏洞进行检测的工具,如 CSRFTester、CSRF Request Builder…
  • 验证 HTTP Referer 字段
  • 添加并验证 token
  • 添加自定义 http 请求头
  • 敏感操作添加验证码
  • 使用 post 请求

参考:

如何保证消息不被重复消费?

消息被重复消费,就是消费方多次接受到了同一条消息。根本原因就是,第一次消费完之后,消费方给 MQ 确认已消费的反馈,MQ 没有成功接受。比如网络原因、MQ 重启等。

所以 MQ 是无法保证消息不被重复消费的,只能业务系统层面考虑。

不被重复消费的问题,就被转化为消息消费的幂等性的问题。幂等性就是指一次和多次请求的结果一致,多次请求不会产生副作用。

保证消息消费的幂等性可以考虑下面的方式:

  • 给消息生成全局 id,消费成功过的消息可以直接丢弃
  • 消息中保存业务数据的主键字段,结合业务系统需求场景进行处理,避免多次插入、是否可以根据主键多次更新而并不影响结果等

JVM 监控与分析工具你用过哪些?介绍一下。

  • jps,显示系统所有虚拟机进程信息的命令行工具

  • jstat,监视分析虚拟机运行状态的命令行工具

  • jinfo,查看和调整虚拟机参数的命令行工具

  • jmap,生成虚拟机堆内存转储快照的命令行工具

  • jhat,显示和分析虚拟机的转储快照文件的命令行工具

  • jstack,生成虚拟机的线程快照的命令行工具

  • jcmd,虚拟机诊断工具,JDK 7 提供

  • jhsdb,基于服务性代理实现的进程外可视化调试工具,JDK 9 提供

  • JConsole,基于JMX的可视化监视和管理工具

  • jvisualvm,图形化虚拟机使用情况的分析工具

  • Java Mission Control,监控和管理 Java 应用程序的工具

  • MAT,Memory Analyzer Tool,虚拟机内存分析工具

  • vjtools,唯品会的包含核心类库与问题分析工具

  • arthas,阿里开源的 Java 诊断工具

  • greys,JVM进程执行过程中的异常诊断工具

  • GCHisto,GC 分析工具

  • GCViewer,GC 日志文件分析工具

  • GCeasy,在线版 GC 日志文件分析工具

  • JProfiler,检查、监控、追踪 Java 性能的工具

  • BTrace,基于动态字节码修改技术(Hotswap)实现的Java程序追踪与分析工具

下面可以重点体验下:

  • JDK 自带的命令行工具方便快捷,不是特别复杂的问题可以快速定位;
  • 阿里的 arthas 命令行也不错;
  • 可视化工具 MAT、JProfiler 比较强大。

Redis支持哪些数据类型?

Redis 支持五种数据类型

  • string:字符串
  • hash:哈希
  • list:列表
  • set:集合
  • sorted set:有序集合

LongAdder与AtomicLong有什么区别?

  • AtomicLong 是基于 CAS 方式自旋更新的;LongAdder 是把 value 分成若干cell,并发量低的时候,直接 CAS 更新值,成功即结束。并发量高的情况,CAS更新某个cell值和需要时对cell数据扩容,成功结束;更新失败自旋 CAS 更新 cell值。取值的时候,调用 sum() 方法进行每个cell累加。
  • AtomicLong 包含有原子性的读、写结合的api;LongAdder 没有原子性的读、写结合的api,能保证结果最终一致性。
  • 低并发场景AtomicLong 和 LongAdder 性能相似,高并发场景 LongAdder 性能优于 AtomicLong。

如何连接MySQL服务端、关闭连接?

  • 连接:使用指令 mysql -u -p -h -P (-u:指定用户名 -p:指定密码 -h:主机 -P:端口) 连接 MySQL 服务端
  • 关闭:使用指令 exit 或 quit

Redis集群之间是如何复制?

2.8 版以前,Redis 通过同步(sync)和指令传播(command propagate)两个操作完成同步

  • 同步(sync):将从节点的数据库状态更新至与主节点的数据库状态一致
  • 指令传播(command propagate):主节点数据被修改,会主动向从节点发送执行的写指令,从节点执行之后,两个节点数据状态又保持一致

2.8 版开始新增 PSYNC 指令,PSYNC 具有两种模式:

  • 完整重同步(full resynchronization),与 SYNC 过程基本一致
  • 部分重同步(partial resynchronization),借助复制偏移量、复制积压缓冲区、服务器运行 ID ,完成主从节点断开连接后,从节点重连主节点后,条件允许,主节点将连接断开期间执行的写指令发送给从节点,从节点接收并执行写指令,将数据库更新至主节点当前状态

Iterator怎么使用?有什么特点?

Iterator 接口源码中的方法

  • java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
  • next() 方法获得集合中的下一个元素
  • hasNext() 检查集合中是否还有元素
  • remove() 方法将迭代器新返回的元素删除
  • forEachRemaining(Consumer<? super E> action) 方法,遍历所有元素

JDK 1.8 源码如下:

//是否有下一个元素
boolean hasNext();
 
//下一个元素
E next();
 
//从迭代器指向的集合中删除迭代器返回的最后一个元素
default void remove() {
    throw new UnsupportedOperationException("remove");
}
 
//遍历所有元素
default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
        action.accept(next());
}

Iterator 的使用示例

public class TestIterator {
    
    static List<String> list = new ArrayList<String>();
    
    static {
        list.add("111");
        list.add("222");
        list.add("333");
    }
    
 
    public static void main(String[] args) {
        testIteratorNext();
        System.out.println();
        
        testForEachRemaining();
        System.out.println();
        
        testIteratorRemove();
    }
    
    //使用 hasNext 和 next遍历 
    public static void testIteratorNext() {
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            System.out.println(str);
        }
    }
    
    //使用 Iterator 删除元素 
    public static void testIteratorRemove() {
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if ("222".equals(str)) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
    
    //使用 forEachRemaining 遍历
    public static void testForEachRemaining() {
        final Iterator<String> iterator = list.iterator();
        iterator.forEachRemaining(new Consumer<String>() {
 
            public void accept(String t) {
                System.out.println(t);
            }
            
        });
    }
}

注意事项

  • 在迭代过程中调用集合的 remove(Object o) 可能会报 java.util.ConcurrentModificationException 异常

  • forEachRemaining 方法中 调用Iterator 的 remove 方法会报 java.lang.IllegalStateException 异常

    //使用迭代器遍历元素过程中,调用集合的 remove(Object obj) 方法可能会报 java.util.ConcurrentModificationException 异常
    public static void testListRevome() {
    ArrayList aList = new ArrayList();
    aList.add(“111”);
    aList.add(“333”);
    aList.add(“222”);
    System.out.println(“移除前:”+aList);

    Iterator<String> iterator = aList.iterator();
    while(iterator.hasNext()) {
        if("222".equals(iterator.next())) {
            aList.remove("222");          
        }
    }
    System.out.println("移除后:"+aList);
    

    }

    //JDK 1.8 Iterator forEachRemaining 方法中 调用Iterator 的 remove 方法会报 java.lang.IllegalStateException 异常
    public static void testForEachRemainingIteRemove () {
    final Iterator iterator = list.iterator();
    iterator.forEachRemaining(new Consumer() {

        public void accept(String t) {
            if ("222".equals(t)) {
                iterator.remove();
            }
        }
    });
    

    }

Java中的 << >> >>> 是什么?

  • << 表示左移,不分正负数,低位补0
  • 表示右移,如果该数为正,则高位补0,若为负数,则高位补1

  • 表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0

测试代码:

System.out.println("16 <<1 : " + (16 <<1));
System.out.println("16 >> 3 : " + (16 >> 3));
System.out.println("16 >> 10 : " + (16 >> 10));
System.out.println("1 >> 1 : " + (1 >> 1));
System.out.println("16 >>> 2 : " + (16 >>> 2));
System.out.println("-16 >> 2 : " + (-16 >> 2));
System.out.println("-16 <<2 : " + (-16 <<2));
System.out.println("-16 >>> 2 : " + (-16 >>> 2));

打印结果:

16 <<1 : 32
16 >> 3 : 2
16 >> 10 : 0
1 >> 1 : 0
16 >>> 2 : 4
-16 >> 2 : -4
-16 <<2 : -64
-16 >>> 2 : 1073741820

简单理解:

  • <<1 相当于乘以2
  • 1 相当于除以2

  • 不考虑高位的正负号,正数的 >>> 等同于 >>

PS:位移操作涉及二进制、原码、补码、反码相关,可参考:

Mapper XML 映射文件中支持哪些标签?分别什么作用?

  • select: 映射查询语句
  • insert: 映射插入语句
  • updae: 映射更新语句
  • delete: 映射删除语句
  • resultMap: 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素
  • parameterMap: 老式风格的参数映射,已废弃
  • sql: 定义可被其它语句引用的可重用语句块
  • include: 引入 sql 片段
  • selectKey: 为不支持自增的主键生成策略标签
  • cache: 该命名空间的缓存配置
  • cache-ref: 引用其它命名空间的缓存配置
  • 9 种动态 SQL 标签(见上题)

什么是索引?什么场景使用?

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息

使用索引目的是加快检索表中数据

使用场景:

  • 中到大数据量表适合使用索引
  • 小数据量表,大部分情况全表扫描效率更高
  • 特大数据量表,建立和使用索引的代价会随之增大,适合使用分区或分库

MyBatis 如何进行 1对1 和 1对多 的关联查询?

  • 在 Mapper xml 中的标签里使用可以完成 1对1 关联查询

  • 在 Mapper xml 中的标签里使用可以完成 1对多 关联查询

    //sql
    create table user (
    id int primary key,
    name varchar(400)
    );
    insert user info VALUES(1, ‘ConstXiong1’);

    create table info (
    user_id int primary key,
    name varchar(400)
    );
    insert into info VALUES(1, ‘大熊’);

    create table article (
    user_id int,
    title varchar(400)
    );
    insert into article VALUES(1, ‘文章1’);
    insert into article VALUES(1, ‘文章2’);

    //Mapper xml

    select user.id, user.name, info.user_id, info.name as info_name from user,info where user.id = info.user_id select user.id, user.name, article.user_id, article.title from user,article where user.id = article.user_id

    //User.java
    /**

    • 用户表模型
      */
      public class User {
      private Integer id;

      private String name;

      private String mc;

      private Info info;

      private List

      articles;

      public User() {
      }

      public User(Integer id, String name) {
      this.id = id;
      this.name = name;
      }

      public Integer getId() {
      return id;
      }

      public void setId(Integer id) {
      this.id = id;
      }

      public String getName() {
      return name;
      }

      public void setName(String name) {
      this.name = name;
      }

      public String getMc() {
      return mc;
      }

      public void setMc(String mc) {
      this.mc = mc;
      }

      public Info getInfo() {
      return info;
      }

      public void setInfo(Info info) {
      this.info = info;
      }

      public List

      getArticles() {
      return articles;
      }

      public void setArticles(List

      articles) {
      this.articles = articles;
      }

      @Override
      public String toString() {
      return “User{” +
      “id=” + id +
      “, name='” + name + ‘’’ +
      “, mc='” + mc + ‘’’ +
      “, info=” + info +
      “, articles=” + articles +
      ‘}’;
      }
      }

    //测试代码
    System.out.println(“------ selectUserWithInfo ------”);
    user = userMapper.selectUserWithInfo();
    System.out.println(user);

    System.out.println(“------ selectUserWithArticles ------”);
    user = userMapper.selectUserWithArticles();
    System.out.println(user);

    //打印
    ------ selectUserWithInfo ------
    User{id=1, name=‘ConstXiong1’, mc=‘null’, info=Info{userId=1, name=大熊}, articles=null}
    ------ selectUserWithArticles ------
    User{id=1, name=‘ConstXiong1’, mc=‘null’, info=null, articles=[Article{userId=1, title=‘文章1’}, Article{userId=1, title=‘文章2’}]}

完整 Demo:

https://javanav.com/val/a9fe4555c1614b40b0da07afeabf2a66.html

数组与链表的区别

  • 存取方式:数组可以顺序存取或者随机存取;链表只能顺序存取
  • 存储位置:数组逻辑上相邻的元素在物理存储位置上也相邻;链表的物理存储位置不确定,一般是分散的
  • 存储空间:链表由于带有指针域,存储密度不如数组大
  • 按序号查找:数组可以随机访问,时间复杂度为 O(1);链表不支持随机访问,平均需要 O(n);
  • 按值查找:若数组无序,数组和链表时间复杂度均为 O(n),当数组有序时,可以采用二分查找将时间复杂度降为O(log n)
  • 插入和删除:数组平均需要移动 n/2 个元素;链表只需修改指针即可
  • 空间分配方面: 数组,在静态存储分配情形下,存储元素数量受限制。动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;链表,存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效

说说反射在你实际开发中的使用

反射使用的不当,对性能影响比较大,一般项目中直接使用较少。

反射主要用于底层的框架中,Spring 中就大量使用了反射,比如:

  • 用 IoC 来注入和组装 bean
  • 动态代理、面向切面、bean 对象中的方法替换与增强,也使用了反射
  • 定义的注解,也是通过反射查找

列值为NULL时,查询是否会用到索引?

MySQL 中存在 NULL 值的列也是走索引的

计划对列进行索引,应尽量避免把它设置为可空,因为这会让 MySQL 难以优化引用了可空列的查询,同时增加了引擎的复杂度

JSP常用的标签

  • 请求转发:jsp:forward
  • 页面传递数据:jsp:param
  • 输出标签:<c:out>
  • 判读标签<c:if>
  • 迭代标签<c:foreach>
  • 多重判断标签<c:choose>

Dubbo和Dubbox之间的关系

Dubbo 是阿里巴巴公司开源的一个基于Java的高性能开源 RPC 框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。

Dubbo 后来没有维护,当当网基于 Dubbo 做了一些扩展,推出 Dubbox:

  • 支持 REST 风格远程调用(HTTP + JSON/XML)
  • 支持基于 Kryo 和 FST 的 Java 高效序列化实现
  • 支持基于 Jackson 的 JSON 序列化
  • 支持基于嵌入式 Tomcat 的 HTTP remoting
  • 把 Spring 从 2.x 版升级到 3.x 版
  • 升级 ZooKeeper 客户端
  • 支持完全基于 Java 代码完成配置
  • 修复了 dubbo 中配置、序列化、管理界面等功能里的一些 bug

safepoint 是什么?

为了减少对象引用的扫描,使用 OopMap 的数据结构在特定的位置记录下栈里和寄存器里哪些位置是引用;

但为了避免给每条指令都生成 OopMap 记录占用大量内存的问题,只在特定位置记录这些信息。

安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,如方法调用、循环跳转、异常跳转等都属于指令序列复用。

什么是包装类?为什么要有包装类?基本类型与包装类如何转换?

Java 中有 8 个基本类型,分别对应的包装类如下

  • byte – Byte
  • boolean – Boolean
  • short – Short
  • char – Character
  • int – Integer
  • long – Long
  • float – Float
  • double – Double

为什么要有包装类

  • 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
  • 不符合面向对象思维
  • 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等

基本数据类型和包装类之间的转换

  • 包装类–>基本数据类型:包装类对象.xxxValue()
  • 基本数据类型–>包装类:new 包装类(基本类型值)
  • JDK1.5 开始提供了自动装箱(autoboxing)和自动拆箱(autounboxing)功能, 实现了包装类和基本数据类型之间的自动转换
  • 包装类可以实现基本类型和字符串之间的转换,字符串转基本类型:parseXXX(String s);基本类型转字符串:String.valueOf(基本类型)

HashSet实现原理是什么?有什么特点?

  • HashSet 是基于 HashMap 实现的,查询速度特别快
  • HashMap 是支持 key 为 null 值的,所以 HashSet 支持添加 null 值
  • HashSet 存放自定义类时,自定义类需要重写 hashCode() 和 equals() 方法,确保集合对自定义类的对象的唯一性判断(具体判断逻辑,见 HashMap put() 方法,简单概括就是 key 进行 哈希。判断元素 hash 值是否相等、key 是否为同个对象、key 是否 equals。第 1 个条件为 true,2、3 有一个为 true,HashMap 即认为 key 相同)
  • 无序、不可重复

如何查看 JVM 当前使用的是什么垃圾收集器?

-XX:+PrintCommandLineFlags 参数可以打印出所选垃圾收集器和堆空间大小等设置

如果开启了 GC 日志详细信息,里面也会包含各代使用的垃圾收集器的简称

Redis的队列如何异步使用?

Redis 的 list 结构可以作为队列使用,rpush 生产消息,lpop 消费消息,lpop 没有取到消息时,可以让线程休眠一会再获取消息

blpop 指令,在队列没有消息时,会阻塞线程直到消息被生产,获取消息

List、Set、Map 是否继承自 Collection 接口?

List、Set 的父接口是 Collection

Map 不是其子接口,与 Collection 接口相互独立

MySQL如何进行慢SQL优化?

思路:

  • 通过慢查询日志去寻找哪些 SQL 执行效率低
  • 使用 explain 获取低效率 SQL 的执行计划
  • 结合 SQL 与执行计划,进行分析与优化​

引起 SQL 查询很慢的原因与解决办法:

1、没有索引。解决办法:

  • 根据 where 和 order by 使用比较频繁的字段创建索引,提高查询效率
  • 索引不宜过多,单表最好不要超过 6 个。索引过多会导致占用存储空间变大;insert、update 变慢
  • 删除未使用的索引

2、索引未生效。解决办法:

  • 避免在 where 子句中对字段进行 null 值判断,创建表默认值是 NULL。尽量使用 NOT NULL,或使用特殊值,如 0、-1
  • 避免在 where 子句中使用 != 或 <> 操作符, MySQL 只有对以下操作符才使用索引:<、<=、=、>、>=、BETWEEN、IN、非 % 开头的 LIKE
  • 避免在 where 子句中使用 or 来连接条件,可以使用 UNION 进行连接
  • 能用 union all 就不用 union,union 过滤重复数据要耗费更多的 CPU 资源
  • 避免部分 like 查询,如 ‘%ConstXiong%’
  • 避免在索引列上使用计算、函数
  • in 和 not in 慎用,能用 between 不要用 in
  • select 子句中避免使用 *

3、单表数据量太大。解决办法:

  • 分页查询(在索引上完成排序分页操作、借助主键进行关联)
  • 单表数据过大,进行分库分表
  • 考虑使用非关系型数据库提高查询效率
  • 全文索引场景较多,考虑使用 ElasticSearch、solr

提升性能的一些技巧:

  • 尽量使用数字型字段
  • 只需要一行数据时使用 limit 1
  • 索引尽量选择较小的列
  • 不需要的数据在 GROUP BY 之前过滤掉
  • 大部分时候 exists、not exists 比 in、not in 效率(除了子查询是小表的情况使用 in 效率比 exists 高)
  • 不确定长度的字符串字段使用 varchar/nvarchar,如使用 char/nchar 定长存储会带来空间浪费
  • 不要使用 select *,去除不需要的字段查询
  • 避免一次性查询过大的数据量
  • 使用表别名,减少多表关联解析时间
  • 多表 join 最好不超过 5 个,视图嵌套最好不超过 2 个
  • or 条件查询可以拆分成 UNION 多个查询
  • count(1) 比 count(*) 有效
  • 判断是否存在数据使用 exists 而非 count,count 用来获取数据行数

建表语句中varchar(50)中50的指是什么?

字段最多存放 50 个字符

如 varchar(50) 和 varchar(200) 存储 “ConstXiong” 字符串所占空间是一样的,后者在排序时会消耗更多内存

Spring mvc 与 Spring boot 有什么区别?

  • Spring 是一个框架,核心功能是 aop 和 ioc,aop 提供了面向切面编程的能力,ioc 提供了依赖注入的容器。提供了丰富的功能:JDBC 层抽象、事务管理、MVC、Java Mail、任务调度、JMX、JMS、JNDI、EJB、动态语言、远程访问、Web Service… 基于 Spring 衍生出 mvc、boot、security、jpa、cloud 等产品,组成了 Spring 家族产品。
  • Spring MVC 是基于 Spring 实现了 servlet 规范的 MVC 框架,用于 Java Web 开发。
  • Spring Boot 是基于 Spring 的一套快速开发整合包。Spring 的配置非常复杂,同时每次开发都需要写很多模板代码与配置,为了简化开发流程,官方推出了 Spring Boot,实现了自动配置,降低项目搭建的复杂度。本质上 Spring Boot 只是配置、整合、辅助的工具,如果是 Java Web 应用,Web 功能的实现还是依赖于 Spring MVC。

如何用 Spring 加载资源?

Spring 中定义了资源接口:

  • 只读资源 Resource
  • 输入流 InputStreamSource
  • 可写资源 WritableResource
  • 带编码的资源 EncodedResource
  • 上下文资源 ContextResource

内建了几种实现:

  • BeanDefinitionResource
  • ByteArrayResource
  • ClassPathResource
  • FileSystemResource
  • UrlResource
  • ServletContextResource

使用层面

  • 可以通过 @Value 注解注入 Resource 对象
  • 注入 ResouceLoader,loader 资源

Map的实现类中,哪些是有序的,哪些是无序的,如何保证其有序性?

  • Map 的实现类有 HashMap、LinkedHashMap、TreeMap

  • HashMap是有无序的

  • LinkedHashMap 和 TreeMap 是有序的。LinkedHashMap 记录了添加数据的顺序;TreeMap 默认是升序

  • LinkedHashMap 底层存储结构是哈希表+链表,链表记录了添加数据的顺序

  • TreeMap 底层存储结构是二叉树,二叉树的中序遍历保证了数据的有序性

Oracle有哪些备份方式?

备份就是把数据库复制到转储设备的过程

从物理与逻辑的角度:

  • 物理备份:对数据库操作系统的物理文件(数据文件、控制文件、日志文件)的备份。物理备份又可以分为脱机备份(冷备份)和联机备份(热备份),前者是在关闭数据库的时候进行的,后者是以归档日志的方式对运行的数据库进行备份
  • 逻辑备份:对数据库逻辑组件(如表和存储过程等数据库对象)的备份。逻辑备份的手段很多,如 EXP、EXPDP、第三方工具

从数据库的备份角度:

  • 完全备份:每次对数据库进行完整备份
  • 增量备份:在上次完全备份或增量备份后被修改的文件才会被备份
  • 差异备份:备份自从上次完全备份之后被修改过的文件

下载整套资料
关注公众号后台回复:java2024
在这里插入图片描述

  • 51
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小藕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值