手写一个jdbc数据库连接池

Java访问mysql的时候,需要用到jdbc驱动,传统连接方式为:

1
2
3
4
5
6
7
8
9
10
11
try {
Driver mysqlDriver = (Driver) Class.forName("com.mysql.jdbc.Driver").newInstance();
DriverManager.registerDriver(mysqlDriver);
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.0.***:3306/rzframe?useSSL=false&serverTimezone=UTC", "root", "*******");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from rz_user");//查询
connection.close();
} catch (Exception e) {
e.printStackTrace();
}

我们对上面的代码做一个简单的性能测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 1000; i++) {
try {
CountDownLatch finalCountDownLatch = countDownLatch;
Thread thread = new Thread(() -> {
try {
doJDBC();
} catch (Exception ex) {

} finally {
finalCountDownLatch.countDown();
}
});
thread.start();
if (i != 0 && i % 100 == 0) {
countDownLatch.await();
System.out.println(i);
countDownLatch = new CountDownLatch(100);
}
} catch (Exception ex) {

}
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));

}

上面代码用了100个线程分批次去完成查询的动作,在我的机器上运行时间45s左右。

从上面的代码可以看出问题,Connection对象每一次都是重新创建,查询完成后,直接是调用close方法,如果不释放,会报连接数过多的异常。 如果查询多次,那浪费在创建Connection的时间就会很多,我们知道在程序优化的手段中,有一个池化可以很好的解决这个问题。

池化的概念就是先创建多个对方存在在一个容器中,当时候的时候可以直接拿出来时候,用完后再进行归还。 跟着这个思想,我们来创建自己的连接池。

编写思路

  1. 创建一个线程安全的容器(由于是多线程访问),队列或者是list,因为Connection的对象并不是有序的,所以可以使用list容器

  2. 对Connection的对象进行封装,增加一个isBusy变量,每次读取的时候就可以选出空闲的Connection对象

  3. 如果取的时候,没有可用的Connection对象,则可以再自动创建对象,可以自动扩容,直到扩容到允许的最大值。

封装的Connection类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PooledConnection {

private boolean isBusy=false;
private Connection connection;

public PooledConnection(Connection connection, boolean b) {
this.isBusy=b;
this.connection=connection;
}

public boolean isBusy() {
return isBusy;
}

public void setBusy(boolean busy) {
isBusy = busy;
}

public Connection getConnection() {
return connection;
}

public void setConnection(Connection connection) {
this.connection = connection;
}
public void close() {
this.setBusy(false);
}
}

包装好Connection后,可以考虑如何对Connection进行创建和分配,需要有以下几个方法:

1
2
3
PooledConnection getPooledConnection();

void createPooledConnection();

为了更好的程序调试,先定义几个初始的参数变量:

1
2
3
4
5
6
7
8
9
10
11
12
//数据库相关参数
private static String jdbcDriver = null;
private static String jdbcUrl = null;
private static String userName = null;
private static String password = null;

//容器参数
private static int initCount;//初始数量
private static int stepSize;//每次扩容的数量
private static int poolMaxSize;//最大数量
//全局锁
private static Lock lock;

为了保证线程安全,使用线程安全的Vector集合。

获得对象方法

  1. 获得对象的方法,应该是先找到一个空闲的PooledConnection变量,如果有就直接返回。

  2. 如果没有空闲的变量,则尝试进行扩充,扩充由一个线程完成,其他线程则等待,或者尝试再次获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public PooledConnection getPooledConnection() throws RuntimeException, SQLException {
PooledConnection realConnection = getRealConnection();
while (realConnection == null) {
if (lock.tryLock()) {//尝试获取锁
createConnections(stepSize);//只能让一个线程扩容 获得锁之后进行扩容
lock.unlock();
} else {
try {
Thread.sleep(200);//线程等待
} catch (InterruptedException e) {
}
}
realConnection = getRealConnection();//再次尝试获取
if (realConnection != null) {
return realConnection;
}
}
System.out.println("线程池线程数量:" + PoolsConnections.size());
return realConnection;
}

private PooledConnection getRealConnection() throws SQLException {
for (PooledConnection pooledConnection : PoolsConnections) {
try {
if (pooledConnection.isBusy())
continue;
Connection connection = pooledConnection.getConnection();
if (!connection.isValid(200)) {//是否有效,200ms 没有被超时
System.out.println("连接无效");
Connection validConnect = DriverManager.getConnection(jdbcUrl, userName, password);
pooledConnection.setConnection(validConnect);
}
pooledConnection.setBusy(true);
return pooledConnection;
} catch (SQLException e) {
return null;
}
}
return null;
}

扩容方法对象

扩容的方法相对比较简单,判断当前对象数量有没有溢出,如果没有溢出,就进行扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void createConnections(int count) throws OutofMaxCountException, IllegalArgumentException {
if (poolMaxSize <= 0) {
System.out.println("创建管道对象失败,最大值参数错误");
throw new IllegalArgumentException("创建管道对象失败,最大值参数错误");
}
//判断是否有溢出
boolean overFlow = isOverFlow(count);
if (overFlow) {
return;
}
System.out.println("扩容");
for (int i = 0; i < count; i++) {
try {
overFlow = isOverFlow(count);
if (overFlow)
return;
Connection connection = DriverManager.getConnection(jdbcUrl, userName, password);
PooledConnection pooledConnection = new PooledConnection(connection, false);
PoolsConnections.add(pooledConnection);
} catch (SQLException e) {
e.printStackTrace();
}
}
System.out.println("扩容数量:" + PoolsConnections.size());
}

private boolean isOverFlow(int count) {
if (PoolsConnections.size() + count >= poolMaxSize) {
return true;
}
return false;
}

上面的代码隐藏一个问题,我们增加对数据的查询方法,方便我们测试。 查询方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ResultSet querySql(String sql) {
try {
PooledConnection pooledConnection = getPooledConnection();
Connection connection = pooledConnection.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
Thread.sleep(1000);
pooledConnection.close();
return resultSet;
} catch (Exception e) {

}
return null;
}

我们对代码做性能测试同样的测试,在我的电脑运行时间为5s左右,大概快了10倍。 但经过多次测试,代码抛出了ConcurrentModificationException异常,这个异常的原因是因为在使用的时候,我们又修改了正在使用的对象。所以在使用的时候要对对象进行加一个读写锁。

为了锁不至于影响到锁的性能,我们把锁碎片化,采用针对每一个对象进行加锁,而不是全局加锁。修改后的封装对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class PooledConnection {
private boolean isBusy = false;
private Connection connection;
private ReentrantReadWriteLock reentrantReadWriteLock;

public PooledConnection(Connection connection, boolean b) {
this.connection = connection;
reentrantReadWriteLock = new ReentrantReadWriteLock();
}
public boolean isBusy() {
return isBusy;
}

public void setBusy(boolean busy) {
isBusy = busy;
}

public Connection getConnection() {
return connection;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
public void close() {
this.setBusy(false);
}
public void shutDown() {
try {
this.connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

//增加读写锁的操作
public void writeLock() {
this.reentrantReadWriteLock.writeLock().lock();
}
public void unWriteLock() {
this.reentrantReadWriteLock.writeLock().unlock();
}
public void readLock() {
this.reentrantReadWriteLock.readLock().lock();
}
public void unReadLock() {
this.reentrantReadWriteLock.readLock().unlock();
}
}

最终结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public PooledConnection getPooledConnection() throws RuntimeException, SQLException {
if (poolMaxSize <= 0) {
System.out.println("创建管道对象失败,最大值参数错误");
throw new IllegalArgumentException("创建管道对象失败,最大值参数错误");
}
PooledConnection realConnection = getRealConnection();
while (realConnection == null) {
if (lock.tryLock()) {//尝试获取锁
createConnections(stepSize);//获得锁之后进行扩容
lock.unlock();
} else {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
realConnection = getRealConnection();
if (realConnection != null) {
return realConnection;
}
}

return realConnection;
}

private PooledConnection getRealConnection() {
for (PooledConnection pooledConnection : PoolsConnections) {
try {
if (pooledConnection.isBusy())
continue;
/*
此处要保证写的时候不能被读取,不然会报ConcurrentModificationException异常
*/
pooledConnection.writeLock();//读写互斥,写写互斥
Connection connection = pooledConnection.getConnection();
if (!connection.isValid(200)) {//是否有效,200ms 没有被超时
Connection validConnect = DriverManager.getConnection(jdbcUrl, userName, password);
pooledConnection.setConnection(validConnect);
}
pooledConnection.setBusy(true);
pooledConnection.unWriteLock();
return pooledConnection;
} catch (SQLException e) {

return null;
}
}
return null;
}
public void createConnections(int count) throws OutofMaxCountException, IllegalArgumentException {
if (poolMaxSize <= 0) {
System.out.println("创建管道对象失败,最大值参数错误");
throw new IllegalArgumentException("创建管道对象失败,最大值参数错误");
}
//判断是否有溢出
boolean overFlow = isOverFlow(count);
if (overFlow) {
return;
}
System.out.println("扩容");
for (int i = 0; i < count; i++) {
try {
overFlow = isOverFlow(count);
if (overFlow)
return;
Connection connection = DriverManager.getConnection(jdbcUrl, userName, password);
PooledConnection pooledConnection = new PooledConnection(connection, false);
PoolsConnections.add(pooledConnection);
} catch (SQLException e) {

}
}
System.out.println("扩容数量:" + PoolsConnections.size());
}

private boolean isOverFlow(int count) {
if (PoolsConnections.size() + count >= poolMaxSize) {
return true;
}
return false;
}

碰到问题

  1. 首先是无法控制连接最大的数量 ,问题出在扩容没有控制一个线程扩容,使用tryLock解决,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    while (realConnection == null) {
    if (lock.tryLock()) {//尝试获取锁
    createConnections(stepSize);//只能让一个线程扩容 获得锁之后进行扩容
    lock.unlock();
    } else {
    try {
    Thread.sleep(200);
    } catch (InterruptedException e) {
    }
    }
    realConnection = getRealConnection();
    if (realConnection != null) {
    return realConnection;
    }
    }

  2. ConcurrentModificationException异常,在读取的使用的时候,对象有写入操作,需要保证读取可以并发,读写不能一起,写不同对象是可以并发,使用读写锁可以解决:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    reentrantReadWriteLock.writeLock().lock();//读写互斥,写写互斥
    if (!connection.isValid(2000)) {//是否有效,200ms 没有被超时
    System.out.println("连接无效");
    Connection validConnect = DriverManager.getConnection(jdbcUrl, userName, password);
    pooledConnection.setConnection(validConnect);
    }
    pooledConnection.setBusy(true);
    reentrantReadWriteLock.writeLock().unlock();

    1
    2
    3
    4
    reentrantReadWriteLock.readLock().lock();
    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(sql);
    reentrantReadWriteLock.readLock().unlock();

    使用上面的代码会存在一个性能问题,就是在写入的时候,如果写入的是不同对象,写入也会进行排斥,所以应该对单个PooledConnection使用锁。

  3. 把锁进行碎片化优化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    public class PooledConnection {
    private boolean isBusy = false;
    private Connection connection;
    private boolean isUsing = false;
    private ReentrantReadWriteLock reentrantReadWriteLock;

    public PooledConnection(Connection connection, boolean b) {
    this.isBusy = b;
    this.connection = connection;
    this.isUsing = false;
    reentrantReadWriteLock = new ReentrantReadWriteLock();
    }
    public PooledConnection() {

    reentrantReadWriteLock = new ReentrantReadWriteLock();
    }
    public boolean isBusy() {
    return isBusy;
    }
    public void setBusy(boolean busy) {
    isBusy = busy;
    }
    public Connection getConnection() {
    this.isUsing = true;
    return connection;
    }
    public boolean isUsing() {
    return isUsing;
    }
    public void setUsing(boolean using) {
    isUsing = using;
    }
    public void setConnection(Connection connection) {
    this.connection = connection;
    }
    public void close() {

    this.isUsing = false;
    this.setBusy(false);
    }
    public void shutDown() {
    try {
    this.connection.close();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    public void writeLock() {
    this.reentrantReadWriteLock.writeLock().lock();
    }
    public void unWriteLock() {
    this.reentrantReadWriteLock.writeLock().unlock();
    }
    public void readLock() {
    this.reentrantReadWriteLock.readLock().lock();
    }
    public void unReadLock() {
    this.reentrantReadWriteLock.readLock().unlock();
    }
    }

读的时候加入读锁:

1
2
3
4
5
6
7
8
9
10
11
12
PooledConnection pooledConnection = getPooledConnection();
/*
此处要保证读的时候不能被修改,使用读锁
*/
pooledConnection.readLock();
Connection connection = pooledConnection.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
Thread.sleep(1000);
pooledConnection.close();
pooledConnection.unReadLock();
return resultSet;

写入加锁:

1
2
3
4
5
6
7
8
9
pooledConnection.writeLock();//读写互斥,写写互斥
Connection connection = pooledConnection.getConnection();
if (!connection.isValid(200)) {//是否有效,200ms 没有被超时
Connection validConnect = DriverManager.getConnection(jdbcUrl, userName, password);
pooledConnection.setConnection(validConnect);
}
pooledConnection.setBusy(true);
pooledConnection.unWriteLock();
return pooledConnection;

优化后耗时:耗时为:3692ms 。

mysql安装和连接踩坑记录

原来一直使用mysql,没有自己真正的搭建,搭建也只是本地的连接,现在手上有两台电脑,想搭建一个主从的架构,在配置mysql的环节费了一点时间,其实都是很小的问题,今天记录下:

  1. mysql 安装和启动

    1
    2
    3
    4
    5
    6
    mysql --install 

    mysql --initialize

    net start mysql

    安装完之后,发现忘了记密码,可以到 mysql-8.0.16-winx64\data中找到.err文件,里面有初始密码 ,本地用localhost连上之后可以自己修改密码。

    mysql安装和连接踩坑记录

mysql安装和连接踩坑记录

  1. 远程连接

    mysql 需要远程连接需要权限,错误的信息如下:

    1
    2
    错误号码1130
    Host 'xxxxxxxxx' is not allowed to connect to this MySQL server

    执行下面的代码可以解决:

    1
    2
    use mysql;
    update user set host = '%' where user = 'root'
  2. 如果碰到无法更新,因为mysql 更新使用安全模式,需要关闭.

    报错信息如下:

    1
    2
    Error Code: 1175. You are using safe update mode and you tried to      update a table without a WHERE that uses a KEY column.  To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.     

    执行以下sql:

    1
    SET SQL_SAFE_UPDATES = 0;   
  3. 连接远程的mysql时报错,原因时mysql8.0的加密方法变了。

    1
    2
    3
    错误号码2058

    Plugin caching_sha2_password could not be loaded: �Ҳ���ָ����ģ�顣
    1
    2
    3
    4
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'password' PASSWORD EXPIRE NEVER; #修改加密规则 
    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; #更新一下用户的密码 
    FLUSH PRIVILEGES; #刷新权限

一篇文章说完Java的垃圾回收过程

想要了解Java的垃圾回收过程首先要理解Java堆的内存模型,简单表示如下:

Java内存结构

从上面的图片可以看出,Java的堆内存可以简单的分为,新生代和老年代,在新生代中有分为三个区域,1个Eden区和2个Survivor区(分别叫from和to),默认比例为8:1

了解了垃圾回收的内存模型,下面就可以看下垃圾回收的过程。

  1. 创建一个新对象,判断是否大于或等于大对象的阈值(JVM用-XX:PretenureSizeThreshold来定义),如果判断为大对象 ,直接进入老年代。

  2. 如果不属于大对象,则优先在新生代Edge区分配内存,如果能够存放,则直接分配内存,对象创建结束,不存在垃圾回收过程

  3. 当Eden没有足够的空间进行分配的时候,虚拟机开始在新生代进行垃圾回收(MinorGC)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    新生代采用复制算法进行回收垃圾,过程如下:

    1. 在MinorGC前会对老年区的剩余空间进行统计,如果剩余区域的和大于新生代的所有对象之和,则开始进行一次`Minor GC`

    2. `Minor GC`采用复制算法进行垃圾回收,具体过程是把Eden区域的存活得对象拷贝到From区域,所有From区域的对象的年龄+1。From中的对象根据对象的年代决定是进入To,还是进入老年代。进入老年代的参数默认是年龄是15,可以通过`-XX:MaxTenuringThreshold`参数设置。

    3. 清空Eden和from区域,把To和from空间进行交换,保证To区域始终未空区域。

    4. 如果老年代剩余的内存空间小于新生代所有的对象,虚拟机会查看是否允许担保失败.

    5. 如果允许,则虚拟机会继续检查可用空间是否大于历次晋升到老年代的平均水平,如果条件成立,则尝试进行一次`MinorGC` ,显然这样回收是`有风险的`, 如果晋升的对象空间大于老年代的剩余空间,则会触发一次`Full GC`

    6. 虚拟机会查看不允许允许担保失败, 则会直接触发`Full GC`
  4. From空间对象晋升为老年代的时候,为了适应更多内存情况,JVM会检查在Survivor空间中相同年龄所有对象的大小综合大于Survivor空间的一半,则年龄大于或等于这个年龄的对象可以直接进入老年代。

  5. 老年代空间不足时、手工调用System.GC()会触发Full GC

Java的垃圾回收

Java内存模型

Java把内存大概分为:方法区,虚拟机栈,本地方法栈,堆和程序计数器5个区。

程序计数器 是一块比较小的内存空间,可以看成是一个线程的运行的指示器,是一个线程私有的内存。由于JVM的运行时通过线程抢占CPU的时间片实现的(在任何一个时刻,一个CPU只能执行一个指令),如果存在线程的上下文切换,每个线程在被移除时间片的时候都需要记录执行的位置。如果当前正在执行的是Native方法,则不需要计数器来标记。

虚拟机栈也是线程私有的内存,Java代码在运行的时候,会对每一个方法创建一个栈帧,用于存储局部变量,操作数据,动态链接等数据。

本地方法栈 和虚拟机栈作用类似,本地方法栈是为了Java调用Native方法服务。

Java堆 是Java常用的引用对象的真实存储的区域,也是修改和访问最为频繁的区域。

Java的内存模型分类大致如下:

Java内存结构

垃圾回收算法


标记清除方法

标记清除算法是最基础的算法,算法分为“标记”和“清除”两个阶段: 先标记出需要回收的对象,然后再统一回收所有标记的对象。

标记清除算法的缺点主要有两个:一个效率比较慢,一个是会产生内存碎片。碎片太多会导致在分配较大对象的时候,无法找到连续的空间,而不得不提前触发一次垃圾回收。

复制算法

为了解决标记清除法的效率问题,出现了复制算法。把内存分为大小相同的两个部分。使用的时候,只是用其中的一块,每次进行垃圾回收的时候,就把存活的对象复制到另外一块内存,然后把当前的内存区域直接清空。

复制算法比较简单高效,但是带来的后果是内存的浪费,可使用的内存只有原来的半。

标记整理算法

结合上面两种算法的有点和缺点,标记整理的算法就能够很好的解决浪费和内存的碎片问题。

标记整理的算法,标记过程与标记清除算法相同,只是在回收的时候,把存活得对象都向一侧移动,然后直接清理掉边界以外的内存。

标记整理虽然解决了内存的碎片化的问题,但还是没有解决性能问题。

分代收集算法

分代收集算法是一个综合算法,根据不同对象的生存周期而采用上面三种算法。

Java一般把堆内存分为老年代和新生代,新生代存活率低,只需要少量存活得空间就可以了回收,则可以使用复制算法。

老年代存活率高,没有额外的空间担保,就只能是由标记整理或者标记清除方法。

HotSpot垃圾回收算法

在Hotspot的虚拟机中,如何实现垃圾内存的标记,首先我们需要理解一个概念:GC Root节点和可达性分析

GC Root节点包含以下几种类型:

 1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。

 2. 本地方法栈中JNI(即一般说的native方法)引用的对象。

 3. 方法区中的静态变量和常量引用的对象。  

在Java中判断一个对象的是否存活,都是通过判断这个对象是否具有可达性,即变量是否被GC Root节点引用。

在HotSpot的虚拟机中,使用一组ooPMap的数据结构来记录对象的引用关系。

Java中常用的收集器


  1. Serial收集器

    Serial是最基本的,时间最长的垃圾收集器,采用复制算法回收。Serial在JDK1.3以前都已经在使用了,从名字可以看出Serial收集器是单线程工作的。单线程带来的问题是在程序在垃圾回收的时候,会出现停顿。 Serial收集器在有的场景中的有点点也很多,由于没有cpu上下文的切换,是的Serial收集器相对比较简单高效,短暂的暂停只要不是过于频繁,还是能够被接受的。

  2. ParNew收集器

    ParNew是Serial收集器的多线程版本,可以通过XX:ParallelGCThread参数来控制会受到的线程数,在单个CPU的环境下,由于存在线程的上下文的切换,所以性能不一定能保证优于Serial收集器。

  3. Parallel Scavenge 收集器

    Parallel Scavenge 收集器是新生代的收集器,也是采用复制算法。和其他收集器不同的是,Parallel Scavenge 收集器只关心吞吐量,而不关心GC的暂停时间。
    举一个简单的场景,如果一个垃圾回收过程,一次GC需要1s,如果分成4次,每次需要0.5s,两次GC的时间分别是1s和2s,对于程序的体验来后,后者的GC时间的停顿间隔低于前者,大多数GC回收期都会采用后面的回收机制,而对于Parallel Scavenge 收集器会选择前者,而不会选多次回收来降低GC的停顿时间。

    Parallel Scavenge 收集器 提供了两个参数来控制吞吐量,XX:MaxGCPausmillis控制停顿时间,-XX:GCTimeRatio设置吞吐量的大小。

  4. Serial Old 收集器

    Serial Old 收集器是Serial收集器的老年代的版本,同样是一个单线程版本,采用标记整理算法。

  5. Parallel Old收集器

    Parallel Old收集器是 Parallel Scavenge 收集器的一个老年代版本,采用标记整理算法。在JDK1.6版本中才开始提供,在此之前,如果新生代采用了Parallel Scavenge收集器,老年代回收除了Serial收集器之外别无选择。由于Serial收集器的效率拖累,所以Parallel Old收集器应运而生。

  6. CMS收集器

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器,大部分运行在BS的服务端。CMS收集器主要有4个步骤:

    1. 初始标记
    2. 并发标记
    3. 重新标记
    4. 并发清除

    初始标记是仅仅标记一下GC Roots直接关联到的对象,速度很快。并发标记就是进行GC Roots Tracing的过程,重新标记是为了修正有变动的的对象。在初始标记和重新标记的时候,需要“Stop the world”。

  7. G1 收集器

    G1收集器是最新的收集器,也是面向服务端的垃圾收集器,G1具有一下几个特点:

    1. 并发和并行
    2. 分代手机
    3. 空间整合
    4. 可预测的停顿

    G1 收集器大致分为下面几个步骤:

    1. 初始标记
    2. 并发标记
    3. 最终标记
    4. 筛选回收

    与CMS对比,最终标记和CMS重新标记相同,不同的在于筛选回收,筛选回收是先对各个回收的Region的回收价值和成本进行综合排序,根据用户的期望来进行回收。

内存分配和垃圾回收的策略


在Java的堆内存中可以简单把内存分为如下结构:

Java内存结构

  1. 对象优先在Eden分配

    在大多数情况下,对象的分配优先在新生代Eden中分配,当Eden没有足够的空间进行分配的时候虚拟机触发一次Minor GC

  2. 大对象直接进入老年代

    大对象通常是指很长字符串和数组,新生代的内存无法安置,就直接进入老年代空间尽心存放,大对象内存的阈值可以用-XX:PretenureSizeThreshold参数来定义。

  3. 长期存活的对象进入老年代

    对象在Eden中出生,并经历了一次MinorGC后仍然存活,并且能够被Survivor存放的话,将会把对象从Eden区域移动到S1区域,年代就记为1岁。当年龄增加到一定(默认是15岁)就会被晋升到老年代,这个阈值可以通过-XX:MaxTenuringThreshold设置。

    为了更好的适应不同程序的内存情况,虚拟机并不是简单的要求对象的年龄必须达到某个阈值,如果在Survivor空间中相同年龄所有对象的大小综合大于Survivor空间的一半,则年龄大于或等于这个年龄的对象可以直接进入老年代。

  4. 空间分配担保

    为了保证Minor GC能够顺利执行,虚拟机会在MinorGC 回收前检查老年代最大可用的连续内存空间是否大于新生代所有对象总和,如果条件成立,该次MinorGC可以安全执行。

    如果不成立,虚拟机会查看是否允许担保失败,如果允许,则虚拟机会继续检查可用空间是否大于历次晋升到老年代的平均水平,如果大于,则尝试进行一次MinorGC,显然这次回收是有风险的,如果分配失败则会重新触发一次FULL GC。

    如果虚拟机设置不允许担保失败,则会进行一次FULL GC。

读懂Java字节码

Java代码的运行过程

Java字节码

栈帧的概念

栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。

每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了 一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。

一个线程中方法调用可能很长,很多方法都处于执行状态。

Java字节码栈帧的结构

从上面的图可以看出,栈帧的组成主要分为4个部分:

局部变量表

顾名思义,用来存储局部变量和方法的内存空间,第一位是当前类的指针this,后面的是当前函数的参数和方法内的变量。比如一个函数有两个参数x,y,第一位是this指针,第二个是x,第三个是y
局部变量占用的空间大小在Java程序被编译成Class文件时就已经确定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
问:是如何在编译器就能够确定方法局部变量表的大小?

```

#### 操作数栈

操作数栈和局部变量表类似,也是用来存储数据变量,但是操作数栈不是通过索引访问变量,而是通过出栈和压栈操作来对数据进行访问。
#### 动态链接

在Class文件的常量池中存有大量的 符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化 称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

### 一个例子

随便写一个例子,尽量多的包含函数中的运算方法,基本的代码如下:

``` Java

public class ClassDemo {
public static void main(String[] args) {
ClassDemo classDemo = new ClassDemo();
classDemo.run(5);
}
public void run(int i) {
int j = 1;
int s = 2;
int d = 3;
int sum = add(i, j);
double result = sum * s / d;
for (int k = 0; k < 3; k++) {
result++;
}
try {
Thread.sleep(100);
}
catch (Exception ex){

}
if (result > 1) {
result--;
}

System.out.println(result);
}

public int add(int x, int y) {
return x + y;
}
}

```


使用`Javac`命令编译成`.class`文件,打开class文件内容如下:

``` content
cafe babe 0000 0034 0031 0a00 0c00 1c07
001d 0a00 0200 1c0a 0002 001e 0a00 0200
1f05 0000 0000 0000 0064 0a00 2000 2107
0022 0900 2300 240a 0025 0026 0700 2701
......

文件中的为16进制代码, 文件开头的4个字节称之为 魔数,唯有以cafe babe开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。

0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号。
Java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。
也就是说,编译生成该class文件的jdk版本为1.8.0。

再使用Javap -c ClassDemo.class >>code.txt命令,反编译代码到code.txt中,反编译后的代码如下,run方法的执行已经写的有注释,不再赘述.


Compiled from "ClassDemo.Java"
public class ClassDemo {
  public ClassDemo();
    Code:
       0: aload_0  //将第一个引用类型本地变量,this指针
       1: invokespecial #1      //调用超类构造方法,实例初始化方法,私有方法   // Method Java/lang/Object."<init>":()V
       4: return //void函数返回

  public static void main(Java.lang.String[]);
    Code:
       0: new           #2   //创建新的对象实例               // class ClassDemo
       3: dup   //复制栈顶一个字长的数据,将复制后的数据压栈
       4: invokespecial #3   //调用超类构造方法,实例初始化方法,私有方法               // Method "<init>":()V
       7: astore_1 //将栈顶引用类型值保存到局部变量1中
       8: aload_1 //从局部变量1中装载引用类型值入栈 classdemo实例
       9: iconst_5 //5(int)值入栈
      10: invokevirtual #4   //运行时方法绑定调用方法  // Method run:(I)V
      13: return

  public void run(int);
    Code: //第一个局部变量是this指针,第二个是形参i j是第三个变量
       0: iconst_1 //1(int)值入栈。    
       1: istore_2 //将栈顶int类型值保存到局部变量2中。
       2: iconst_2  //2(int)值入栈
       3: istore_3 //将栈顶int类型值保存到局部变量3中。
       4: iconst_3  //3(int)值入栈
       5: istore        4 //d=3
       7: aload_0   //将第一个引用类型本地变量 this指针
       8: iload_1   //从局部变量1中装载int类型值入栈 i
       9: iload_2   //从局部变量2中装载int类型值入栈 j
      10: invokevirtual #5  //调用add方法                // Method add:(II)I
      13: istore        5 //把函数的返回值,存到第五个变量sum中。
      15: iload         5 //把第五个变量加载出来
      17: iload_3         //iload_3读出s变量
      18: imul     //将栈顶两int类型数相乘,结果入栈  s*d 
      19: iload         4  //读取s变量
      21: idiv    //将栈顶两int类型数相除,结果入栈
      22: i2d    //将栈顶int类型值转换为double类型值    
      23: dstore        6 //把doubLe值存入第六个变量中
      25: iconst_0       
      26: istore        8
      28: iload         8
      30: iconst_3
      31: if_icmpge     46   //若栈顶两int类型值前大于等于后则跳转46
      34: dload         6
      36: dconst_1
      37: dadd            //将栈顶两double类型数相加,结果入栈 
      38: dstore        6
      40: iinc          8, 1  //将整数值constbyte加到indexbyte指定的int类型的局部变量中,把第8个局部变量加1
      43: goto          28   //跳转到28行
      46: ldc2_w        #6   //长整型100入栈                // long 100l
      49: invokestatic  #8   //调用静态方法               // Method Java/lang/Thread.sleep:(J)V
      52: goto          57   //goto 跳转到57行
      55: astore        8    //将栈顶引用类型值保存到局部变量8中 
      57: dload         6    //从局部变量6中装载double类型值入栈
      59: dconst_1
      60: dcmpl              //比较栈顶两double类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈;有NaN存在,-1入栈
      61: ifle          70   //若栈顶int类型值小于等于0则跳转70行
      64: dload         6     //加载第6个变量 
      66: dconst_1
      67: dsub                //将栈顶两double类型数相减,结果入栈
      68: dstore        6
      70: getstatic     #10   //获取静态字段的值               // Field Java/lang/System.out:LJava/io/PrintStream;
      73: dload         6     //加载第6个变量的值
      75: invokevirtual #11   // 调用实例方法  print           // Method Java/io/PrintStream.println:(D)V
      78: return
    Exception table:
       from    to  target type
          46    52    55   Class Java/lang/Exception

  public int add(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn
}

run栈帧的结构局部变量结构如下:

Java字节码栈帧的结构

Java中的引用类型

在Java中对于变量的访问分为3种,分别为强引用,软引用和弱引用 . 在这篇博客中可以认识到三种引用的类型的特点和使用场景。 首先通过一段代码来认识三者的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

package com.pay.utils;

import Java.lang.ref.SoftReference;
import Java.lang.ref.WeakReference;

/**
* @author liuhaibo on 2018/03/06
*/
public class WeakRefDemo {

public static void main(String... args) {

// all these objects have a strong reference
Object objA = new Object();
Object objB = new Object();
Object objC = new Object();

// other references to these objects
Object strongA = objA;
SoftReference<Object> softB = new SoftReference<>(objB);
WeakReference<Object> weakC = new WeakReference<>(objC);

objA = null;
objB = null;
objC = null;

System.out.println("GC前各变量的值:");
System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));

System.out.println("执行GC");

System.gc();

System.out.println("GC之后:");
System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));
}
}


打印结果如下:

ThreadLocal

可看到三个类型的引用在GC之后的表现:

  1. 强类型: 是常用的引用类型,如果变量正在被引用,在内存不足的情况下,jvm抛出OutofMemory也不会回收。
  2. 软类型: 在内存不足的情况下才会被回收,比较适合作为缓存使用。
  3. 弱引用: 等同于没有引用,在GC回收的时候,如果发现都是弱引用,则会判断为没有引用,可以直接被GC垃圾回收。

一以贯之搭建神经网络的过程

在神经网络中预测识别和预测的过程中,其实都是一个函数的对应关系:$$y=f(x)$$ 的函数关系,为了找到这个函数的关系,我们需要做大量的训练,具体的过程可以总结下面几个步骤:

  1. 获得随机矩阵w1b1,经过一个激活函数,得到隐藏层。

  2. 获得随机矩阵w2b2,计算出对应的预测y

  3. 利用y与样本y_的差距,有平方差和交叉熵等方式,来定义损失函数

  4. 利用指数衰减学习率,计算出学习率

  5. 用滑动平均计算输出的参数的平均值

  6. 利用梯度下降的方法,减少损失函数的差距

  7. 用with结构初始化所有的参数

  8. 利用for循环喂入数据,反复训练模型

利用这个八个步骤拟合出的一个圆的函数,代码和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# coding:utf-8
import os
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

xLine = 300
aLine = 10
BATCHSIZE = 10
regularizer=0.001
BASE_LEARN_RATE=0.001
LEARNING_RATE_DECAY = 0.99
MOVING_VERAGE_DECAY=0.99

# 1 生成随机矩阵x
rdm = np.random.RandomState(2)
X = rdm.randn(xLine, 2)
print("1.生成X:", X)

# 2 生成结果集Y
Y = [int(x1 * x1 + x2 * x2 < 2) for (x1, x2) in X]
print("2.生成Y:", Y)
Y_c = [['red' if y else 'blue'] for y in Y]

print("3. 生成Y_c:", Y_c)
X = np.vstack(X).reshape(-1, 2)
Y = np.vstack(Y).reshape(-1, 1)
print("4. 生成X:", X)
print("5. 生成Y:", Y)
# plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(Y_c))
# plt.show()

# 3. 这里如何保存数据集为样本?

# 4. 训练样本占位
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))
global_step=tf.Variable(0,trainable=False)

# 5. 获得随机矩阵w1和b1,计算隐藏层a
w1 = tf.Variable(tf.random_normal(shape=([2, aLine]))) # 2*100
tf.add_to_collection("losses",tf.contrib.layers.l2_regularizer(regularizer)(w1))
b1 =tf.Variable(tf.constant(0.01, shape=[aLine])) # 3000*100

# 6.使用激活函数计算隐藏层a
a = tf.nn.relu(tf.matmul(x, w1) + b1) # 3000*2 x 2*100 +3000*100

# 7. 获得随机矩阵w2和b2 计算预测值y。
w2 = tf.Variable(tf.random_normal(shape=([aLine, 1])))
tf.add_to_collection("losses",tf.contrib.layers.l2_regularizer(regularizer)(w2))
b2 = tf.Variable(tf.random_normal(shape=([1])))
y = tf.matmul(a, w2) + b2

# 8. 损失函数
loss = tf.reduce_mean(tf.square(y - y_))


# loss = tf.reduce_mean(tf.square(y - y_))
# 滑动学习率
learning_rate=tf.train.exponential_decay(BASE_LEARN_RATE,
global_step,
BATCHSIZE,
LEARNING_RATE_DECAY,
staircase=True)
# 9. 梯度下降方法训练
train_step = tf.train.GradientDescentOptimizer(0.001).minimize(loss)

# 滑动平均
# ema=tf.train.ExponentialMovingAverage(MOVING_VERAGE_DECAY,global_step)
# ema_op=ema.apply(tf.trainable_variables())
# with tf.control_dependencies([train_step,ema_op]):
# train_op=tf.no_op(name="train")

saver=tf.train.Saver()

# 10. 开始训练
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)

STEPS = 40000
for i in range(STEPS):
start = (i * BATCHSIZE) % xLine
end = start + BATCHSIZE

# print(Y[start:end])
sess.run(train_step, feed_dict={
x: X[start:end],
y_: Y[start:end]
})

if i % 2000 == 0:
# learning_rate_val=sess.run(learning_rate)
# print("learning_rate_val:",learning_rate_val)
loss_mse_v = sess.run(loss, feed_dict={x: X, y_: Y})
print("After %d training steps,loss on all data is %s" % (i, loss_mse_v))
saver.save(sess,os.path.join("./model/","model.ckpt"))
# print(global_step)

xx, yy = np.mgrid[-3:3:0.1, -3:3:.01]

grid = np.c_[xx.ravel(), yy.ravel()]

probs = sess.run(y, feed_dict={x: grid})
probs = probs.reshape(xx.shape)

print("w1:", sess.run(w1))
print("b1:", sess.run(b1))
print("w2:", sess.run(w2))
print("b2:", sess.run(b2))

plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(Y_c))
plt.contour(xx, yy, probs, levels=[.5])
plt.show()


一以贯之搭建神经网络的过程

ThreadLocal使用

ThreadLocal是一个以当前线程为key,存储变量的map容器,能够解决线程安全问题。

首先我们先看下ThreadLocal的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
public class ThreadLocalDemo {

ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> new Integer(0));

public int getNext() {
Integer value = count.get();
value++;
count.set(value);

return value;

}

public static void main(String[] args) {
new Thread(() -> {
ThreadLocalDemo demo = new ThreadLocalDemo();
while (true) {
System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

new Thread(() -> {
ThreadLocalDemo demo = new ThreadLocalDemo();
while (true) {
System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
ThreadLocalDemo demo = new ThreadLocalDemo();
while (true) {
System.out.println(Thread.currentThread().getName() + " " + demo.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

}
}


```

打印结果如下:

![ThreadLocal](/img/assets/53/04.png)

从上面的结果可以看出,ThreadLocal对应的变量可以被线程单独访问,能够避免线程安全问题。

为什么ThreadLocal能够解决线程问题呢?我们先看下ThreadLocal的get方法:

``` Java

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//getMap(t)的方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
```

看到这个方法,我总觉的这段代码写的有问题,好像和自己写的代码差不多。。。(JDK源码的水平也有差的)


![ThreadLocal](/img/assets/53/01.png)


ThreadLocal和Thread的关系如下:

![ThreadLocal](/img/assets/53/02.png)


当看到这个引用图的时候,隐隐觉得会有问题,在当前的线程中,如果线程不消亡,那`ThreadLocalMap` 的map实例就永远不会被回收。

因为在GC每次计算对象还有没有引用的时候, 判断永远是有在用用。这样程序中set的值就永远存在,这样就导致了内存的泄露。

在线程池中,由于线程的`复用性`,就容易造成`ThreadLocal`内存泄露的问题,所以最好在线程执行完成后,调用remove手工删除数据。


对于上面的这个问题,jdk也有对应的优化,具体的方法是,`ThreadLocalMap`使用弱引用(`对象为空的时候会释放`)但优化的范围有限,具体的引用关系如下:


![ThreadLocal](/img/assets/53/03.png)


上面的优化过程,如果ThreadLocal被置空,则LocalThreadMap只会有`弱引用`,原来的`ThreadLocal对象(为了方便,称为对象A)`也会在下次直接被GC回收,所以在下次GC的时候, 对象A的引用会成为null.

但这样在`LocalThreadMap`中会带来一个问题,这样导致了在Key-Value中,key的引用的地址的值成为了null,这样也就导致了value无法被找到,也会造成内存泄露。

在`LocalThreadMap`中在每次`get()/set()/remove()`操作map的中的值的时候,会自动清理key为null的值,这样就能够避免了泄露的问题。具体的源码如下:

``` Java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

ThreadLocal多少还是有点风险,除非明确能够使用和删除ThreadLocal的值,否则尽量还是慎用。

Idea的主题和设置

从.net转Java很多ide的编译期的快捷键不一样,而且idea的代码颜色过于单调,所以就花了点时间研究了下idea的相关的设置,把主题改成了自己看着舒服的模式,如下:

Redis

快捷键列表如下:

Ctrl+N: 搜索类

Ctrl+1: 折叠当前方法

Ctrl+2: 展开当前方法

Ctrl+F12: 转到类实现

Ctrl+-: 后退

下载地址

Redis中的Incr函数的一个坑

首先看一段代码:


  String key = "mytestKey";
  RedisUtils.set(key, 0, 10);
  for (int i = 0; i < 60; i++) {
      String result=RedisUtils.get(key);
      if(StringUtils.isEmpty(result)){
          System.out.println("time end");
      }
      System.out.println(RedisUtils.incr(key));
      SleepUtils.SleepSecond(1);
  }

上面的代码向缓存充塞入了一个0,10s后过期,然后在循环中使用incr方法对key对应的值进行加1,看下打印结果:

Redis

我们发现,在缓存过期的时候,缓存中的值有重新从0 开始计算,而且过期时间大于5秒。

原因是因为:Redis Incr 命令将 key 中储存的数字值增一,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作,且将key的有效时间设置为长期有效。

所以 ,即使key已经过期,incr方法会重新生成一个永久性的key.

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×