裸泳的猪

沾沾自喜其实最可悲

0%

经典面试题:

synchronized 和 ReentrantLock 区别是什么?

相同点:

它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

区别:

  1. synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
  2. synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  3. synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  4. synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  5. synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  6. synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
  7. 锁的细粒度和灵活度,很明显ReenTrantLock优于Synchronized。

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

  • AQS(AbstractQueuedSynchronizer)抽象的队列式的同步器。是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建。
    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
private Lock lock = new ReentrantLock();

public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
// ignored
}finally {
lock.unlock();
}
}

ReentrantLock原理

ThreadLocal原理和用法

用法:

  • ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,
    但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离
  • 实现线程级别的全局变量,避免使用一层层的传参。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Requestcontext {
    private static final ThreadLocal<String> traceldLocal = new ThreadLocal<>();

    public static String getTraceId() {
    return threadIdLocal.get();
    }
    public static String setTraceId (String traceld){
    threadldLocal.set(traceld);
    }
    }
    1
    2
    3
    4
    /*一处设置traceid*/
    Requestcontext.setTraceId(UUID.randomUUID().toString());
    /*另一处读取traceId,不在同一个类中调用*/
    RequestContext.getTraceId();

使用举例:

java8之前的日期组件,SimpleDataFormat。当我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。从而导致了线程安全问题。当然我们可以每用一次new一个新对象出来,不过这样效率太低。

所以我们可以使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

1
static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(SimpleDateFormat::new);

不过Java8之后可以用java.time.format.DateTimeFormatter了。

注意(弱引用引起的内存泄露问题):

当线程没有结束,但是 ThreadLocal 已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object> 的键值对,造成内存泄露。(ThreadLocal 被回收,ThreadLocal 关联的线程共享变量还存在。

解决方案:

  1. 使用完线程共享变量后,显式调用 ThreadLocalMap.remove 方法清除线程共享变量。

原理

简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开放定址法(区别于hashmap,没有链表,发生冲突时往后找空位就行了)实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

结构图:

ThreadLocal
每个Thread对象中都维护了一个ThreadLocalMap,这个Map使用ThreadLocal对象作为key。

可以看到的是,ThreadLocal对象虽然是同一个,但是每个线程中的ThreadLocalMap都是单独的,所以各个线程之间是互不影响的。具体数据则时放在ThreadLocalMap的entry数组下的。

父子线程怎么共享数据

可以使用interitableThreadLocals,子线程能继承到父线程的数据,子线程中的修改不影响父线程。

private

  1. #{}和${}的区别是什么?
  • #{}是预编译处理#{}可以防止Sql 注入,它会将所有传入的参数作为一个字符串来处理。
  • ${}是字符串替换。${} 则将传入的参数拼接到Sql上去执行,一般用于表名和字段名参数,$ 所对应的参数应该由服务器端提供,前端可以用参数进行选择,避免 Sql 注入的风险
    Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
    Mybatis在处理时,就是把{}替换成变量的值。

    使用#{}可以有效的防止SQL注入,提高系统安全性。
  1. Mybatis是如何进行分页的?分页插件的原理是什么?

    Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

    分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
阅读全文 »

mysql的4大特性+4种隔离级别:

4大特性即ACID

  • 1 原子性。事务是一个不可分割的整体,事务开始的操作,要么全部执行,要么全部不执行。
  • 2 隔离性。同一时间,只允许一个事务请求同一组数据。不同的事务彼此之间没有干扰。
  • 3 一致性。事务开始前和结束后,数据库的完整性约束没有被破坏 。
  • 4 稳定性。事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

4种隔离级别

大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read

阅读全文 »

设计模式的六大原则

  • 单一职责原则

    一个类只负责一个功能领域的相应职责

    实现高内聚,低耦合的指导方针

  • 开闭原则

    对扩展开放,对修改关闭

  • 里氏替换原则LSP

  • 依赖倒转原则

  • 接口隔离原则

  • 迪米特法则

    • 最少知道原则,一个实体类应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对对立

    • 类之间的耦合度越低,就越有利用于复用

设计模式的三大设计模式分类

  • 创建型

    1
    2
    常用:工厂模式,抽象工厂模式,单例模式,建造者模式
    不常用:原型模式
  • 结构型

    1
    2
    常用:适配器、桥接、装饰器、代理
    不常用:组合、外观、享元
  • 行为型

    1
    2
    常用: 观察者模式、模板模式、策略模式、责任链模式、迭代器模式、状态模式。
    不常用: 访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

入门问题

1.请列举出在 JDK 中几个常用的设计模式?

单例模式(Singleton pattern)用于 Runtime,Calendar 和其他的一些类中。工厂模式
(Factory pattern)被用于各种不可变的类如 Boolean,像 Boolean.valueOf,观察者模式
(Observer pattern)被用于 Swing 和很多的事件监听中。装饰器设计模式(Decorator
design pattern)被用于多个 Java IO 类中。

2.什么是设计模式?你是否在你的代码里面使用过任何设计模式?

设计模式是世界上各种各样程序员用来解决特定设计问题的尝试和测试的方法。设计模式
是代码可用性的延伸。

模板方法模式定义一个操作中的算法的骨架,将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。

应用场景如下:

对于一些功能,在不同的对象身上展示不同的作用,但是功能的框架是一样的。

3.Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式

单例模式重点在于在整个系统上共享一些创建时较耗资源的对象。整个应用中只维护一个
特定类实例,它被所有组件共同使用。Java.lang.Runtime 是单例模式的经典例子。从 Java
5 开始你可以使用枚举(enum)来实现线程安全的单例。

阅读全文 »

适配器模式(Adapter Pattern)

  • 见名知意,是作为两个不兼容的接⼝之间的桥梁,属于结构型模式

  • 适配器模式使得原本由于接⼝不兼容⽽不能⼀起⼯作的 那些类可以⼀起⼯作

  • 常⻅见的⼏类适配器

    • 类的适配器模式
      • 想将⼀个类转换成满⾜另⼀个新接⼝的类时,可 以使⽤类的适配器模式,创建⼀个新类,继承原 有的类,实现新的接⼝即可
      • 对象的适配器模式
        • 想将⼀个对象转换成满⾜另⼀个新接⼝的对象 时,可以创建⼀个适配器类,持有原类的⼀个实 例,在适配器类的⽅法中,调⽤实例的⽅法就⾏
      • 接⼝的适配器模式
        • 不想实现⼀个接⼝中所有的⽅法时,可以创建⼀ 个Adapter,实现所有⽅法,在写别的类的时 候,继承Adapter类即

总结

  • 在使⽤⼀些旧系统或者是类库时,经常会出现接⼝不兼 容的问题,适配器模式在解决这类问题具有优势

  • 学习设计模式⼀定不要局限代码层⾯,要从软件系统整 体去考虑,⽽不是为了使⽤设计模式,⽽去使⽤设计模式

优点

  • 可以让任何两个没有关联的类⼀起运⾏,使得原本由于 接⼝不兼容⽽不能⼀起⼯作的那些类可以⼀起⼯作 增加灵活度, 提⾼复⽤性,适配器类可以在多个系统使 ⽤,符合开闭原则

缺点

整体类的调⽤链路增加,本来A可以直接调⽤C,使⽤适 配器后 是A调⽤B,B再调⽤C

在研究java的IO模型前有必要先对操作系统的IO模型有个认识。

操作系统层面的IO模型

在Linux(UNIX)操作系统中,共有五种IO模型,分别是:

  1. 阻塞IO模型

    • 阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。
  2. 非阻塞IO模型

    • 应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。
  3. IO复用模型

    • 多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
  4. 信号驱动IO模型

    • 应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。

以上模型都是同步的,原因是因为,无论以上那种模型,真正的数据拷贝过程,都是同步进行的。信号驱动IO模型只能说 数据准备阶段是异步,数据拷贝操作还是同步的。只有系统将数据已经全都从内核空间拷贝到用户空间然后再发信号通知线程已经完成

  1. 异步IO模型
    • 应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。

阅读全文 »

定义

单例模式是比较常见的一种设计模式,目的是保证一个类只能有一个实例,而且自行实例化(不对外开放new )并向整个系统提供这个实例,避免频繁创建对象,节约内存。

典型应用场景

  • 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步

    饿汉式和懒汉式,

    饿汉式:提前创建对象。在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。

    懒汉式:延迟创建对象。在类加载时不初始化,等到第一次被使用时才初始化。

饿汉式

在类加载的时候就完成了实例化,避免了多线程的同步问题。缺点就是浪费内存。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {

private final static Singleton INSTANCE = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return INSTANCE;
}

}
阅读全文 »