田维常

田维常 查看完整档案

北京编辑华东师范大学  |  应用统计 编辑自由职业  |  自由职业 编辑 www.woaijava.cc/ 编辑
编辑

欢迎关注我的公众号“Java后端技术全栈”,每天解锁一本电子书

个人动态

田维常 发布了文章 · 2月4日

徒手撸一个Spring Boot中的starter

starter背景

Spring Boot目前已经变成了后端开发这必备技能之一,其中一个主要原因是Spring Boot中有个非常重要的机制(starter机制)。

starter能够抛弃以前繁杂的配置,将其统一集成进starter,使用的时候只需要在maven中引入对应的starter依赖即可,Spring Boot就能自动扫描到要加载的信息并启动相应的默认配置。

starter让我们摆脱了各种依赖库的处理,以及各种配置信息的烦恼。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。Spring Boot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

[[金三银四,如何涨薪看这里]](http://mp.weixin.qq.com/s?__b...

我们经常会看到或者使用到各种xxx-starter。比如下面几种:

img

Spring Boot starter原理

从总体上来看,无非就是将Jar包作为项目的依赖引入工程。而现在之所以增加了难度,是因为我们引入的是Spring Boot Starter,所以我们需要去了解Spring Boot对Spring Boot Starter的Jar包是如何加载的?下面我简单说一下。

SpringBoot 在启动时会去依赖的 starter 包中寻找 /META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包,这类似于 Java 的 SPI 机制。

细节上可以使用@Conditional 系列注解实现更加精确的配置加载Bean的条件。

JavaSPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

自定义starter的条件

如果想自定义Starter,首选需要实现自动化配置,而要实现自动化配置需要满足以下两个条件:

  1. 能够自动配置项目所需要的配置信息,也就是自动加载依赖环境;
  2. 能够根据项目提供的信息自动生成Bean,并且注册到Bean管理容器中;

实现自定义starter

pom.xml依赖

<dependencies>
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.0.0.RELEASE</version>
 </dependency>
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.0.0.RELEASE</version>
    <optional>true</optional>
  </dependency>
</dependencies>

根据需要自定义Starter的实现过程大致如下(以我定义的Starter为例):

img

定义XxxProperties类,属性配置类,完成属性配置相关的操作,比如设置属性前缀,用于在application.properties中配置。

TianProperties代码:

import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.tian")
public class TianProperties {
    private String name;
    private int age;
    private String sex = "M";
    //省略 get set 方法
}

创建XxxService类,完成相关的操作逻辑 。

TianService代码:

public class TianService {
    private TianProperties properties;
    public TianService() {
    }
    public TianService(TianProperties userProperties) {
        this.properties = userProperties;
    }
    public void sayHello(){
        System.out.println("hi, 我叫: " + properties.getName() +
        ", 今年" + properties.getAge() + "岁"
         + ", 性别: " + properties.getSex());
    }
}

定义XxxConfigurationProperties类,自动配置类,用于完成Bean创建等工作。

TianServiceAutoConfiguration代码:

@Configuration
@EnableConfigurationProperties(TianProperties.class)
@ConditionalOnClass(TianService.class)
@ConditionalOnProperty(prefix = "spring.tian", value = "enabled", matchIfMissing = true)
public class TianServiceAutoConfiguration {
    @Autowired
    private TianProperties properties;
    @Bean
    @ConditionalOnMissingBean(TianService.class)
    public TianService tianService() {
        return new TianService(properties);
    }
}

在resources下创建目录META-INF,在 META-INF 目录下创建 spring.factories,在SpringBoot启动时会根据此文件来加载项目的自动化配置类。

「spring.factories中配置」

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.tian.TianServiceAutoConfiguration

把上面这个starter工程打成jar包:

使用自定义starter

创建一个Spring Boot项目test,项目整体如下图:

图片

在项目中把自定义starter添加pom依赖

<dependency>
    <groupId>com.tian</groupId>
    <artifactId>spring-boot-tian-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

TestApplication启动类

@SpringBootApplication
@EnableEurekaServer
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

application.properties中配置

spring.tian.name=tian
spring.tian.age=22
spring.tian.sex=M

写一个TestController.java类

RestController
@RequestMapping("/my")
public class TestController {
    @Resource
    private TianService tianService;
    @PostMapping("/starter")
    public Object starter() {
        tianService.sayHello();
        return "ok";
    }
}

把我们自定义的starter打成的jar依赖进来后,

图片

可以看到其中多了一个json的文件。

最后启动项目,输入

http://localhost:9091/my/starter

图片

controller成功返回ok,再看后台打印

hi, 我叫: tian, 今年22岁, 性别: M

这就成功的现实了自定义的starter。

关键词:开箱即用、减少大量的配置项、约定大于配置

总结

  1. Spring Boot在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包,
  2. 然后读取spring.factories文件获取配置的自动配置类AutoConfiguration`,
  3. 然后将自动配置类下满足条件(@ConditionalOnXxx)的@Bean放入到Spring容器中(Spring Context)
  4. 这样使用者就可以直接用来注入,因为该类已经在容器中了。

「只要我们的方向对了,就不怕路远!」

推荐阅读

面试:Zookeeper常见11个连环炮
金三银四,准备跳槽的看这里!
面试:说说几个常见的Linux性能

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 2月1日

最基础的3道java面试题,别说你不会

想更好的应对面试,还是需要不断学习不断总结,下面我们来分析三道面试题。

  涨薪必备的面试小抄

下面是一道入门级面试题,这道题基本上都是问初级的小伙伴比较多,但如果你是中级,或者高级。我觉得未必都能回答上来。

说说 Java语言有哪些特点

尽量答出以下几个关键词:

1)简单易学。Java有丰富的类库,能够通过静态方法封装,降低API的学习成本,提高工作效率。

2)面向对象。这个也是Java最重要的特性,java能够使得程序耦合度更低,内聚性更高。

3)可靠安全。Java生态系统包括用于分析和报告安全性问题的各种工具。

4)与平台无关。Java能够跨平台使用,Write Once Run AnyWhere。实际上就是对应操作系统上都有个虚拟机作为一个中间转换。

5)支持多线程。Java可以采用多线程+协程方式实现更多的并发操作。

下面也是一道入门级面试题,面向过程与面向对象的区别,这里最好的是搞过C或C++的同学,后面来搞Java了,这样对面向过程和面向对象编程的体会更深。

说说面向对象和面向过程的区别

1)从概念上来说。面向过程:字面意义上就是面向的是过程,先做什么、在做什么、最后做什么,然后用函数把这些步骤一步一步地实现,在使用的时候一一调用则可。面向对象:字面意义上就是面向的是对象,是把构成问题的事务分解成各个对象,但是建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。

2)从性能上来说。面向过程性能较高,所以单片机、嵌入式开发等一般采用面向过程开发。从性能上来说,面向对象比面向过程要低。

3)从可用性来说。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展,可以设计出低耦合的系统。

扩展面试题:说说面向切面和面向对象的区别。

下面还是一道入门面试题,每个面试题都不要掉以轻心,都是考验基本功是否扎实。

说说方法重载和方法重写的区别

都是和方法有关系的,那Java中方法的签名包含什么呢?

要完整的描述一个方法,需要指出方法名以及参数类型,这就叫方法的签名。

比如说String类中有4个称为indexOf的公有方法:

`indexOf(int)
indexOf(int,int)
indexOf(String)
indexOf(String,int)
`

返回类型不是方法签名的一部分,也就是说,不能有两个名字相同、参数类型也相同却有着不同类型返回值的方法。

`public void say(String name){
}
public String say(String name){
}
`

我们继续聊方法的重写与重载:

「方法的重写(Override)」

从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。

`public class Father {
    public static void main(String[] args) { 
        Son s = new Son();
        s.sayHello();
    }
    public void sayHello() {
        System.out.println("Father say Hello");
    }
}
class Son extends Father{
    //Son重写了Father的 sayHello方法
    @Override
    public void sayHello() { 
        System.out.println("Son say hello ");
    }
}
`

总结:

1.发生在父类与子类之间 。

2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同。

3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private) 。

4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。

「方法的重载(Overload)」

Java中允许同一个类中可以定义多个同名方法,只要形参列表(方法入参)不同就行。如果同一个类种包含了两个或者多个以上方法的名称相同,但是形参列表不同,则被称之为方法重载。

在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。

`public class Father {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Father s = new Father();
        s.sayHello();
        s.sayHello("wintershii");
    }
    public void sayHello() {
        System.out.println("Hello");
    }
    public void sayHello(String name) {
        System.out.println("Hello" + " " + name);
    }
    
    public void sayHello(Long userId, String name) {
        sayHello("Hello" + " " + name);
    }
}
`

总结:

方法重载要求两同一不同:

1.同一个类中。

2.方法名相同。

3.参数列表不相同。

方法返回值、修饰符,与方法重载没有任何关系。

不推荐使用以下方式:

`public void sayHello(String name){
}
public void sayHello(String... name){
}
`

因为这么做实在是没有太大的意义,并且容易降低程序的可读性。

总结

本文分享了三道入门级面试题,你都能回答上来吗?你回答都是背的吗还是自己真正理解的?

「稍微认真点、踏实点、努力点,发光是迟早的事」

推荐阅读

《Spring Cloud微服务实战》.pdf
面试官:说说你对序列化的理解
JVM真香系列:堆内存详解

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月30日

奇葩java迭代器笔试题,很少有人能做对

有位小朋友最近正在为年后换工作做准备,但是遇到一个问题,觉得很不可思议的一道笔试题。然后我把这道题发到技术群里,发现很多人居然不知道,很多都是连蒙带猜的说。感觉很有必要写一篇文章来说道说道。

奇怪的笔试题

阅读下面这段代码,请写出这段代码的输出内容:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.*;
​
public class Test {
    public static void main(String[] args) {
​
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = (String) iterator.next();
            if (str.equals("2")) {
                iterator.remove();
            }
        }
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        System.out.println("4");
    }
}

他写出来的答案是:

1
3
4

奇怪的是,你把这道题目发给你身边人,让他们回答这道面试题输出结果是什么,说这个结果的人非常多。不行你试试

~

答案明显不对,因为在第一个while里的 iterator.hasNext()==false后才会到第二个while里来,同一个Iterator对象,前面调一次iterator.hasNext()==false,再判断一次结果不还是一样吗?,

所以第二个while判断为false,也就不会再去遍历iterator了,由此可知本体答案是:4。

下面我们来分析一下为什么是具体底层是怎么实现的。

这里的Iterator是什么?

  • 迭代器是一种模式、详细可见其设计模式,可以使得序列类型的数据结构的遍历行为与被遍历的对象分离,即我们无需关心该序列的底层结构是什么样子的。只要拿到这个对象,使用迭代器就可以遍历这个对象的内部
  • Iterable 实现这个接口的集合对象支持迭代,是可以迭代的。实现了这个可以配合foreach使用~
  • Iterator 迭代器,提供迭代机制的对象,具体如何迭代是这个Iterator接口规范的。

Iterator说明

public interface Iterator<E> { 
    //每次next之前,先调用此方法探测是否迭代到终点     boolean hasNext();
    //返回当前迭代元素 ,同时,迭代游标后移     E next(); 
    /*删除最近一次已近迭代出出去的那个元素。
 只有当next执行完后,才能调用remove函数。 比如你要删除第一个元素,不能直接调用 remove()   而要先next一下( ); 在没有先调用next 就调用remove方法是会抛出异常的。 这个和MySQL中的ResultSet很类似 */
    default void remove() {
        throw new UnsupportedOperationException("remove");
    } 
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

这里的实现类是ArrayList的内部类Itr。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return         int lastRet = -1; // index of last element returned; -1 if no such         //modCountshi ArrayList中的属性,当添加或删除的时候moCount值会增加或者减少         //这里主要是给fail-fast使用,避免一遍在遍历,一遍正在修改导致数据出错         //此列表在结构上被修改的次数。结构修改是指改变结构尺寸的修改列表,         //或者以这样的方式对其进行扰动,进步可能会产生错误的结果。         int expectedModCount = modCount;
​
        public boolean hasNext() {
            //cursor初始值为0,没掉一次next方法就+1             //size是ArrayList的大小             return cursor != size;
        }
​
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            //把ArrayList中的数组赋给elementData             Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            //每调用一次next方法,游标就加1             //cursor=lastRet+1             cursor = i + 1;
            //返回ArrayList中的元素             return (E) elementData[lastRet = i];
        }
​
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
​
            try {
                //调用ArrayList中remove方法,溢出该元素                 ArrayList.this.remove(lastRet);
                //cursor=lastRet+1,                 //所以此时相当于cursor=cursor-1                 cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
}

再回到上面题目中:

第一个iterator.hasNext()

第1次循环

  • hasNext方法中:cursor==0, size==3,所以cursor != size返回true。
  • next方法中:cursor=0+1。返回"1"。

第2次循环

  • hasNext方法中:cursor==1, size==3,所以cursor != size返回true。
  • next方法中:cursor=1+1。返回"2"。
  • remove方法中:cursor==cursor-1==2-1=1,把ArrayList中的"2"给删除了,所以size==2。

第3次循环

  • hasNext方法中:cursor==1, size==2,那么cursor != size返回true。
  • next方法中:cursor=1+1==2;返回"3"。

第4次循环

  • hasNext方法中:cursor==2, size==2,那么cursor != size返回false。

第二个iterator.hasNext()

hasNext方法中:cursor==2, size==2,所以cursor != size返回false。

所以,最后只输出"4",即答案为4.

Iterator与泛型搭配

  • Iterator对集合类中的任何一个实现类,都可以返回这样一个Iterator对象。可以适用于任何一个类。
  • 因为集合类(List和Set等)可以装入的对象的类型是不确定的,从集合中取出时都是Object类型,用时都需要进行强制转化,这样会很麻烦,用上泛型,就是提前告诉集合确定要装入集合的类型,这样就可以直接使用而不用显示类型转换.非常方便.

foreach和Iterator的关系

  • for each以用来处理集合中的每个元素而不用考虑集合定下标。就是为了让用Iterator简单。但是删除的时候,区别就是在remove,循环中调用集合remove会导致原集合变化导致错误,而应该用迭代器的remove方法。

使用for循环还是迭代器Iterator对比

  • 采用ArrayList对随机访问比较快,而for循环中的get()方法,采用的即是随机访问的方法,因此在ArrayList里,for循环较快
  • 采用LinkedList则是顺序访问比较快,iterator中的next()方法,采用的即是顺序访问的方法,因此在LinkedList里,使用iterator较快
  • 从数据结构角度分析,for循环适合访问顺序结构,可以根据下标快速获取指定元素.而Iterator 适合访问链式结构,因为迭代器是通过next()和Pre()来定位的.可以访问没有顺序的集合.
  • 而使用 Iterator 的好处在于可以使用相同方式去遍历集合中元素,而不用考虑集合类的内部实现(只要它实现了 java.lang.Iterable 接口),如果使用 Iterator 来遍历集合中元素,一旦不再使用 List 转而使用 Set 来组织数据,那遍历元素的代码不用做任何修改,如果使用 for 来遍历,那所有遍历此集合的算法都得做相应调整,因为List有序,Set无序,结构不同,他们的访问算法也不一样.(还是说明了一点遍历和集合本身分离了)。

总结

  • 迭代出来的元素都是原来集合元素的拷贝。
  • Java集合中保存的元素实质是对象的引用,而非对象本身。
  • 迭代出的对象也是引用的拷贝,结果还是引用。那么如果集合中保存的元素是可变类型的,那么可以通过迭代出的元素修改原集合中的对象。
查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月29日

15道类和对象面试题,会一半算你厉害了

1.类与对象有哪些区别?

类是一个抽象的概念,是对某一事物的描述;而对象是类的实例,是实实在在存在的个体。

比如:“男人”就是一个类(一个概念),而老田(田维常)就是实实在在的一个“对象”。

注意:对象中又有类对象,即Class对象,但是类对象始终还是对象,不是类,这两个概念别搞混淆了。

2.Java 中可以多继承吗?

Java 中只能单继承,但可以实现多接口,并且支持多层继承。

3.Java 中为什么不能实现多继承?

答:从技术的实现角度来说,是为了降低编程的复杂性。假设 A 类中有一个 m() 方法,B 类中也有一个 m() 方法,如果 C 类同时继承 A 类和 B 类,那调用 C 类的 m() 方法时就会产生歧义,这无疑增加了程序开发的复杂性,为了避免这种问题的产生,Java 语言规定不能多继承类,但可以实现多接口。

4.覆盖和重载有哪些区别?

  • 重写(Override)从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
public class Father {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Son s = new Son();
        s.sayHello();
    }

    public void sayHello() {
        System.out.println("Hello");
    }
}

class Son extends Father{

    @Override
    public void sayHello() {
        // TODO Auto-generated method stub
        System.out.println("hello by ");
    }

}

重写 总结:

1.发生在父类与子类之间

2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同

3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

4.重写方法一定不能抛出新的检查异常或者比被重写方法的更加宽泛的检查型异常

  • 重载(Overload)在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
public class Father {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Father s = new Father();
        s.sayHello();
        s.sayHello("wintershii");

    }

    public void sayHello() {
        System.out.println("Hello");
    }

    public void sayHello(String name) {
        System.out.println("Hello" + " " + name);
    }
}

重载 总结:1.重载Overload是一个类中多态性的一种表现 2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序) 3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准。

5.为什么方法不能根据返回类型来区分重载?

答:因为在方法调用时,如果不指定类型信息,编译器就不知道你要调用哪个方法了。比如,以下代码:

float max(int x,int y);
int max(int x,int y);
// 方法调用
max(1,2);

因为 max(1,2) 没有指定返回值,编译器就不知道要调用哪个方法了。

6.说说构造方法的特点有哪些?

答:构造方法的特征如下:

  • 构造方法必须与类名相同;
  • 构造方法没有返回类型(默认返回本类类型);
  • 构造方法不能被继承、覆盖、直接调用;
  • 类定义时提供了默认的无参构造方法;
  • 构造方法可以私有,外部无法使用私有构造方法创建对象。

构造函数能不能被覆盖?能不能被重载?

构造函数可以重载,但不能覆盖。

7.以下程序执行的结果是?

class ExecTest {
    public static void main(String[] args) {
        Son son = new Son();
    }
}
class Parent{
    {
        System.out.print("1");
    }
    static{
        System.out.print("2");
    }
    public Parent(){
        System.out.print("3");
    }
}
class Son extends Parent{
    {
        System.out.print("4");
    }
    static{
        System.out.print("5");
    }
    public Son(){
        System.out.print("6");
    }
}

结果是:251346

8.类加载顺序

整体

img

细分

img

以下程序执行的结果是?

class A {
    public int x = 0;
    public static int y = 0;
    public void m() {
        System.out.print("A");
    }
}
class B extends A {
    public int x = 1;
    public static int y = 2;
    public void m() {
        System.out.print("B");
    }
    public static void main(String[] args) {
        A myClass = new B();
        System.out.print(myClass.x);
        System.out.print(myClass.y);
        myClass.m();
    }
}

结果是:00B

注意:在 Java 语言中,变量不能被重写。

9.Java 中的 this 和 super 有哪些区别?

this 和 super 都是 Java 中的关键字,起指代作用,在构造方法中必须出现在第一行,它们的区别如下。

  • 基础概念:this 是访问本类实例属性或方法;super 是子类访问父类中的属性或方法。
  • 查找范围:this 先查本类,没有的话再查父类;super 直接访问父类。
  • 使用:this 单独使用时,表示当前对象;super 在子类覆盖父类方法时,访问父类同名方法。

10.在静态方法中可以使用 this 或 super 吗?为什么?

在静态方法中不能使用 this 或 super,因为 this 和 super 指代的都是需要被创建出来的对象,而静态方法在类加载的时候就已经创建了,所以没办法在静态方法中使用 this 或 super。

11.静态方法的使用需要注意哪些问题?

静态方法的使用需要注意以下两个问题:

  • 静态方法中不能使用实例成员变量和实例方法;
  • 静态方法中不能使用 this 和 super。

12.final 修饰符的作用有哪些?

final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了:

  • 被final修饰的类不可以被继承
  • 被final修饰的方法不可以被重写
  • 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
  • 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
  • 被final修饰的常量,在编译阶段会存入常量池中.

除此之外,编译器对final域要遵守的两个重排序规则更好:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.

经典使用场景:Integer,String等类中有使用到。

13.覆盖 equals() 方法的时候需要遵守哪些规则?

Oracle 官方的文档对于 equals() 重写制定的规则如下。

  • 自反性:对于任意非空的引用值 x,x.equals(x) 返回值为真。
  • 对称性:对于任意非空的引用值 x 和 y,x.equals(y) 必须和 y.equals(x) 返回相同的结果。
  • 传递性:对于任意的非空引用值 x、y 和 z,如果 x.equals(y) 返回值为真,y.equals(z) 返回值也为真,那么 x.equals(z) 也必须返回值为真。
  • 一致性:对于任意非空的引用值 x 和 y,无论调用 x.equals(y) 多少次,都要返回相同的结果。在比较的过程中,对象中的数据不能被修改。
  • 对于任意的非空引用值 x,x.equals(null) 必须返回假。

此题目不要求记忆,能知道大概即可,属于加分项题目。

14.在 Object 中 notify() 和 notifyAll() 方法有什么区别?

notify() 方法随机唤醒一个等待的线程,而 notifyAll() 方法将唤醒所有在等待的线程。

如何使用 clone() 方法?

如果是同一个类中使用的话,只需要实现 Cloneable 接口,定义或者处理 CloneNotSupportedException 异常即可,请参考以下代码:

public class CloneTest implements Cloneable {
    int num;
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
}

如果非内部类调用 clone() 的话,需要重写 clone() 方法,请参考以下代码:

class CloneTest implements Cloneable {
    int num;
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class CloneTest2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
}

对象克隆是原型模式的经典实现。

15.java中对象的创建方式有哪几种?

java中提供了以下四种创建对象的方式:

  • new创建新对象
  • 通过反射机制
  • 采用clone机制
  • 通过序列化机制
查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月29日

类和object,so easy啦

最近老是有小伙伴问类和Object相关的问题,感觉还是很多人对此不是很明白,那我们今天就干掉这两个怪物。

类介绍

Java 程序是由若干个类组成的,类也是面向对象编程思想的具体实现。

以下为类的定义:

public class User {
//私有属性
private Long userId;
private String name;
private Integer age;
// 构造方法
public User() {
}
//有残构造方法
public User(Long userId, String name, Integer age) {
this.userId = userId;
this.name = name;
this.age = age;
}

//普通方法
public void say() {
System.out.println("hello world");
}
// 对外包装属性
public String getName() {
return this.name;
}
}

关键字import的三种用法

单类型导入

当我们需要使用不同包下的类时,就需要使用 import 导入包或类,这个时候才能正常使用。例如,我们要使用java.util下的 ArrayList 就必须使用 import java.util.ArrayList,代码如下:

// 导入 ArrayList 类
import java.util.ArrayList;
class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
}
}

按需类型导入

如果我们在同一个类中使用了同一个包下面的较多类时候,就会使用按需类型导入。

// 用到了java.util包目录下的List、ArrayList和LinkedList类
//import java.util.ArrayList;
//import java.util.LinkedList;
//import java.util.List;
//如果不想类上有太多import,就可以直接import 包名.*
import java.util.*;
public class Test {
public static void main(String[] args) {
List list = new ArrayList<>();
List list1 = new LinkedList();
}
}

这个只是表象,其实也是一个一个的导入的,只是在代码层面看起来是一次性全部倒入了。

静态导入

import 还可以导入静态方法和静态域的功能,比如以下代码:

//精准导入
//直接导入具体的静态变量、常量、方法方法,注意导入方法直接写方法名不需要括号。
import static com.assignment.test.StaticFieldsClass.staticField;
import static com.assignment.test.StaticFieldsClass.staticFunction;

//或者使用如下形式:
//按需导入不必逐一指出静态成员名称的导入方式
//import static com.assignment.test.StaticFieldsClass.*;

public class StaticTest {
public static void main(String[] args) {
//这里直接写静态成员而不需要通过类名调用
System.out.println(staticField);
staticFunction();
}
}

以上代码也可以顺利的执行,这也是 import 好玩的一个地方。

构造方法

构造方法也叫构造器或构造函数,它的作用是类在进行初始化的时候会调用对应的构造方法,比如以下代码:

public class User {
//私有属性
private Long userId;
private String name;
private Integer age;
// 构造方法
public User() {
}
//有参构造方法
public User(Long userId, String name, Integer age) {
this.userId = userId;
this.name = name;
this.age = age;
}

//普通方法
public void say() {
System.out.println("hello world");
}
public static void think() {
System.out.println("thinking");
}
// 对外包装属性
public String getName() {
return this.name;
}

构造方法五大原则

  1. 构造方法必须与类同名;
  2. 构造方法的参数可以没有或者有多个;
  3. 构造方法不能定义返回值(默认返回类型就是本类类型);
  4. 每个类可以有一个或多个构造方法;
  5. 构造方法总是伴随着 new 操作一起使用。

注意:如果勒种没有显示的定义构造方法,那么在编译的时候回默认为其添加一个无惨构造方法。构造方法实际开发中通常都是public修饰,还有就是我们想要单例的情况下搞成private修饰。

Object

Object 类是 Java 中的一个特殊类,它是所有类的父类,Java 中的类都直接或间接的继承自 Object 类。

Object 类的常用方法如下:

  • equals():对比两个对象是否相同
  • getClass():返回一个对象的运行时类
  • hashCode():返回该对象的哈希码值
  • toString():返回该对象的字符串描述
  • wait():使当前的线程等待
  • notify():唤醒在此对象监视器上等待的单个线程
  • notifyAll():唤醒在此对象监视器上等待的所有线程
  • clone():克隆一个新对象

关于更多 Object 的内容,如克隆(深克隆、浅克隆)、线程的几个常用方法wait、notify、notifyAll,对象比较,对象的hashCode值等。

继承

Java 中只支持单继承:即一个子类只能继承两个父类,而一个父类可以被多个子类继承。

每个人都只能有一个亲生父亲,一个父亲是可以有多个儿子的。

用法:使用 extends 关键字来实现类的继承,示例代码如下:

class Person {
public void say() {
System.out.println("hello");
}
}
public class User extends Person {
public static void main(String[] args) {
Person user = new User();
user.say();
}
}

以上程序执行结果:hello

继承的注意点

  1. 单一继承性。(在Java中是不支持多继承的,通俗的说子类只能有一个父类,而父类可以有很多子类。)
  2. 支持多层继承。(继承可以一直传下去,子类有父类,父类又有父类...)
  3. 如果父类成员使用private修饰,那么子类不能被继承。(private只是对本类有效)
  4. 如果一个子类继承了父类的属性和方法还可以有自己特有的属性和方法。(不光有父类的属性(可继承的)和方法(可继承的),也有自己独有的属性和方法。)
  5. 当子类和父类的成员变量重名的时候,子类优先。(就近原则)

继承使用技巧

  • 将公共的变量或者方法提取到超类中;
  • 除非所有的方法都有继承的意义,否则不要使用继承;
  • 在方法覆盖时不要改变原有方法的预期行为。
  • 一般在写代码的时候发现代码中存在重复代码,需要向上抽取,考虑继承。
  • 当某个类的设计非常复杂的时候可以考虑继承

继承的优点

  • 代码的可重用性。
  • 使用继承可以轻松的定义子类。
  • 父类的属性和方法可以用于子类中(非private修饰)。
  • 设计应用程序变得更加简单。

设计模式中大量使用

比如:模板方法模式,就是采用继承,子类自己去实现自己的业务逻辑。

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月28日

程序员不得不知的线程池,附10道面试题

为什么要用线程池呢?

下面是一段创建线程并运行的代码:

for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println("run thread->" + Thread.currentThread().getName());
userService.updateUser(....);
}).start();
}

我们想使用这种方式去做异步,或者提高性能,然后将某些耗时操作放入一个新线程去运行。

这种思路是没问题的,但是这段代码是存在问题的,有哪些问题呢?下面我们就来看看有哪些问题;

  • 创建销毁线程资源消耗;我们使用线程的目的本是出于效率考虑,可以为了创建这些线程却消耗了额外的时间,资源,对于线程的销毁同样需要系统资源。
  • cpu资源有限,上述代码创建线程过多,造成有的任务不能即时完成,响应时间过长。
  • 线程无法管理,无节制地创建线程对于有限的资源来说似乎成了“得不偿失”的一种作用。

既然我们上面使用手动创建线程会存在问题,那有解决方法吗?

答案:有的,使用线程池。

线程池介绍

线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。

线程池有什么优点?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。

线程池使用

在JDK中rt.jar包下JUC(java.util.concurrent)创建线程池有两种方式:ThreadPoolExecutor 和 Executors,其中 Executors又可以创建 6 种不同的线程池类型。

ThreadPoolExecutor 的使用

线程池使用代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));

public static void main(String[] args) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("田先生您好");
}
});
}
}

以上程序执行结果如下:

田先生您好

核心参数说明

ThreadPoolExecutor的构造方法有以下四个:

img

可以看到最后那个构造方法有 7 个构造参数,其实前面的三个构造方法只是对最后那个方法进行包装,并且前面三个构造方法最终都是调用最后那个构造方法,所以我们这里就来聊聊最后那个构造方法。

参数解释

corePoolSize

线程池中的核心线程数,默认情况下核心线程一直存活在线程池中,如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,如果线程池一直闲置并超过了 keepAliveTime 所指定的时间,核心线程就会被终止。

maximumPoolSize

最大线程数,当线程不够时能够创建的最大线程数。

keepAliveTime

线程池的闲置超时时间,默认情况下对非核心线程生效,如果闲置时间超过这个时间,非核心线程就会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,核心线程如果超过闲置时长也会被回收。

unit

配合 keepAliveTime 使用,用来标识 keepAliveTime 的时间单位。

workQueue

线程池中的任务队列,使用 execute() 或 submit() 方法提交的任务都会存储在此队列中。

threadFactory

为线程池提供创建新线程的线程工厂。

rejectedExecutionHandler

线程池任务队列超过最大值之后的拒绝策略,RejectedExecutionHandler 是一个接口,里面只有一个 rejectedExecution 方法,可在此方法内添加任务超出最大值的事件处理。ThreadPoolExecutor 也提供了 4 种默认的拒绝策略:

  • DiscardPolicy():丢弃掉该任务,不进行处理。
  • DiscardOldestPolicy():丢弃队列里最近的一个任务,并执行当前任务。
  • AbortPolicy():直接抛出 RejectedExecutionException 异常(默认)。
  • CallerRunsPolicy():既不抛弃任务也不抛出异常,直接使用主线程来执行此任务。

包含所有参数的使用案例:

public class ThreadPoolExecutorTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
class MyThreadFactory implements ThreadFactory {
private AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
String threadName = "MyThread" + count.addAndGet(1);
t.setName(threadName);
return t;
}
}

运行输出:

main
MyThread1
main
MyThread1
MyThread1
....

这里仅仅是为了演示所有参数自定义,并没有其他用途。

execute() 和 submit()的使用

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 方法可以接收线程池执行的返回值。

下面分别来看两个方法的具体使用和区别:

// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 使用
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("老田您好");
}
});
// submit 使用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("田先生您好");
return "返回值";
}
});
System.out.println(future.get());

以上程序执行结果如下:

老田您好
田先生您好
返回值

Executors

Executors 执行器创建线程池很多基本上都是在 ThreadPoolExecutor 构造方法上进行简单的封装,特殊场景根据需要自行创建。可以把Executors理解成一个工厂类 。Executors可以创建 6 种不同的线程池类型。

下面对这六个方法进行简要的说明:

newFixedThreadPool

创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。

newCacheThreadPool

短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。如果现有线程没有可用的,则创建一个新线程并添加到池中,如果有被使用完但是还没销毁的线程,就复用该线程。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

newScheduledThreadPool

创建一个数量固定的线程池,支持执行定时性或周期性任务。

newWorkStealingPool

Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器CPU 处理器数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newSingleThreadScheduledExecutor

此线程池就是单线程的 newScheduledThreadPool。

线程池如何关闭?

线程池关闭,可以使用 shutdown() 或 shutdownNow() 方法,它们的区别是:

  • shutdown():不会立即终止线程池,而是要等所有任务队列中的任务都执行完后才会终止。执行完 shutdown 方法之后,线程池就不会再接受新任务了。
  • shutdownNow():执行该方法,线程池的状态立刻变成 STOP 状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

下面用代码来模拟 shutdown() 之后,给线程池添加任务,代码如下:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolExecutorAllArgsTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
//提交任务
threadPoolExecutor.execute(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("提交任务" + i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
});
threadPoolExecutor.shutdown();
//再次提及任务
threadPoolExecutor.execute(() -> {
System.out.println("我想再次提及任务");
});
}
}

以上程序执行结果如下:

提交任务0
提交任务1
提交任务2

可以看出,shutdown() 之后就不会再接受新的任务了,不过之前的任务会被执行完成。

面试题

面试题1:ThreadPoolExecutor 有哪些常用的方法?

ThreadPoolExecutor有如下常用方法:

  • submit()/execute():执行线程池
  • shutdown()/shutdownNow():终止线程池
  • isShutdown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取核心线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的任务队列
  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

这些方法可以用来终止线程池、线程池监控等。

面试题2:说说submit(和 execute两个方法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

说说线程池创建需要的那几个核心参数的含义

ThreadPoolExecutor 最多包含以下七个参数:

  • corePoolSize:线程池中的核心线程数
  • maximumPoolSize:线程池中最大线程数
  • keepAliveTime:闲置超时时间
  • unit:keepAliveTime 超时时间的单位(时/分/秒等)
  • workQueue:线程池中的任务队列
  • threadFactory:为线程池提供创建新线程的线程工厂
  • rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

面试题3:shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

面试题6:了解过线程池的工作原理吗?

img

当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

面试题5:线程池中核心线程数量大小怎么设置?

「CPU密集型任务」:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

「IO密集型任务」:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

面试题7:线程池为什么需要使用(阻塞)队列?

主要有三点:

  • 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
  • 创建线程池的消耗较高。

面试题8:线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下

while (task != null || (task = getTask()) != null) {})。

不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

面试题9:了解线程池状态吗?

通过获取线程池状态,可以判断线程池是否是运行状态、可否添加新的任务以及优雅地关闭线程池等。

img

  • RUNNING:线程池的初始化状态,可以添加待执行的任务。
  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。
  • STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。
  • TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。
  • TERMINATED:线程池终止状态。

面试题10:知道线程池中线程复用原理吗?

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

总结

本文通过没有使用线程池带来的弊端,Executors介绍,Executors的六种方法介绍、如何使用线程池,了解线程池原理,核心参数,以及10到线程池面试题。

「成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。」

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月27日

负载均衡就那么点事,看这篇就够了

最近有小伙伴在后台留言,让我写一篇负载均衡的文章,说网上文章其实已经很多了,每次都觉得某某文章讲的不错,可是一旦过段时间,啥都不记得了。那今天我们就用生活中的故事来聊聊负载均衡。文章中部分可能有点啰嗦,但是为了更好能让大家理解,我也是拼了

img

,真真切切的想让大家掌握知识。

img

什么是负载均衡?

负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。

负载均衡通常有两种目的:均摊压力和提供冗余(也可以理解为备份)。

生活案列

上面还看不懂的话,我们继续用生活案列来说:

img

高速路出口处,如果只有一个出口时,突然有一天出现大量车辆(假设大家都没有办理ETC)这个高速出口下高速, 比如有几百两这会都要下高速,但是下高速要交过路费,每辆车至少也要耽搁几分钟,几百辆!!!意味着后面的可能要等几个小时,如果有多个出口呢?那就没必要等那么久了。

img

如果在增加一个出口,这时候就是两个出口可以均摊车辆下高速,还得分收费员快慢,车辆3看到车1那边要快点,然后就跟上车1。

img

如果再增加n个就可以想象效果了。但是太多了,貌似也会造成资源浪费,很多出口一天都没有几辆车出入,如果搞得太多岂不浪费,所以我们一般看到大多数都是两个,可以理解备用急用。

img

「我们就把司机理解为负载均衡器,可以根据前方路况进行判别走哪个出口。判别的方法就可以理解为负载均衡算法。」

用我们技术领域的术语叫做冗余。收费员的速度我就可以理解为我们系统某个服务的性能。

技术领域

下面用一张图来描述我们技术领域的负载均衡:

img

结合生活中的场景和技术领域的场景一起理解更酸爽。

注意:集群指的是我们同一个App应用服务的部署多个节点,集群的主要目的就是为了分担压力的。负载均衡器(系统)就可以理解为指挥员。来一个请求,指挥员把这个请求根据一定方法交给集群中的某个服务。指挥员就可以按照各种方式进行分配请求到集群中的某个服务。随机给、排队给、谁反应快给谁等方法,也就是形成了负载均衡算法。

以上比喻仅仅是个人理解。

负载均衡的种类

DNS

(Domain Name System 域名系统 )它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。DNS是最简单也是最常见的负载均衡方式,一般用来实现“地理级别”的负载均衡,比如说:北方人访问北京的机房,南方人访问广州的机房,西方人访问成都的机房。DNS负载均衡的本质是DNS解析同一个域名可以返回不同的IP地址。比如说:https://www.sina.com.cn/在北方的用户使用时会解析成10.210.1.12(北京机房)返回,南方的用户使用时会解析成14.213.164.27返回(广州机房)。

DNS简单示意图

img

优点

  • 配置简单,无成本费用
  • 将负载均衡的工作交给了DNS服务器,省去了管理的麻烦。

缺点

  • 记录的添加与修改是需要一定时间才能够生效的(因为DNS缓存了A记录)。一旦有一台服务器坏了需要下线,即使修改了A记录,要使其生效也需要较长的时间,这段时间,DNS仍然会将域名解析到已下线的服务器上,最终导致用户访问失败。
  • 不能按需分配负载,DNS并不知道各服务器的真实负载情况,所以负载效果不是很好

实际的情况:在实际的项目部署,我们一般会将部分服务器使用DNS解析,利用域名解析作为第一级负载均衡.再在服务器中使用nginx负载均衡作为第二级负载均衡。

硬件负载均衡

硬件负载均衡是通过单独的设备来实现负载均衡的功能,这类设备和路由器交换机有那么一些类似,更或者可以理解为一个用于负载均衡的基础网络设备。目前业界主要有两款硬件负载均衡:F5和A10。这类设备性能好,功能强大,但是价格可以用昂贵来形容,一般只有银行,国企等大型有钱的企业开会考虑使用此类设备,本人也只是在银行里见识过F5。至于A10没接触过就不撤了。

优点

  • 功能强大:全面支持各层级的负载均衡,支持各种负载均衡算法,支持全局负载均衡。
  • 性能好:一般软件负载均衡能支撑10w+并发已经很不错了,但是硬件的负载均衡却可以支持100w+以上的并发。
  • 高稳定性:因为是商业品,所以经过了良好严格的测试,经过大规模的使用,所以稳定非常高。
  • 安全性高:硬件负载均衡设备除了能处理负载均衡以外,还具有防火墙、防DDOS攻击等效果。

缺点

  • 价格昂贵:我记得之前银行购买F5花了上百万,据说还有更贵的,所以价格可想而知。
  • 扩展性不好:硬件设备可以根据业务进行配置,但无法进行扩展和定制化。

软件负载均衡

软件负载均衡是通过负载均衡软件来实现负载均衡功能的。常见的负载均衡软件有LVS和Nginx。其中LVS是Linux内核的四层负载均衡,四层和七层的区别在于他们协议和灵活性的不同。Nginx是7层负载均衡,支持HTTP,E-mail协议,而LVS是四层负载均衡,所以和协议无关,基本上所有应用都可以做到,比如说:聊天、数据库等。

以下是Nginx的负载均衡简单示意图:

img

优点

  • nginx由C编写,同样的web服务器,占用的资源和内存低性能高。
  • 当启动nginx服务器,会生成一个master进程,master进程会fork出多个worker进程,由worker线程处理客户端的请求。
  • nginx支持高并发,每个worker子进程是独立平等的,当有客户端请求时,worker进程公平竞争,抢到的worker进程会把请求提交给后端服务器,当后端服务器没有及时响应时,此worker进程会继续接收下一个request,当上一个请求有响应后会触发事件,此worker进程继续之前的执行,知道响应结束。一个request不会被两个worker进程执行。
  • nginx支持反向代理(用户有感知的访问叫正向代理如使用vpn访问youtube,用户无感知访问叫反向代理如负载均衡),支持7层负载均衡(拓展负载均衡的好处)。
  • nginx是异步非阻塞型处理请求(第三点印证),采用的epollandqueue模式,apache是阻塞型处理请求。
  • nginx处理静态文件速度快(原因:
  • nginx高度模块化,配置简单。
  • nginx是单进程多线程)。

缺点

  • 对比apache不稳定,由于是单进程多线程,进程死掉会影响很多用户。

负载均衡有什么用?

  • 「流量分发」负载均衡能对多台主机流量进行分发,提高用户系统的业务处理能力,提升服务可用性
  • 「会话保持」在会话周期内,会话保持可使来自同一IP或网段的请求被分发到同一台后端服务器上。
  • 「健康检查」支持自定义健康检查方式和频率,可定时检查后端主机运行状态,提供故障转移,实现高可用;
  • 「负载均衡」解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);
  • 提高扩展性通过添加或减少服务器数量,提供网站伸缩性(扩展性);
  • 提高安全性安全防护,在负载均衡器上做一些过滤,黑白名单、防盗链等处理;

常用负载均衡算法

轮训

负载均衡系统接收到请求后,按照一定顺序将请求分发给服务器上。轮训是一种简单的负载均衡算法策略,不会去关注服务器状态。

优点:如果服务器都是正常的,那么轮训是最理想的,因为它会使得每个服务都得到相等量的请求,可以用"雨露均沾"来形容。

缺点:上面的有点是理想状态的,但是现实往往不是那样的,现实还是很骨感滴,线上系统往往出现各种各样的问题,比如:当有一台服务器挂了,轮训算法不会管服务器状态,就是会导致大量的请求到一台已经挂掉的服务器上,从而导致系统不可用,进而造成用户流失。另外一种常见的问题就是有的服务器响应快,有的响应慢(比如32核的服务器和16核的服务器),轮训算法也不关注相应快慢,所以会导致很多服务请求响应时间慢,简单的导致用户体验不好,由于响应时间慢甚至可能拖垮其他系统。

加权轮训

负载均衡系统根据服务器权重进行请求任务分派到对应的服务器上,这里的权重一般是根据系统硬件配置进行静态配置的,采用动态的方式计算会更加适合业务,但是复杂度相比简单的轮训就高很多。

加权轮训是轮训的一种特殊方式,主要目的是解决服务器处理能力的差异问题,比如:集群中有的服务器是32核,有的老系统却是16核,那么理论上我们可以对其进行权重配置值,即就是32核服务器的处理能力是16核的两倍,负载均衡算法权重比例调整为2:1,让更多的请求分发给32核的服务器。

加权轮训解决了轮训算法中误服根据服务器的配置的差异任务进行更好的分配的问题,其实还是会存在无法根据服务器的状态差异性进行请求任务分配的问题。

负载最低优先

负载系统将请求分配给当前负载最低的服务器,这里的负载根据不同请求类型和业务处理场景,可以用不同的指标来衡量。比如以下几个场景,

  • LVS这种4层网络负载均衡设备,可以以连接数来判断服务器的状态,服务器连接数量越大,表明服务器压力就越大。
  • Nginx这种7层网络负载均衡系统,可以以HTTP请求数量判断服务器的状态(Nginx内置的负载均衡算法不支持这种方式,需要自行进行扩展)。
  • 如果我们是自己研发负载均衡系统,可以根据业务特点来选择衡量系统压力的指标。如果CPU是密集型,可以以CPU负载来衡量系统的压力;如果是IO密集型,则可以以IO负载来衡量系统压力。

负载最低优先算法解决了轮训算法中无法感知服务器状态的问题,但是由此带来的代价是复杂度增加很多,比如:

  • 最少链接数优先的算法要求负载系统统计每个服务器当前简历的链接,其应用场景仅限于负载均衡接收的任何请求都会转发给服务器进行处理,否则如果负载均衡系统和服务之间是固定的连接池方式,就不适合采取这种算法。LVS可以采取这种算法进行负载均衡,而一个通过连接池的方式链接数据库Mysql集群的负载均衡系统就不适合采取这种算法进行负载均衡了。
  • CPU负载均衡最低优先的算法要求负载均衡系统以某种方式收集每个服务器的CPU的具体负载情况,同时要确定是以一分钟的负载标准,还是以10分钟、15分钟的负载标准,不存在1分钟肯定比15分钟的好或差。不同业务最优的时间间隔也是不一样的,时间间隔太短容易造成频繁波动,时间太长可能造成峰值来临时响应缓慢。

负载最低优先的算法基板上能够很完美解决了轮训算法的缺点,也因为采用负载最低优先算法后,负载均衡系统需要感知服务器当前运行状态,此时,同样造成代价上升很多。对于开发者来说也许轮训算法只要简短的代码就可以实现,然而负载最低优先算法需要大量的代码来实现。

负载最低优先看起来是解决了轮训中的缺点,然后由于其复杂度的提升,导致真正使用中比例还不如轮训或者轮训加权算法。

性能最优

负载最低优先算法是站在服务器的角度来进行请求分配的,而性能最优算法是站在客户端的角度进行分配的,优先将请求分配给处理速度快的服务器,通过这种方式达到了最快响应给客户端。

性能优先其实也负载最低优先有点类似,都是需要感知服务器的状态,与之不同的是性能最优是通过响应时间这个标准,在外部进行感应服务器状态而已,同样的实现复杂度也很高,主要体现在以下方面:

  • 负载均衡系统需要收集每次请求的响应时间,如果在大量请求处理的场景下,这种收集再加上响应时间的统计本身也会消耗系统的性能。
  • 为了减少这种统计上的消耗,可以采取采样的方式进行统计,即就是不用很完全的去统计所有服务器的所有请求时间,而是抽样统计部分任务的响应时间来估算整体请求所花的响应时间。采样统计虽然能减轻性能的消耗,但使得实现的复杂度增加了很多,因为要确定合适的采样率,采样率太低会导致数据的正确性,采样率高同样会造成性能的消耗,要找到一个合适的采样率的复杂度也是可想而知的。
  • 无论全部统计,还是采样统计,都需要选择合适的周期,是30秒性能最优还是1分钟最优?目前是没有标准的周期,都是需要具体业务场景进行决策,是不是感觉到了其复杂性,尤其是线上系统需要不断的调试,然后找出相对合适的标准。

Hash类

负载均衡系统根据请求中某些关键字进行hash运算,得到的相同值得分发到同一台服务器上去,这样做的目的主要是为了满足特定的业务需求,比如:

  • 源地址Hash:将来源于同一个IP地址的请求分配给同一个服务器进行处理,适合于存在事务、会话的业务。例如:当我们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是临时的,关闭浏览器后就会失效。网上银行后台无须持久会话信息,只需要在某台服务器临时保留这个会话就可以了,但需要保证用户在会话存在期间,每次请求都能访问在同一个服务器,这种业务场景就是通过源地址hash来实现的。
  • ID hash :将某个ID表示的业务分配到同一台服务器上进行处理,比如:userId session id。上述的网上银行登录的例子,用session id hash可以实现同一个会话期间,用户每次都是访问同一台服务器上的目的。

负载均衡算法应用

Dubbo中使用了哪些负载均衡算法?

  • Random LoadBalance(随机算法,默认)
  • RoundRobin LoadBalance(权重轮训算法)
  • LeastAction LoadBalance(最少活跃调用数算法)
  • ConsistentHash LoadBalance(一致性Hash法)

类图

img

nginx中使用了哪些负载均衡算法?

「round robin(默认)」:轮询方式,依次将请求分配到各个后台服务器中,默认的负载均衡方式。适用于后台机器性能一致的情况。挂掉的机器可以自动从服务列表中剔除。

「weight」:根据权重来分发请求到不同的机器中,指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 例如:

upstream bakend {
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}

「IP_hash」:根据请求者ip的hash值将请求发送到后台服务器中,可以保证来自同一ip的请求被打到固定的机器上,可以解决session问题。例如:

upstream bakend {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}

「url_hash(第三方)」:根据请求的url的hash值将请求分到不同的机器中,当后台服务器为缓存的时候效率高。

例如:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法 。

「fair(第三方)」:根据后台响应时间来分发请求,响应时间短的分发的请求多。例如:

upstream backend {
server server1;
server server2;
fair;
}

总结

我们用生活中的故事来讲述了负载均衡,讲述了什么是负载均衡,负载均衡的作用,负载均衡的种类,负载均衡算法种类,以及我们在Dubbo和nginx中负载均衡算法的应用。

希望老铁能get到点!如有疑问或什么建议的加我微信tj20120622我们慢慢聊。直到让大家理解掌握为止。

「只要我们的方向对了,就不怕路远!」

码字不易,点个赞呗

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月25日

谷歌面试题:如何从无序链表中移除重复项?

一位小伙伴来问一道谷歌的笔试题,关于单链表操作的,问到底有多少种解决方案,今天我们就来聊聊。

题目的大致意思是:

假设存在一个无序单链表,将重复结点去除后,并保原顺序。去重前:1→3→1→5→5→7去重后:1→3→5→7

顺序删除

通过双重循环直接在链表上执行删除操作。外层循环用一个指针从第一个结点开始遍历整个链表,然后内层循环用另外一个指针遍历其余结点,将与外层循环遍历到的指针所指结点的数据域相同的结点删除,如下图所示。

假设外层循环从outerCur开始遍历,当内层循环指针innerCur遍历到上图实线所示的位置(outerCur.data==innerCur.data)时,此时需要把innerCur指向的结点删除。

具体步骤如下:

  • 用tmp记录待删除的结点的地址。
  • 为了能够在删除tmp结点后继续遍历链表中其余的结点,使innerCur指针指向它的后继结点:innerCur=innerCur.next
  • 从链表中删除tmp结点。

实现代码如下:

img

img

img

运行结果:

img

算法性能分析

由于这种方法采用双重循环对链表进行遍历,因此,时间复杂度为O(N^2)。其中,N为链表的长度。在遍历链表的过程中,使用了常量个额外的指针变量来保存当前遍历的结点、前驱结点和被删除的结点,因此,空间复杂度为O(1)

递归法

主要思路为:对于结点cur,首先递归地删除以cur.next为首的子链表中重复的结点,接着从以cur.next为首的子链表中找出与cur有着相同数据域的结点并删除。

实现代码如下:

img

img

img

算法性能分析

这种方法与方法一类似,从本质上而言,由于这种方法需要对链表进行双重遍历,因此,时间复杂度为O(N^2)。其中,N为链表的长度。由于递归法会增加许多额外的函数调用,因此,从理论上讲,该方法效率比前面的方法低。

空间换时间

通常情况下,为了降低时间复杂度,往往在条件允许的情况下,通过使用辅助空间实现。

具体而言,主要思路如下。

  • 建立一个HashSet,HashSet中的内容为已经遍历过的结点内容,并将其初始化为空。
  • 从头开始遍历链表中的所以结点,存在以下两种可能性:
  • 如果结点内容已经在HashSet中,则删除此结点,继续向后遍历。
  • 如果结点内容不在HashSet中,则保留此结点,将此结点内容添加到HashSet中,继续向后遍历。

「引申:如何从有序链表中移除重复项?」

如链表:1,3、5、5、7、7、8、9

去重后:1,3、5、7、8、9

分析与解答

上述介绍的方法也适用于链表有序的情况,但是由于以上方法没有充分利用到链表有序这个条件,因此,算法的性能肯定不是最优的。本题中,由于链表具有有序性,因此,不需要对链表进行两次遍历。所以,有如下思路:用cur 指向链表第一个结点,此时需要分为以下两种情况讨论。

  • 如果cur.data==cur.next.data,那么删除cur.next结点。
  • 如果cur.data!=cur.next.data,那么cur=cur.next,继续遍历其余结点。

总结

对于无序单链表中,想要删除其中重复的结点(多个重复结点保留一个)。删除办法有按照顺序删除、使用递归方式删除以及可以使用空间换时间(HashSet中元素的唯一性)。

点赞越多,bug越少~

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月24日

五分钟学会模板模式

img

概述

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

使用场景

喝茶水

我们都知道泡茶基本步骤(算法骨架)有:

烧水、泡茶、喝茶水。

整个过程中很关键的步骤是泡茶,泡茶需要跑什么茶呢?泡多久?(留给子类自己去实现)。

img

API

写过API接口的码友们都知道,写API一般有四个步骤:

参数解析、参数校验、处理业务、组织返回参数。

把请求参数解析成该业务的请求参数json解析成实体类;参数校验,您可以使用通用的方式就是判断参数是否为空,也可以自己定义特殊的校验方式;处理业务一般每个接口都是不一样的,基本上都是自己去实现;至于返回参数,可能您得根据该API接口业务来返回。

支付订单

做过支付相关的系统的人都清楚,支付订单大致分这三个步骤:

组织请求银行或者第三方支付公司的请求参数、发起支付、处理返回结果。

以上三个场景中的步骤就是算法骨架,至于每个步骤可能每个人喝茶偏好不一样,API接口业务不一样、银行或者第三方支付的支付处理不一样,可能需要自己做特殊的处理。

场景现实

实现一个API接口

算法类

package com.tian.springbootdemo.controller;
import com.tian.springbootdemo.rep.Result;
/**
 * @auther: 老田
 * @Description: 模板类
 */
public abstract class AbstractTemplate {
​
 /**
 * 算法骨架
 */
 public Result execute() {
 //第一步:解析参数
 parseRequestParameters();
 //第二步:校验参数
 checkRequestParameters();
 //第三步:业务处理
 Object data= doBusiness();
 //第四步:组织返回参数
 return assembleResponseParameters(data);
 }
​
 /**
 * 解析参数
 */
 public abstract void parseRequestParameters();
​
 /**
 * 校验参数
 */
 public abstract void checkRequestParameters();
​
 /**
 * 业务处理
 */
 public abstract Object doBusiness();
​
 /**
 * 组织返回参数
 */
 public abstract Result assembleResponseParameters(Object object);
}

实现类一

import com.tian.springbootdemo.rep.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
​
/**
 * @auther: 老田
 * @Description: api接口
 */
@RequestMapping("/api")
@Controller
public class MyApiController extends AbstractTemplate {
​
 @RequestMapping(value = "/users", method = RequestMethod.POST)
 @ResponseBody
 @Override
 public Result execute() {
 return super.execute();
 }
​
 @Override
 public void parseRequestParameters() {
 System.out.println("*****解析参数*****");
 }
​
 @Override
 public void checkRequestParameters() {
 System.out.println("*****校验参数*****");
 }
​
 @Override
 public Object doBusiness() {
 System.out.println("*****处理业务*****");
 // TODO: 2018/11/17 调用service处理业务
 User user = new User();
 user.setName("小田哥");
 user.setId(1);
 user.setAge(20);
 user.setSex("man");
 return user;
 }
​
 @Override
 public Result assembleResponseParameters(Object object) {
 System.out.println("*****返回参数*****");
 Result result = new Result("200", "处理成功");
 result.setData(object);
 return result;
 }
}

实现类二

import com.tian.springbootdemo.dao.domain.User;
import com.tian.springbootdemo.rep.Result;
import org.springframework.web.bind.annotation.*;
​
/**
 * @auther: 老田
 * @Description: api接口
 */
@RequestMapping("/api")
@RestController
public class LoginController extends AbstractTemplate {
 @PostMapping(value = "/login")
 @Override
 public Result execute() {
 return super.execute();
 }
 @Override
 public void parseRequestParameters() {
 System.out.println("解析登录参数");
 }
​
 @Override
 public void checkRequestParameters() {
 System.out.println("校验登录用户名是否为空,密码是否为空");
 }
​
 @Override
 public Object doBusiness() {
 System.out.println("通过用户名查询是否存在此用户");
 System.out.println("校验用户密码是否正确");
 System.out.println("登录成功");
 User user = new User();
 user.setName("小田哥");
 user.setId(1);
 user.setAge(20);
 user.setSex("man");
 return user;
 }
​
 @Override
 public Result assembleResponseParameters(Object object) {
 System.out.println("*****返回参数*****");
 Result result = new Result("200", "登录成功");
 result.setData(object);
 return result;
 }
}

相关类

/**
 * @auther: 老田
 * @Description: 返回信息
 */
public class Result {
 //返回码
 private String responseCode;
 //描述
 private String message;
 //数据
 private Object data;
​
 public Result() {
 }
​
 public Result(String responseCode, String message) {
 this.responseCode = responseCode;
 this.message = message;
 }
​
 public Result(String responseCode, String message, Object data) {
 this.responseCode = responseCode;
 this.message = message;
 this.data = data;
 }
​
 public String getResponseCode() {
 return responseCode;
 }
​
 public void setResponseCode(String responseCode) {
 this.responseCode = responseCode;
 }
​
 public String getMessage() {
 return message;
 }
​
 public void setMessage(String message) {
 this.message = message;
 }
​
 public Object getData() {
 return data;
 }
​
 public void setData(Object data) {
 this.data = data;
 }
}
​
import java.io.Serializable;
​
/**
 * @auther: 老田
 * @Description: 数据
 */
public class User implements Serializable {
 //id
 private Integer id;
 //用户姓名
 private String name;
 //性别
 private String sex;
 //年龄
 private int age;
​
 public User() {
 }
​
 public User(Integer id, String name, String sex, int age) {
 this.id = id;
 this.name = name;
 this.sex = sex;
 this.age = age;
 }
​
 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 getSex() {
 return sex;
 }
​
 public void setSex(String sex) {
 this.sex = sex;
 }
​
 public int getAge() {
 return age;
 }
​
 public void setAge(int age) {
 this.age = age;
 }
}

测试

这里使用的是ideaTools下面的REST Client进行接口测试:

img

enter image description here

img

enter image description here

再看看控制台Console打印出来的信息:

img

enter image description here

img

enter image description here

这样我们就把模板设计模式应用到我们的具体代码里了,同样的我们也可以实现其他API的实现类。

另外,参数校验也可以在 AbstractTemplate 中实现一个 default 的方式,比如说:校验参数是否为空,但是子类也可以重写这个方法,自己做一个特殊的校验;比如说:如果参数中有手机号码,那么我们不仅要校验手机号是否为空,还可以校验这个手机号码是不是11位,是否合法的校验等等。

模板模式优缺点

优点

  • 提高代码的复用性,将相同部分的代码放到抽象类里;
  • 提高拓展性,将不同的放到不同的实现类里,通过实现类的扩展增加一些自己需要的行为;
  • 实现反向控制,通过一个父类调用实现类的操作,通过对实现类的扩展增加新行为,实现反向控制。

缺点

  • 因为引入了抽象类,每个不同的实现都需要一个子类来现实,这样会导致类的数量增多,从而导致系统实现的复杂度。

大佬们在框架里是怎么使用的?

Spring中

AbstractApplicationContext 中的refreash方法就是模板方法,源码为:

@Override
public void refresh() throws BeansException, IllegalStateException {
 synchronized (this.startupShutdownMonitor) { 
 //调用容器准备刷新的方法,获取容器的当时时间,
 //同时给容器设置同步标识
 prepareRefresh();
 //告诉子类启动refreshBeanFactory()方法,
 //Bean定义资源文件的载入从
 //子类的refreshBeanFactory()方法启动
 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
 //为BeanFactory配置容器特性,例如类加载器、事件处理器等
 prepareBeanFactory(beanFactory);
 try { 
 //为容器的某些子类指定特殊的BeanPost事件处理器
 //-----子类实现
 postProcessBeanFactory(beanFactory);
 //调用所有注册的BeanFactoryPostProcessor的Bean
 invokeBeanFactoryPostProcessors(beanFactory);
 //为BeanFactory注册BeanPost事件处理器.
 //BeanPostProcessor是Bean后置处理器,
 //用于监听容器触发的事件
 registerBeanPostProcessors(beanFactory);
 //初始化信息源,和国际化相关.
 initMessageSource();
 //初始化容器事件传播器.
 initApplicationEventMulticaster();
 //调用子类的某些特殊Bean初始化方法
 //-----子类实现
 onRefresh();
 //为事件传播器注册事件监听器.
 registerListeners();
 //初始化所有剩余的单例Bean
 finishBeanFactoryInitialization(beanFactory);
 //初始化容器的生命周期事件处理器,
 //并发布容器的生命周期事件
 finishRefresh();
 //.....

该方法就是上下文启动模板方法。这就是模板模式在Spring中应用场景之一。

Mybatis中

img

BaseExecutor中的update方法就是一个模板方法

 /**
 * SqlSession.update/insert/delete会调用此方法
 * 模板方法
 */
 @Override
 public int update(MappedStatement ms, Object parameter) throws SQLException {
 ErrorContext.instance().resource(ms.getResource()).activity("executing an                update").object(ms.getId());
 if (closed) {
 throw new ExecutorException("Executor was closed.");
 }
 //先清局部缓存,再更新,如何更新交由子类,
 //模板方法模式
 clearLocalCache();
 //由子类实现(钩子方法)
 return doUpdate(ms, parameter);
 }

BaseExecutor里只是定义了方法,但是实现是在子类里

//更新 
protected abstract int doUpdate(MappedStatement ms, Object parameter)
 throws SQLException;
//查询
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds 
 rowBounds, ResultHandler resultHandler, BoundSql boundSql)
 throws SQLException;
 //...do开头的方法都是交给具体子类自己去实现

BaseExecutor的实现类如下:

img

enter image description here

实现类SimpleExecutor中的doUpdate方法的实现

@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 Statement stmt = null;
 try {
 Configuration configuration = ms.getConfiguration();
 //新建一个StatementHandler
 //这里看到ResultHandler传入的是null
 StatementHandler handler = configuration.newStatementHandler(
 this, ms, parameter,          RowBounds.DEFAULT, null, null);
 //准备语句
 stmt = prepareStatement(handler, ms.getStatementLog());
 //StatementHandler.update
 return handler.update(stmt);
 } finally {
 closeStatement(stmt);
 }
}

实现类ReuseExecutor中的doUpdate方法的实现

@Override
 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 Configuration configuration = ms.getConfiguration();
 //和SimpleExecutor一样,
 //新建一个StatementHandler
 //这里看到ResultHandler传入的是null
 StatementHandler handler = configuration.newStatementHandler(
 this, ms, parameter,       RowBounds.DEFAULT, null, null);
 //准备语句
 Statement stmt = prepareStatement(handler, ms.getStatementLog());
 return handler.update(stmt);
 }

这就是Mybatis中的模板方法模式的经典应用。

总结

模板方法模式就是定义了一个算法骨架,然后每个实现类自己去实现自己的业务逻辑。在Spring、Mybatis、Dubbo等框架中有很好实现案例。相对来说模板方法模式是算比较简单的哈,在面试中也能和面试官扯一会儿了。

「为了未来好一点 ,现在苦一点算什么」

查看原文

赞 0 收藏 0 评论 0

田维常 发布了文章 · 1月22日

程序员该如何写好简历?

昨晚上有朋友私下问我,他的简历投了好家公司,并且都说在招人,却面试机会都不给一个,还有这位朋友他说他曾经因为跳槽太频繁,导致直接被叫回去等通知。他说其实每次跳槽都是一次成长, 但是最后他把简历发给我看了看,然后我问了他几个技术问题后,发现他的技能并没有成长,其实从他的简历上就能看出没多大成长。没有收到面试机会,其实和他的简历和履历都是息息相关的。

img

工作这么多年,我也面试过几百个求职者,我也为东家推荐过很多简历。这个过程中,我发现一些求职者的简历确实有写得不尽如人意的地方,有的简历写得太简单缺少必要信息,而有的简历又写得太多没有突出重点。

今天我将分四个方面和大家谈谈如何写好简历:

  1. 什么是简历
  2. 为什么要写好简历
  3. 简历的内容
  4. 五点建议

什么是简历

简历(英语:resume),顾名思义,就是对个人学历、经历、特长、爱好及其它有关情况所作的简明扼要的书面介绍。简历是有针对性的自我介绍的一种规范化、逻辑化的书面表达。对应聘者来说,简历是求职的“敲门砖”。

所以我们可以把简历总结为两个点:一是凸显出我们的亮点,二是让面试官迅速发现我们的亮点,从而引导他如何面试你。

img

切记:不要不所有的项目都写上去,要有针对性的选择自己负责或者参与过的项目。因为你的项目写的太多,但面试官的时间是有限的,看到一些相对普通的项目,可能面试官都难得看完,会给自己造成在面试官眼里的印象分打折扣,严重的可能会导致你的亮点面试官都没看到,所以尽量挑几个有挑战性的,相对熟悉的项目。

「懒婆娘的裹脚——又长又臭」

为什么要写好简历

因为HR不认识你,对你一无所知,轻易约面试或者发offer,求职者可能是简历造假、过往挪用公款等用人风险,直接转嫁到她们身上。HR是吃饭的职业,对饭碗造成影响或者损害的事情,她们会谨慎对待。

退一步说,你的过往清白,无不良记录,用人风险不高。但没有简历,HR不了解你过去的工作情况,无法判断你是否能胜任目标职位。贸然约你面试,三两句发现话不投机,求职者浪费了通勤时间,HR浪费了招聘时间,彼此求职/招聘效率都十分低下。

另外,简历写得好坏不仅会反映出你的表述能力,还能反映你做事的态度,因为找工作这么重要的事情你如果连简历都不愿意认真写,那以后进入公司工作了做事可能也不会非常认真。

我时常和群里的小伙伴们调侃:面试就和相亲一样,但是你的简历写的不好,连相亲的机会没有。并且好的简历产生的引导却能帮忙提升运气,所以一定要谨慎对待。

img

简历的内容

基本信息

简历中肯定是少不了姓名、手机号、邮箱、常住地址以及毕业时间。姓名、手机这两个的重要性就不用在强调了,邮箱还是要强调一下,因为邮箱可以用来跟踪当前面试进展,也可以说使用邮箱也显得该公司比较正规化,另外可能还会通过邮箱来发笔试题目。

如果你是本科或更高的学历,建议也写在简历相对靠前的地方。但大专或其他学历不妨,在建立中不写学历这一项,有的公司不是很看重学历的情况下,HR没看到学历,只是觉得个人履历和公司非常匹配,于是就给你机会了,这是个技巧。

加分项有自己的博客、GitHub或码云上参与过项目开源,参与开源项目是有一些技术难度的,但是强烈建议写技术博客,写技术博客的好处很多,比如:面试官会觉得你对技术是有热情的,但是写技术博客是需要有自己的见解,并且自己有时间也可以不断对每个技术进行深挖,也是对自己职业的一种技术积累(写技术博客切记不要太随意,不然反而面试官觉得你根本就没有用心写,从而印象分大打折扣)。另外如果是做过开源项目的话那是最好,这样体现出你的实战能力。

img

选填项:英语阅读听写能力,如果自己不是很擅长,那在简历中就不写。如果有个四六级证书,那就写上去。

应聘信息

主要是咱们应聘的是什么样职位以及期望工作地点,这样HR拿到简历就能大致知道我们的期望是否在她们能提供的范围之内。比如:田维常应聘高级java工程师/架构师/技术专家,期望地点北京或上海。 另外,应聘的职位和投递的职位要相符,比如对方招算法工程师,你的简历就应该写应聘算法工程师,而不应该写应聘其他职位。这些细节很重要。

自我介绍

自我介绍是用事实介绍自己的亮点和擅长技能,是简历中最重要的部分。自我介绍要用事实描述,而不要用观点描述。那么什么是事实,什么是观点呢?比如:“我从xx年开始从事java开发到现在/我有xx年java开发经验”,这个就是事实;而“我崇尚团队合作,学习能力强”,这个就是观点。事实是可以证明的,而观点很难,所以用事实描述会比用观点描述更有说服力。

img

下面这是我从网上找的一个自我介绍,大家可以按照这个模板去套:

img

教育背景

教育背景包括学历、毕业院校和毕业时间。面试官需要用毕业时间计算你的工作年限,不同的工作年限要求不一样。比如说:工作不到三年的,要求基础功底扎实和较强的学习能力,公司通过这两份来判断是否是潜力股,想进BAT这类公司肯定就需要更高的潜力,如果是个高学历的或者相对比较知名的大学毕业的可能会占到一些优势。三到五年的,主要是看我们的项目经历、技术的广度(深度也会有所考察)以及解决问题的能力。超过五年的,这个段位的朋友基本上技术已经定型,这时候主要考察综合能力,包括沟通能力,架构能力等。

工作经验

工作经验的内容包括公司名称、公司规模、公司类型、任职时间、职位、做的事情和取得的成绩,时间上应由近至远写起。其中,职位可以写软件开发工程师、高级开发工程师、架构师、技术主管和技术总监等;做的事情可以写负责某产品的架构升级,某系统的开发和设计工作等。举个例子:腾飞在某大型互联网公司任技术专家,负责过多个金融系统架构和建设,推动融资平台架构演进,组织过几十人的项目组完成双11大促支持,并获得业务方好评。

项目经验

项目经验部分是用事实描述法写出你在这个项目中做的事情。由近至远写,但注意不要把所有项目经验都写上,主要是写亮点项目,或最能体现你技术能力的项目。举个例子:我在某项目中担任项目架构师和PM工作,负责该项目的架构,还负责项目计划推进工作,推动5支团队60名人员开发完成该项目。我还在某项目中承担核心开发工作,负责用户管理模块的设计和开发工作,主要运用了Redis做缓存、采用的是Spring cloud微服务架构等技术。

如何在简历中通过项目经验体现进步?假如你换了三次工作,在这三家公司的职位依次是工程师、高级工程师、技术主管或技术专家等,简历上就可以写“两年时间从开发工程师晋升为高级工程师和技术主管”,面试官从你这段经历就可以看出你一定很不错并很有潜力。

五点建议

  • 简历的长度一到两页最合适。一般我推荐两页。若页数太多就要思考下简历是不是没有突出重点,是否按照前面说的只展示出自己最擅长的技艺。简历最多三页哈,不在于长,在于精简。
  • 不要在简历中写之前的待遇和期望待遇。写期望待遇有百害而无一利。首先,假如面试官发现你的期望待遇比他的还高,那面试官可能就会用更高的要求来面试你,或者由于这个职位给不到这么高的期望待遇,而导致起初简历筛选就没通过。其次,待遇的计算很复杂,期望待遇是税前还是税后呢?是否包含年终奖呢?是否包含其他个人所得奖金呢?所以我建议还是根据面试状况最后再谈期望待遇。
  • 不要频繁跳槽。若求职者每年换一次工作,面试官就会认为他的职业规划想不清楚,并且忠诚度偏低,招进来后可能很快又会跳槽,技术积累也不够。其实,这样的简历在起初也很难通过筛选的。
  • 强烈建议不要使用精通XX技术。形容技术能力可以用“使用、掌握、熟练和精通”,很多求职者的简历上写自己“精通Java”,其实一部分求职者只是用过JDK,连源码都没看过,这种仅仅停留在使用阶段,如果写了“精通”,那面试官肯定会问JDK源码和实现原理等问题,若回答不出来很可能会直接导致面试的失败。精通Java是件非常难的事情,因为Java技术体系太庞大了,但是Java里的某些知识点,比如垃圾回收、类加载、多线程和网络编程等,你可以选择某个知识点深入学习,在这些知识点上写“精通”,比如“精通类加载,并使用类加载技术开发了一个模块化框架”。
  • 找一个好的简历模板。这能突出你做事情的专业度。建议简历排版简洁,用统一的字体,内容统一字体大小,每一段前面空两格,段与段之间空一行。

总结

简历是我们找工作的敲门砖,我们在再会说再会吹再牛逼,简历这关都过不了,展现实力的机会的没有。

另外再次强调:「简历中一定要突出自己的技术亮点和有点像样的项目」

人只要不失去方向,就不会失去自己

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 46 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-10-24
个人主页被 4.2k 人浏览