Earyant的技术博客

欢迎来到Earyant的技术博客,在这里我将与你分享新技术。

基本类型以及包装类型

java中定义了基本类型:

    * 整形: byte、short、int、long
    * 浮点型:float、double
    * 逻辑型: boolean
    * 字符型: char

以及对应的包装类型:

    * 整形: Byte、Short、Integer、Long
    * 浮点型: Float、Double
    * 逻辑型: Boolean
    * 字符型: Char 

自动装箱、拆箱

java为满足程序员基本类型和包装类型混合使用,按需自动装箱拆箱。
自动装箱拆箱使基本类型和包装类型之间的区别逐渐模糊,但是语义上有些许差别,性能上也有明显差别。

基本类型和包装类型差别

  • 基本类型有默认值,例如int值默认为0 ,而Integer没有默认引用,默认为null。
    所以在写数据库实体类或者网络请求接收字段的时候要使用包装类型,防止接受参数为null的情况。
  • 包装类的频繁更改并不是在原来的基础上更改,而是产生新的对象,所以包装类的频繁更改性能很差.

    使用Long情况

    1
    2
    3
    4
    5
    6
    7
    8
    long now = System.currentTimeMillis();
    Long sum = 0L;
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
    }
    System.out.println(sum);
    long end = System.currentTimeMillis();
    System.out.println("using Long total time = " + (end - now));

    耗费时间为

    1
    2
    2305843005992468481
    all time = 8832

    使用long情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private static void testlongAutoBoxing() {
    long now = System.currentTimeMillis();
    long sum = 0L;
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
    }
    System.out.println("using long ,sum =" + sum);
    long end = System.currentTimeMillis();
    System.out.println("using long total time = " + (end - now));
    }

    总耗费时间为

    1
    2
    2305843005992468481
    all time = 787

    在使用Long的时间比long增加10倍,原因是Long的操作过程中会构造大概2^31个实例。

懒汉

线程不安全

1
2
3
4
5
6
7
8
9
10
 public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

饿汉

1
2
3
4
5
6
7
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

饿汉变种

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

双重校验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

线程状态

  • 新建状态(New) :

    线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。

  • 就绪状态(Runnable):

    也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

  • 运行状态(Running) :

    线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

  • 阻塞状态(Blocked) :

    阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    • 等待阻塞 — 通过调用线程的wait()方法,让线程等待某工作的完成。
    • 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    • 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
      这5种状态涉及到的内容包括Object类, Thread和synchronized关键字。
  • Object类,定义了wait(), notify(), notifyAll()等休眠/唤醒函数。

  • Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数, interrupt()中断函数, getName()获取线程名称等。
  • synchronized,是关键字;它区分为synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。

实现多线程的两种方式:Thread和Runnable

  • Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:
    public interface Runnable {
    public abstract void run();
    }
    Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。
    Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:
    public class Thread implements Runnable {}
    Thread的作用,实现多线程。

    Thread和Runnable的异同点:

  • Thread 和 Runnable 的相同点:

    都是“多线程的实现方式”。

  • Thread 和 Runnable 的不同点:

    Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
    此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
    通常,建议通过“Runnable”实现多线程!

实现方式


  • -[ ] 公平锁
    -[ ] 非公平锁
    • 隐式锁Synchronized
    • 显式锁Lock
      • ReentrantLock
      • ReadWriteLock
      • ReentrantReadWriteLock
      • StampedLock
  • 无锁
    • atomic
    • concurrent
    • blocking
    • threadLocal
    • volatile
    • CAS

  • Synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    public class Counter{
    private int count = 0;
    public int inc(){
    synchronized(this){
    return ++count;
    }
    }
    }
  • Lock
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Counter{
    private Lock lock = new Lock();
    private int count = 0;
    public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
    }
    }
    不可重入自旋锁的简单实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Counter{
    public class Lock{
    private boolean isLocked = false;
    public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
    wait();
    }
    isLocked = true;
    }
    public synchronized void unlock(){
    isLocked = false;
    notify();
    }
    }}

threadLocal

如果想知道为什么使用ThreadLocal,就得先了解局部变量和全局变量对线程安全的影响。
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。
当多个线程引用同一个对象时,因为对象引用是放在栈上的,但是对象实例存储在堆中,所以不是线程安全的。

ThreadLocal就是拷贝一份到线程缓存中,Thread正是操作这个对象,就不会出现安全问题。

  • InheritableThreadLocal

    InheritableThreadLocal类是ThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有值,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值。

Java同步关键字(synchronzied)

所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

使用方法

  • 实例方法同步
    作用在实例上
    1
    2
    3
     public synchronized void add(int value){
    this.count += value;
    }
  • 静态方法同步
    作用在对象上
    1
    2
    3
     public static synchronized void add(int value){
    this.count += value;
    }
  • 实例方法中同步块
    作用在this这个实例上
    1
    2
    3
    4
    5
    public void add(int value){
    synchronized(this){
    this.count += value;
    }
    }
  • 静态方法中同步块
    作用在MyClass这个class上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
    log.writeln(msg1);
    log.writeln(msg2);
    }
    public static void log2(String msg1, String msg2){
    synchronized(MyClass.class){
    log.writeln(msg1);
    log.writeln(msg2);
    }
    }
    }

[参考]{http://ifeve.com/synchronized-blocks/}

[参考2]{http://cmsblogs.com/?hmsr=toutiao.io&p=2071&utm_medium=toutiao.io&utm_source=toutiao.io}

什么是负载均衡

负载均衡(Load Balance)是分布式系统架构设计中必须考虑的因素之一,它通常是指,将请求/数据【均匀】分摊到多个操作单元上执行,负载均衡的关键在于【均匀】。

常见互联网分布式架构如上,分为客户端层、反向代理nginx层、站点层、服务层、数据层。可以看到,每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现“将请求/数据【均匀】分摊到多个操作单元上执行”。

  • “客户端层->反向代理层”的负载均衡
    【客户端层】到【反向代理层】的负载均衡,是通过“DNS轮询”实现的:DNS-server对于一个域名配置了多个解析ip,每次DNS解析请求来访问DNS-server,会轮询返回这些ip,保证每个ip的解析概率是相同的。这些ip就是nginx的外网ip,以做到每台nginx的请求分配也是均衡的。
  • “反向代理层->站点层”的负载均衡
    【反向代理层】到【站点层】的负载均衡,是通过“nginx”实现的。通过修改nginx.conf,可以实现多种负载均衡策略:
    • 请求轮询:和DNS轮询类似,请求依次路由到各个web-server
    • 最少连接路由:哪个web-server的连接少,路由到哪个web-server
    • ip哈希:按照访问用户的ip哈希值来路由web-server,只要用户的ip分布是均匀的,请求理论上也是均匀的,ip哈希均衡方法可以做到,同一个用户的请求固定落到同一台web-server上,此策略适合有状态服务,例如session.
  • “站点层->服务层”的负载均衡
    【站点层】到【服务层】的负载均衡,是通过“服务连接池”实现的。
    上游连接池会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。
  • “数据层”的负载均衡
    在数据量很大的情况下,由于数据层(db,cache)涉及数据的水平切分,所以数据层的负载均衡更为复杂一些,它分为“数据的均衡”,与“请求的均衡”。
    • 数据的均衡是指:水平切分后的每个服务(db,cache),数据量是差不多的。
    • 请求的均衡是指:水平切分后的每个服务(db,cache),请求量是差不多的。
      业内常见的水平切分方式有这么几种:
      • 按照range水平切分
        • 好处
          • 规则简单,service只需判断一下uid范围就能路由到对应的存储服务
          • 数据均衡性较好
          • 比较容易扩展,可以随时加一个uid[2kw,3kw]的数据服务
        • 不足是:
          • 请求的负载不一定均衡,一般来说,新注册的用户会比老用户更活跃,大range的服务请求压力会更大
      • 按照id哈希水平切分
        • 好处:
          • 规则简单,service只需对uid进行hash能路由到对应的存储服务
          • 数据均衡性较好
          • 请求均匀性较好
        • 不足:
          • 不容易扩展,扩展一个数据服务,hash方法改变时候,可能需要进行数据迁移

代理分类

正向代理

>服务器ip地址不变,用户ip地址变。例如用户(1.1.1.1)使用正向代理(2.2.2.2)访问服务器(3.3.3.3),那么服务器收到的消息来自2.2.2.2.服务器不知道用户真正的地址。

反向代理

> 反之,用户不知道服务器的地址。

场景

对用户屏蔽高可用、屏蔽web-server扩展、内网等一些细节。
由于web-server有多台,需要进行负载均衡。

[负载均衡]{}

软件

  • nginx/apache
  • F5
  • lvs

什么是四层(转发/交换),什么是七层(转发/交换)?

这个是来源于OSI七层模型
由图可见,4层指传输层,7层指应用层
更具体的,对应到nginx反向代理hash:

  • 四层:根据用户ip+port来做hash
  • 七层:根据http协议中的某些属性来做hash

为什么中间少了几层?

OSI应用层、表示层、会话层合并到TCP/IP的应用层啦。

上面有四层,七层,那有没有二层,三层呢?

  • 二层:根据数据链路层MAC地址完成数据交换
  • 三层:根据网络层IP地址完成数据交换

http://zhuanlan.51cto.com/art/201709/550451.htm

JVM内存模型

打印出来的信息:

PSYoungGen      total 3072K, used 128K 
    eden space 2560K, 5% used  
    survivor  space 
        from space 512K, 0% used  
         to   space 512K, 0% used  

ParOldGen       total 6656K, used 408K 
    object space 6656K, 6% used   

PSPermGen       total 4096K, used 3039K    
    object space 4096K, 74% used  

GC过程

  • 年轻代:
    存放对象的特点: 生命周期短,多为临时变量。

    新实例化的对象都会在eden中new出来,经过手动System.gc()(本质上也是调用Runtime的gc)、Runtime.getRuntime().gc()(本地方法)或者一段时间后系统自动触发gc,年轻代使用 复制清除算法 ,从Root对象开始标记为存活,存活的对象引用的对象也会标记为存活,扫描整个eden区后,将存活的对象存放到其中一个survivor区,清除eden区和另外一个eden区。下次再产生新的对象仍然是放到eden区内,再gc时将eden区和有对象存储的survivor存活的对象复制到另外一个survivor区内,清除这两个区,反复如此,清除一定次数后(之前看过介绍15次,不太确定)将仍然存活的放到年老代。

  • 年老代
    存放对象特点: 生命周期长。

    年老代使用标记整理算法,还是从Root对象开始搜索,标记所有存活的对象,把所有存活的对象复制到一个区域内,再清除其他区域。

示例

栈内存溢出(java.lang.StackOverflowError)

 > -Xmx10m
   -XX:MaxPermSize=5m
   -XX:MaxDirectMemorySize=5m
   -XX:+PrintGCDetails

永久代溢出(java.lang.OutOfMemoryError: GC overhead limit exceeded)