BeanPropertyRowMapper

使用BeanPropertyRowMapper将数据库查询结果转换为Java类对象。

一、应用

常应用于使用Spring的JdbcTemplate查询数据库,获取List结果列表,数据库表字段和实体类自动对应。

示例:

@Override
public List<Demo> findAll() {
    String sql = "SELECT * FROM user";
    /**
     * BeanPropertyRowMapper将查询结果转换为类对象
     */
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper(Demo.class));
}
@Override
public List<Demo> selectUser(int uid) {
    String sql = "SELECT * FROM user WHERE id = ?";
    /**
     * 带条件查询
     */
    return jdbcTemplate.query(sql, new Object[]{uid}, new BeanPropertyRowMapper(Demo.class));
}

二、内部实现浅析

BeanPropertyRowMapper的initialize内部实现

如上图红框中所示:

image.png

mapperFields是一个HashMap,用来匹配Java对象的属性和MySQL表的字段名的。mapperFields中存放的是所有可能与MySQL表的字段名映射上的那些Java属性名字。

this.mappedFields.put(this.lowCaseName(pd.getName()),pd);

在initialize方法中,BeanPropertyRowMapper会把传入的泛型Java类的所有属性名称的全小写形式放入mapperFields中,

String underscoredName = this.underscoreName(pd.getName());

将Java类的属性名转化成下划线分割的形式,如userName会被转化成user_name,这是因为:

数据库设计字段名称,一般会使用下划线分割形式,如:user_name

而Java类设置属性,一般使用驼峰命名形式,如:userName。

使用BeanPropertyRowMapper自动绑定,需要确保数据库表列名称与Java实体类属性名称相同

Java集合-Map

什么是Map

1.不同于List单列的线性结构,Map提供的是一种双列映射的存储集合,它能够提供一对一的数据处理能力,双列中的第一列我们称为key,第二列就是value,一个key只能够在一个Map中出现最多一次,通过一个key能够获取Map中唯一一个与之对应的value值,正是它的这种一对一映射的数据处理关系,在实际应用中可以通过一个key快速定位到对应的value。综合上面的概念,可以概括出以下几个核心点:

2.Map存储是以k-v键值对的方式进行存储的,是双列的

Map中的key具有唯一性,不可重复

每个key对应的value值是唯一的

3.Java中Map是一个接口,它不继承任何其他的接口,可以说它是java中所有Map的顶级父接口。它的设计理念完全遵循上面的规则,只是具体的实现类种类很多,对应不同应用场景的使用,所以可能具体细节以及设计上存在差异。

Java的Map中提供了三种Map视图以便于展示Map中的内容:

1.只包含key的Set集合

2.只包含value的Collection

3.同时包含key-value映射的EntrySet

另外需要额外注意:不能使用可变的对象作为Map的key,因为一旦该对象出现变化它会导致Map的行为无法预期(这里的变化指的是影响equals方法比较结果的变化);同时不能将Map本身作为一个Map的key,但是允许将Map本身作为value存入Map结构中。

使用Map的时机

1.存储双列结果

有很多情况下我们都需要将数据梳理成双列的结果集存储起来,最常见的就是当查询数据库时,它返回的结果集中,对应字段名key和记录值value就是一个map,当然如果是列表查询,还会在Map的基础上包装一层List,但是它的每一条记录结果的表示形式就是借助Map来存储的,再比如在数据接收时,如果没有合适的对象接收时,可以考虑使用Map进行接收,最常见的就是前端传入json字符串,后端使用Map来接收数据,但是现在基本都采用JSONObject的方式来接收了,但是Map也是可以作为一个备用选项,在没有其他第三方插件可用的情况下,可以考虑使用Map,或者String接收,然后转成Map。

2.快速定位数据

因为Map的一对一映射的数据关系,利用这一特性,可以快速定位具体数据,现在的一些缓存操作就是利用的这一特点,我们将数据以Map的形式存储在内存中,在缓存的众多数据当中,未来如果需要获取数据时只需要给一个指定的key,可以快速定位到缓存的内容,而不必像List结构那样,需要记住具体的位置才能快速定位,当然如果能够确切记得元素位置,也可以使用List,而且效率更高,但是更多时候是不现实的,我们需要记住每一个元素在List中的位值,数据过多时就比较麻烦了,而且写出来的程序可读性也很差,因为只是通过整型的Index获取,而Map的key可以是任何类型,完全可以定义一个具有明确意义的内容。

3.需要唯一性存储的时候

因为Map的key具有唯一性的特点,我们完全可以利用这一特点作为一个“变异版”的Set来使用,我们知道Set的特点就是不可重复,实际上在Java中,HashSet确实就是这么干的,它将存入的元素放入一个HashMap(Map的一种实现)的key中,而Map中所有的value都是一个固定的Object类型的PRESENT常量,因为它的key不可冗余的特性正好符合了Set的特点,所以在HashSet的底层实现就依托于HashMap,而且Map本身也是无需的,注意:这里的“无序”是“不保证有序”,而不是“保证无序”,这两个概念是有区别的,前者说明结果可能会有序,也可能无序,不能保证;而后者说明结果一定是无序的。所以有时可以发现在遍历HashSet时竟然是有序的,这其实并不冲突。

Map的实现原理

因为Java中Map只是一个接口,它只是定义了一些方法以及规范,所以提到实现原理,还得结合具体的实现类来说明,而且Map的不同实现类,实现的原理也有所不同;Map的实现类很多,就以我当前使用的Java 10.0.2为例,Map接口的实现类就达到了31个之多(包括抽象类),这还只是JDK内部的实现类,不包括第三方库的实现。所以这里仅仅以几个常见的Map来详细说明,也是面试中经常被提及的几个Map类型:HashMap、HashTable、ConcurrentHashMap、LinkedHashMap等。

HashMap

概念

它是基于Hash表对Map接口的实现,所谓Hash其实就是散列,这里只需要记住:散列就是将一段变长的数据转换成一个固定长度的数据,这个过程称之为散列。在这个过程中会有一系列的算法计算,这里暂不深究;而Java获取对象的hash值比较方便,因为Object类中定义了一个hashCode方法,所以Java中的任何类都有直接或间接继承了hashCode方法,而HashMap就是根据key的hash散列结果来具体定位Map中的元素的;另外在HashMap中是可以使用null键和null值的;而且HashMap不是线程安全的;如果抛去这两点来看,HashMap其实与HashTable大致是相同的。它不能保证映射的顺序恒久不变,即:无法保证有序性;它的容量和加载因子紧密关系着它的迭代性能。

实现原理

整体设计概念上来说,HashMap采用的是数组加链表的方式来存储元素,因为hash可能存在冲突的问题,所以增加链表来存储hash值相同的元素,这种解决hash冲突的方法也叫链地址法。

首先,如果要深入了解HashMap,就必须明白以下四个概念:

capacity:它是指HashMap的容量,表示的是当前HashMap中最大可以存储的数据个数。

size:它反映的是当前HashMap中已经存放的元素个数

loadFactor:加载因子,它表示当前HashMap中的装满程度,默认为0.75,,它跟threshold计算有关

threshold:表示临界值,因为HashMap的容量不是一层不变的,当达到一定程度,就需要对HashMap进行扩容,而这个扩容的临界值就是用threshold表示,它的计算方式是capacity*loadFactor;因为加载因子默认是0.75,也就是说达到最大容量的四分之三时,再往里面加元素,就会扩容。

上面只是一个大致的介绍,下面就来逐步深入介绍相关的设计原理。

hash冲突

hash本身的意思就是散列,它的作用前面也介绍过,就是将一个变长的数据散列成一个固定长度的数据,在Java中,散列的结果是一个int整形数,也就是说hash的结果最大也不会超过Java中整型的最大值,任何对象都具有hashCode方法,我们可以重写hashCode方法,但是对象的数量是海量的,但是hash值就那么多,所以必然存在一种情况:hash值相同但是对象不同,这种现象就是hash冲突。在Java中如果是比较两个对象是否相同也是有策略的,首先会比较两个对象的hashCode方法,hashCode不同,对象肯定不同,这种判断可以隔绝大多数的比较了,而当hashCode相同时,会再去调用equals方法比较,如果equals也为true,这才能说明两个对象是相等的。所以equals为true的对象,hashCode一定是相等的,反之则不然。

HashMap的内部存储容器

map中最基础的存储结构就是数组,具体在HashMap中就是一个Node<K, V>类型的数组table(即hash表),这是一个可以存储键值对的数据类型组成的数组,我们放入map中的元素全部都被存入到这个数组中,而map容量的扩充实际上也就是对这个数组的不断变更。

HashMap的初始容量设计

使用HashMap的时候,最常用的就是无参的构造方法,然后调用put方法放入键值对,这种方式采用的是默认的初始化策略,此时当我们刚刚new出一个HashMap对象时,它的内部table数组实际上是一个null(我这里是以java 10.0.2为版本介绍的,其他版本可能略有不同)。只有当我们第一次调用put时才会进行table的初始化。此时它的capacity会被设置为DEFAULT_INITIAL_CAPACITY = 1 << 4,也就是16。加载因子loadFactor是默认的0.75,所以这时候它的threshold是12。

但是我们在创建HashMap时也可以指定capacity甚至是loadFactor,这里一般不推荐修改loadFactor:

//DEFAULT_LOAD_FACTOR = 0.75f

public HashMap(int initialCapacity) {

 this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

需要注意的是:它传入的initialCapacity并不是最终我们构建的HashMap对象的容器大小,它会经过一次运算,得到一个比initialCapacity值稍大的数值,并且一定是离initialCapacity最近的那个2的N次幂的值,比如:我们传入5,它就会找到8,传入9,就会找到16。也就是说最终得到的capacity的值一定是2的某个次幂。这个过程是需要计算的,JDK中采用是一种比较高效的位移运算,具体如下:

首先来看几个简单示例:

5   0000 0000 0000 0101      |      19  0000 0000 0001 0011

7   0000 0000 0000 0111      |      31  0000 0000 0001 1111

8   0000 0000 0000 1000      |      32  0000 0000 0010 0000

—————————————————————————————————

9   0000 0000 0000 1001      |      37  0000 0000 0010 0101

15  0000 0000 0000 1111      |      63  0000 0000 0011 1111

16  0000 0000 0001 0000      |      64  0000 0000 0100 0000

根据上面的数据展示,可以看到如果我们想要将5最终变成离它最近的那个8,需经历:5 — 7 — 8这么一个过程,也就是将它的原本二进制数据有效部分全部转换成1,然后在此基础上加1就可以得到目标值,同理9到16,19到32等也是如此,而JDK中HashMap源码采用就是这种不断右移然后按位或运算,最终得到一个有效数据部分全为1的数值,然后加1得到结果,这样再来看HashMap的这部分计算源码就不会迷惑了:

static final int tableSizeFor(int cap) {

 int n = cap – 1;

 n |= n >>> 1;

 n |= n >>> 2;

 n |= n >>> 4;

 n |= n >>> 8;

 n |= n >>> 16;

 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

可以看到,最终计算出来的n与MAXIMUM_CAPACITY比较,只要比它小,就会加1,而在此之前的逻辑主要就是为了将对应的二进制位全部转换成1。这里有一点需要注意:按照上面介绍的逻辑会有有个情况,就是如果我们传入的数值本身就是2的次幂值,那就会有问题,因为此时传入的capacity本身就已经可以作为初始容量了,解决方式很简单,就是上面方法中的第一步操作,先将传入的capacity减1,再进行计算,此时如果传入的是8,那么减1之后,n其实是7,经过计算后正好是8,符合逻辑;那如果此时传入的是9,虽然减1后得到了8,但是此时需要的是更大数值的16,经过位移计算,正好得到15,加1后得到16,所以逻辑上也没有问题。

HashMap的存储

HashMap的存储方式就是利用了map中的key的hash值作为定位基础的,具体做法就是:我们在存入一个k-v键值对的时候,它会计算key的hash值,也就是调用key对象的hashCode方法,因为hashCode的取值范围是整个整型范围,所以可能会非常大,为了让它能够和table数组的下标index挂钩,这里就会将得到的hashCode值与table的长度取模,这样得到的数据肯定是在table.length之内的整数,然后用它作为数组对应的下标。注意:这里JDK采用了一定的优化,它的取模并不是常规的hash % table.length,它使用的是hash & (table.length – 1)这种按位与的操作,这么做有两个好处:

首先就是效率很高,比正常的取模运算符要快

避免了负数问题,结合前面的初始容量设计可以知道,table.length – 1得到的一定是一个正数值,所以它的最高位一定是0,而0与任何数按位与运算,得到的一定是0,此时无论hashCode值是整数还是负数,计算出来的结果一定是正数。

而为何会出现hash & (table.length – 1) == hash % table.length呢?其实想想也能明白:

经过前面分析可以得到table的长度必然是:table.length == 2^N,则hash % table.length实际上就是hash / table.length然后取余数,也就是hash >> N 位移运算过程中,移出的那部分数据即为需要的模运算结果。而table.length – 1的结果必定是一个二进制有效位全为1的数据(参考前面容量初始化设计),此时hash与减1后的值进行按位与,可以保证低位的结果保留,而超过table.length – 1数值的高位部分得0,这个结果正好就是前面位移运算过程中需要得到的那个移出的部分。

按照上面的介绍,最终可以根据key的hash得到一个对应的数组位置index,但是我们也知道hash是会冲突的,这里如果出现了冲突,经过计算后得到的index位置上已经有元素了怎么办,这时候链表结构就发生作用了,它会将最新添加的元素放在数组中,将该位置上之前的元素以链表的形式放在新加入的元素的后面,Node的设计本身就是一种链表存储格式:

static class Node<K,V> implements Map.Entry<K,V> {

 final int hash;

 final K key;

 V value;

 Node<K,V> next;

 …

}

所以:数组中的元素肯定是最新放入的元素,之前的老元素会按照链表的方式挂在最新元素的后面。这种存储就是数组加链表的存储方式。

需要注意的是:HashMap中使用到的hash值并非是对应key对象上调用hashCode方法,它又经历了一步计算:

static final int hash(Object key) {

 int h;

 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这么做的目的也就是为了降低hash冲突,尽可能的让key的每一位数据都会影响到散列的结果,所以它会对hash进行扰动计算,防止hashCode的高位不同但低位相同导致的hash冲突,将hash的高位和低位的特征结合起来,共同影响hash结果,尽可能将每一位数据都与hash结果相关联,从而降低冲突。具体的计算,强烈推荐H大的博文:透彻分析hash()

随着数据的不断增多,冲突的可能性就会越来越大,极端情况就是链表部分会变得很长,而链表结构的增删的效率很高,但查询的效率很低,这样如果链表过长,会影响Map的查询性能,所以JDK1.8之后,对链表部分做了改动,一旦链表的长度超过了8个Node,就会直接转换成红黑树结构,检索的时间复杂度大大降低。

另外对于扩容方面,如果元素个数超过了capacity*threshold的值,容器就会调用resize方法扩容,扩容很简单:就是将原来的oldCapacity << 1,也就是直接扩大一倍,这样也保证了capacity的值始终都是2的指数值。

使用HashMap时机

总体上来说,HashMap的查找效率比较高,常见的应用场景中,需要使用Map的地方,HashMap足以解决大多数问题,而且它的效率也是很高的,如果数据量较大,这个效率提升还是很可观的,如果没有线程安全的要求,并且可以允许Key出现null值的情况下,可以考虑使用HashMap。

HashMap的问题

线程安全

观察HashMap的源码,可以看到,没有任何线程安全的概念,对于put,remove等操作方法,没有任何加锁、同步化的操作,所以它是线程不安全的,在多线程环境下,可以考虑如下方案:

使用HashTable,但是效率极低,它是方法加锁,加锁粒度很粗糙,性能较低

使用Collections工具类可以创建一个线程安全的HashMap,实际的结果跟前面一种类似,也是一种方法加锁,不推荐

可以考虑在自己的业务逻辑中自行实现同步逻辑,灵活性较高,但是会影响代码的整体阅读逻辑

可以使用ConcurrentHashMap,它是也是线程安全的map,引入了段加锁机制,效率比HashTable要高很多

另外,据网上有些博客提出:在并发状况下,多个线程同时对一个map进行put,可能会导致get死循环,具体可能表现为:

多线程put操作后,get操作导致死循环。

多线程put非NULL元素后,get操作得到NULL值。

多线程put操作,导致元素丢失。

但是这种情况我写了一些demo,暂时没有出现上面这种情况,可能是因为JDK版本的缘故,新版本的HashMap做了一定的优化,故暂时无法重现这个问题。

性能开销

hash函数的计算会影响这个性能,对于一些极端情况,例如对象比较复杂,hash的计算可能比较耗时,这部分性能的损耗也应该考虑在后续实际生产中,如果Map中存储的key比较复杂,就要考虑是否需要换一种存储方式了。

初始容量设置问题

我们在创建一个HashMap的时候,一般都是用默认的无参构造方法,这种方法虽然方便,但是性能上面可能并不是最符合我们当前正在运行的程序环境,所以一般都建议指定一个初始容量,这样可以尽可能保证减少map扩容带来的性能耗损问题,同时也减少了rehash(扩容后重新进行hash计算)的次数,对性能的提升也有一定好处。

但是初始容量的设计也是有讲究的,不能越大越好,要最符合当前的应用环境,当然前提是能够预知到map中到底会存储多少元素,否则默认的构造方法是最好的选择。在阿里巴巴的Java开发者手册中给出了一个计算初始容量的公式:

capacity = (要存储元素个数 / 加载因子loadFactor) + 1

//加载因子loadFactor默认0.75

其实这种计算方案在Google的Java开源工具库guava也有,最终的灵感来源还是JDK源码中HashMap的putAll方法得来的,它里面采用的就是这种计算方式,它这么设计是有一定的好处的:

例如现在需要加入7个元素,如果只是直接传入7,那么最终的HashMap容量会被设置为8,但是由于此时threshold = 8 * 0.75 = 6,所以在加入第七个元素的时候会有一个hash表扩容,这个是比较耗费资源的操作。而如果使用上面推荐的公式,可以计算最终传入的值为:7 / 0.75 + 1 = 10;那么最终计算后的capacity为16,在进行元素装入的时候就可以有效避免过于频繁的hash表扩容操作,从而提升性能。

其他

说到HashMap,这里顺便说一下HashSet,它在前面也稍微提到了一下,它是一种Set,Set的特性就是:无序,唯一。而HashSet的底层实现就是利用的HashMap的key唯一性特点,而且HashMap本身也是不保证有序的,所以正好与Set的特性不谋而合,所以HashSet在存储元素的时候,都是将元素存入HashMap的key中,value部分始终都是一个固定的常量Object对象,没有实际意义。

HashTable

首先在概念上,它基本上与HashMap没有太大区别,甚至于使用的方式都差不多,不同的只是它的底层设计上与HashMap存在一定的差异,而正是这些差异,使它的使用场景更加单一。另外:HashTable是不存能出null值的,无论是key还是value,这点与HashMap完全不同,而且它是线程安全的。

实现原理

它的整体设计思路基本和HashMap差不多,都是基于Node数组进行存储,这个Node实际上就是一个链表的节点类型,内部维护一个Node类型的数组,Node的结构设计基本都差不多,下面主要从几个与HashMap不同的地方说明以下具体的细节差异。

线程安全

首先明确一点,HashTable是线程安全的,看它的源码就知道了,它里面的所有涉及到更新HashTable的操作都被加了synchronized锁。

继承关系

与HashMap不同的是HashTable继承自Dictionary(HashMap继承自AbstractMap),这是一个非常古老的抽象类,它从java1.0的时候就已经存在了,现在连这个类的头部注释中都说这个类已经是obsolete(过时的)类了,对于现在的JDK而言,Dictionary所能达到的效果,Map接口都能达到,甚至还比它的灵活性更高(因为接口可以多实现,类只能单继承),所以官方推荐后续的实现都基于Map接口了。

迭代

HashTable的遍历使用的是枚举Enumeration,而不是我们常见的Iterator迭代器,例如我们如果调用它的keys方法,它会返回一个Enumeration类型的对象,这个查看一下源码就可以很清楚的看到。当然,它本身也是有Iterator的,Enumeration是因为历史遗留问题才一直存在的。它的Iterator迭代与HashMap都是支持fast-fail的,fail-fast这里就不再赘述,参考Java集合–List。

初始容量设计

它默认的初始容量为11,并非HashMap的16,但是它的默认加载因子loadFactor却仍然是0.75,官方采用0.75做加载因子其实也是经过时间空间的消耗权衡得到的结果,按照官方的注释解释:如果过高,虽然会减少空间上的开销,但是会增加查询上的时间成本,所以才说不建议修改loadFactor。

扩容设计

它的扩容不同于HashMap的直接翻倍,每次当容量达到threshold后,新的capacity = 2 * oldCapacity + 1。所以它的到的capacity值始终都是一个奇数,默认是从11开始的。另外它也没有对传入的capacity再计算的过程,它的源码中只有如下设计:

if (initialCapacity==0)

 initialCapacity = 1;

this.loadFactor = loadFactor;

table = new Entry<?,?>[initialCapacity];

threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

可以看到,只是对initialCapacity进行了是否为0的判断,如果为0,默认赋值为1,否则capacity的值就是传入的值。所以我们在构建HashTable对象时,如果需要传入capacity,最好也按照它的设计初衷,传入一个奇素数。

hash计算

HashTable的hash计算就是直接调用key对象的hashCode方法,没有进行进一步扩散计算,而且它的取模运算也没有HashMap那种采用效率更高的&操作,据说这是跟HashTable的长度数值有关,当哈希表的大小为素数时,简单的取模,哈希的结果会更加均匀。没有具体深入研究过,有待探讨。

HashTable的问题

性能

它的性能问题是广为诟病的,因为它的synchronized锁都是加在方法上的,synchronized本身就是重量级锁,并且加锁粒度又很粗糙(方法级锁),我们在现实场景中,其实99%的情况可能都不会出现线程安全问题,所以仅仅为了那1%的并发安全去使用它多少有点浪费性能,完全可以自己控制同步,而且如果有选择,选择ConcurrentHashMap也是一种不错的方案。只有在一些特殊的应用场景中可能会采用HashTable存储数据。

并发迭代修改

虽然HashTable被描述为线程安全的,似乎在多线程环境下就不存在任何问题了,但是仍需注意,如果我们使用迭代器对HashTable进行遍历的时候,它采用的是fail-fast机制,所以仍然有可能抛出ConcurrentModificationException异常。另外HashTable的另一种迭代方式:Enumeration,它是不支持fail-fast的,所以如果有需要检测这种并发修改迭代的情况,Iterator是唯一的选择。

ConcurrentHashMap

原理

因为HashTable的加锁粒度过大,所以JDK1.5以后出现了这个同样支持并发操作的Map结构,ConcurrentHashMap引入了Segment(段)的机制,所谓段,就是将Map的容器进行分段(也就是常说的“桶”),每段单独加锁,所以当并发修改时,如果不是同时操作同一个段内的数据,段与段之间是互不影响的,这就是所谓的锁分段技术,正是因为段与段之间是独立的加锁,所以大大提升了并发性能。

对于java6和java7的版本中,主要就是segment机制的引入,内部拥有一个Entry数组,数组中每个元素又是一个链表结构,但是同时也是一个ReentrantLock(Segment继承了ReentrantLock)。

而Java8以后,ConcurrentHashMap做了很大的改动,有些甚至是颠覆性的,它摒弃了之前一直使用的Segment机制,虽然源码中仍然存在该类,但是源码的注释中已经说明了它是一个unused(无用的)类,它只是用来兼容之前版本的ConcurrentHashMap。新版本的ConcurrentHashMap采用了CAS算法,无锁化的操作大大提高了性能,底层仍然采用HashMap的那套实现,即:“数组+链表+红黑树”的方式。同时为了做到并发,也增加了一些辅助类,如:TreeBin、Traverser等内部类。

继承关系

ConcurrentHashMap除了和HashMap一样继承了AbstractMap,同时它也实现了一个叫ConcurrentMap的接口,这个接口的作用主要是为Map提供线程安全以及原子性的保证。另外不同于HashMap,它没有实现Cloneable接口,所以如果涉及对象复制,需要额外考虑其他方式。其他的基本都与HashMap一致了。

初始化容量和扩容机制

对于初始容量的设置,默认加载因子以及扩容的方式,ConcurrentHashMap采用的方案与HashMap的机制是一模一样,所以对于HashMap的那一套在这里是通用的,它的内部结构也是一个Node数组,并且它的Node类的内部定义几乎与HashMap的一致,不同的是此时的Node中代表Value的val和指向下一个Node节点的引用next是volatile修饰的,这个也是为了在高并发情况下,为不同线程间数据可见性而考虑的。而且不仅仅是这两个字段,在整个类中,除去静态常量,其余的变量几乎全部都用volatile修饰。

如果在创建ConcurrentHashMap对象时,我们手动传入了capacity值,这里它不是像HashMap那样直接对传入的capacity值进行计算求取最近的2的指数值,而是会将传入的initialCapacity进行如下运算:

tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)

而这里的tableSizeFor方法就是根据传入的值,对capacity进行不断位移以及按位或的操作,最终求出一个合适的2的指数值。即:如果创建ConcurrentHashMap对象时,如果指定了capacity,在实际创建最大容量时,会先对传入的capacity扩大3倍并加1,再根据此时的值再此进行求取最近的不小于该值的2的指数幂值。

几个重要的属性

前面HashMap中介绍过的几个重要属性这里就不再重复了,这里就重点提一下sizeCtl属性,它的作用很大,用途比较多,根据不同的值,可以分为以下几种情况:

负数代表正在进行初始化或扩容操作

-1代表正在初始化

-N表示当前有N – 1个线程正在进行扩容

整数或0代表hash还没有被初始化,此时它的值表示的是初始化大小或者是下一次扩容的大小,有点类似于前面介绍过的threshold(阈值)的概念,它的值始终是当前ConcurrentHashMap容量的0.75倍,与loadFactor正好相对应。

//下面两个值主要是用于与hash值进行比较时使用,判断当前节点类型

static final int MOVED     = -1; // hash值是-1,表示这是一个forwardNode节点

static final int TREEBIN   = -2; // hash值是-2  表示这时一个TreeBin节点</pre>

MOVED和TREEBIN在容器扩容,遍历以及存放元素的时候,有很重要的作用。

核心类

Node

跟HashMap一样,它是包装了K-V键值对的类,前面说过,它的整体设计思路跟HashMap几乎一样,只是它的value和next属性使用了volatile修饰,保证了在并发环境下线程间的可见性,同时比较有意思的是它的setValue方法:

public final V setValue(V value) {

 throw new UnsupportedOperationException();

}

可以看到,它不允许直接使用setValue方法对Node中的value属性进行修改,如果这么做,会直接抛出异常。这个方法在HashMap中是可以正常使用的。同时,相较于HashMap,它又新增了一个find方法,注释上解释它的功能主要是为了辅助map.get方法,在子类中会进行重写。

TreeNode

当Node的链表结构过长时(一般是为8个元素),HashMap就是将其转换成红黑树形式,而转换的基础就是直接借助TreeNode,但是ConcurrentHashMap中却不是直接使用TreeNode,它是将这些TreeNode节点放在TreeBin对象中,然后由TreeBin完成红黑树的包装,而且这里的TreeNode是继承自ConcurrentHashMap中的Node类。

TreeBin

TreeBin的作用很明确,它的构造函数就一个,接收的参数就是TreeNode,它对TreeNode进行包装,甚至于当链表转换成树结构的时候,哪怕它的根节点也是TreeBin,并非HashMap中的TreeNode,所以可以说:ConcurrentHashMap中,如果数组中某个数组位置上的结构转变成了树结构,那么存储在数组中的实际元素就是TreeBin对象。而对于其他没有转换成树结构的位置(链表长度仍然在8个以内),仍然是原本的Node类型。

ForwardingNode

一个用于连接两个table的节点类,这个类在进行扩容的时候有很大的作用,它内部包含了一个nextTable引用,类型是Node类型,而且它的key、value以及next都是null,hash值为-1,后面在介绍扩容的时候会说道它的作用。

CAS无锁化

UnSafe静态代码块

在我当前使用的java10.0.2版本中,源码的6373行到6403行这段代码是无锁化的静态语句块部分,它里面利用了jdk.internal.misc.Unsafe类对一些重要属性的修改采用CAS操作,大大提高了程序的性能,因为没有锁的开销问题,许多锁带来的问题也就不存在了。

三个无锁化的核心方法

//获取tab数组中i位置上的Node

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {

 return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);

}

//利用CAS设置tab数组中i位置上的Node节点,这里就是典型的CAS操作,设置值的时候,传入待修改的值v和比较值c

//在修改时,将c的值与内存中的值比较,只有相等时,才将该位置上的Node设置为传入的v,否则修改失败

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,

 Node<K,V> c, Node<K,V> v) {

 return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);

}

//顾名思义,这个方法就是利用CAS操作,将tab数组中的i位置设置为传入的v

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {

 U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);

}

正是有了上面这三个核心操作,才保证了ConcurrentHashMap的线程安全的特性。

初始化initTable

在构建ConcurrentHashMap对象的时候,它的构造方法采用的策略和HashMap中的大致是一样,在构造方法中其实并没有对具体的存储数组进行指定,只是简单初始化了一些必要的参数指标,具体的table的初始化都是放在插入元素时进行的,在插入前会对table进行null值判断。

private final Node<K,V>[] initTable() {

  Node<K,V>[] tab; int sc;

  while ((tab = table) == null || tab.length == 0) {

    //根据前面的介绍,此时sizeCtl若小于0,表示有其他线程正在进行初始化操作

    //此时当前线程放弃初始化,自旋直至其他线程初始化结束

    if ((sc = sizeCtl) < 0)

      Thread.yield(); 

    else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {

      //此时表示当前线程得到了初始化权限,此时将sizeCtl设置为-1,阻止其他线程进入

      try {

        if ((tab = table) == null || tab.length == 0) {

          //DEFAULT_CAPACITY = 16

          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

          @SuppressWarnings("unchecked")

          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

          table = tab = nt;

          //此时n是当前table容器的大小,n>>>2实际上就等于0.25n

          //所以sc = 0.75n

          sc = n – (n >>> 2);

        }

      } finally {

//修改sizeCtl的值为当前容器大小的0.75倍值

        sizeCtl = sc;

      }

      break;

    }

}

return tab;

}

扩容transfer方法

由于这个方法过长,这里就不再贴它的源码了,强烈建议琢磨一下源代码,它里面的涉及到的设计思路还是比较精妙的,它的大致扩容思路是这样的:

开始的时候它会进行一次CPU的可用线程的计算,根据可用线程数目和 Map 数组的长度,平均分配处理当前待扩容的“桶”。默认的是每个线程需要处理16个“桶”,所以换句话说,如果当前map的容量为16的时候,那么扩容阶段就只有一个线程在扩容。

它在扩容时需要用到一个额外的数组空间nextTable,它的大小是原table的两倍,一旦扩容完成,原来的table就会被新的nextTable所取代。

扩容后,就是将原来数组中的内容复制到新的nextTable数组中去,这里的复制转移并不是简单的copy,它是有策略的

通过前面的分析我们知道,数组中的元素存在不同的情况,既有Node(此时说明是链表结构),也会有TreeBin(链表过长转换成了红黑树)以及null(这部分是还未存放元素的“桶”)。

首先在进行遍历复制的时候,原数组中null的位置在新的数组中同样位置上会插入一个占位符forwarNode节点,当其他线程在遍历到当前节点是forwardNode类型,就直接跳过该节点,继续向后遍历。

剩下的链表和红黑树结构中,在复制到新的数组中时会对其进行拆分,拆分的规则是将当前节点的hash值与length进行取余操作,假如在原数组中的位置是index,那么根据取余的结果,如果为0,就在新数组的index位置防止,否则不为0的话,就将其放在index+n的位置,这里的n一般就是16。

这样原来数组中的链表和红黑树部分就都各自被拆分成了两份存储在新的数组中,原来的null位置依然为null,没有任何变化,这样就完成了数组的扩容操作。下面一份关于扩容的示意图:

首先我们现在假设当前map的容量为16:

concurrentHashMap01.png

其余数组中的内容暂时不用考虑,单独看这里的index为1和4的位置上的内容,假设其他位置上的内容都是null,那么扩容后,数组的容量就会变成32,然后1位置上的蓝色节点会组成一个新的链表,放在新数组中的1位置上,而1位置上的黄色节点会组成新的链表放在新数组的17位置上。同样的4位置上此时链表长高度达到8,应为树结构,但是为了方便表示,这里也将其画成了链表结构,在拆分后,蓝色黄色节点各自组成新的链表,且长度减到了4,重新变成了链表结构,如果拆分后链表长度仍然过长,扩容后仍然会保持红黑树结构。

concurrentHashMap02.png

put方法存放元素

首先需要明确的是,ConcurrentHashMap中put是不能存放key或者value为null的元素的,否则会直接抛出空指针异常,这一点有别于HashMap。另外因为ConcurrentHashMap可以运行于多线程环境中,所以它的put方法逻辑比较复杂,简单来说,它的put方法的主要逻辑就是:

首先根据传入的key计算对应的table中的位置index

判断table[index]当前位置中是否存在元素,如果没有元素,直接放入,没有加锁操作

如果当前位置上已经存在元素了,节点上锁,然后依次遍历链表节点,如果遇到了key和hash都是一致的元素,就更新这个位置上的value,注意这里的上锁的节点可以理解为hash值相同组成的链表的头结点。

如果一致遍历到节点链的末尾,都没有找到key和hash都相同的元素,那么就可以认为它是一个插入操作,此时就把这个节点插入到链表末尾。

如果table[index]位置上的内容为树节点,就按照树的方式是插入节点

在插入结束后,如果是链表结构,需要判断当前链表长度是否达到了8,如果是,还需要将其转换成红黑树。

最后同步更新一下当前map中的元素数量

可以看到,新版本的ConcurrentHashMap中,put时锁住的是Node节点,而不是像之前JDK1.6和1.7那样锁住整个segment。而且在锁住Node之前的操作也是线程安全的,它的线程安全就依托于前面介绍过的三个核心的CAS方法。

size属性

因为可能存在高并发的情况,所以不像HashMap那样直接调用size方法就可以准确获取当前map中的元素个数,在高并发下,可能存在当我们需要获取size值的时候,其他线程正在往map中写数据,我们不能像虚拟机的垃圾回收一样,在统计时“Stop The World”,所以得到的size值其实并不是一个精确的值。对于这个大概的size值的获取,ConcurrentHashMap也是利用了一些策略才得到的,并非直接返回size属性值。

辅助的内部类和变量

@jdk.internal.vm.annotation.Contended static final class CounterCell {

 volatile long value;

 CounterCell(long x) { value = x; }

}

//它是一个基于计数器的值,其实也就是存放的是map中的元素个数,使用CAS更新

//但是它并不是强制的等于当前map中元素的个数

private transient volatile long baseCount;

//当调整大小、创建CounterCells时用于CAS自旋锁操作

private transient volatile int cellsBusy;

//计数表,当非空的时候,它的容量是2的幂

private transient volatile CounterCell[] counterCells;

size方法

public int size() {

 long n = sumCount();

 return ((n < 0L) ? 0 :

 (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :

 (int)n);

}

//JDK1.8开始新增的一个方法,它与size方法很类似,而且从它的注释上来看,很推荐使用它来代替size方法

public long mappingCount() {

 long n = sumCount();

 return (n < 0L) ? 0L : n; // ignore transient negative values

}

//无论是size还是mappingCount,都没有直接返回baseCount的值

//而是在baseCount的基础上继续进行了计算,即便如此,它得到的仍然是一个估计值

final long sumCount() {

   CounterCell[] as = counterCells; CounterCell a;

   long sum = baseCount;

   if (as != null) {

     for (int i = 0; i < as.length; ++i) {

       if ((a = as[i]) != null)

         sum += a.value;

       }

     }

   return sum;

}

addCount方法

前面在介绍put方法的时候,在最后一步说到“同步更新一下当前map中的元素数量”,这句话在put方法的代码中其实就是调用addCount方法,而这个方法会做两件事情:利用CAS对baseCount的值进行更新,然后判断此时是否需要扩容。

ConcurrentHashMap的问题

size的准确性

上面说过,它的size方法得到的值并不是一个准确的值,但是在大多数情况下,这个值是可以保证的,只有在极端并发环境下才有可能出现不一致的情况,所以我们在使用时,不能依赖于size方法来确定map中精确的元素个数,应当有适当误差。并且现在也更加推荐了mappingCount方法来代替size方法使用。

数据覆盖问题

当我们在高并发环境下,纯粹put或者get操作,其实是没有问题的,但是当我们调用get之后,在下次调用put方法之前,如果有其他线程也对该map调用了put方法,那么后续在调用put方法的时候,就有可能把刚才其他线程填入的值覆盖掉,即使ConcurrentHashMap中使用CAS操作,仍然不可能完全避免这种情况。但是这中属于具体编码问题,控制合理的话,编码人员可以避免这样做,只要稍加注意,可以采用一些额外的手段保证它的一致性。

空值问题

一定不能使用ConcurrentHashMap的put方法放入一个key或value为null的元素,否者直接NullPointerException。

LinkedHashMap

原理

可以看以下它的定义:

public class LinkedHashMap<K,V>

 extends HashMap<K,V>

 implements Map<K,V>

从它的定义中可以明显的看出,它就是HashMap的子类,所以它的一切都是基于HashMap的,查看它的源代码可以看到,它的初始化,扩容,元素的存放与获取底部都是基于HashMap的那一套原理,所以这里就不再继续介绍了,但是它有一个HashMap没有的特点,就是双向链表,简单来说就是:它的本身的存储结构依然采用HashMap的那套数组加链表的方式,但是它的节点内部又多维护了两个引用before和after,before指向当前元素之前放入的元素,next指向紧接着放入的元素引用,所以它们的引用关系与放入Map中的元素顺序有关,也就是说它除了之前介绍的每个“桶”内部的链表结构,桶与桶之间的不同节点也有一个引用在维护,并且是双向的链表结构,这样整体的感觉就是:

LinkedHashMap01.png

概括起来就是:它的主体存储结构仍然是HashMap的那套逻辑,只是在HashMap的基础上,每个节点又多维护了一份之前存入的元素引用以及之后存入的元素引用,这样每个节点内部算起来实际上有三个引用(HashMap本身就有一个链表引用,主要是hash值相同的元素之间的引用,然后又有了这个新增的两个引用)。

LinkedHashMap02.png

特点

从它的结构可以看出,HashMap所拥有的特性它都是存在的,同时因为加入了双向链表的维护,所以它是一个有序的Map,前面说过,HashMap是不保证有序的,也就是它遍历的结果可能与它放入的顺序不是一致的(不保证结果有序性,并不是一定是无序的),而LinkedHashMap的结果必定是有序的,所以如果需要使用有序的Map场景,可以考虑使用LinkedHashMap。

使用场景

前面介绍过的Map的使用时机这里都完全可以适用于LinkedHashMap,此外它还可以保证结果的有序性,所以如果对遍历的有序性有要求,可以使用它。

另外:它还可以用来实现LRU(Least recently used, 最近最少使用)算法,这是因为它内部有一个removeEldestEntry方法,这个方法的返回值为boolean类型,如果我们需要实现LRU,可以继承自LinkedHashMap,重写它的方法,此时,如果需要使用实现LRU,它有一个属性必须设置为true,那就是:accessOrder,只有它为true的时候,双向链表中的元素就会按照访问先后顺序排列。这里有一个简单使用的例子:

public class LRU<K, V> extends LinkedHashMap<K, V> implements Map<K, V> {

   public LRU(int initialCapacity, float loadFactor, boolean accessOrder) {

   super(initialCapacity, loadFactor, accessOrder);

 }

 @Override

 protected boolean removeEldestEntry(Entry<K, V> eldest) {

   //保证map中元素个数

   return size() > 6;

 }

 public static void main(String[] args) {

   LRU<Character, Integer> lru = new LRU<Character, Integer>(

     16, 0.75f, true);

   String s = "abcdefghijk";

   for (int i = 0, len = s.length(); i < len; i++) {

     lru.put(s.charAt(i), i);

   }

   System.out.println(lru); //{f=5, g=6, h=7, i=8, j=9, k=10}

 }

}

最终得到的结果可以看到,map中始终都是只包含6个元素,如果过多,之前插入的节点都会被抛弃,抛弃的都是最近最少使用的节点。

存在的问题

总体来说,前面说道到的HashMap的存在的问题在这里仍然是存在的,它不是线程安全的,而且它的存储开销比HashMap更大,这个也好理解,毕竟它又多了一个双向链表的维护,所以无论是复杂度还是维护成本都会高一些。

ConcurrentSkipListMap

介绍

最后再来看一个基于跳表的数据结构,它是一个支持排序和并发的Map,是线程安全的,这种跳表的结构我们正常开发中很少用到,所以对于我而言,它是一个知识盲点,下面就简单介绍一下这个跳表。

原理

在介绍跳表之前,首先需要知道传统的链表结构是什么样子,传统的链表是一个线性结构,如果我们需要查询某个元素,需要从链表的头部挨个遍历访问,然后找出目标元素。所以,链表结构的查询需要O(n)的时间,它与链表长度紧密相关。而跳表就是基于传统链表上做的一次改进,它是采用空间换时间的方式来提高查询效率,这里以一个最简单的方式来描述一下它的数据结构,实际情况中它的结构会比下面的图示复杂一点,后面会介绍:

SkipListMap1.png

上面的结构就是最简单的一种跳表的结构,首先需要明确的是:跳表天然是有序的,所以上面的链表结构是有序的。它除了正常的链表结构(有一个引用指向下一个节点)外,每个节点多了一个引用,用于指向下下个节点,这里,如果我们查询一个值为12的元素,具体查询步骤就是:

SkipListMap2.png

首先12与1比较,比1大,向后找到6节点

12与6比较,比6节点大,向后找到9节点

12与9比较,比9节点大,向后找到17节点,说明12在9节点与17节点之间

12与9比较,比9节点大,向后找到12节点,即为目标节点

可以看到,它的查询是跳跃式进行的,跳过了中间的3和7节点,所以它查询的时间复杂度为O(n/2)。但是前面也说过了,这只是最简单的一种方式,实际情况中,一个链表节点的内部可能包含的不仅仅只有两个引用(下一个节点+下下个节点),每个节点内部到底会带有多少个后续节点的引用是随机生成的,所以实际情况可能是这样:

SkipListMap3.png

每个节点中拥有的后续节点的引用个数不定,这是一种概率均衡技术,而不是强制性均衡约束,所以对于节点的插入和删除比传统的平衡树算法更为简洁高效。

查找

SkipListMap4.png

例如查找值为12的元素,就会按照红色箭头上的数字步骤查询即可,这里就不再赘述了。

插入

找到需要插入的位置

申请新的节点

调整指针

SkipListMap5.png

上图的解释如下:

假设我们这里需要插入一个值为15的节点,最后找到的位置就是上图中红色圆圈的位置,然后申请新的节点,将节点调整指针,放入12的后面即可,这里有一个技巧:使用一个update数组保存将要插入位置之前的节点直接引用,这里就是上图中红色线框框住的三个引用,因为在插入节点时,只有这三个引用可能涉及到指针的调整。调整后的情况即为:

SkipListMap6.png

删除

删除操作其实和插入很类似,找到节点,将其删除,然后调整指针即完成整个删除操作,插入逻辑中用到的update数组技巧在这里仍然适用。

Java中的ConcurrentSkipListMap实现

在java中,跳跃表是具有层级结构的,即所谓的level,整体结构大致如下:

SkipListMap7.png

跳表的结构具备以下特征:

最底层包含所有节点的一个有序的链表

每一层都是一个有序的链表

每个节点都有两个指针,一个指向右侧节点(没有则为空),一个指向下层节点(没有则为空)

必备一个头节点指向最高层的第一个节点,通过它可以遍历整张表,如上图中的左上角的蓝色节点BASE_HEADER

在新增节点时,假设此时我们加入一个值为80的节点,它是通过如下步骤来添加的:

首先,它会将80节点加入level1中的链表中

SkipListMap8.png

根据概率算法,计算处一个level值,这里假设为4,然后根据跳表算法描述,构建新的索引

SkipListMap9.png

将各个索引层次上的节点连接

SkipListMap10.png

其他

目前常用的key-value数据结构有三种:Hash表、红黑树以及SkipList,它们各自有着不同的优缺点(不考虑删除操作):

Hash表:插入、查找最快,为O(1),数据的有序化需要显式的排序操作

红黑树:插入、查找为O(logN),但是常数项较小,如果采用无锁化实现,复杂度很高,一般需要加锁,数据天然有序

SkipList:插入、查找为O(n),常数项比红黑树要大,底层结构为链表,可无锁实现,数据天然有序

Redis使用

# Redis

1. 概念: redis是一款高性能的NOSQL系列的非关系型数据库

1.1.什么是NOSQL

NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,是一项全新的数据库理念,泛指非关系型的数据库。

随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。

1.1.1. NOSQL和关系型数据库比较

优点:

1)成本:nosql数据库简单易部署,基本都是开源软件,不需要像使用oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。

2)查询速度:nosql数据库将数据存储于缓存之中,关系型数据库将数据存储在硬盘中,自然查询速度远不及nosql数据库。

3)存储数据的格式:nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。

4)扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。

缺点:

1)维护的工具和资料有限,因为nosql是属于新的技术,不能和关系型数据库10几年的技术同日而语。

2)不提供对sql的支持,如果不支持sql这样的工业标准,将产生一定用户的学习和使用成本。

3)不提供关系型数据库对事务的处理。

1.1.2. 非关系型数据库的优势:

1)性能NOSQL是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过SQL层的解析,所以性能非常高。

2)可扩展性同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。

1.1.3. 关系型数据库的优势:

1)复杂查询可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。

2)事务支持使得对于安全性能很高的数据访问要求得以实现。对于这两类数据库,对方的优势就是自己的弱势,反之亦然。

1.1.4. 总结

关系型数据库与NoSQL数据库并非对立而是互补的关系,即通常情况下使用关系型数据库,在适合使用NoSQL的时候使用NoSQL数据库,

让NoSQL数据库对关系型数据库的不足进行弥补。

一般会将数据存储在关系型数据库中,在nosql数据库中备份存储关系型数据库的数据

1.2.主流的NOSQL产品

键值(Key-Value)存储数据库

相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB

典型应用: 内容缓存,主要用于处理大量数据的高访问负载。 

数据模型: 一系列键值对

优势: 快速查询

劣势: 存储的数据缺少结构化

列存储数据库

相关产品:Cassandra, HBase, Riak

典型应用:分布式的文件系统

数据模型:以列簇式存储,将同一列数据存在一起

优势:查找速度快,可扩展性强,更容易进行分布式扩展

劣势:功能相对局限

文档型数据库

相关产品:CouchDB、MongoDB

典型应用:Web应用(与Key-Value类似,Value是结构化的)

数据模型: 一系列键值对

优势:数据结构要求不严格

劣势: 查询性能不高,而且缺乏统一的查询语法

图形(Graph)数据库

相关数据库:Neo4J、InfoGrid、Infinite Graph

典型应用:社交网络

数据模型:图结构

优势:利用图结构相关算法。

劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。

1.3 什么是Redis

Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供测试数据,50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s ,且Redis通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:

1) 字符串类型 string

2) 哈希类型 hash

3) 列表类型 list

4) 集合类型 set

5) 有序集合类型 sortedset

1.3.1 redis的应用场景

缓存(数据查询、短连接、新闻内容、商品内容等等)

聊天室的在线好友列表

任务队列。(秒杀、抢购、12306等等)

应用排行榜

网站访问统计

数据过期处理(可以精确到毫秒

分布式集群架构中的session分离

2. 下载安装

1. 官网:https://redis.io

2. 中文网:http://www.redis.net.cn/

3. 解压直接可以使用:

* redis.windows.conf:配置文件

* redis-cli.exe:redis的客户端

* redis-server.exe:redis服务器端

3. 命令操作

1. redis的数据结构:

* redis存储的是:key,value格式的数据,其中key都是字符串,value有5种不同的数据结构

* value的数据结构:

1) 字符串类型 string

2) 哈希类型 hash : map格式  

3) 列表类型 list : linkedlist格式。支持重复元素

4) 集合类型 set  : 不允许重复元素

5) 有序集合类型 sortedset:不允许重复元素,且元素有顺序

2. 字符串类型 string

1. 存储: set key value

127.0.0.1:6379> set username zhangsan

OK

2. 获取: get key

127.0.0.1:6379> get username

"zhangsan"

3. 删除: del key

127.0.0.1:6379> del age

(integer) 1

3. 哈希类型 hash

1. 存储: hset key field value

127.0.0.1:6379> hset myhash username lisi

(integer) 1

127.0.0.1:6379> hset myhash password 123

(integer) 1

2. 获取: 

* hget key field: 获取指定的field对应的值

127.0.0.1:6379> hget myhash username

"lisi"

* hgetall key:获取所有的field和value

127.0.0.1:6379> hgetall myhash

1) "username"

2) "lisi"

3) "password"

4) "123"

3. 删除: hdel key field

127.0.0.1:6379> hdel myhash username

(integer) 1

4. 列表类型 list:可以添加一个元素到列表的头部(左边)或者尾部(右边)

1. 添加:

1. lpush key value: 将元素加入列表左表

2. rpush key value:将元素加入列表右边

127.0.0.1:6379> lpush myList a

(integer) 1

127.0.0.1:6379> lpush myList b

(integer) 2

127.0.0.1:6379> rpush myList c

(integer) 3

2. 获取:

* lrange key start end :范围获取

127.0.0.1:6379> lrange myList 0 -1

1) "b"

2) "a"

3) "c"

3. 删除:

* lpop key: 删除列表最左边的元素,并将元素返回

* rpop key: 删除列表最右边的元素,并将元素返回

5. 集合类型 set : 不允许重复元素

1. 存储:sadd key value

127.0.0.1:6379> sadd myset a

(integer) 1

127.0.0.1:6379> sadd myset a

(integer) 0

2. 获取:smembers key:获取set集合中所有元素

127.0.0.1:6379> smembers myset

1) "a"

3. 删除:srem key value:删除set集合中的某个元素

127.0.0.1:6379> srem myset a

(integer) 1

6. 有序集合类型 sortedset:不允许重复元素,且元素有顺序.每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

1. 存储:zadd key score value

127.0.0.1:6379> zadd mysort 60 zhangsan

(integer) 1

127.0.0.1:6379> zadd mysort 50 lisi

(integer) 1

127.0.0.1:6379> zadd mysort 80 wangwu

(integer) 1

2. 获取:zrange key start end [withscores]

127.0.0.1:6379> zrange mysort 0 -1

1) "lisi"

2) "zhangsan"

3) "wangwu"

127.0.0.1:6379> zrange mysort 0 -1 withscores

1) "zhangsan"

2) "60"

3) "wangwu"

4) "80"

5) "lisi"

6) "500"

3. 删除:zrem key value

127.0.0.1:6379> zrem mysort lisi

(integer) 1

7. 通用命令

1. keys * : 查询所有的键

2. type key : 获取键对应的value的类型

3. del key:删除指定的key value

4. 持久化

1. redis是一个内存数据库,当redis服务器重启,获取电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中。

2. redis持久化机制:

1. RDB:默认方式,不需要进行配置,默认就使用这种机制

* 在一定的间隔时间中,检测key的变化情况,然后持久化数据

1. 编辑redis.windwos.conf文件

#   after 900 sec (15 min) if at least 1 key changed

save 900 1

#   after 300 sec (5 min) if at least 10 keys changed

save 300 10

#   after 60 sec if at least 10000 keys changed

save 60 10000

2. 重新启动redis服务器,并指定配置文件名称

D:\JavaWeb2018\day23_redis\资料\redis\windows-64\redis-2.8.9>redis-server.exe redis.windows.conf

2. AOF:日志记录的方式,可以记录每一条命令的操作。可以每一次命令操作后,持久化数据

1. 编辑redis.windwos.conf文件

appendonly no(关闭aof) –> appendonly yes (开启aof)

# appendfsync always : 每一次操作都进行持久化

appendfsync everysec : 每隔一秒进行一次持久化

# appendfsync no : 不进行持久化

5. Java客户端 Jedis

* Jedis: 一款java操作redis数据库的工具.

* 使用步骤:

1. 下载jedis的jar包

2. 使用

//1. 获取连接

Jedis jedis = new Jedis("localhost",6379);

//2. 操作

jedis.set("username","zhangsan");

//3. 关闭连接

jedis.close();

* Jedis操作各种redis中的数据结构

1) 字符串类型 string

set

get

//1. 获取连接

        Jedis jedis = new Jedis();//如果使用空参构造,默认值 "localhost",6379端口

        //2. 操作

        //存储

        jedis.set("username","zhangsan");

        //获取

        String username = jedis.get("username");

        System.out.println(username);

        //可以使用setex()方法存储可以指定过期时间的 key value

        jedis.setex("activecode",20,"hehe");//将activecode:hehe键值对存入redis,并且20秒后自动删除该键值对

        //3. 关闭连接

        jedis.close();

2) 哈希类型 hash : map格式  

hset

hget

hgetAll

//1. 获取连接

        Jedis jedis = new Jedis();//如果使用空参构造,默认值 "localhost",6379端口

        //2. 操作

        // 存储hash

        jedis.hset("user","name","lisi");

        jedis.hset("user","age","23");

        jedis.hset("user","gender","female");

        // 获取hash

        String name = jedis.hget("user", "name");

        System.out.println(name);

        // 获取hash的所有map中的数据

        Map<String, String> user = jedis.hgetAll("user");

        // keyset

        Set<String> keySet = user.keySet();

        for (String key : keySet) {

            //获取value

            String value = user.get(key);

            System.out.println(key + ":" + value);

        }

        //3. 关闭连接

        jedis.close();

3) 列表类型 list : linkedlist格式。支持重复元素

lpush / rpush

lpop / rpop

lrange start end : 范围获取

//1. 获取连接

        Jedis jedis = new Jedis();//如果使用空参构造,默认值 "localhost",6379端口

        //2. 操作

        // list 存储

        jedis.lpush("mylist","a","b","c");//从左边存

        jedis.rpush("mylist","a","b","c");//从右边存

        // list 范围获取

        List<String> mylist = jedis.lrange("mylist", 0, -1);

        System.out.println(mylist);

        

        // list 弹出

        String element1 = jedis.lpop("mylist");//c

        System.out.println(element1);

        String element2 = jedis.rpop("mylist");//c

        System.out.println(element2);

        // list 范围获取

        List<String> mylist2 = jedis.lrange("mylist", 0, -1);

        System.out.println(mylist2);

        //3. 关闭连接

        jedis.close();

4) 集合类型 set  : 不允许重复元素

sadd

smembers:获取所有元素

//1. 获取连接

        Jedis jedis = new Jedis();//如果使用空参构造,默认值 "localhost",6379端口

        //2. 操作

        // set 存储

        jedis.sadd("myset","java","php","c++");

        // set 获取

        Set<String> myset = jedis.smembers("myset");

        System.out.println(myset);

        //3. 关闭连接

        jedis.close();

5) 有序集合类型 sortedset:不允许重复元素,且元素有顺序

zadd

zrange

//1. 获取连接

        Jedis jedis = new Jedis();//如果使用空参构造,默认值 "localhost",6379端口

        //2. 操作

        // sortedset 存储

        jedis.zadd("mysortedset",3,"亚瑟");

        jedis.zadd("mysortedset",30,"后裔");

        jedis.zadd("mysortedset",55,"孙悟空");

        // sortedset 获取

        Set<String> mysortedset = jedis.zrange("mysortedset", 0, -1);

        System.out.println(mysortedset);

        //3. 关闭连接

        jedis.close();

* jedis连接池: JedisPool

* 使用:

1. 创建JedisPool连接池对象

2. 调用方法 getResource()方法获取Jedis连接

//0.创建一个配置对象

        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(50);

        config.setMaxIdle(10);

        //1.创建Jedis连接池对象

        JedisPool jedisPool = new JedisPool(config,"localhost",6379);

        //2.获取连接

        Jedis jedis = jedisPool.getResource();

        //3. 使用

        jedis.set("hehe","heihei");

        //4. 关闭 归还到连接池中

        jedis.close();

* 连接池工具类

public class JedisPoolUtils {

    private static JedisPool jedisPool;

    static{

        //读取配置文件

        InputStream is = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.properties");

        //创建Properties对象

        Properties pro = new Properties();

        //关联文件

        try {

            pro.load(is);

        } catch (IOException e) {

            e.printStackTrace();

        }

        //获取数据,设置到JedisPoolConfig中

        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));

        config.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));

        //初始化JedisPool

        jedisPool = new JedisPool(config,pro.getProperty("host"),Integer.parseInt(pro.getProperty("port")));

    }

    /**

     * 获取连接方法

     */

    public static Jedis getJedis(){

        return jedisPool.getResource();

    }

}

## 案例:

案例需求:

1. 提供index.html页面,页面中有一个省份 下拉列表

2. 当 页面加载完成后 发送ajax请求,加载所有省份

* 注意:使用redis缓存一些不经常发生变化的数据。

* 数据库的数据一旦发生改变,则需要更新缓存。

* 数据库的表执行 增删改的相关操作,需要将redis缓存数据情况,再次存入

* 在service对应的增删改方法中,将redis数据删除。

java8 方法引用

https://www.cnblogs.com/xiaoxi/p/7099667.html

/src/method_reference/Person.java

package method_reference;

import java.time.LocalDate;

public class Person {
    public Person(String name, LocalDate birthday){
        this.name = name;
        this.birthday = birthday;
    }

    String name;
    LocalDate birthday;

    public LocalDate getBirthday(){
        return birthday;
    }

    public static int compareByAge(Person a, Person b){
        return a.birthday.compareTo(b.birthday);
    }

    @Override
    public String toString(){
        return this.name;
    }

}

/src/method_reference/testMethodReference.java

package method_reference;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;
import org.junit.Test;
import method_reference.Person;

public class testMethodReference {

    @Test
    public void test(){
        Person[] pArr = new Person[]{
                new Person("003", LocalDate.of(2016,9,1)),
                new Person("001", LocalDate.of(2016,2,1)),
                new Person("002", LocalDate.of(2016,3,1)),
                new Person("004", LocalDate.of(2016,12,1))
        };

//        //使用匿名类
//        Arrays.sort(pArr, new Comparator<Person>() {
//            @Override
//            public int compare(Person o1, Person o2) {
//                return o1.getBirthday().compareTo(o2.getBirthday());
//            }
//        });

//        //使用lambda表达式 未调用已存在的方法
//        Arrays.sort(pArr, (Person o1, Person o2) -> {
//            return o1.getBirthday().compareTo(o2.getBirthday());
//        });

        //使用方法引用,引用的是类的静态方法
        Arrays.sort(pArr, Person::compareByAge);

        System.out.println(Arrays.asList(pArr));
    }
}

微擎 人人商城 路由追踪

随便举个栗子

https://ssl.infowei.com/app/index.php?i=2&c=entry&m=ewei_shopv2&do=mobile&r=get_recommand&page=1&merchid=0&_=1568885489792

从链接可以看到访问的是/app/index.php

i(uniacid)->2

controller ->entry

module->ewei_shopv2

r->get_recommand

首先来看/app/index.php这个文件

1.初始化了框架

2.一直到最后controller 都是entry 然后运行_forward, 然后我们跟着forward找到$file(/app/source/entry/site.ctrl.php)

在执行__init.php之前 action 参数是空

$init = IA_ROOT . "/app/source/{$controller}/__init.php";
if(is_file($init)) {
	require $init;
}

然后/app/source/entry/__init.php里

if (empty($action)) {
	$action = 'site';
}

然后我们就获取到了action,用来在下面拼接file的路径(/app/source/entry/site.ctrl.php)

我不是太确定在这里是如何执行site.ctrl.php的, 追踪的时候发现require之后会直接执行site.ctrl.php

ok先不管 继续往下走

require _forward($controller, $action);
function _forward($c, $a) {
	$file = IA_ROOT . '/app/source/' . $c . '/' . $a . '.ctrl.php';
	return $file;
}

3.在site.ctrl.php会执行$site->$method()

在这里

$site->ewei_shopv2(这个应用)

$method->doMobileMobile

它会直接执行到/addons/ewei_shopv2/site.php 的 doMobileMobile方法

<?php
/**
 * [WeEngine System] Copyright (c) 2014 WE7.CC
 * WeEngine is NOT a free software, it under the license terms, visited http://www.we7.cc/ for more details.
 */

defined('IN_IA') or exit('Access Denied');

if (!empty($_W['uniacid'])) {
	$link_uniacid = table('uni_link_uniacid')->getMainUniacid($_W['uniacid'], $entry['module']);
	if (!empty($link_uniacid)) {
		$_W['uniacid'] = $link_uniacid;
		$_W['account']['link_uniacid'] = $link_uniacid;
	}
}

$site = WeUtility::createModuleSite($entry['module']);
if(!is_error($site)) {
	$do_function = $site instanceof WeModuleSite ? 'doMobile' : 'doPage';
	$method = $do_function . ucfirst($entry['do']);
	exit($site->$method());
}
exit();
require_once IA_ROOT . '/addons/ewei_shopv2/version.php';
require_once IA_ROOT . '/addons/ewei_shopv2/defines.php';
require_once EWEI_SHOPV2_INC . 'functions.php';
class Ewei_shopv2ModuleSite extends WeModuleSite
{
   public function getMenus()
   {
      global $_W;
      return array(
   array('title' => '管理后台', 'icon' => 'fa fa-shopping-cart', 'url' => webUrl())
   );
   }

   public function doWebWeb()
   {
      m('route')->run();
   }

   public function doMobileMobile()
   {
      m('route')->run(false);
   }

   public function payResult($params)
   {
      return m('order')->payResult($params);
   }
}

4.跟踪m()方法到 /addons/ewei_shopv2/core/inc/functions.php

if (!function_exists('m')) {
    function m($name = '')
    {
        static $_modules = array();
        if (isset($_modules[$name])) {

            return $_modules[$name];
        }
        $model = EWEI_SHOPV2_CORE . "model/" . strtolower($name) . '.php';

        if (!is_file($model)) {
            die(' Model ' . $name . ' Not Found!');
        }
        require_once $model;
        $class_name = ucfirst($name) . "_EweiShopV2Model";
        $_modules[$name] = new $class_name();
        return $_modules[$name];
    }
}

5.执行到了/addons/ewei_shopv2/core/model/route.php 的run方法

方法很长我没有仔细去看 我们的路由参数只有一段 所以直接进入这里

case 0:
   $file = $root . 'index.php';
   $class = 'Index';
case 1:
   $file = $root . $routes[0] . '.php';

   if (is_file($file)) {
      $class = ucfirst($routes[0]);
   }
   else if (is_dir($root . $routes[0])) {
      $file = $root . $routes[0] . '/index.php';
      $class = 'Index';
   }
   else {
      $method = $routes[0];
      $file = $root . 'index.php';
      $class = 'Index';
   }

   $_W['action'] = $routes[0];
   break;

最后执行到这里  $instance是Index_EweiShopV2Page的实例 $method是get_recommand

include $file;
$class = ucfirst($class) . '_EweiShopV2Page';
$instance = new $class();

if (!method_exists($instance, $method)) {
   show_message('控制器 ' . $_W['controller'] . ' 方法 ' . $method . ' 未找到!');
}

$instance->$method();
exit();

$file-> /addons/ewei_shopv2/core/mobile/index.php

文件的类名就是“Index_EweiShopV2Page”

6.在index.php里可以找到function

function get_recommand(){
   global $_W, $_GPC;
   $args = array(
      'page' => $_GPC['page'],
      'pagesize' => 6,
      'isrecommand' => 1,
      'order' => 'displayorder desc,createtime desc',
      'by' => ''
   );
   $recommand = m('goods')->getList($args);
   show_json(1,array('list'=>$recommand['list'], 'pagesize'=>$args['pagesize'], 'total'=>$recommand['total'], 'page'=>intval($_GPC['page'])));
}

 

Java之泛型 T与T的用法

<T> T表示返回值是一个泛型,传递啥,就返回啥类型的数据,而单独的T就是表示限制你传递的参数类型,这个案例中,通过一个泛型的返回方式,获取每一个集合中的第一个数据, 通过返回值<T> T 和T的两种方法实现


<T> T 用法

这个<T> T 表示的是返回值T是泛型,T是一个占位符,用来告诉编译器,这个东西先给我留着,等我编译的时候,告诉你。

package com.yellowcong.test;import java.util.ArrayList;import java.util.List;import org.apache.poi.ss.formula.functions.T;public class Demo {

    public static void main(String[] args) {

        Demo demo = new Demo();        //获取string类型
        List<String> array = new ArrayList<String>();
        array.add("test");
        array.add("doub");
        String str = demo.getListFisrt(array);
        System.out.println(str);        //获取nums类型
        List<Integer> nums = new ArrayList<Integer>();
        nums.add(12);
        nums.add(13);

        Integer num = demo.getListFisrt(nums);
        System.out.println(num);
    }    /**
     * 这个<T> T 可以传入任何类型的List
     * 参数T
     *     第一个 表示是泛型
     *     第二个 表示返回的是T类型的数据
     *     第三个 限制参数类型为T
     * @param data
     * @return
     */
    private <T> T getListFisrt(List<T> data) {        if (data == null || data.size() == 0) {            return null;
        }        return data.get(0);
    }

}

T 用法

返回值,直接写T表示限制参数的类型,这种方法一般多用于共同操作一个类对象,然后获取里面的集合信息啥的。

package com.yellowcong.test;import java.util.ArrayList;import java.util.List;

public class Demo2<T> {

    public static void main(String[] args) {        //限制T 为String 类型
        Demo2<String> demo = new Demo2<String>();        //获取string类型
        List<String> array = new ArrayList<String>();
        array.add("test");
        array.add("doub");        String str = demo.getListFisrt(array);        System.out.println(str);        //获取Integer类型 T 为Integer类型
        Demo2<Integer> demo2 = new Demo2<Integer>();        List<Integer> nums = new ArrayList<Integer>();
        nums.add(12);
        nums.add(13);        Integer num = demo2.getListFisrt(nums);        System.out.println(num);
    }    /**
     * 这个只能传递T类型的数据
     * 返回值 就是Demo<T> 实例化传递的对象类型
     * @param data
     * @return
     */
    private T getListFisrt(List<T> data) {        if (data == null || data.size() == 0) {            return null;
        }        return data.get(0);
    }
}

java 泛型详解

对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。

本文参考java 泛型详解、Java中的泛型方法、 java泛型详解

1. 概述

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2. 一个栗子

一个被举了无数次的例子:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

毫无疑问,程序的运行结果会以崩溃结束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,在使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<String>();
...//arrayList.add(100); 在编译阶段,编译器就会报错

3. 特性

泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}

输出结果:泛型测试: 类型相同

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

4. 泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

1)泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{  
    private 泛型标识 /*(成员变量类型)*/ var; 
    .....
  }
}

一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;    
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.Generic<Integer> genericInteger = new Generic<Integer>(123456);
//传入的实参类型需与泛型的类型参数类型相同,即为String.Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());

09-16 09:20:04.432 13063-13063/ 泛型测试: key is 123456
09-16 09:20:04.432 13063-13063/ 泛型测试: key is key_vlaue

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

看一个例子:

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());

泛型测试: key is 111111
泛型测试: key is 4444
泛型测试: key is 55.55
泛型测试: key is false

 

注意:

  • 泛型的类型参数只能是类类型,不能是简单类型。

  • 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

  if(ex_num instanceof Generic<Number>){ }

2)泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {    
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" 
*/
 
 class FruitGenerator<T> implements Generator<T>{
    @Override    
    public T next() {        
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 
*/
 public class FruitGenerator implements Generator<String> {    
    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override    
    public String next() {
        Random rand = new Random();        
        return fruits[rand.nextInt(3)];
    }
}

3)泛型通配符

我们知道IngeterNumber的一个子类,同时在特性章节中我们也验证过Generic<Ingeter>Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?

为了弄清楚这个问题,我们使用Generic<T>这个泛型类继续看下面的例子:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

 

Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gInteger);// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>// showKeyValue(gInteger);

 

通过提示信息我们可以看到Generic<Integer>不能被看作为`Generic<Number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<Integer>类型的类,这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。

我们可以将上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!

此处’?’是类型实参,而不是类型形参 ! 

此处’?’是类型实参,而不是类型形参 !

再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

可以解决当具体类型不确定的时候,这个通配符就是 ?  ;

当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。

尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。

泛型类,是在实例化类的时候指明泛型的具体类型;

泛型方法,是在调用方法的时候指明泛型的具体类型 。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 
 */
 public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,IllegalAccessException{
        T instance = tClass.newInstance();        
        return instance;
}

 

Object obj = genericMethod(Class.forName("com.test.test"));

1)泛型方法的基本用法

光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把泛型方法再总结一下。

public class GenericTest {   
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;        
        public Generic(T key) {            
            this.key = key;
        }        
        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。        
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。        
        //所以在这个方法中才可以继续使用 T 这个泛型。
        
        public T getKey(){            
            return key;
        }        
        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
            public E setKey(E key){
                 this.key = keu
            }        
        */
    }    
    
    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }     
    */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());        
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();        
        return test;
    }   
     
    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }    
    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?    
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }     
    
    /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }    
    */

    public static void main(String[] args) {


    }
}

2)类中的泛型方法

当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下

public class GenericFruit {    
    class Fruit{
        @Override        
        public String toString() {            
            return "fruit";
        }
    }    
    
    class Apple extends Fruit{
        @Override        
        public String toString() {            
            return "apple";
        }
    }    
    
    class Person{
        @Override        
        public String toString() {            
            return "Person";
        }
    }    
    
    class GenerateTest<T>{        
        public void show_1(T t){
            System.out.println(t.toString());
        }        
        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。        
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }        
        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }    
    
    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();  
              
        //apple是Fruit的子类,所以这里可以        
        generateTest.show_1(apple);      
          
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person        
        //generateTest.show_1(person);
                
        //使用这两个方法都可以成功        
        generateTest.show_2(apple);
        generateTest.show_2(person); 
               
        //使用这两个方法也都可以成功        
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

3) 泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

public <T> void printMsg( T... args){    
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa","2323.4",55.55);

4) 静态方法与泛型

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

public class StaticGenerator<T> {
    ....
    ....    
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"     
     */
    public static <T> void show(T t){

    }
}

5) 泛型方法总结

泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

6) 泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

如果我们把泛型类的定义也改一下:

public class Generic<T extends Number>{    
    private T key;    
    public Generic(T key) {        
        this.key = key;
    }    
    public T getKey(){        
        return key;
    }
}

 

//这一行代码也会报错,因为String不是Number的子类Generic<String> generic1 = new Generic<String>("11111");

再来一个泛型方法的例子:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();    return test;
}

通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。

7) 关于泛型数组要提一下

看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:

List<String>[] ls = new ArrayList[10];

下面使用Sun的一篇文档的一个例子来说明这个问题:

List<String>[] lsa = new List<String>[10]; 
// Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; 
// Unsound, but passes run time store check    
String s = lsa[1].get(0); 
// Run-time error: ClassCastException.

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; 
// OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; 
// Correct.    
Integer i = (Integer) lsa[1].get(0); 
// OK

5. 最后

本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。

laravel __construct函数 无法使用session() 解决方法和实例

从网上查询资料得知,在laravel中使用__construct方法的时候,session还没开启,此时就需要用到中间件来解决。

$this->middleware(function ($request, $next) {
    var_dump(\Session::get('userid'));
    exit;
});

我这里用到的__construct函数,是用来判断同一个账号是否在另一个客户端登录,比如A客户端正在登录,此时B客户端也要登录,B客户端登录成功后,A客户端就会退出

通过验证当前的登录IP是否一致来判断。

这是一个公共类,该类继承了Controller类,所以只要用到这个公共类,其他类只需把继承的Controller类换成该公共类就可以了

public function __construct(Request $request)
{
    // 判断是否已在客户端登录
    $this->middleware(function ($request, $next) {
        $ip = md5($_SERVER['REMOTE_ADDR']);//当前的IP,md5加密
        $res = DB::table('users')->where(['uid'=>\Session::get('userid')])->first();
        if ($ip != $res->IP) {
            return redirect('/adminlogin');<br>            exit;
        }
          
        return $next($request);
    });
}

如何在群晖NAS上面使用Crontab

与其他类Unix系统不同的是,群晖上面并不能直接crontab -e来管理计划任务。

默认群晖是不允许root直接登陆的,所以我们要先来切换用户身份到root。

admin@DiskStation:~$ sudo -i
Password:

切换到root用户之后,我们就可以使用文本编辑工具(vim或者nano)来便捷crontab的计划任务了。

root@DiskStation:~# vim /etc/crontab
MAILTO=""PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
#minute hour    mday    month   wday    who command0 0 * * * echo "hello synology"

最后restart一下crontab服务就可以了。

root@DiskStation:~# synoservice -restart crond

注:After a shutdown or reboot, you’ll need to restart the cron deamon to enable it again.

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed

每分钟执行一次

*/1   *  *  *  * root run-parts   /root/runcron

100.64.0.0/10运营商级(Carrier-grade)NAT保留IP地址

在一次跟踪路由的网络操作时发现自己路由器下一跳路由节点的IP地址比较奇怪,是100.64.0.1。好奇促使我查询了这个IP地址的归属,结果是保留地址,到这里觉得比较奇怪了,按照常理以IPv4为例保留的IP地址一般为以下几种,常用于内网通讯或者特殊用途:

地址块 起始 结束 备注
10.0.0.0/8 10.0.0.0 10.255.255.255 局域网分配
172.16.0.0/12 172.16.0.0 172.31.255.255 局域网分配
192.0.0.0/24 192.168.0.0 192.168.255.255 局域网分配
169.254.0.0/16 169.254.0.0 169.254.255.255 两台主机对等连接,当Windows获取不到IP地址的时候会自动分配此类地址
127.0.0.0/8 127.0.0.0 127.255.255.255 回环(loopback)地址,表示本机
255.255.255.255/32 255.255.255.255 255.255.255.255 广播地址

这个100.64开头的IP地址又是什么鬼?搜索维基百科的Reserved IP Address词条才发现原来教科书上介绍的只是部分保留地址,整个保留地址家族的成员还是比较多的,那么100.64打头的IP地址对应地址块为100.64.0.0/10,地址范围为100.64.0.0~100.127.255.255,共包含有4,194,304个IP地址,这个保留地址也是用于内网,但是这个内网不是一般内网而是Carrier-grade NAT,这个英文对应的翻译是“运营商级NAT”。进一步搜索得知2012年4月的 RFC 6598 (IANA-Reserved IPv4 Prefix for Shared Address Space) 将100.64.0.0/10 (Shared Address Space) 地址块,用于给运营商ISP使用:

NetRange:       100.64.0.0 - 100.127.255.255
CIDR:           100.64.0.0/10
OriginAS:
NetName:        SHARED-ADDRESS-SPACE-RFCTBD-IANA-RESERVED
NetHandle:      NET-100-64-0-0-1
Parent:         NET-100-0-0-0-0
NetType:        IANA Special Use

据此可以推断出电信的黑科技实际上是使用了一个类似路由的设备将我们的网络组织成一个局域网,再通过网址转换NAT技术来实现网络通信,可以这么理解吧,我们以前通过PPPoE拨号获取的是随机分配的公网IP地址,这个IP地址是完全可以被互联网上任何设备访问的无障碍地址,但是现在通过拨号获取的只是一个内网地址,也就是说我们自己的路由器或者电脑拨号连接到了电信运营商的“大路由器”上,再也没有公网IP地址了。

为什么电信要使用这种方式来管理用户上网?首先我们要知道IPv4地址已经告枯竭,2011年2月4日全球顶级IP地址分配机构IANA分配完了最后5个A级地址块,因此IANA宣告IPv4地址告罄。2011年4月15日,APNIC宣布其剩余的可自由分配的IPv4地址已全部分配完毕,亚太地区进入IPv4地址耗尽阶段。随后几个月,中国各大运营商从APNIC以会员资格申请到了最后的几个C,至此各运营商也陷入了IPv4地址耗尽的困境。由于IPv6技术进展缓慢,为了解决IPv4耗尽对电信企业日益增长的用户对于网络需求的影响,NAT444/NAT44的方案由此诞生。

NAT444/NAT44方案说白了就是将过去每个宽带用户独立分配公网IP的方式改为分配内网IP给每个用户,运营商再对接入的用户统一部署NAT设备,NAT的作用就是将用户网络连接发起的内网IP,以端口连接的形式翻译成公网IP,再对外网资源进行连接。这样做以后,可以实现多个用户共享一个IP地址,在技术实现原理上和我们家用的路由器一样,其中NAT444对应传统宽带用户,NAT44则对应3G或者4G移动上网终端。

这样做的好处显而易见,首先延缓了IPv4耗尽对电信业务带来的威胁并进一步为IPv6技术部署留足时间,其次是内网保护,所有接入互联网的主机都不再直接接入,而是出于电信NAT设备的保护之下,没有独立的公网IP,所有端对端的远控软件将会失效,比如以前的灰鸽子木马,由于主机丧失了公网IP,灰鸽子反弹的IP地址将是电信NAT设备所使用的公网IP,黑客无法以此定位到中了灰鸽子木马的具体主机,不过这不影响第三方端口转发的远控软件,比如TeamViewer,但是对传统远控软件杀伤力是巨大的。最后增强了隐私保护,对于远端的网站通过IP地址跟踪等形式采集用户数据,或者以后台记录IP的形式定位用户身份的行为将不再有效,因为这个IP地址可以同一时间段内被大量用户所使用,所以部分广告商可能需要修改广告跟踪代码的相关逻辑,这边顺带说一句并不意味着处于电信NAT保护的用户可以做违法的事情,比较电信公司不是傻瓜,电信级NAT设备会有日志记录系统,所以处于其下的网络设备的上网流量都是得到有效审计的。

这样做的坏处是什么呢?首先丧失了公网IP让极客们的生活失去了不少乐趣,比如原先通过花生壳动态域名解析服务可以定位到我们家的上网设备,然后可以在上面架设一些服务等,从最原始的网站服务(电信封锁了80端口)到远程私有云存储,甚至是个人VPN等等,现在都不能直接使用了,因为动态域名服务解析的IP地址是电信级NAT设备所使用的公网IP,同时也是若干电信用户所共享的IP地址,NAT并不会转发你的请求到特定的电信用户,所以这些服务都失效了,其实这对于安装远程网络监控的公司或者个人带来的麻烦最大,原来只要配置好动态域名解析就可以在任何地方使用网络进行监控查看,现在都不可以了。其次共用IP地址容易让部分网络服务提供商误杀合法用户,比如一位非法的资源滥用电信用户和你在共享同一个公网IP地址,非法的资源滥用者由于滥用某个网络服务商资源从而被加入黑名单,注意这里只有将你们所共享的公网IP地址加入了黑名单,所以导致你这个合法的用户也不能正常使用该网络服务商所提供的资源了。最后这样做的坏处还有影响到电驴等P2P软件的使用,因为处于内网下导致电驴等软件一直是LowID,影响传输速度。

如何解决此问题?要想重新获取公网IP,最后还是要联系电信运营商,不行只有进行投诉解决了。

后来发现这种现象普遍存在,甚至有电信运营商直接分配172或者10开头的内网IP地址,但这种分配方式容易与用户自己的路由器IP地址冲突,所以还是100.64开头这种好一些,毕竟新设立专用于ISP运营的保留地址,个人网络设备占用的可能性较低。

希望IPv6能够尽快全面推广开,从而解决现在尴尬的局面。

让电信更改一下账号模式(非nat模式)