笔记

线程池的任务队列是blockingqueue 阻塞队列,线程池有管理默认线程的集合

线程池拒绝处理任务时的四种策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

线程间通信
volatile 
wait() 和 notify()
CountDownLatch CycleBarrier  Condition(await signal )  LockSupport ( park ,unpark)

多核访问统一块数据的时候或者对线程多共享数据修改的时候会存在数据不一致的情况,
而java内存模型主要是定义程序内各变量的访问规则,所有的变量存在主内存,工作内存的只是数据的拷贝副本,
数据从主内存到工作内存这么来回的过程,

反射机制 java编译后生成class文件,反射是通过字节码文件找到某个类、方法、属性。反射的实现主要是借助 Class, constructor filed  method
作用: 反射机制指程序在运行期能获取自身的信息,

可重入锁 ReentrantLock synchronized  可重入锁最大的作用是避免死锁


公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。等待的线程都会放入队列的尾部,新加入的线程也是一样入队列,不会去抢锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
ReentrantLock 可公平 也可非公平
Synchronized 非公平锁

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有
ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
Synchronized而言,当然是独享锁


互斥锁/读写锁
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock


乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
synchronized关键字的实现也是悲观锁
原子变量类就是使用了乐观锁的一种实现方式CAS实现的

自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。


netty 设计模式
观察者模式  writeAndFlush()  定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新
责任链模式  pipeline    将多个对象形成一条链,多个对象都能处理请求
单例模式 ReadTimeoutException          spring的单例通过类反射获取实例
策略模式 EventExecutorChooserFactory   一种算法,将它们一个个封装起来, 并且使它们可相互替换,不影响外部使用者
装饰者模式 WrappedByteBuf  

spring设计模式

简单工厂模式:BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象

工厂方法:即应用程序将对象的创建及初始化职责交给工厂对象。

单例模式:

观察者(Observer):listener的实现。如ApplicationListener

代理模式:aop,spring的Proxy模式在aop中有体现,比如JdkDynamicAopProxy和Cglib2AopProxy。

 

Spring AOP

Aspect Oriented Programming,面向切面编程,提供了模块化,关键单元是切面或者说关注点,一些切面可能有集中的代码,也可能是分散的代码,这些分散的切面称为横切关注点。一个横切关注点是一个可以影响到整个应用的关注点,而且应该被尽量地集中到代码的一个地方,例如事务管理、权限、日志、安全等。

关注点是我们想在应用的模块中实现的行为。关注点可以被定义为:我们想实现以解决特定业务问题的方法。比如,在所有电子商务应用中,不同的关注点(或者模块)可能是库存管理、航运管理、用户管理等。
横切关注点是贯穿整个应用程序的关注点。像日志、安全和数据转换,它们在应用的每一个模块都是必须的,所以他们是一种横切关注点。

Spring AOP通过以下两种方式来使用

1.使用AspectJ 注解风格

2.使用Spring XML 配置风格

aop的通知类型(advice):前置通知,后置通知,返回之后通知,环绕通知,抛出异常后通知

连接点 (Joint Point):代表一个目标方法

切入点(Point cut):匹配连接点的断言或表达式

目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。

织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

 

aop代理:spring aop是基于代理实现的,由aop框架创建的运行时实现切面协议,Spring AOP默认为 AOP 代理使用标准的 JDK 动态代理。这使得任何接口(或者接口的集合)可以被代理。Spring AOP 也可以使用 CGLIB 代理。这对代理类而不是接口是必须的。

 

spring 事务 

Spring的事务管理机制实现的原理,就是通过这样一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的 方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。Spring中的AOP实 现更为复杂和灵活,不过基本原理是一致的

事务是一个不可分割操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。


编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理 本质是aop的实现  其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务

事务传播行为

      所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

  TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
  TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务隔离级别

  隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
  TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
  TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
  TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

jdk 动态代理  代理类实现JDK中的InvocationHandler接口,实现其中的invoke方法,在此方法中通过反射的方式调用被代理类的方法, 代理类是由Proxy这个类通过newProxyInstance方法动态生成的,生成对象后使用“实例调用方法”的方式进行方法调用,那么代理类的被代理类的关系只有在执行这行代码的时候才会生成,因此成为动态代理。
JDK 动态代理机制只能对接口进行代理,其原理是动态生成一个代理类,这个代理类实现了目标对象的接口,目标对象和代理类都实现了接口

cglib 代理  使用CGLib代理 Cglib子类代理工厂需要实现其MethodInterceptor接口 Enhancer 一个cglib工具类,可以设置代理目标,目标对象类,设置单一回调对象,在调用中拦截对目标方法的调用

JDK 使用反射机制调用目标类的方法,CGLIB 则使用类似索引的方式直接调用目标类方法,所以 JDK 动态代理生成代理类的速度相比 CGLIB 要快一些,但是运行速度比 CGLIB 低一些,并且 JDK 代理只能对有接口的目标对象进行代理。

AOP 代理其实是

package controllers;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
 * 
 * 所有方法的代理的回调
 * 
 * @version 1.0
 * @since JDK1.7
 */
public class ProxyFactory implements MethodInterceptor {

    //要代理的真实对象
    private Object obj;
    
    public Object createProxy(Object target) {
        this.obj = target;
        Enhancer enhancer = new Enhancer();
        //设置代理目标
        enhancer.setSuperclass(this.obj.getClass());
        //设置单一回调对象,在调用中拦截对目标方法的调用
        enhancer.setCallback(this);
        //设置类加载器
        enhancer.setClassLoader(this.obj.getClass().getClassLoader());
        
        return enhancer.create();
    }
    /**
     * 
     * 方法描述 当对基于代理的方法回调时,在调用原方法之前会调用该方法
     * 拦截对目标方法的调用
     *
     * @param obj 代理对象
     * @param method 拦截的方法
     * @param args 拦截的方法的参数
     * @param proxy 代理
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy proxy) throws Throwable {
        Object result = null;
        try {
            //前置通知
            before();
            result = proxy.invokeSuper(obj, args);
            //后置通知
            after();
        } catch (Exception e) {
            //异常通知
            exception();
        } finally {
            //方法返回前通知
            beforeReturning();
        }
        
        return result;
    }

    private void before() {
        System.out.println("before method invoke...");
    }
    private void after() {
        System.out.println("after method invoke...");
    }
    private void exception() {
        System.out.println("exception method invoke...");
    }
    private void beforeReturning() {
        System.out.println("beforeReturning method invoke...");
    }
}

由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法

Spring AOP 的实现原理其实很简单:AOP 框架负责动态地生成 AOP 代理类,这个代理类的方法则由 Advice 和回调目标对象的方法所组成。
Spring AOP AOP 要实现的是在我们原来写的代码的基础上,进行一定的包装,如在方法执行前、方法返回后、方法抛出异常后等地方进行一定的拦截处理或者叫增强处理。
AspectJ 能干很多 Spring AOP 干不了的事情,它是 AOP 编程的完全解决方案。Spring AOP 致力于解决的是企业级开发中最普遍的 AOP 需求(方法织入),而不是力求成为一个像 AspectJ 一样的 AOP 编程完全解决方案

Spring AOP 一共有三种配置方式,Spring 做到了很好地向下兼容,所以大家可以放心使用。
Spring 1.2 基于接口的配置:最早的 Spring AOP 是完全基于几个接口的,想看源码的同学可以从这里起步。
Spring 2.0 schema-based 配置:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间 <aop />
Spring 2.0 @AspectJ 配置:使用注解的方式来配置,这种方式感觉是最方便的,还有,这里虽然叫做 @AspectJ,但是这个和 AspectJ 其实没啥关系。
Spring 2.0 @AspectJ 

@Aspect 注解的 bean 中,我们需要配置哪些内容。

首先,我们需要配置 Pointcut 切入点,Pointcut 在大部分地方被翻译成切点,用于定义哪些方法需要被增强或者说需要被拦截,
配置 Advice。前置通知 后置通知 环绕通知 异常通知

创建切面类(注解方式配置):@Aspect// 注解该类为切面类
连接点(JoinPoint) 

 

依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念,具体的讲:当某个角色需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在spring中创建被调用者的工作不再由调用者来完成,因此称为控制反转。创建被调用者的工作由spring来完成,然后注入调用者 

因此也称为依赖注入。 
spring以动态灵活的方式来管理对象 , 注入三种注入方式 :构造器注入、setter方法注入、根据注解注入。 

 

BeanFactory和ApplicationContext有什么区别?

        BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。

(1)BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:

①继承MessageSource,因此支持国际化。

②统一的资源文件访问方式。

③提供在监听器中注册bean的事件。

④同时加载多个配置文件。

⑤载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

(2)①BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。

        ②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。

        ③相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。

(3)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

(4)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

 

请解释Spring Bean的生命周期?

 首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy;

 Spring上下文中的Bean生命周期也类似,如下:

(1)实例化Bean:

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):

实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。

(3)处理Aware接口:

接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:

①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;

②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。

③如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;

(4)BeanPostProcessor:

如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean 与 init-method:

如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(6)如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(7)DisposableBean:

当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(8)destroy-method:

最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
 


HashMap java 7 

数组+单向链表 
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor

put 过程分析  当插入第一个元素的时候,需要先初始化数组大小,如果 key 为 null,最终会将这个 entry 放到 table[0] 中,之后计算hash值找到数组对应下标
遍历下标处的链表,查看是否存在重复的key,有直接覆盖返回旧值,没有则加在链表表头上。
数组初始化
在第一个元素插入 HashMap 的时候做一次数组的初始化,就是先确定初始的数组大小,保证数组大小一定是 2 的 n 次方,并计算数组扩容的阈值。
计算具体数组位置 ,就是取 hash 值的低 n 位。如在数组长度为 32 的时候,其实取的就是 key 的 hash 值的低 5 位,作为它在数组中的下标位置。
添加节点到链表中时 ,如果 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容,扩容的话将原来数组中的值迁移到新的更大的数组中


get 过程分析
相对于 put 过程,get 过程是非常简单的。

根据 key 计算 hash 值。
找到相应的数组下标:hash & (length - 1)。
遍历该数组位置处的链表,直到找到相等(==或equals)的 key
key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了


Java7 ConcurrentHashMap 支持并发操作

 ConcurrentHashMap 由一个个 Segment , 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel   并行级别,默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,旦初始化以后,它是不可以扩容的
initialCapacity:初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。

loadFactor:负载因子,之前我们说了,Segment 数组不可以扩容,这个负载因子是给每个 Segment 内部使用的。

初始化的时候会有 segmentShift 的值为 32 - 4 = 28,segmentMask 为 16 - 1 = 15,姑且把它们简单翻译为移位数和掩码

put时候先计算hash值,再找到segment数组的位置j,这里有一个计算方法 无符号右移 segmentShift 位,然后和 segmentMask 做一次与操作
若j处有segment 直接put,没有则创建后put

Segment 内部的 put 操作
Segment 内部是由 数组+链表 组成的。
在往该 segment 写入前,需要先获取该 segment 的独占锁 ,利用segment 内部的数组长度和hash值计算数组位置,循环该处的链表,看链表是否有节点,有节点查找相同的key并覆盖,
若没有则将它放入链表头,插入之后判断是否超过segment阈值,超过则扩容


ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],这里需要考虑并发,多个线程同时初始化,对于并发操作使用 CAS 进行控制。

获取写入锁: scanAndLockForPut  循环获取锁,  tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。


扩容: rehash, segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry\<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍。 该方法不需要考虑并发,因为到这里的时候,是持有该 segment 的独占锁的。
创建新数组 大小2 倍, 遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置

get 过程分析
相对于 put 来说,get 真的不要太简单。

计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
槽中也是一个数组,根据 hash 找到数组中具体的位置
到这里是链表了,顺着链表进行查找即可

put 操作的线程安全性。

初始化槽,这个我们之前就说过了,使用了 CAS 来初始化 Segment 中的数组。
添加节点到链表的操作是插入到表头的,所以,如果这个时候 get 操作在链表遍历的过程已经到了中间,是不会影响的。当然,另一个并发问题就是 get 操作在 put 之后,需要保证刚刚插入表头的节点被读取,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
扩容。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。
remove 操作的线程安全性。

get 操作需要遍历链表,但是 remove 操作会"破坏"链表。

如果 remove 破坏的节点 get 操作已经过去了,那么这里不存在任何问题。

如果 remove 先破坏了一个节点,分两种情况考虑。  1、如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。


Java8 HashMap

 数组+链表+红黑树 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

 
 
 
 CMS收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于“”标记--清理”算法实现的,整个过程分为四个步骤:   1. 初始标记 

           2. 并发标记 

           3. 重新标记  

           4. 并发清理 

初始标记:仅仅是标记一下GC roots 能直接关联的对象,速度很快  

并发标记:就是进行gc roots tracing测过程 

重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

 

优点:并发收集,低停顿 

理由: 由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的
缺点: 

   1.CMS收集器对CPU资源非常敏感 

      在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 

的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐) 

  2. CMS处理器无法处理浮动垃圾 

      CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” , 

 3. CMS是基于“标记--清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。 
为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了 

G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,

G1运作步骤:
1、初始标记;2、并发标记;3、最终标记;4、筛选回收

 

 

AbstractQueuedSynchronizer  AQS 等待队列  ,队列的每个node都   thread + waitStatus(状态值:CANCELLED  SIGNAL CONDITION  PROPAGATE   ) + pre + next  属性
ReentrantLock  内部公平锁   内部类 Sync 来管理锁 真正的获取锁和释放锁是由 Sync 的实现类


开始tryAcquire ,tryAcquire(arg)没有成功,这个时候需要把当前线程挂起,放到阻塞队列中,
tryAcquire内部实现,获取状态值看有没有线程持有锁,没有的话看是否是在队列里有等待的节点,没有CAS尝试获取锁,如果有线程持有锁,判断是否是同一个线程,如果是则可重入

AQS内有addWaiter方法,将线程包装成node,队列不为空 CAS设置自己在队尾,如果队列不为空或者入队尾失败,则自旋入队

acquireQueued方法  当前节点的前驱节点为head,可以尝试获取锁,没有获取成功 ,判断该节点状态,是否需要挂起
需要挂起线程,调用parkAndCheckInterrupt ,内部LockSupport来挂起

唤醒的话就是查看当前状态,看是否有线程持有锁,如果为0,则LockSupport.UNPARK

并发环境下,加锁和解锁需要以下三个部件的协调:

锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用

 

BlockingQueue 实现之 ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。

其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。
内部属性 
// 下一次读取操作的位置
int takeIndex;
// 下一次写入操作的位置
int putIndex;
// 队列中的元素数量
int count;
控制并发用的同步器
ReentrantLock 
private final Condition notEmpty; 控制读写的
private final Condition notFull;

ArrayBlockingQueue 实现并发同步的原理就是,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。


BlockingQueue 实现之 LinkedBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用
有队列容量 队头 队尾  锁ReentrantLock, take poll peek的时候要用,Condition  控制队列为空,或队列为满的
takeLock 和 notEmpty 怎么搭配:如果要获取(take)一个元素,需要获取 takeLock 锁,但是获取了锁还不够,如果队列此时为空,还需要队列不为空(notEmpty)这个条件(Condition)。

putLock 需要和 notFull 搭配:如果要插入(put)一个元素,需要获取 putLock 锁,但是获取了锁还不够,如果队列此时已满,还需要队列不是满的(notFull)这个条件(Condition)。

如何合理的设置线程池大小
配置线程池的大小可根据以下几个维度进行分析来配置合理的线程数:

任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。
任务的执行时长。
任务是否有依赖——依赖其他系统资源,如数据库连接等。


CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。

IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数+1。 
因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。


某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU

线程池大小的估算公式:
       最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

 


流量削峰 秒杀之类的活动 在某个时间点流量瞬间激增,达到峰值, 削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。
针对秒杀这一场景,削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求,它遵从“请求数要尽量少”的原则。
1 消息队列来缓冲瞬时流量
2 秒杀时的验证码或答题类似的操作也可以起到削峰的作用。这个重要的功能就是把峰值的下单请求拉长,从以前的1s之内延长到2s~10s。这样一来,请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要,会大大减轻压力
3 分层过滤  分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。
分层校验的目的是:

在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;

在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。

自旋存在的意义 TODO

 


AQS    https://segmentfault.com/a/1190000017372067
aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
可以这么说,只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握。

AQS的两种功能
从使用层面来说,AQS的功能分为两种:独占和共享

独占锁,每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁
共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
ReentrantLock 实现了NofairSync(非公平锁),FailSync(公平锁)

非公平锁源码分析  Unsafe  native 方法 调用C++
先加锁
1 开始调用lock方法,先去通过cas( if (compareAndSetState(0, 1))  unsafe方法 )去抢占锁,如果抢占锁成功,保存获得锁成功的当前线程,抢占锁失败,调用acquire来走锁竞争逻辑
2 acquire 方法  主要逻辑是
通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
acquireQueued,将Node作为参数,通过自旋去尝试获取锁。

tryAcquire
这个方法的作用是尝试获取锁,如果成功返回true,不成功返回false。内部实现方法 nonfairTryAcquire 
实现逻辑 : 获取当前state的值判断是否有锁,如果是无锁,通过cas操作来替换state的值改为1,如果锁是同一个线程持有,则直接增加重入次数,否则返回false

addWaiter  从队尾加的
当tryAcquire方法获取锁失败以后,则会先调用addWaiter将当前线程封装成Node,判断当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列
如果为空或者cas失败,调用enq将节点添加到AQS队列

enq
enq就是通过自旋操作把当前节点加入到队列中,如果是第一次添加到队列,tail=null,CAS的方式创建一个空的Node作为头结点,此时队列中只一个头结点,所以tail也指向它
进行第二次循环时,tail不为null,将当前线程的Node结点的prev指向tail,然后使用CAS将tail指向Node

acquireQueued
将添加到队列中的Node作为参数传入acquireQueued方法,这里面会做抢占锁的操作
获取prev节点,如果前驱为head才有资格进行锁的抢夺,获取锁成功后就不需要再进行同步操作了,获取锁成功的线程作为新的head节点,获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
最后,通过cancelAcquire取消获得锁的操作

shouldParkAfterFailedAcquire
判断一个争用锁的线程是否应该被阻塞,它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置-如果锁释放,则应当通知它,所以它可以安全的阻塞了,返回true。
如果前继节点是“取消”状态,则不断往前遍历,找到waitstatus大于0的节点,如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。

parkAndCheckInterrupt
如果shouldParkAfterFailedAcquire返回了true,则会执行:parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。


锁的释放
ReentrantLock.unlock
释放锁的过程,调用release方法,这个方法里面做两件事,1,释放锁 ;2,唤醒park的线程
tryRelease:将锁的数量减1,如果c==0则把当前线程释放,然后需要唤醒下一个节点的线程


Condition  AQS维护着两个队列,一个是Sync队列,用于参与竞争,一个是Condition队列,用来保存await状态的对象
await实现 
1 将当前线程作为内容构造的节点node放入到Condition队列中并返回此节点
2 释放当前线程所拥有的锁,返回值为AQS的状态位
3 检测此节点是否在同步队列上 ,如果不在,说明此线程还没有资格竞争锁,此线程就继续挂起睡觉。直到检测到此节点在同步队列上,并检测此线程有没有被中断

addConditionWaiter: 如果condition等于-2,将新的节点添加到队列的尾部。如果此节点的状态不是CONDITION,则需要将此节点在条件队列中移除

fullyRelease:获取当前节点状态,释放锁,考虑重入锁情况,如果失败,标记状态,移出节点
isOnSyncQueue:用来判断是否是在Sync队列中

LockSupport
先唤醒线程,在阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

当调用park()方法时,会将_counter置为0,同时判断前值,小于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。
当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。


数据库的查询优化方法

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。

4.应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num=10 or num=20可以这样查询:select id from t where num=10 union all select id from t where num=20

5.in 和 not in 也要慎用,否则会导致全表扫描,如:select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3

6.下面的查询也将导致全表扫描:select id from t where name like ‘%李%’若要提高效率,可以考虑全文检索。

7. 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:select id from t where num=@num可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num

8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where num/2=100应改为:select id from t where num=100*2

9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where substring(name,1,3)=’abc’ ,name以abc开头的id应改为:
select id from t where name like ‘abc%’

10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

12.不要写一些没有意义的查询,如需要生成一个空表结构:select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)

13.很多时候用 exists 代替 in 是一个好的选择:select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)

14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

15. 索引并不是越多越好,索引固然可 以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

16. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

21.避免频繁创建和删除临时表,以减少系统表资源的消耗。

22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27. 与临时表一样,游标并不是不可使 用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC 消息。

29.尽量避免大事务操作,提高系统并发能力。

30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。`

 

大量的访问量到数据库 如何优化

主从复制 读写分离,负载均衡,分库分表(数据散列在多个块,这样一来,多块硬盘同时处理不同的请求,从而提高磁盘I/O读写性能,实现比较简单,包括水平分区和垂直分区)

 

使用索引的全部意义就是通过缩小一张表中需要查询的记录/行的数目来加快搜索的速度

数据库索引有哪些

  1. 聚集索引(主键索引):在数据库里面,所有行数都会按照主键索引进行排序。
  2. 非聚集索引:就是给普通字段加上索引。
  3. 联合索引:就是好几个字段组成的索引,称为联合索引        联合索引遵从最左前缀原则

索引对数据库系统的负面影响是什么?

负面影响:

创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。

c)、为数据表建立索引的原则有哪些?

在最频繁使用的、用以缩小查询范围的字段上建立索引。

在频繁使用的、需要排序的字段上建立索引
 

实现索引的方式有两种:针对一张表的某些字段创建具体的索引,如对oracle: create index 索引名称 on 表名(字段名);在创建表时为字段建立主键约束或者唯一约束,系统将自动为其建立索引。

索引的原理:根据建立索引的字段建立索引表,存放字段值以及对应记录的物理地址,从而在搜索的时候根据字段值搜索索引表的到物理地址直接访问记录。

索引类型:

普通索引 唯一索引 主键索引 组合索引 全文索引

 

B-Tree索引是一个平衡查找树,叶子到根部的节点距离相等。所有的记录都是按照键值的大小排列,叶子结点由指针连接。

哈希索引,对于比较字符串是否相等的查询能够极快的检索出的值

 

B-Tree索引的特点
1、B-tree索引可以加快数据的查询速度
  存储引擎不需要进行全表扫描来获得需要的数据,取而代之的是从索引的根节点开始进行搜索。然后根据指针逐层向下查找,通过比较节点页的值和有目标值就可以找到合适的指针进入下层节点,而这些指针实际上定义了子节点页中值的上限和下限。
  
2、B-tree索引更适合进行范围查询
  因为前面说过,B-tree对索引是顺序组织存储的,所以就很适合进行查找范围数据
————————————————
版权声明:本文为CSDN博主「z_ryan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/z_ryan/article/details/82322418

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值