zoukankan      html  css  js  c++  java
  • JAVA基础

     
    基本语法
    多态
    解藕,包括重载和重写;
    重载:编译时多态,从JVM的角度来讲,这是一种静态分派;(发生在编译时,可以根据参数选择)
    重写:运行时多态,从JVM的角度来讲,这是一种动态分派。(发生在运行时,编译时不知道该调用)
    static/final/private,子类不能重写父类
     
    final
    被final修饰的类不可以被继承
    被final修饰的方法不可以被重写
    被final修饰的变量不可以被改变(什么不可以被改变呢,是变量的引用?还是变量里面的内容?还是两者都不可以被改变)
    引用不可变,引用指向的内容可变
     
    static
    被static修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要new出一个类来
    被static修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要new出一个类来
    静态代码块是严格按照父类静态代码块->子类静态代码块的顺序加载的,且只加载一次。
    static一般情况下来说是不可以修饰类的,如果static要修饰一个类,说明这个类是一个静态内部类
     
    序列化
    / 反序列化 / transient
    序列化之后保存的是对象的信息
    Java为用户定义了默认的序列化、反序列化方法,其实就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法
    进行序列化、反序列化时,虚拟机会首先试图调用对象里的writeObject和readObject方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。换言之,利用自定义的writeObject方法和readObject方法,用户可以自己控制序列化和反序列化的过程。
     
    什么时候不会被序列化?
    被声明为transient的属性不会被序列化,这就是transient关键字的作用
    被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于对象的,因此序列化的时候不会序列化它
     
    实现方式
    1.原生实现序列化接口Serializable,
    即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化。
    需要注意的是JavaBean实体类必须实现Serializable接口,否则无法序列化。
     
    2.Json序列化
    Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。
    ObjectMapper mapper = new ObjectMapper(); mapper.writeValueAsBytes mapper.readValue(writeValueAsBytes, User.class)
     
    3.FastJson序列化
    4、ProtoBuff序列化
     
    用java原生序列化方式的缺点:
    多语言环境下,使用Java序列化方式进行存储后,很难用其他语言还原出结果
    占用的字节数比较大,而且序列化、反序列化效率也不高
     
     
    String
    StringBuilder、StringBuffer
    String 的 “+” 底层是通过StringBuilder,然后调用append,最后toString
    StringBuilder底层是一个char数组,在toString()的时候再通过new String(),缺点是当空间不足的时候需要扩容,而且不是线程安全的
     
     
    集合
    List
    ArrayList 允许空,允许重复,有序,非线程安全(Collections.synchronizedList)
    LinkedList 允许空,允许重复,有序,非线程安全
    Vector 线程安全
    HashSet底层是HashMap
    java.util.Collections.SynchronizedList
    java.util.concurrent.CopyOnWriteArrayList
    java.util.concurrent.CopyOnWriteArraySet
     
    List 是可重复集合,Set 是不可重复集合,这两个接口都实现了 Collection 父接口。
    List 的实现类有 ArrayList,Vector 和 LinkedList:
    ArrayList 和 Vector 内部是线性动态数组结构,在查询效率上会高很多,Vector 是线程安全的,相比 ArrayList 线程不安全的,性能会稍慢一些。
    LinkedList:是双向链表的数据结构存储数据,在做查询时会按照序号索引数据进行前向或后向遍历,查询效率偏低,但插入数据时只需要记录本项的前后项即可,所以插入速度较快。
     
    Set 的实现类有 HashSet 和 TreeSet;
    HashSet:内部是由哈希表(实际上是一个 HashMap 实例)支持的。它不保证 set 元素的迭代顺序。
    TreeSet:TreeSet 使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序。
     
    List的特点:元素有放入顺序,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉(注意:元素虽然没有放入顺序,但是元素在set中的位置是有该元素的hashcode决定的,其位置其实是固定的,加入set的Object必须定义equals()方法,另外list支持for循环,也就是通过下标来进行遍历,也可以用迭代器,但是set只能迭代,因为它是无序的,无法用下标来取得想要的值)
     
    CopyOnWrite(简称:COW):即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。
    public boolean add(E e) {     // 加锁     final ReentrantLock lock = this.lock;     lock.lock();     try {         // 获取原始集合         Object[] elements = getArray();         int len = elements.length;         // 复制一个新集合         Object[] newElements = Arrays.copyOf(elements, len + 1);         newElements[len] = e;         // 替换原始集合为新集合         setArray(newElements);         return true;     } finally {         // 释放锁         lock.unlock();     } }
    添加元素时,先加锁,再进行复制替换操作,最后再释放锁。
    获取元素并没有加锁。
    这样做的好处是,在高并发情况下,读取元素时就不用加锁,写数据时才加锁,大大提升了读取性能。
    只适合于读多写少的情况,如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。
     
    HashSet类是如何实现添加元素保证不重复的---哈希码的原理
    public boolean add(E e) { return map.put(e, PRESENT)==null; } private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map用来匹配Map中后面的对象的一个虚拟值 private static final Object PRESENT = new Object();
     
    public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
    可以看到for循环中,遍历table中的元素,
    1,如果hash码值不相同,说明是一个新元素,存;
    如果没有元素和传入对象(也就是add的元素)的hash值相等,那么就认为这个元素在table中不存在,将其添加进table;
    2(1),如果hash码值相同,且equles判断相等,说明元素已经存在,不存;
    2(2),如果hash码值相同,且equles判断不相等,说明元素不存在,存;
    如果有元素和传入对象的hash值相等,那么,继续进行equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
     
    Map
    hasCode
    hash是散列的意思,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值
     
    hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等;
    (1)equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的;
    (2)hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
    对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!
    然而hashCode()和equal()一样都是基本类Object里的方法,而和equal()一样,Object里hashCode()里面只是返回当前对象的地址,如果是这样的话,那么我们相同的一个类,new两个对象,由于他们在内存里的地址不同,则他们的hashCode()不同,所以这显然不是我们想要的,所以我们必须重写我们类的hashCode()方法.
     
    泛型
    参数化类型
    类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性
    消除了代码中许多的强制类型转换,增强了代码的可读性
    为较大的优化带来了可能
     
    内部类
    一旦编译成功,就会生成两个完全不同的.class文件了
    内部类分为四种:成员内部类、局部内部类、匿名内部类、静态内部类
    成员内部类:
    Outer outer = new Outer(0);
    Outer.PublicInner publicInner = outer.new PublicInner();
    成员内部类是依附其外部类而存在的
    局部内部类:
    public static void main(String[] args) { final int i = 0; class A { public void print() { System.out.println("AAA, i = " + i); } } A a = new A(); }
    局部内部类是定义在一个方法或者特定作用域里面的类
    局部内部类没有访问修饰符,另外局部内部类要访问外部的变量或者对象,该变量或对象的引用必须是用final修饰的
    匿名内部类:
    在多线程模块中的代码示例中大量使用,匿名内部类是唯一没有构造器的类
    静态内部类:
    Outer.staticInner os = new Outer.staticInner();
     
     
    使用内部类的好处:
    1、Java允许实现多个接口,但不允许继承多个类,使用成员内部类可以解决Java不允许继承多个类的问题。在一个类的内部写一个成员内部类,可以让这个成员内部类继承某个原有的类,这个成员内部类又可以直接访问其外部类中的所有属性与方法,是不是相当于多继承了呢?
    2、成员内部类可以直接访问其外部类的private属性,而新起一个外部类则必须通过setter/getter访问类的private属性
    3、有些类明明知道程序中除了某个固定地方都不会再有别的地方用这个类了,为这个只用一次的类定义一个外部类显然没必要,所以可以定义一个局部内部类或者成员内部类,写一段代码用用就好了
    4、内部类某种程度上来说有效地对外隐藏了自己,比如我们常用的开发工具Eclipse、MyEclipse,看代码一般用的都是Packge这个导航器,Package下只有.java文件,我们是看不到定义的内部类的.java文件的
    5、使用内部类可以让类与类之间的逻辑上的联系更加紧密
     
    @RequestParam的要求
    - 均支持POST,GET请求
    - 只支持Content-Type: 为 application/x-www-form-urlencoded编码的内容。Http协议中,如果不指定Content-Type,则默认传递的参数就是application/x-www-form-urlencoded类型)
     
    @RequestBody
    不支持get请求,因为get请求没有HttpEntity
    - 必须要在请求头中申明content-Type(如application/json).springMvc通过HandlerAdapter配置的HttpMessageConverters解析httpEntity的数据,并绑定到相应的bean上
    - 只能一个@RequestBody。且不能与@RequestParam一起使用
     
     
     
    import net.sf.json.JSONArray;
    import net.sf.json.JSONObject;
    JSONArray jsonArray = JSONArray.fromObject(tagList);
     
    fastjson
    JSON.toJSONString() ;
    JSONObject.toJSON(dto);
    JSONObject.parseObject(data);
    JSONArray.parseArray(data, SingleShipmentReq.class);
     
    过滤器与拦截器
    过滤器:Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序
    其工作原理是,只要你在web.xml文件配置好要拦截的客户端请求,它都会帮你拦截到请求,此时你就可以对请求或响应(Request、Response)统一设置编码,简化操作;同时还可进行逻辑判断,如用户是否已经登陆、有没有权限访问该页面等等工作。它是随你的web应用启动而启动的,只初始化一次,以后就可以拦截相关请求,只有当你的web应用停止或重新部署的时候才销毁。
     
    创建一个Filter只需两个步骤:
    1.创建Filter处理类
    2.web.xml文件中配置Filter
     
    拦截器是在面向切面编程中应用的,就是在你的service或者一个方法前调用一个方法,或者在方法后调用一个方法。是基于JAVA的反射机制。
    Filter
    Interceptor
    Summary
    Filter 接口定义在 javax.servlet 包中
    接口 HandlerInterceptor 定义在org.springframework.web.servlet 包中
     
    Filter 定义在 web.xml 中
     
     
    Filter在只在 Servlet 前后起作用。Filters 通常将 请求和响应(request/response) 当做黑盒子,Filter 通常不考虑servlet 的实现。
    拦截器能够深入到方法前后、异常抛出前后等,因此拦截器的使用具有更大的弹性。允许用户介入(hook into)请求的生命周期,在请求过程中获取信息,Interceptor 通常和请求更加耦合。
    在Spring构架的程序中,要优先使用拦截器。几乎所有 Filter 能够做的事情, interceptor 都能够轻松的实现
    Filter 是 Servlet 规范规定的。
    而拦截器既可以用于Web程序,也可以用于Application、Swing程序中。
    使用范围不同
    Filter 是在 Servlet 规范中定义的,是 Servlet 容器支持的。
    而拦截器是在 Spring容器内的,是Spring框架支持的。
    规范不同
    Filter 不能够使用 Spring 容器资源
    拦截器是一个Spring的组件,归Spring管理,配置在Spring文件中,因此能使用Spring里的任何资源、对象,例如 Service对象、数据源、事务管理等,通过IoC注入到拦截器即可
    Spring 中使用 interceptor 更容易
    Filter 是被 Server(like Tomcat) 调用
    Interceptor 是被 Spring 调用
    因此 Filter 总是优先于 Interceptor 执行
     
    注解
    @Transactional和@Async传播有效性
     
     
    自定义注解
    注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
    注解用来修饰,类、方法、变量、参数、包。
    注解不会对所修饰的代码产生直接的影响。
     
     
     
     
    设计模式
    创建型(5):工厂、抽象工厂、建造者、原型、单例
    结构型(7):适配器、装饰、代理、桥接、外观、享元、组合
    行为型(11):迭代器、策略、模板、观察者、责任链、命令、备忘录、状态、访问者、中介者、解释器
     
     
    简单工厂模式:线程池、ThreadFactory
    单例模式:饿汉、懒汉、懒汉双检锁、应用(Runtime)
    策略模式:jdk代理还是cgllib
    装饰器模式:输入流InputStream
     
     
    装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,而适配器模式对象适配并不需要如此。
     
    适配器模式和代理模式的区别
    很明显,适配器模式是因为新旧接口不一致导致出现了客户端无法得到满足的问题,但是,由于旧的接口是不能被完全重构掉的,因为我们还想使用实现了这个接口的一些服务。那么为了使用以前实现旧接口的服务,我们就应该把新的接口转换成旧接口;实现这个转换的类就是抽象意义的转换器。
            就比如在java中早期的枚举接口是Enumeration而后定义的枚举接口是Iterator;有很多旧的类实现了enumeration接口暴露出了一些服务,但是这些服务我们现在想通过传入Iterator接口而不是Enumeration接口来调用,这时就需要一个适配器,那么client就能用这个服务了(服务端只想用Iterator或者只知道这个接口)。
            相比于适配器的应用场景,代理就不一样了,虽然代理也同样是增加了一层,但是,代理提供的接口和原本的接口是一样的,代理模式的作用是不把实现直接暴露给client,而是通过代理这个层,代理能够做一些处理。
     
    外观模式就是门面模式
    桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。
    桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样
     
    实现方式是通过一个抽象桥接类
    public abstract class Bridge { private Sourceable source; public void method(){ source.method(); } public Sourceable getSource() { return source; } public void setSource(Sourceable source) { this.source = source; } } public class MyBridge extends Bridge { public void method(){ getSource().method(); } }
     
    组合模式:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树。
    享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
    命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。
     
    单例模式
    饿汉模式是安全的,懒汉模式
    懒汉双检锁:
    getInstance(){
    static volatile A instance = null; // volatile是为了指令重排序
    if( instance != null ){
    synchronized(A.class){
    if(instance != null){
    instance = new A(); // 非原子操作 复杂操作
    }
    }
    }
    }
     
     
    问题:为什么需要两次判断if(singleTon==null)?
      分析:第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
      第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
      需要注意的是,private static volatile SingleTon3 singleTon=null;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
     
     
    volatile作用:以下会涉及到Java内存模型的知识
    禁止指令重排序。我们知道new Singleton()是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的引用)在初始化对象前完成】。而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。
     
    保证可见性。线程A在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程B在主存中判断instance还是null,那么线程B又将在自己的工作线程中创建一个实例,这样就创建了多个实例。
     
    单例模式的好处:
    控制资源的使用,通过线程同步来控制资源的并发访问
    控制实例的产生,以达到节约资源的目的
    控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信
    之所以用单例,是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存;
    之所以用多例,是为了防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;
    用单例和多例的标准只有一个:
     当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),则多例,否则单例;
     
     
    今天用旅游吃饭来区分下门面模式和代理模式
    门面模式是给用户提供一种服务,就相当于我们的饭店,可以给顾客提供美味的食物
    代理模式是根据用户的需求,提供解决该需求的方案,相当于我们的导游,给游客提供自己想知道的信息
     
    情景:某某某天去某地旅游,但是他又不知道当地的风土人情、名胜古迹以及饭店
    操作:他就找了一个导游,导游可以给某某介绍这某地的旅游地点以及饭店,然后某某也玩的很开心。
    进化:导游扩充了自己的资料库,可以提供游客更喜欢去的旅游景点以及更符合胃口的饭店
     
    确认是一个合格的吃货
    以上是为了区分门面模式和代理模式的区别,本人感觉代理模式只是一个特殊的门面模式,因为提供解决方案也是一种服务。
     
     
     
     
     
    Jdk
    1.7 1.8
    1.  接口的默认方
    Java1.8以前,接口里的方法要求全部是抽象方法,java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可。
    2.  lambda表达式
    它将允许我们将行为传到函数里。在Java 8之前,如果想将行为传入函数,仅有的选择就是匿名类,需要6行代码。而定义行为最重要的那行代码,却混在中间不够突出。Lambda表达式取代了匿名类,取消了模板,允许用函数式风格编写代码。这样有时可读性更好,表达更清晰。
    3.  函数式接口
    如果一个接口定义个唯一一个抽象方法,那么这个接口就成为函数式接口。一个函数式接口非常有价值的属性就是他们能够用lambdas来实例化。
    4.  方法与构造函数引用
    使用关键字来传递方法或者构造函数引用。
    5.  Lambda作用域
    在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
    6.  访问局部变量
    可以直接在lambda表达式中访问外层的局部变量。
    7.  访问对象字段与静态变量
    和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的。
    8.  访问接口的默认方法
    JDK1.8 API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了注解以便能用在lambda上。
    Java 8API同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自Google Guava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。
     
    jdk1.8新特性知识点:
    • Lambda表达式
    • 函数式接口
    • *方法引用和构造器调用
    • Stream API
    • 接口中的默认方法和静态方法
    • 新时间日期API
    Lmabda表达式的语法总结: () -> ();
    前置 语法
    无参数无返回值 () -> System.out.println(“Hello WOrld”)
    有一个参数无返回值 (x) -> System.out.println(x)
    有且只有一个参数无返回值 x -> System.out.println(x)
    有多个参数,有返回值,有多条lambda体语句 (x,y) -> {System.out.println(“xxx”);return xxxx;};
    有多个参数,有返回值,只有一条lambda体语句 (x,y) -> xxxx
     
     
     
    多线程
    Thread类也是实现的Runnable接口
     
    >>> interrupt()方法的作用:在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞状态。
    换句话说,没有被阻塞的线程,调用interrupt()方法是不起作用的。
    原理:通过无限轮询自己的中断标识位,中断了则打印、退出,否则一直运行
    总结:中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
     
    >>> join()方法会使调用join()方法的线程所在的线程无限阻塞,直到调用join()方法的线程销毁为止
    join()方法内部使用的是wait(),因此会释放锁
     
    CAS
    无锁执行者CAS的核心算法原理然后分析Java执行CAS的实践者Unsafe类,该类中的方法都是native修饰的,因此我们会以说明方法作用为主介绍Unsafe类
    执行函数:CAS(V,E,N)
    其包含3个参数
    • V表示要更新的变量
    • E表示预期值
    • N表示新值
    如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
      由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。
     
    CPU指令对CAS的支持
          或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
     
     
    volatile
    加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。
    可见性,禁止重排序,但是并不保证原子性
    A、原子性 :对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
    B、可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    Java代码:
    instance = new Singleton();//instance是volatile变量
    汇编代码:
    0x01a3de1d: movb $0x0,0x1104800(%esi);
    0x01a3de24: lock  addl $0x0,(%esp);
    有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
    • 将当前处理器缓存行的数据会写回到系统内存。
    lock指令前缀在执行指令的期间,会产生一个lock信号,lock信号会保证在该信号期间会独占任何共享内存。lock信号一般不锁总线,而是锁缓存。因为锁总线的开销会很大。
    • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
     
    处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
     
    ThreadLocal
    ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
     
    方法:set(T value)、get()、remove()
    1、每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象
    实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
    2、每一个ThreadLocal对象都有一个循环计数器
    3、ThreadLocal.get()取值,就是根据当前的线程,获取线程中自己的ThreadLocal.ThreadLocalMap,然后在这个Map中根据第二点中循环计数器取得一个特定value值
     
    public T get(){ Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if(map != null){ ThreadLocalMap.Entry e = map.getEntry(this); if(e != null){ return (T)e.value; } return setInitialValue; } } ThreadLocalMap getMap(Thread t){ return t.threadLocals; //t.threadLocals实际上就是访问Thread类中的ThreadLocalMap这个成员变量 }
     
     
    >>> 应用场景:
    主要用途是为了保持线程自身对象和避免参数传递,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
    比如数据库连接:
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
    }
    };
     
    public static Connection getConnection() {
    return connectionHolder.get();
    }
     
    >>> 存在问题:
    1.内存泄露
    弱引用导致内存泄漏,那为什么key不设置为强引用
    这个问题就比较有深度了,是你谈薪的小小资本。
    如果key设置为强引用, 当threadLocal实例释放后, threadLocal=null, 但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal, 这样会导致threadLocal不能正常被GC回收。
    弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施, 方案上略胜一筹。
    线程执行结束后会不会自动清空Entry的value
     
    2.降低代码的可重用性,代码的耦合度高,且测试不易
     
    地址:https://www.jianshu.com/p/98b68c97df9b
     
    ThreadLocal底层的数据模型是ThreadLocalMap,该Map是一个基于开放地址法实现的哈希表,key是当前的threadlocal对象,value是当前线程放在这个threadlocal里面的值;
    每个线程持有一个ThreadLocalMap对象A,A里面放的是和当前线程相关的threadlocal;
    如果认真看过源码,还会知道ThreadLocalMap的实现和WeakHashMap实现有异曲同工之妙,都通过WeakReference封装了key值,防止可怕的内存泄漏;
    再举个get()数据的例子。第一步得到当前线程;第二步获取当前线程对应的ThreadLocalMap;第三步以当前threadlocal对象为key,去ThreadLocalMap中把值捞出来;
     
     
     
    线程池
    ExecutorService extends Executor,ExecutorService底层是通过ThreadPoolExecutor实现;
    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
     
    corePoolSize:核心池的大小。在创建了线程池之后,默认情况下,线程池中没有任何线程,而是等待有任务到来才创建线程去执行任务。默认情况下,在创建了线程池之后,线程池钟的线程数为0,当有任务到来后就会创建一个线程去执行任务
    maximumPoolSize:池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量,当任务数量比corePoolSize大时,任务添加到workQueue,当workQueue满了,将继续创建线程以处理任务,maximumPoolSize表示的就是wordQueue满了,线程池中最多可以创建的线程数量
    workQueue:存储还没来得及执行的任务
    threadFactory:执行程序创建新线程时使用的工厂
    handler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
     
    >>> 四种线程池
    newSingleThreadExecutos() 单线程线程池,线程池中运行的线程数肯定是1
    newFixedThreadPool(int nThreads) 固定大小线程池
    newCachedThreadPool() 无界线程池,采用了SynchronousQueue
    newScheduleThreadPool()
     
    >>> 拒绝策略
    AbortPolicy直接抛出一个RejectedExecutionException,这也是JDK默认的拒绝策略
    CallerRunsPolicy尝试直接运行被拒绝的任务,如果线程池已经被关闭了,任务就被丢弃了
    DiscardOldestPolicy移除最晚的那个没有被处理的任务,然后执行被拒绝的任务。同样,如果线程池已经被关闭了,任务就被丢弃了
    DiscardPolicy不能执行的任务将被删除
     
     
    >>> qps
    并发量
    io线程数
    计算线程数:一来服务器CPU核数有限,同时并发的线程数是有限的
     
     
    IO 字节流 / 字符流
    一个字节有8bit,一个字符有2个字节
    File file = new File(location);
    FileOutputStream out = new FileOutputStream(file);
    out.write(byte [] );
     
    FileInputStream in = new FileInputStream(file);
    in.read(new byte [ file.length ] );
     
     
    用户空间(常规进程,该部分执行的代码不能直接访问硬件设备)和内核空间(操作系统运行时所使用的程序调度/虚拟内存/连接硬件资源)
    所有的内核都直接或者间接的通过内核空间,保证操作系统的稳定性和安全性。
    每一次系统调用都会存在两个内存空间之间的相互切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户控件的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是牺牲了一部分的效率。
     
    out.write(byte [] );
     
    Exchanger
    Exchanger 是 JDK 1.5 开始提供的一个用于两个工作线程之间交换数据协作的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。其定义为 Exchanger<V> 泛型类型,其中 V 表示可交换的数据类型。
     
     
     
    零拷贝
    在下面这些组件、框架中有听说过零拷贝 (Zero-Copy)?
    Kafka
    Netty
    rocketmq
    nginx
    apache
     
    零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
    ➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
    ➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
     
     
    JUC并发包
    BlockingQueue线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
    ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue
     
    抛出异常 特殊值 阻塞 超时
    插入 add(e) offer(e) put(e) offer(e, time, unit)
    移除 remove() poll() take() poll(time, unit)
    1)add:把Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则招聘异常
    2)offer:表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
    3)put:把Object加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
    4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
    5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
     
     
    CAS的缺点:
    1⃣️可以解决原子性问题,但是如果CAS失败,则会一直循环尝试,循环时间长开销很大;
    2⃣️只能保证一个共享变量的原子操作;
    3⃣️ABA问题,中途被其他线程改变过,但是又改回来了,使用要考虑是否有风险.
     
     
    ReentrantReadWriteLock可重入,锁降级(允许写锁降级为读锁),中断锁,支持Condition
    AQS的state表示写锁和读锁的个数,state的高16位表示读锁的个数,低16位表示写锁的个数
    线程池、阻塞队列、CountDownLatch、Semaphore、Exchanger CyclicBarrier、Callable、Future、FutureTask
    通过Semaphore控制并发并发数的方式和通过控制线程数来控制并发数的方式相比,粒度更小
     
     
    背景:对于Java开发者来说就可以很方便的使用这些锁及常用类。但是,随着锁的频繁使用及错用,随之而来的就是程序执行效率变低、应用变的缓慢。为了提高线程对共享数据访问的效率,HotSpot虚拟机从JDK1.5到JDK1.6做了重大改进,提供了很多锁优化技术,包括自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
     
    自旋锁:当线程挂起或恢复执行的时,会从用户态转入内核态中完成,这种操作是很消耗时间的。自旋锁在JDK1.6中是默认开启的,默认自旋次数是10次,可以使用参数-XX:PreBlockSpin修改默认值。虽然,自旋锁避免了线程挂起和恢复的开销,但是它占用了处理器的执行时间,如果锁占用时间很短,自旋锁效果很好,否则会浪费处理器的执行时间,影响应用的整体性能。
     
    自适应自旋锁:在JDK1.6中引入了自适应自旋锁,自旋的次数由上一次在同一个锁上自旋的时间和锁持有者的状态来决定。如果上一次同一个锁通过自旋刚刚被获取,并且持有锁的线程正在运行,那么虚拟机认为本次自旋也会成功,将会自旋相对长的时间获取锁。如果同一个锁很少通过自旋成功被获取,那么虚拟机认为本次自旋也会失败,不会执行自旋操作。
     
    锁消除
    一些使用了锁控制的代码,在虚拟机即时编译器运行时检测到不存在对共享数据的竞争访问,也就是代码只会被一个线程访问,此时会对锁进行消除,这项优化称为锁消除。锁消除的主要判断依据来源于逃逸分析(即分析对象的动态作用域,一个对象在方法内被定义后,在别的方法或线程中无法通过任何途径访问到这个对象,则可以进行一些优化操作)的数据支持。
     
    锁粗化
    大多数情况下,为了提高程序的执行效率,会缩小锁作用的范围。但是,对于一些连续操作都对同一个对象进行反复加锁、释放锁的情况来说,缩小锁的作用范围会消耗更多的资源,这种情况需要扩大锁的作用范围,这项优化称为锁粗化。
     
    轻量级锁
    在HotSpot虚拟机中,Java对象在内存中存储的布局分为3块区域:对象头、实例数据和对齐填充。对象头包含两部分,第一部分包含对象的HashCode、分代年龄、锁标志位、线程持有的锁、偏向线程ID等数据,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称为Mark World。
    当代码执行到同步代码时,如果此时对象的锁未被锁定(锁标志位位01),虚拟机将在当前线程的栈帧中创建一个名为Lock Record空间,这个空间用于存储当前对象的Mark World拷贝,具体如下图所示。
    接着,虚拟机使用CAS尝试将对象的对象头Mark Wolrd指向Lock Record,也就是在Mark Wolrd的30bit存储Lock Record的起始地址,具体如下图所示。如果上述操作执行成功,当前线程就持有了对象的锁,此时对象处于轻量级锁锁定状态,对应的锁标志位为00。
    如果上述操作执行失败,首先会检查对象的对象头Mark World是否指向了当前线程栈帧中的Lock Record,如果指向了则表示当前线程已经持有了对象的锁,否则表示对象的锁已经被其它线程持有,锁膨胀为重量级锁,线程挂起等待。
    轻量级锁的释放过程,通过CAS将Lock Record中存储的Mark Wolrd拷贝替换回对象的对象头Mark Wolrd中,替换成功则锁释放成功,否则表示有其它线程尝试获取过锁,释放锁的同时,唤醒挂起的线程,这里笔者的理解是此时锁膨胀为重量级锁,唤醒等待线程竞争。
     
    偏向锁
    锁对象第一次被线程持有的时候,虚拟机通过CAS把获取到这个锁的线程ID记录到对象头Mark World中,操作成功则成功获取偏向锁,对象头中的锁标志位设置为01。持有偏向锁的线程每次执行到这段同步代码时,不需要任何同步操作,这项优化称为偏向锁。
    当有其它线程尝试获取对象的锁时,终止偏向模式,同时根据锁是否处于锁定状态,撤销偏向锁恢复到未锁定或轻量级锁状态。
     
    偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
    • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
    • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
    • 重量级锁:有实际竞争,且锁竞争时间长。
     
    synchronized
    synchronized可用于修饰普通方法、静态方法和代码块,都可以实现对同步代码的并发安全控制。
    synchronized修饰普通方法时,同步代码执行前,需要获取当前实例对象的锁(对象锁)。
    synchronized修饰静态方法时,同步代码执行前,需要获取当前类的锁(类锁)。
    第一种修饰代码块,对实例对象加锁,同步代码执行前,需要获取当前实例对象的锁(对象锁)。 synchronized (this)
    第二种修饰代码块,对Class加锁,同步代码执行前,需要获取当前类的锁(类锁)。
     
    1⃣️对象锁
    2⃣️锁重入
    3⃣️异常自动释放锁
     
    使用javap -c -v命令对class文件进行反编译
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    当执行到synchronized修饰的语句块时,通过monitorenter指令获取锁,成功获取锁之后,执行同步代码,同步代码正常执行完成后,通过monitorexit指令释放持有的锁。JVM为了保证同步代码执行非正常结束时也释放持有的锁,所以,在发生异常时,再次通过monitorexit指令释放持有的锁。
     
    ReentrantLock
    ReentrantLock持有的是对象监视器,但是和synchronized持有的对象监视器不是一个意思,虽然我也不清楚两个持有的对象监视器有什么区别,不过把methodB()方法用synchronized修饰,methodA()不变,两个方法还是异步运行的。
     
    相同点
    • 都实现了多线程同步和内存可见性语义
    • 都是可重入锁
    不同点
    • 实现机制不同 synchronized 通过 java 对象头锁标记和 Monitor 对象实现 reentrantlock 通过 CAS、ASQ(AbstractQueuedSynchronizer)和 locksupport(用于阻塞和解除阻塞)实现 synchronized 依赖 jvm 内存模型保证包含共享变量的多线程内存可见性 reentrantlock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性
    • 使用方式不同 synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象) reentrantlock 显示调用 trylock()/lock() 方法,需要在 finally 块中释放锁
    • 功能丰富程度不同 reentrantlock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、signal 等方法)等丰富语义 reentrantlock 提供公平锁和非公平锁实现 synchronized 不可设置等待时间、不可被中断(interrupted)
     
     
     
    Maven
    命令行创建工程
    mvn archetype:create -DgroupId=com.leqee -DartifactId=report -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=Internal
    mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.leqee
    -DartifactId=report -DpackageName=com.leqee -Dversion=1.0-SNAPSHOT -DinteractiveMode=false
     
    mvn clean package依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7个阶段。
    mvn clean install依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8个阶段。
    mvn clean deploy依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9个阶段。
     
    由上面的分析可知主要区别如下,
    package命令完成了项目编译、单元测试、打包功能,但没有把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
    install命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库,但没有布署到远程maven私服仓库
    deploy命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
     
     
    Log
    总结这篇文章,我具有充分的理由的来选择SLF4J而不是直接选用Log4j, commons logging, logback 或者 java.util.logging。
    1)在你的开源库或者私有库中使用SLF4J,可以使它独立于任何的日志实现,这就意味着不需要管理多个库和多个日志文件。你的客户端将会体会到这一点。
    2)SLF4J提供了占位日志记录,通过移除对isDebugEnabled(), isInfoEnabled()等等的检查提高了代码的可读性。
    3)通过使用日志记录方法,直到你使用到的时候,才会去构造日志信息(字符串),这就同时提高了内存和CPU的使用率。
    4)做一个侧面的说明,越少的临时字符串,垃圾回收器就意味着越少的工作,这就意味着为你的应用程序提供更好的吞吐量和性能。
    这些优势都只是冰山一角,当你开始使用SL4J并阅读它,你会学到更多的好处。我强烈建议,在java中任何新的代码开发,都应使用SLF4J而不是任何的日志API,包括log4J。
     
     
    Linux
    ps对进程进行监测和控制,显示进程的当前状态
    ps -ef
    ps -mp
    cat -n X.log | grep ''
    more +n X.log
     
    查看线程数
    ps -ef | grep tomcat
    watch ps -o nlwp 27004
    368
     
    tcp支持最大连接数
    ulimit -n 查看
     
    losf -i:8083
    netstat -an | grep 8083
    使用ps命令,具体用法是 ps -mp PID
    这样可以看到指定的进程产生的线程数目
     
     
     
     

  • 相关阅读:
    使用正则表达式,取得点击次数,函数抽离
    爬取校园新闻首页的新闻
    网络爬虫基础练习
    Hadoop大作业
    Hive基本操作与应用
    熟悉HBase基本操作
    爬虫大作业
    熟悉常用的HDFS操作
    数据结构化与保存
    使用正则表达式,取得点击次数,函数抽离
  • 原文地址:https://www.cnblogs.com/novalist/p/11621336.html
Copyright © 2011-2022 走看看