0%

Java异常分类和处理

Java注解

  • @Target

    表示这个注解使用的范围

  • @Retention

    表示这个注解的存活时间范围。比方说是

    作者:RednaxelaFX

    链接:https://www.zhihu.com/question/60835139/answer/180750670

    来源:知乎

    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    RetentionPolicy.SOURCE:只在本编译单元的编译过程中保留,并不写入Class文件中。这种注解主要用于在本编译单元(这里一个Java源码文件可以看作一个编译单元)内触发注解处理器(annotation processor)的相关处理,例如说可以让注解处理器相应地生成一些代码,或者是让注解处理器做一些额外的类型检查(@Override),等等。

    RetentionPolicy.CLASS:在编译的过程中保留并且会写入Class文件中,但是JVM在加载类的时候不需要将其加载为运行时可见的(反射可见)的注解。这里很重要的一点是编译多个Java文件时的情况:假如要编译A.java源码文件和B.class文件,其中A类依赖B类,并且B类上有些注解希望让A.java编译时能看到,那么B.class里就必须要持有这些注解信息才行。

    • 同时我们可能不需要让它在运行时对反射可见(例如说为了减少运行时元数据的大小之类),所以会选择CLASS而不是RUNTIME。

    RetentionPolicy.RUNTIME:在编译过程中保留,会写入Class文件,并且JVM加载类的时候也会将其加载为反射可见的注解。这就不用多说了,例如说Spring的依赖注入就会在运行时通过扫描类上的注解来决定注入啥。

  • @Document

    描述javadoc

  • @Inherited

    如果值是是的话 被标注了这个注解的类的子类也会有这个注解

Spring 原理

Spring Bean 生命周期

生命周期

生命周期

生命周期

Bean的定义

可以通过在xml中定义Bean

1
2
3
4
5
6
<beans>
<bean id = "" class = "">
<property name="name" value="root"/>
<property name="password" value="${jdbc.password]}"
</bean>
</beans>

或者在类上面使用@Component的注解。 通过ComponentScan的方式要找到相关的类。插一句 ComponentScan的怎么做

  • ComponentScanAnnotationParser#parse

  • ClassPathBeanDefinitionScanner#doScan

  • ClassPathScanningCandidateComponentProvider#findCandidateComponents

    ​ 要做的事情就是读取Class的文件流,通过JVM中class的定义获得实现的接口等的信息,然后把这些信息写入到beandefinition 对象中。

    • 将class path 中的.替换成/

    • 拼装成 classpath:经过转换的basePackage地址 + **/\.class

    • MetadataReaderFactory#getMetadataReader

    • SimpleMetadataReader#getMetadataReader

    • ClassReader#accept

      这里涉及到ASM的内容ASM4使用指南 根据规范编译后的class文件的格式是固定的,我们可以根据ASM工具来从class文件中读取我们关心的信息。

    返回含有父类、实现接口、注解等信息的包装类(RootBeanDefination)

从DefaultListableBeanFactory 管中窥豹 看Bean的生命流程

因为BeanFactory是懒加载的 ,当我们调用getBean()的时候,会

  • AbstractBeanFactory#doGetBean(inal String name, final Class requiredType, final Object[] args, boolean typeCheckOnly)

    • transformedBeanName

      得到bean Name,入参的beanName中如果有&符号 截取到最后一个&开始到结束的子字符串作为bean name

    • DefaultSingletonBeanRegistry#getSingleton

      背景知识 Bean的循环依赖和解决

      以下内容来自 Spring-bean的循环依赖以及解决方式

      Spring的单例对象的初始化主要分为三步:

      • create Bean Instance 实例化

      • popluate Bean 填充属性

      • Initalize Bean 初始化

        在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存

        DefaultSingletonBeanRegistry.class

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        /** Cache of singleton objects: bean name --> bean instance */
        // 单例对象的cache
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

        /** Cache of singleton factories: bean name --> ObjectFactory */
        // 单例工厂的cache
        private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

        /** Cache of early singleton objects: bean name --> bean instance */
        // 提前曝光的单例对象
        private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

        尝试从cache中获得对象

        DefaultSingletonBeanRegistry#getSingleton

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        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 (singletonObject != NULL_OBJECT ? singletonObject : null);
        }

        上面的代码需要解释两个参数:

        isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象, 或则在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)
        allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象
        分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:

        1
        2
        this.earlySingletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
 从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

 从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory,定义如下:

 
1
2
3
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
这个接口在下面被引用
1
2
3
4
5
6
7
8
9
10
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (! this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
这里就是解决循环依赖的关键,这段代码发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。 这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。 知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。 参考 http://www.jianshu.com/p/6c359768b1dc 如果在三级缓存中找到了 再通过BeanName做一次判断 判断这个 我们想要的是这个bean 还是生成这个bean的factory。 在最初的时候我们是把bean name 给处理了一次的 看上面的**transformedBeanName** 说明。
  • getObjectForBeanInstance

    在之前我们在cache中找到了bean。但是这个bean可能是factoryBean。 我们需要通过工厂Bean 获得产品Bean

    AbstractBeanFactory#getObjectForBeanInstance

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    protected Object getObjectForBeanInstance(
    Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {

    // Don't let calling code try to dereference the factory if the bean isn't a factory.
    // 因为在尝试获得bean的使用bean的名字是去掉&符号的。&表示工厂bean。 但是根据这个beanName获取道的并不是FactoryBean的 抛错
    if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) {
    throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
    }

    // Now we have the bean instance, which may be a normal bean or a FactoryBean.
    // If it's a FactoryBean, we use it to create a bean instance, unless the
    // caller actually wants a reference to the factory.
    // 相当于 ! beanInstance instanceof FactoryBean && ! BeanFactoryUtils.isFactoryDereference(name) 确定 名字并不是BeanFactory类型的 且 得到的类并不是工厂类型的, return bean
    if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
    return beanInstance;
    }
    // 否则就是通过BeanFactory构建Bean
    Object object = null;
    if (mbd == null) {
    object = getCachedObjectForFactoryBean(beanName);
    }
    if (object == null) {
    // Return bean instance from factory.
    FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
    // Caches object obtained from FactoryBean if it is a singleton.
    if (mbd == null && containsBeanDefinition(beanName)) {
    mbd = getMergedLocalBeanDefinition(beanName);
    }
    boolean synthetic = (mbd != null && mbd.isSynthetic());
    object = getObjectFromFactoryBean(factory, beanName, !synthetic);
    }
    return object;
    }

    通过getObjectFromFactoryBean获得Bean,在factoryBeanObjectCache中是不是已经存在Bean了,如果存在直接返回, 如果没有 通过BeanFactory 构建,构建的Bean还是要AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization 进行组装。 同时根据factory bean 是否是isSingleton的来确定是否要加入到factoryBeanObjectCache中

    FactoryBeanRegistrySupport#getObjectFromFactoryBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    /**
    * Obtain an object to expose from the given FactoryBean.
    * @param factory the FactoryBean instance
    * @param beanName the name of the bean
    * @param shouldPostProcess whether the bean is subject to post-processing
    * @return the object obtained from the FactoryBean
    * @throws BeanCreationException if FactoryBean object creation failed
    * @see org.springframework.beans.factory.FactoryBean#getObject()
    */
    protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
    if (factory.isSingleton() && containsSingleton(beanName)) {
    synchronized (getSingletonMutex()) {
    Object object = this.factoryBeanObjectCache.get(beanName);
    if (object == null) {
    object = doGetObjectFromFactoryBean(factory, beanName);
    // Only post-process and store if not put there already during getObject() call above
    // (e.g. because of circular reference processing triggered by custom getBean calls)
    Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
    if (alreadyThere != null) {
    object = alreadyThere;
    }
    else {
    if (object != null && shouldPostProcess) {
    if (isSingletonCurrentlyInCreation(beanName)) {
    // Temporarily return non-post-processed object, not storing it yet..
    return object;
    }
    beforeSingletonCreation(beanName);
    try {
    object = postProcessObjectFromFactoryBean(object, beanName);
    }
    catch (Throwable ex) {
    throw new BeanCreationException(beanName,
    "Post-processing of FactoryBean's singleton object failed", ex);
    }
    finally {
    afterSingletonCreation(beanName);
    }
    }
    if (containsSingleton(beanName)) {
    this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
    }
    }
    }
    return (object != NULL_OBJECT ? object : null);
    }
    }
    else {
    Object object = doGetObjectFromFactoryBean(factory, beanName);
    if (object != null && shouldPostProcess) {
    try {
    object = postProcessObjectFromFactoryBean(object, beanName);
    }
    catch (Throwable ex) {
    throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
    }
    }
    return object;
    }
    }
  • 如果这个Bean在这之前没有创建过,那么要进行创建。

    • 如果这个Beanfactroy有parent Bean factory的话。 那么通过委托的方式看能不能得到bean

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // Check if bean definition exists in this factory.
      BeanFactory parentBeanFactory = getParentBeanFactory();
      if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
      // Not found -> check parent.
      String nameToLookup = originalBeanName(name);
      if (args != null) {
      // Delegation to parent with explicit args.
      return (T) parentBeanFactory.getBean(nameToLookup, args);
      }
      else {
      // No args -> delegate to standard getBean method.
      return parentBeanFactory.getBean(nameToLookup, requiredType);
      }
      }
      • 保证创建这个bean的时候,它构造方法中的Bean都已经创建好了

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        String[] dependsOn = mbd.getDependsOn();
        if (dependsOn != null) {
        for (String dep : dependsOn) {
        if (isDependent(beanName, dep)) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
        "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
        }
        registerDependentBean(dep, beanName);
        try {
        getBean(dep);
        }
        catch (NoSuchBeanDefinitionException ex) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
        "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
        }
        }
        }
      • DefaultSingletonBeanRegistry#isDependent

        通过递归的方式进行调用 保证这个Bean 构造方法中所依赖的Bean,Bean 构造方法中所依赖的Bean的构造方法中(如果这个Bean还没有被创建的话)所依赖的Bean… 都已经准备好了,否则报错💥

        *这里就显示了通过构造方法,依赖注入的方式创建Bean,Bean之间不允许互相依赖,但是通过setter的方式,依赖注入Bean, Bean之间是允许 *

        特别说明

        举个简单例子:例如 B 依赖了 A,则 dependentBeanMap 缓存中应该存放一对映射:其中 key 为 A,value 为含有 B 的 Set;而 dependenciesForBeanMap 缓存中也应该存放一对映射:其中 key 为:B,value 为含有 A 的 Set。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
              private boolean isDependent(String beanName, String dependentBeanName, Set<String> alreadySeen) {
        if (alreadySeen != null && alreadySeen.contains(beanName)) {
        return false;
        }
        String canonicalName = canonicalName(beanName);
        Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);
        if (dependentBeans == null) {
        return false;
        }
        if (dependentBeans.contains(dependentBeanName)) {
        return true;
        }
        for (String transitiveDependency : dependentBeans) {
        if (alreadySeen == null) {
        alreadySeen = new HashSet<String>();
        }
        alreadySeen.add(beanName);
        if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) {
        return true;
        }
        }
        return false;
        }
  1. BeanFactoryPostProcessor

    将${}中的值做替换成*.properties中的内容

  2. InstantiationAwareBeanPostProcessor#postProcessBeforInstantiation

    Instantiation [ɪnstænʃɪ’eɪʃən]

    执行Bean的构造器

  3. InstantiationAwareBeanPostProcessor#postProcessPropertyValue

    根据xml中的定义设置property的值

  4. ApplicationContextAwareProcessor#postProcessBeforeInitialization

    ApplicationContextAwareProcessor是BeanPostProcessor的一个实现 在内部会调用invokeAwareInterfaces方法 里面会有很多的if 来判断这个类是不是特定的aware类型,比方说

1
2
3
if (object instanceof ApplicationContextAware) {
((ApplicationContextAware) object).setApplicationContext();
}

Java 多线程并发

线程的创建方式

  1. 继承Thread类

    1
    2
    3
    4
    5
    6
    Thread thread = new Thread() {
    @Override
    public void run() {
    }
    }

  2. 实现Runnable接口

    1
    2
    3
    4
    5
    6
    7
    Runnable run = new Runnable() {
    @Override
    public void run() {
    //do something
    }
    };
    Thread t = new Thread(run);
  3. ExecutorService Callable, Future

    1
    2
    3
    4
    ExecutorSrevice threadPool = Executors.cachedThreadPool();
    Future f = threadPool.submit(new Runnable() {
    // 各种run任务的描述
    });

    线程池

    1. newCachedThreadPool

      调用execute将重用之前构造的线程(如果线程可用) 如果现有的线程没有可用的,则创建一个新线程并添加到线程池中.终止并从缓存中移除那些已有60s未被使用的线程

    2. newFixedThreadPool

    3. newScheduledThreadPool

    4. newSingleThreadExecutor

    线程的结束

    定义一个退出标志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Thread t = new Thread() {
    volatile Boolean stop;

    {
    stop = false;
    }

    @Override
    public void run() {
    while (! stop) {

    }
    }
    }

interrupted

用interrupted 有两种方法

  1. 当线程处于blocked的状态的时候.我们调用该线程的interrupt()方法会抛出InterruptedException

  2. 在线程在处于非blocked状态的时候 去判断它的中断标示,这个方法会和方法1同时使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Thread t = new Thread() {
    volatile Boolean stop;

    {
    stop = false;
    }

    @Override
    public void run() {
    while (! stop && ! isInterrupted()) {
    // do something
    }
    }
    }

sleep 和wait之间的区别

​ - sleep 是属于Thread 的方法. wait是属于object的方法.

​ - sleep 是让渡cpu时间. 而wait是让渡出临界资源的控制权

Java 锁

悲观锁

悲观锁可以叫做独占锁, 在得到这个资源之后到让渡出这个资源之前其他的事物都不能访问这个资源.

乐观锁

相对于悲观锁, 乐观锁认为在很大的程度下这个资源都不会发生变化 所以也没有必要进行独占(大家都可以访问), 在准备对这个资源进行修改的时候要去验证一下这个资源是不是发生变化了, 如果发生变化了就放弃修改, 如果是没有发生变化,那就修改它. (Compare And Swap)

自旋锁

  • 自旋
1
2
3
4
5
long offset = Unsafe.getUnsafe().objectFieldOffset(Stuednt.class.getDeclaredField("name"));
Object o;
do {
v = Unsafe.getUnsafe().getObjectVolatile(o, offset);
} while (! Unsafe.getUnsafe().compareAndSwapObject(student, offset, v,"new name");)
  • 如果持有锁的线程能在很短的时间内释放锁资源, 那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,只要等一等就好了.

Synchronized

作用范围

  • 作用方法
  • 作用静态方法
  • sychronized(something)

核心组件

详细说明

  • contention list

    竞争队列 所有请求锁的线程首先放在这个竞争队列中

  • entry list

    Contention list zhong 有资格称为候选资源的线程被移动到这个竞争队列中

  • wait set

    调用wait的线程在这个队列中

  • OnDeck

    任意时刻最多只能有一个线程正在竞争锁,该线程被称为OnDeck

  • Owner

    获得锁的进程称为Owener

  • ! Owener

    释放锁的线程

队列说明

  • ContentionList 虚拟队列

    ContentionList 并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指 针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行, 通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock- Free的队列。

    因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。

    LIFO

  • EntryList

    EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。

    OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不 发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒, 则再次转移到EntryList。

  • 自旋锁

    那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数)LockSupport.park() LockSupport.unPark()。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响 锁的性能

    缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法 获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非 常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。

    还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机 会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋的周期选择显得非常重要,但这与操作系统、硬 件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。

    自旋优化策略

    对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略,具体如下:

    如果平均负载小于CPUs则一直自旋

    如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

    如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

    如果CPU处于节电模式则停止自旋

    自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

    自旋时会适当放弃线程优先级之间的差异

    那synchronized实现何时使用了自旋锁?答案是在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时 首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。

  • 偏向锁

    在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题:

    现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些 CAS操 作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。但还有很多概念需要解释、很多引入的 问题需要解决:

    SMP

    其意思是所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。

    而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现, 其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的 接口。

    Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总 线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致 性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。

    而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。

上面提到Cache一致性,其实是有协议支持的,现在通用的协议是MESI(最早由Intel开始支持),具体参考:http://en.wikipedia.org/wiki/MESI_protocol,以后会仔细讲解这部分。

其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

NUMA(Non Uniform Memory Access Achitecture)架构:

与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存,针对这种结构此处不做讨论。

ReentrantLock

他是一种重入锁,除了能完成sychronized所能完成的所有工作之外,还提供了诸如可响应中断锁、可轮训锁、定时锁能避免多线程死锁的方法.

  1. void lock()

    如果锁处于空闲的状态,当前线程将获得锁

  2. boolean tryLock()

    如果锁可用,则获得锁,并且立即返回true,否则返回false.tryLock只是”试图”获取锁,如果锁不可用,不会导致这个线程处于wait状态

  3. void unlock()

    当前线程将释放持有的锁

  4. Condition newCondition()

    条件对象,获取等待通知组件. 该组件和当前的锁绑定.当前线程只有拥有了锁,才能调用该组件的await()方法.而调用了之后,当前线程会释放lock, 被加入到这个condition的等待队列中,这个线程不再会活动,until 其他线程操作这个conditional 调用signal()或者 signalAll(). 被await的线程会到lock的竞争队列中去竞争condition.

  5. int getHoldCount()

    查询当前线程保持此锁的次数, 重入的次数,也就是执行此线程执行lock方法的次数

  6. int getQueueLength()

    返回正等待获取此锁的线程数量

  7. int getWaitQueueLength(Condition condition)

    返回等待与此锁相关的给定条件线程估计数.比方说10个线程,用同一个condition对象,而且10对象都执行了await的方法.那么返回的值为10

  8. bool hasWaiters(Condition condition)

    某个Condition 是否有等待的Thread数量

  9. boolean hasQueuedThread(Thread thread)

    查询给定的线程是否拥有这个锁

  10. boolean hasQueuedThreads()

    是否有线程在等待这个lock

  11. isFair()

    是否是公平锁

  12. isHeldByCurrentThread()

    这个lock是不是被当前线程占有

  13. isLock()

    这个锁是否被其他线程占用

  14. lockInterruptibly()

    尝试获取锁,仅在调用时锁未被中断,获取锁

  15. tryLock(Long timeout, TimeUnit unit)

    在给定的时间内尝试获取锁,如果没有获取到,则放弃

公平锁和非公平锁

JVM 按随机、就近原则分配原则锁的机制则称为不公平锁,按照对锁提出获取请求的先后顺序分配到锁的机制被称为公平锁

ReetrantLock 与 Sychronized

reetrantLock通过lock()和unlock() 实现加锁和解锁. ReetrantLock 必须在finally控制块中进行解锁操作.

Semaphore

Semaphore是一种基于基数的型号量.它可以设定一个阈值.多个线程获取许可型号(acquire),做完自己的申请后归还(release)

共享锁和独占锁

独占锁模式下,每次只有一个线程能持有锁.共享锁允许多个线程同时获取锁,并发共享资源. 比方说ReadWriteLock.

READ WRITE
READ ☑️ ✖️
WRITE ✖️ ✖️
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MyReadWriteReetraintLock {
/**
* 目前正在读的数量
*/
private volatile Integer readerNumber;
/**
* 正在读的数量
*/
private volatile Integer writerNumber;

public MyReadWriteReetraintLock() {
readerNumber = 0;
writerNumber = 0;
}

public synchronized void tryRead() throws Exception {
while (writerNumber > 0) {
wait();
}
readerNumber ++;
}

public synchronized void readDone() {
readerNumber --;
notifyAll();
}

public synchronized void tryWrite() throws InterruptedException {
while (readerNumber > 0 || writerNumber > 0) {
wait();
}
writerNumber ++;
}

public synchronized void writeDone() {
writerNumber --;
notifyAll();
}

}

Interrupt

中断一个线程,本意是给这个线程一个通知信号,会影响这个线程内部一个中断标识.

  • 运行中的线程不能响应interrupt. 但是标识位会被设置.可以通过isInterrupted方法来知晓标识位的状态
  • 在阻塞状态的进程被调用interrupt方法会抛出InterruptedException, 在抛出错误后会将标识位清除.

线程池的组成

  • 线程池管理器
  • 工作线程
  • 任务接口
  • 任务队列

构建一个线程池

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
10,
100,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy());
}

拒绝策略

当线程池里面所有的线程都在运行中, 并且队列都满了情况下有若干的拒绝方式

  • AbortPolicy

    直接抛出异常,阻止系统正常运行

  • CellerRunsPolicy

​ 该策略直接在调用者线程中运行当前被丢弃的任务. 优点不会真的丢弃任务. 但是任务提交的性能可能会急剧下降

  • DiscardOldestPolicy

​ 抛弃队列中最老的线程

  • DiscardPolicy

​ 默默的丢弃将要处理的任务不做任何的处理

线程池的工作流程

当调用execute()方法添加一个任务的时候,线程池会做如下判断:

a. 如果正在运行的线程数量小于corePoolSize, 会马上创建线程运行这个任务

b. 如果运行的线程数量大于或者等于corePoolSize 会将这个任务放入队列

c. 如果队列满了, 线程数量小于maximumPoolSize, 那么还是要创建非核心线程来运行这个任务

f. 如果队列满了,而且现在正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会抛出RejectExecutionExecution;

当一个线程无事可做,超过一定时间,线程池会判断,如果当前运行的线程数量大于corePoolSize,那么这个线程就会被停掉,最终线程池会被压缩到corePoolSize的大小

队列的方法

插入 移除 检查
会抛出异常 add remove element
不会抛出异常 offer poll peek
阻塞 put take
  • ArrayBlockingQueue

    数组形式实现的有界阻塞队列

  • LinkedBlockingQueue

    对于消费者生产者 采用不同的锁来控制数据同步 生产者不停的在队尾增加内容. 消费者不停的在队列头部生产内容

  • PriorityBlockingQueue

    队列使用大根堆的方式进行组织的. 每次取堆的根节点.每次添加内容后会进行堆的rearrange.

  • DelayQueue

    也是用小根堆的方式进行组织, 以延迟的时间作为权重值, 每次都从队列中取内容, 然后delay指定的时间

  • SynchronousQueue

    每一个put必须等待一个take操作.可以看作是一个数据暂存的地方

  • LinkedTransferQueue

    由一个链表组成的无界阻塞TransferQueue队列.相对于LinkedBlockingQueue多了transfer()方法

CountDownLunch

CyclicBarrier

Semaphore

Volatile

在 JMM(Java Memory Model)数据的执行主要分为 lock、unlock、read、load、use、assign、store、write。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主内存复制变量到当前工作内存,use-assign执行代码改变共享变量值,store-write用工作内存数据刷新主存相关内容。

要保证所谓的多线程的数据执行正确,就是要保证happen-before。A操作如果发生在B操作之前,那么我们能保证数据的正确。所以我们需要内存屏障的帮助, Java内存屏障主要分为四种,它保证在栅栏前初始化的load和store指令,能够严格有序的在栅栏后的load和store指令之前执行。

  • Load - Load

    确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。

  • Store-Store

    确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)

  • Load-Store

    确保Load1的数据在Store2和后续Store指令被刷新之前读取。

  • Store-Load

    ifever

    确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

    在下面讨论的所有处理器中,执行StoreLoad的指令也会同时获得其他三种屏障的效果。所以StoreLoad可以作为最通用的(但通常也是最耗性能)的一种Fence。(这是经验得出的结论,并不是必然)。反之不成立,为了达到StoreLoad的效果而组合使用其他屏障并不常见。

ThreadLocal 作用

ThreadLocal的说明1

ThreadLocal的说明2

这两篇说的很清楚了

每个Thread 对象内部都有ThreadMap<Entity, T>类型的属性,其中Entity extend WeakReference < ThreadLocal>的定义为.

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

WeakReferences说明

WeakReference如字面意思,弱引用, 当一个对象仅仅被weak reference(弱引用)指向,而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收。我们定义了Entity, 其中ThreadLocal 是弱引用,value 是强引用。 Entity中的ThreadLocal的值能被顺利的GC, 但是value中的值不能呗顺利的GC, 需要手动的设置,让JVM 让其GC。

每次ThreadLocal的set和get方法实质是在ThreadMap中放置或者读取值

ThreadLocal 内存泄露问题

在程序的上下文中

1
2
3
4
5
void dosomething() {
ThreadLocal<String> name = new ThreadLocal<>();
name.set("something");
// do something else
}

那么在结束之后, name会被gc, 因为entity的定义, 其对象也会被回收,但是ThreadLocalMap 中的table[i] 中还是放着value的值,导致内存泄漏 比较建议的做法是

1
2
3
4
5
6
7
8
9
void dosomething() {
ThreadLocal<String> name = new ThreadLocal<>();
try {
name.set("something");
// do something else
} finally {
name.remove();
}
}

ReetrantLock和Synchronized 之间的区别

  1. ReentrantLock 显示的获得锁和释放锁, Synchronized隐式的获得锁
  2. ReentrantLock可响应中断, Synchronized不可以
  3. ReetrantLock 是api级别 Sychronized 是JVM 级别
  4. ReetrantLock 可以实现公平锁
  5. ReetrantLock 通过condition可以绑定多个条件
  6. synchronized 是同步阻塞, lock是同步非阻塞
  7. synchronized发生异常的时候会自动释放锁,而Lock需要在finally中调用unlock()
  8. 通过Lock可以知道是否获得锁,而Synchronized不可以

ConcurrentHashMap

concurrentHashMap的效率要比HashTable效率高的原因是ConcurrentHashMap采用了分片的策略,默认分为16片。

Java 集合

集合框架

Collection

List

  • ArrayList

    1. 按照插入顺序排序, 可重复

    2. 底层使用数组

    3. 可以随机进行读取、增删慢 (要挪元素在数组中的位置)

    4. 线程不安全

    ConcurrentModificationException分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    List<Integer> list = new ArrayList<Integer>();
    list.add(1);
    list.add(2);
    list.add(3);

    /**
    * example 1
    * 在这个情况下不会抛错
    */
    int length = list.size();
    for (int i = 0; i < length; i++) {
    if (list.get(i).equals(2)) {
    list.add(10);
    }
    }
    /**
    * example 2
    * 在这种情况下会抛错
    */
    for(int temp : list) {
    if(temp == 2) {
    list.add(10);
    }
    }

    /**
    * exmple 3
    * 在这种情况下会抛错
    */
    Iterator<Integer> iterator = list.iterator();
    while(iterator.hasNext()) {
    if(iterator.next().equals(2)) {
    list.add(10);
    }
    }
    /**
    * example 4
    * 在这种情况下不会抛错
    */
    ListIterator<Integer> listIterator = list.listIterator();
    while (listIterator.hasNext()) {
    if (listIterator.next().equals(2)) {
    listIterator.add(10);
    }
    }

    会抛出错误的原因

    在ArrayList 内有个内部类Itr

    1
    2
    3
    4
    5
    6
    private class Itr implements Iterator<E> {
    /**
    * 预期修改(modification)次数
    */
    int expectedModCount = modCount;
    }

    在迭代器调用next 方法的时候会检查当前类的修改次数和迭代器内的修改次数是不是一致, 如果是不一致的话 就爆炸.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public E next() {
    checkForComodification();
    try {
    int i = cursor;
    E next = get(i);
    lastRet = i;
    cursor = i + 1;
    return next;
    } catch(IndexOutOfBoundsException e) {
    checkForComodification();
    throw new NoSuchElementException();
    }
    }
    public E add(E e) {
    checkForComodification();

    try {
    int i = cursor;
    ArrayList.this.add(i, e);
    cursor = i + 1;
    lastRet = -1;
    exceptedModCount = modCount;
    } catch (IndexOutOfBoundException ex) {
    throw new ConcurrentModificationException();
    }
    }

    在案例1 中没有使用到迭代器相关的api

    在案例 2, 3 中使用了迭代器相关的api 在外部类的mod time 发生了改变的时候没有对迭代器内部的mod time 同时进行修正, 当再次调用next 的时候发现内部类记录的修改次数和类外部的修改次数不一样.爆炸.

  • Vector

    1. 按照插入顺序排序 可重复
    2. 底层使用数组
    3. 可以随机读取 增删慢(要挪元素在数组中的位置)
    4. 线程安全
  • LinkedList

    1. 按照插入顺序排序 元素可重复
    2. 通过链表的方式保存数据
    3. 不可以随机读取数据 要通过头节点 或者尾节点 以遍历的方式获得所得的元素 但是 add 和remove的方法快

Set

  • HashSet

    排列无序 不可重复 底层是HashMap.

  • TreeSet

    内部是TreeMap的sortedSet

  • LinkedHashSet

    内部是LinkedHashMap

Queued

  • PriorityQueued

Map

  • HashMap

    key不可重复 value 可以重复

    底层hash表

    线程不安全 (可能会造成endless loop)

    允许key值为null

  • HashTable

    key不可重复

    底层hashtable

    线程安全

    key value 都不允许为null

  • TreeMap

    key不可以重复

MyBatis 快速入门

Mybatis示例

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8">
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"/>
<configuration>
<properites>
<property name = "username" value ="root"/>
<property name = "id" value = "123"/>
</properites>
<settings>
<setting name="cacheEnabled" value = "true"/>
</settings>
<typeAliases>
<typeAlias type = "com.xxx.Blog" alias = "Blog"/>
</typeAliases>
<enviroments default = "development">
<enviroment id = "development">
<transcationManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value = "com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</dataSource>
</enviroment>
</enviroments>
<mappers>
<mapper resource="com/***/BlogMapper.xml"/>
</mappers>
</configuration>
</xml>

执行的java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Main {
public static void main(String[] args) throws Exception {
String resource = "com/xxx/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuillder().build(inputStream);

SqlSession sqlsession = sqlSessionFactory.openSession();
try {
Map<String,Object> parameter = new HashMap<>();
parameter.put("id", 1);
Blog blog = sqlSession.selectOne("com.xxx.BlogMapper.selectBlogDetails", parameter);
//
} catch(Exception e) {
e.printStackTrace();
}
}
}

MyBatis 整体架构

基础支持层

  • 反射模块

  • 类型转换模块

    jdbcType和JavaType之间类型做转换

  • 日志模块

  • 资源加载模块

    对类加载器进行封装

  • 解析器模块

    对Xpath进行封装 为config处理提供支持

    为处理sql语言中的占位符提供支持

  • 数据源模块

  • 事务管理

  • 缓存模块

  • Binding模块

核心处理层

  • 配置解析

  • Sql解析与sripting 模块

  • Sql执行

    sqlSession -> executor -> statementHandler -> parameterHandler -> statement -> db -> resultSet ->ResultSetHandler -> StatementHandler ->executor -> sqlSession

Java基础知识点整理

JVM

JVM 后台运行的系统进程

  • 虚拟机线程

    这个线程等待JVM到达安全点操作出现. Stop the world 垃圾回收、线程栈dump、线程暂停、线程偏向锁解除.

  • 周期性任务线程

    这线程负责定时器事件(中段),用来调度周期性操作的执行

  • GC线程

    不同的GC

  • 编译器线程

    将字节码编译成本地码

  • 信号分发线程

    这个线程发送到JVM的信号并调用适当的JVM方法处理.

JVM 内存区域

  1. 线程私有区域

    1. 程序计数器

      计数器记录的是虚拟机字节码指令的地址(当前的地址)

    2. 虚拟机 栈 知乎介绍

      1. 局部变量表 里面会包括 这个方法的临时变量和入参数信息

      2. 操作数栈 操作数栈

        当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,

        再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作

        比方说 int result = parameter1 + parameter2

        实际上就是iadd, 它将两个int添加在一起.要使用它,你在堆栈上推两个值,然后使用它

        1
        2
        3
        iload_0     # Push the value from local variable 0 onto the stack
        iload_1 # Push the value from local variable 1 onto the stack
        iadd # Pops those off the stack, adds them, and pushes the result

        比方说 int result = parameter1 + parameter2 + parameter3

        1
        2
        3
        4
        5
        iload_0     # Push the value from local variable 0 onto the stack
        iload_1 # Push the value from local variable 1 onto the stack
        iadd # Pops those off the stack, adds them, and pushes the result
        iload_2 # Push the value from local variable 2 onto the stack
        iadd # Pops those off the stack, adds them, and pushes the result
      3. 动态链接

        Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用.这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。

      4. 方法返回

        一个方法可能有两种方法退出 正常返回和出现异常返回.一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

    3. 本地方法区

      方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。

  2. 线程共享区

    1. Java堆

      从GC的角度来说可以分为 新生代、老年代

      新生代

      新生代占用堆区1/3空间. 新生代分为 Eden、ServivorFrom、ServivorTo 三个区域.分配的比例是 8:1:1.在新生代发生的垃圾被称为

      老年代

      老年代的数据比较的稳定,所以MajorGC 不会平凡的发生 然后使用的算法是标记-清除算法. 在触发Major Gc之前一定会触发一次MinorGC

      垃圾回收算法

      • 标记-清除

        ​ 没有连续的可用的内存空间,会有很多的碎片.

      • 复制算法

        将内存分为同等大小的两块.当某一块内存满后将尚存活的对象复制到另一块上去.对象的Age会+1.默认年龄到达15会移动到老年区.

      • 标记整理算法

        标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端.

    2. 方法区

      主要存放类信息和meta-data. 在Java8中永久带已经被元空间的概念取代.元空间不存在虚拟机中,而是使用本地内存.在默认情况下元空间的大小仅受本地内存限制.

  3. Java的四种引用类型

    1. 强引用

      最常见的就是强引用.当一个对象(Object)信息被赋值给一个引用(Reference)时,就是强引用.当处于可达状态时不可能被回收的

    2. 软引用

      SoftReference.当内存不够时候会被回收

    3. 弱引用

      WeakReference 当垃圾回收机制一运行总会回收该对象占有的内存. 当我们创建一个ThreadLoad的值的时候,实际上是加入到ThreadLocalMap<WeakReference<ThreadLocal<?>>, Object> 中

    4. 虚引用

      需要PhantomReference 类实现,它不能单独使用, 必须和引用队列联合使用.虚引用的主要作用是跟踪对象被垃圾回收的状态.

  4. 垃圾回收算器

    针对不同的内存区域采用不同的垃圾回收算器.

    • 年轻带:

    ​ Serial 单线程的垃圾收集器. STW. 复制

    ​ ParNew Serial的多线程版本.STW. 复制

    ​ Parallel scavenge 多线程的垃圾收集器. 通过自适应的调节策略来达到最大的吞吐量. STW. 复制

    • 年老带:

    ​ Serial Old 是Serial老年区版本 单线程 STW. 标记整理

    ​ Parallel Old 是Parallel scagenge老年区版本 STW 标记整理

    • CMS

      初始标记 (只是标记一下GC Roots能够直接关联的对象 STW)

      并发标记

      重新标记

      并发清除(和用户线程一起工作 不需要暂停工作线程)

    • G1

      基于标记-整理算法 不产生内存碎片

      非常准确控制停顿时间,在不牺牲吞吐量前提下实现低停顿垃圾回收

重新认识spring

基于servlet 的web开发

首先我们会定义

如果开发一个简单的 hello world web应用

  • 一个项目结构

  • 一个web.xml 文件 或者WebApplicationInitializer实现.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0"encoding="UTF-8"?>
    <web-app id="WebApp_ID" version="2.4"xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/j2eehttp://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/controllers.xml,/WEB-INF/service.xml</param-value>
    </context-param>
    <servlet>
    <servlet-name>dispatch</servlet>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
    </init-param>
    </servlet>
    <servlet-mapping>
    <servlet-name>dispatch</servlet-name>
    <servlet-pattern>*.*</servlet-pattern>
    </servlet-mapping>
    </web-app>

响应式编程

  • I/O 阻塞限制了并行性
  • 同步交互限制了并行性
  • 轮询减少了使用更少资源的机会
  • 一个节点会拖慢所有的其他的节点 需要隔离错误
  • 系统的部署能够弹性

建立实例

目标 传统部署 Akka方法
扩展性 混合使用线程池 共享数据库的互斥状态,和RPC网络服务进行扩展 Actor 发送和接收信息
提供交互信息 对信息进行轮训 当时间发生的时候进行推送
在网络上进行扩展 同步RPC 异步消息.完成的时候会通过ask和tell方法进行结果的推送
错误处理 处理所有的异常, 只有一切正常才能继续 隔离错误, 通过上级的监督 对出错的信息做处理

传统扩展

传统扩展和持久性 一切移入数据库

对象的状态不再是简单的驻留在web服务器的内存中了, 操纵对象的状态不能是简单的getter/setter 而是数据库的DDL,DML. 对于数据库的操作而不是内存的操作会遇到两个问题

  • 操作数据库可能会失败. 网络 数据库宕机
  • 要通过数据库的锁来保证数据的正确, 无论是关系型数据库的MMVC, 还是Redis的乐观锁, 这时候就没有用到内存锁的机制
  • 对程序做测试需要做数据的隔离. @Test(transaction=rollback)

传统扩展和交互应用 轮询

每个数据库的变化需要JDBC做DML才知道变化, 程序不会被主动告知

传统扩展和交互应用 web

在一个controller中我们可能会调用其他的web服务, 网络是不可靠的, 对方的服务也不一定可靠.如果远程的服务调用失败,则程序中同一流程中的其他部分也会失败.

Akka进行扩展

目标 传统方式 Akka方式
保持数据的持久性 所有的程序的持久性都由数据库进行维护 继续使用内存中的状态所有的状态的改变作为消息发送到日志. 如果应用重新启动, 需要重新读取日志
交互 对数据库进行轮训 把事件推推送到相关的组建

最大的挑战是安全的保存数据.

用Akka扩展和持久化

改变保存为事件序列

所有更改都保存为事件序列, 在本例中是MessageAdded事件, Coversation的当前状态可以回放到内容中, Conversation 的状态在内存中进行重建,因此可以在中断的地方继续.这种类型的数据库通常被称为是日志, 这种技术通常为称为事件源.

会话分割

通常采用的使用的技术是 分片 分区

消息推送

事件通过发布订阅的方式进行推送, 而不是组件之间直接通信.

Actor 操作

  • 创建
  • 发送
  • 改变
  • 监督

横向扩展 集群化

为什么要微服务化

  1. 单机能提供的资源有限(primary)

一个物理机器的硬件资源,网络资源总是有限的, 可能需要多个机器才能支持需求.并且在GC的时候可能会stop the world. 将其余的线程暂停, GC线程启动, 对Eden区的对象做连通的计算. 对象目前是存活对象的图的予以保留, 对于在不在的予以清理.如果Survice的空间很大的话 那stop the world 所需要的时间也就会很长, 导致业务暂停的时间也会延长.

  1. 方便项目管理、发布测试等的流程的进行(secondary)

集群和集群内部节点的状态

集群就是一组可以互相通信的服务器, 他们向外界透明的提供一致服务. 集群可以动态的修改大小, 并且在发生错误的时候时候继续运行.集群需要具备两个功能

  1. 发现失败
  2. 集群中的所有成员最终能提供统一的视图

###失败检测

我们要知道集群中那些节点现在处于不能服务的状态, 我们就不把任务分配给这些节点了.在这里有若干的算法可以使用, 以下内容抄袭自分布式中几种服务注册与发现组件的原理与比较

  1. Eureka

    节点定时给注册中心发送心跳进行注册, 它会告知服务中心自己的ip port 以及自己的service-id (能提供怎样的服务). 注册中心在响应节点请求的时候告诉节点目前集群中存在的其他节点的信息.

    Eureka中没有使用任何的数据强一致性算法保证不同集群间的Server的数据一致,仅通过数据拷贝的方式争取注册中心数据的最终一致性,虽然放弃数据强一致性但是换来了Server的可用性,降低了注册的代价,提高了集群运行的健壮性。

  2. Consul

    Consul提供强大的一致性保证,因为服务器使用Raft协议复制状态 。Consul支持丰富的健康检查,包括TCP,HTTP,Nagios / Sensu兼容脚本或基于Eureka的TTL。客户端节点参与基于八卦的健康检查,该检查分发健康检查工作,而不像集中式心跳检测那样成为可扩展性挑战。发现请求被路由到选举出来的领事领导,这使他们默认情况下强烈一致。允许陈旧读取的客户端使任何服务器都可以处理他们的请求,从而实现像Eureka这样的线性可伸缩性.

    内部有若干的角色

    • Client:作为一个代理(非微服务实例),它将转发所有的RPC请求到Server中。作为相对无状态的服务,它不持有任何注册信息。

    • Server:作为一个具备扩展功能的代理,它将响应RPC查询、参与Raft选举、维护集群状态和转发查询给Leader等。

    • Leader-Server:一个数据中心的所有Server都作为Raft节点集合的一部分。其中Leader将负责所有的查询和事务(如服务注册),同时这些事务也会被复制到所有其他的节点。

    • Data Center:数据中心作为一个私有的,低延迟和高带宽的一个网络环境。每个数据中心会存在Consul集群,一般建议Server是3-5台(考虑到Raft算法在可用性和性能上取舍),而Leader只能唯一,Client的数量没有限制,可以轻松扩展。

所有的要求问询集群中状态的请求都会被发送到client. Client 不存储任何集群的的信息. 它会把请求转发给Server.Client之间通过gossip 来知晓目前集群中能够服务的Server有哪些.然后请求任何一个能够服务的Server.

Server分为Leader Server 和 Follower Server. Follower Server 只是Leader的copy. 所有的事务信息都是被Leader记录 然后通知到Follower的. 这里由Raft算法提供一致性保证.

示意图

  1. Zookeeper
  • Leader-Server:Leader负责进行投票的发起和决议,更新系统中的数据状态

  • Server:Server中存在两种类型:Follower和Observer。其中Follower接受客户端的请求并返回结果(事务请求将转发给Leader处理),并在选举过程中参与投票;Observer与Follower功能一致,但是不参与投票过程,它的存在是为了提高系统的读取速度

  • Client:请求发起方,Server和Client之间可以通过长连接的方式进行交互。如发起注册或者请求集群信息等。

在Akka cluster 中是通过Gossip 协议来同步事务的状态的. 每个节点都会监控它后面的节点(默认最大监控数量是5). 然后”邻居”把自己所知道的情况告诉自己的”邻居” 然后整个节点都知道了.

CAP

c为Consistency 一致性. A为Available 可用性. P为Partition Tolerance 分区容错性. 因为系统是不稳定的, 肯定是会P的.CAP 其实就是发生了不一致的情况P, 是要C还是要A. 对于一个事情, 比方说扣账户里面的余额, 减库存. 当发生了请求扣余额的动作超时的时候, 我们要怎么做. 减库存那就是选择了A, 如果是报错就是选择了C.

  • CP

    比方说一个系统有一个关系型数据库.leader和若干个follower. follower通过读leader的bin log 来实现数据的同步. 当leader不能工作的时候,那么整个系统也就不能处理请求了. 这个系统是是CP的.

  • AP

    比方说有一个存储单元, 里面有三个节点,他们都是互相独立的,我们可以访问当中的任意一个节点来获取数据.即使一个不能工作了,我们可以访问其他任意一个来获得数据.

  • CP和AP的着重程度是可以转变的

    比方说对于之前CP的情况,我们可以把follower当作是读库, Leader 当作是写库, 通过乐观锁来保证最终一致性.(update 的数量和期望的一致, 否则就说明这个数据从follower中拿到的不是最新的, 抛错). 对于AP的情况比方说当中的某个数据是需要被超过1/2节点所认可的.

Akka Cluster

分区与线性扩展

通过hash mode x = n来计算结果存放在哪个节点, 对于这个x可以适当的大于目前的值, 比方说64 或者128 然后定义结果在[a-b) 之间的分配到 1 , [b-c) 之间分配到2.

节点冗余

读取请求会发送到任意一个节点,该节点负责进行协调, 计算 partition key的哈希值, 然后把请求发送给实际存储数据的3个节点, 当至少两个节点返回结果成功,即成功.

获取Identity

1
2
3
Identify identify = new Identify(1);
Future future = ask(selection, identify, 1000);
return (ActorIdentity)Await.result(future, timeout.duration());