死锁(Dead Lock)指的是两个或两个以上的运算单元(进程、线程或协程),都在等待对方停止执行,以取得系统资源,但是没有一方提前退出,就称为死锁。

1.死锁演示

死锁的形成分为两个方面,一个是使用内置锁 synchronized 形成的死锁,另一种是使用显式锁 Lock 实现的死锁,接下来我们分别来看。

1.1 死锁 synchronized 版

public class DeadLockExample {
public static void main(String[] args) {
Object lockA = new Object(); // 创建锁 A
Object lockB = new Object(); // 创建锁 B // 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 先获取锁 A
synchronized (lockA) {
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试获取锁 B
System.out.println("线程 1:等待获取 B...");
synchronized (lockB) {
System.out.println("线程 1:获取到锁 B!");
}
}
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 先获取锁 B
synchronized (lockB) {
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试获取锁 A
System.out.println("线程 2:等待获取 A...");
synchronized (lockA) {
System.out.println("线程 2:获取到锁 A!");
}
}
}
});
t2.start(); // 运行线程
}
}

以上程序的执行结果如下:



从上述结果可以看出,线程 1 和线程 2 都在等待对方释放锁,这样就造成了死锁问题。

1.2 死锁 Lock 版

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class DeadLockByReentrantLockExample {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockA.lock(); // 加锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
lockB.lock(); // 加锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
}
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
}
}

以上程序的执行结果如下:

2.死锁产生原因

通过以上示例,我们可以得出结论,要产生死锁需要满足以下 4 个条件

  1. 互斥条件:指运算单元(进程、线程或协程)对所分配到的资源具有排它性,也就是说在一段时间内某个锁资源只能被一个运算单元所占用。
  2. 请求和保持条件:指运算单元已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它运算单元占有,此时请求运算单元阻塞,但又对自己已获得的其它资源保持不放。
  3. 不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。
  4. 环路等待条件:指在发生死锁时,必然存在运算单元和资源的环形链,即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。

只有以上 4 个条件同时满足,才会造成死锁问题。

3.死锁排查工具

如果程序出现死锁问题,可通过以下 4 种方案中的任意一种进行分析和排查。

3.1 jstack

我们在使用 jstack 之前,先要通过 jps 得到运行程序的进程 ID,使用方法如下:



“jps -l”可以查询本机所有的 Java 程序,jps(Java Virtual Machine Process Status Tool)是 Java 提供的一个显示当前所有 Java 进程 pid 的命令,适合在 linux/unix/windows 平台上简单查看当前 Java 进程的一些简单情况,“-l”用于输出进程 pid 和运行程序完整路径名(包名和类名)。

有了进程 ID(PID)之后,我们就可以使用“jstack -l PID”来发现死锁问题了,如下图所示:



jstack 用于生成 Java 虚拟机当前时刻的线程快照,“-l”表示长列表(long),打印关于锁的附加信息。

PS:可以使用 jstack -help 查看更多命令使用说明。

3.2 jconsole

使用 jconsole 需要打开 JDK 的 bin 目录,找到 jconsole 并双击打开,如下图所示:



然后选择要调试的程序,如下图所示:



之后点击连接进入,选择“不安全的连接”进入监控主页,如下图所示:





之后切换到“线程”模块,点击“检测死锁”按钮,如下图所示:



之后稍等片刻就会检测出死锁的相关信息,如下图所示:

3.3 jvisualvm

jvisualvm 也在 JDK 的 bin 目录中,同样是双击打开:



稍等几秒之后,jvisualvm 中就会出现本地的所有 Java 程序,如下图所示:



双击选择要调试的程序:



单击鼠标进入“线程”模块,如下图所示:



从上图可以看出,当我们切换到线程一栏之后就会直接显示出死锁信息,之后点击“线程 Dump”生成死锁的详情信息,如下图所示:

3.4 jmc

jmc 是 Oracle Java Mission Control 的缩写,是一个对 Java 程序进行管理、监控、概要分析和故障排查的工具套件。它也是在 JDK 的 bin 目录中,同样是双击启动,如下图所示:



jmc 主页信息如下:



之后选中要排查的程序,右键“启动 JMX 控制台”查看此程序的详细内容,如下图所示:





然后点击“线程”,勾中“死锁检测”就可以发现死锁和死锁的详情信息,如下图所示:

4.死锁解决方案

4.1 死锁解决方案分析

接下来我们来分析一下,产生死锁的 4 个条件,哪些是可以破坏的?哪些是不能被破坏的?

  • 互斥条件:系统特性,不能被破坏。
  • 请求和保持条件:可以被破坏。
  • 不可剥夺条件:系统特性,不能被破坏。
  • 环路等待条件:可以被破坏。

通过上述分析,我们可以得出结论,我们只能通过破坏请求和保持条件或者是环路等待条件,从而来解决死锁的问题,那上线,我们就先从破坏“环路等待条件”开始来解决死锁问题。

4.2 解决方案1:顺序锁

所谓的顺序锁指的是通过有顺序的获取锁,从而避免产生环路等待条件,从而解决死锁问题的。

当我们没有使用顺序锁时,程序的执行可能是这样的:



线程 1 先获取了锁 A,再获取锁 B,线程 2 与 线程 1 同时执行,线程 2 先获取锁 B,再获取锁 A,这样双方都先占用了各自的资源(锁 A 和锁 B)之后,再尝试获取对方的锁,从而造成了环路等待问题,最后造成了死锁的问题。

此时我们只需要将线程 1 和线程 2 获取锁的顺序进行统一,也就是线程 1 和线程 2 同时执行之后,都先获取锁 A,再获取锁 B,执行流程如下图所示:



因为只有一个线程能成功获取到锁 A,没有获取到锁 A 的线程就会等待先获取锁 A,此时得到锁 A 的线程继续获取锁 B,因为没有线程争抢和拥有锁 B,那么得到锁 A 的线程就会顺利的拥有锁 B,之后执行相应的代码再将锁资源全部释放,然后另一个等待获取锁 A 的线程就可以成功获取到锁资源,执行后续的代码,这样就不会出现死锁的问题了。

顺序锁的实现代码如下所示:

public class SolveDeadLockExample {
public static void main(String[] args) {
Object lockA = new Object(); // 创建锁 A
Object lockB = new Object(); // 创建锁 B
// 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 1:等待获取 B...");
synchronized (lockB) {
System.out.println("线程 1:获取到锁 B!");
}
}
}
});
t1.start(); // 运行线程
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("线程 2:获取到锁 A!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 2:等待获取B...");
synchronized (lockB) {
System.out.println("线程 2:获取到锁 B!");
}
}
}
});
t2.start(); // 运行线程
}
}

以上程序的执行结果如下:



从上述执行结果可以看出,程序并没有出现死锁的问题。

4.3 解决方案2:轮询锁

轮询锁是通过打破“请求和保持条件”来避免造成死锁的,它的实现思路简单来说就是通过轮询来尝试获取锁,如果有一个锁获取失败,则释放当前线程拥有的所有锁,等待下一轮再尝试获取锁。

轮询锁的实现需要使用到 ReentrantLock 的 tryLock 方法,具体实现代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class SolveDeadLockExample { public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB);
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
} /**
* 轮询锁
*/
public static void pollingLock(Lock lockA, Lock lockB) {
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
}
// 等待一秒再继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以上程序的执行结果如下:



从上述结果可以看出,以上代码也没有出现死锁的问题。

4.4 轮询锁优化

使用轮询锁虽然可以解决死锁的问题,但并不是完美无缺的,比如以下这些问题。

4.4.1 问题1:死循环

以上简易版的轮询锁,如果遇到有一个线程一直霸占或者长时间霸占锁资源的情况,就会导致这个轮询锁进入死循环的状态,它会尝试一直获取锁资源,这样就会造成新的问题,带来不必要的性能开销,具体示例如下。

反例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class SolveDeadLockExample { public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB);
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果此处代码未执行,线程 2 一直未释放锁资源
// lockB.unlock();
}
}
});
t2.start(); // 运行线程
} /**
* 轮询锁
*/
public static void pollingLock(Lock lockA, Lock lockB) {
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
}
// 等待一秒再继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以上代码的执行结果如下:



从上述结果可以看出,线程 1 轮询锁进入了死循环的状态。

优化版

针对以上死循环的情况,我们可以改进的思路有以下两种:

  1. 添加最大次数限制:如果经过了 n 次尝试获取锁之后,还未获取到锁,则认为获取锁失败,执行失败策略之后终止轮询(失败策略可以是记录日志或其他操作);
  2. 添加最大时长限制:如果经过了 n 秒尝试获取锁之后,还未获取到锁,则认为获取锁失败,执行失败策略之后终止轮询。

以上策略任选其一就可以解决死循环的问题,出于实现成本的考虑,我们可以采用轮询最大次数的方式来改进轮询锁,具体实现代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class SolveDeadLockExample { public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB, 3);
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 线程 2 忘记释放锁资源
// lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
} /**
* 轮询锁
*
* maxCount:最大轮询次数
*/
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
// 轮询次数计数器
int count = 0;
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
} // 判断是否已经超过最大次数限制
if (count++ > maxCount) {
// 终止循环
System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");
return;
} // 等待一秒再继续尝试获取锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以上代码的执行结果如下:



从以上结果可以看出,当我们改进之后,轮询锁就不会出现死循环的问题了,它会尝试一定次数之后终止执行。

4.4.2 问题2:线程饿死

我们以上的轮询锁的轮询等待时间是固定时间,如下代码所示:

// 等待 1s 再尝试获取(轮询)锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

这样在特殊情况下会造成线程饿死的问题,也就是轮询锁一直获取不到锁的问题,比如以下示例。

反例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class SolveDeadLockExample { public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB, 3);
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} finally {
lockB.unlock(); // 释放锁
}
// 等待一秒之后继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t2.start(); // 运行线程
} /**
* 轮询锁
*/
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
// 循环次数计数器
int count = 0;
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(100); // 等待 0.1s(获取锁需要的时间)
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
} // 判断是否已经超过最大次数限制
if (count++ > maxCount) {
// 终止循环
System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");
return;
} // 等待一秒再继续尝试获取锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以上代码的执行结果如下:



从上述结果可以看出,线程 1(轮询锁)一直未成功获取到锁,造成这种结果的原因是:线程 1 每次轮询的等待时间为固定的 1s,而线程 2 也是相同的频率,每 1s 获取一次锁,这样就会导致线程 2 会一直先成功获取到锁,而线程 1 则会一直处于“饿死”的情况,执行流程如下图所示:

优化版

接下来,我们可以将轮询锁的固定等待时间,改进为固定时间 + 随机时间的方式,这样就可以避免因为获取锁的频率一致,而造成轮询锁“饿死”的问题了,具体实现代码如下:

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class SolveDeadLockExample {
private static Random rdm = new Random(); public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B // 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB, 3);
}
});
t1.start(); // 运行线程 // 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} finally {
lockB.unlock(); // 释放锁
}
// 等待一秒之后继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t2.start(); // 运行线程
} /**
* 轮询锁
*/
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
// 循环次数计数器
int count = 0;
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(100); // 等待 0.1s(获取锁需要的时间)
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
} // 判断是否已经超过最大次数限制
if (count++ > maxCount) {
// 终止循环
System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");
return;
} // 等待一定时间(固定时间 + 随机时间)之后再继续尝试获取锁
try {
Thread.sleep(300 + rdm.nextInt(8) * 100); // 固定时间 + 随机时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以上代码的执行结果如下:



从上述结果可以看出,线程 1(轮询锁)加入随机等待时间之后就不会出现线程饿死的问题了。

5.总结

本文介绍了死锁的概念,以及产生死锁的 4 个条件,排查死锁可以通过本文提供的 4 种工具中的任意一种来检测,从易用性和性能方面来考虑,推荐使用 jconsole 或 jvisualvm,最后我们介绍了死锁问题的两种解决方案:顺序锁和轮询锁。

并发原创文章推荐

  1. 线程的 4 种创建方法和使用详解!
  2. Java中用户线程和守护线程区别这么大?
  3. 深入理解线程池 ThreadPool
  4. 线程池的7种创建方式,强烈推荐你用它...
  5. 池化技术到达有多牛?看了线程和线程池的对比吓我一跳!
  6. 并发中的线程同步与锁
  7. synchronized 加锁 this 和 class 的区别!
  8. volatile 和 synchronized 的区别
  9. 轻量级锁一定比重量级锁快吗?
  10. 这样终止线程,竟然会导致服务宕机?
  11. SimpleDateFormat线程不安全的5种解决方案!
  12. ThreadLocal不好用?那是你没用对!
  13. ThreadLocal内存溢出代码演示和原因分析!
  14. Semaphore自白:限流器用我就对了!
  15. CountDownLatch:别浪,等人齐再团!
  16. CyclicBarrier:人齐了,司机就可以发车了!
  17. synchronized 优化手段之锁膨胀机制!
  18. synchronized 中的 4 个优化,你知道几个?
  19. ReentrantLock 中的 4 个坑!
  20. 图解:为什么非公平锁的性能更高?
  21. 死锁的 4 种排查工具!
  22. 死锁终结者:顺序锁和轮询锁!
  23. 轮询锁在使用时遇到的问题与解决方案!

关注公号「Java中文社群」查看更多有意思、涨知识的 Java 并发文章。

1.3w字,一文详解死锁!的更多相关文章

  1. PHP中IP地址与整型数字互相转换详解

    这篇文章主要介绍了PHP中IP地址与整型数字互相转换详解,本文介绍了使用PHP函数ip2long与long2ip的使用,以及它们的BUG介绍,最后给出自己写的两个算法,需要的朋友可以参考下 IP转换成 ...

  2. 一文详解Hexo+Github小白建站

    作者:玩世不恭的Coder时间:2020-03-08说明:本文为原创文章,未经允许不可转载,转载前请联系作者 一文详解Hexo+Github小白建站 前言 GitHub是一个面向开源及私有软件项目的托 ...

  3. 一文详解 Linux 系统常用监控工一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop)具(top,htop,iotop,iftop)

    一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop)     概 述 本文主要记录一下 Linux 系统上一些常用的系统监控工具,非常好用.正所谓磨刀不误砍柴工,花点时间 ...

  4. 一文详解 WebSocket 网络协议

    WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议.WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据(从而解决Http 1.1协议 ...

  5. 一文详解 OpenGL ES 3.x 渲染管线

    OpenGL ES 构建的三维空间,其中的三维实体由许多的三角形拼接构成.如下图左侧所示的三维实体圆锥,其由许多三角形按照一定规律拼接构成.而组成圆锥的每一个三角形,其任意一个顶点由三维空间中 x.y ...

  6. 6000字Locust入门详解

    目录 一.Locust 性能测试 (一). 性能测试工具 主流性能测试工具对比 认识Locust (二) locust 基本用法 1.安装locust 2.编写用例 3. 启动测试 GUI 模式启动 ...

  7. 一文详解Redis键过期策略

    摘要:Redis采用的过期策略:惰性删除+定期删除. 本文分享自华为云社区<Redis键过期策略详解>,作者:JavaEdge. 1 设置带过期时间的 key # 时间复杂度:O(1),最 ...

  8. 从零入门 Serverless | 一文详解 Serverless 技术选型

    作者 | 李国强 阿里云资深产品专家 今天来讲,在 Serverless 这个大领域中,不只有函数计算这一种产品形态和应用类型,而是面向不同的用户群体和使用习惯,都有其各自适用的 Serverless ...

  9. 一文详解MySQL的锁机制

    一.表级锁.行级锁.页级锁 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则. MySQL数据库由于其自身架构的特点,存在多种数据存储引擎, ...

随机推荐

  1. jvm源码解读--02 Array<u1>* tags = MetadataFactory::new_writeable_array<u1>(loader_data, length, 0, CHECK_NULL); 函数引入的jvm内存分配解析

    current路径: #0 Array<unsigned char>::operator new (size=8, loader_data=0x7fd4c802e868, length=8 ...

  2. create-react-app 创建的项目执行npm run eject后,运行报错

    create-react-app 创建的项目执行npm run eject后,运行报错:Cannot find module '@babel/plugin-transform-react-jsx-so ...

  3. jquery 阻止表单提交方法

    <form name="message_form" action="?m=mobilecenter&c=index&a=service" ...

  4. Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)

    Apache Tomcat 7.0.0~7.0.79 直接发送以下数据包即可在Web根目录写入shell: PUT /1.jsp/ HTTP/1.1 Host: 192.168.49.2:8080 A ...

  5. 那些 Unix 命令替代品们「GitHub 热点速览 v.21.32」

    作者:HelloGitHub-小鱼干 好用的 Unix 命令替代工具能让你事半功倍,例如,bat 便是个带着高亮特性的加强版 cat,就像你用了 oh my zsh 之后便会感受到它的强大.同样好用的 ...

  6. .NET 6 预览版 7 发布——最后一个预览版

    原文:bit.ly/2VJxjxQ 作者:Richard 翻译:精致码农-王亮 说明:文中有大量的超链接,这些链接在公众号文章中被自动剔除,一部分包含超链接列表的小段落被我删减了,如果你对此感兴趣,请 ...

  7. MySQL-初见

    目录 初识MySQL 数据库基本命令 创建数据库表 数据库引擎 修改和删除表字段 MySQL数据管理 DML语言 DQL查询数据 单表查询 WHERE条件子句 模糊查询:比较操作符 联表查询 排序查询 ...

  8. Run Clojure Script with External Dependencies without leiningen

    The normal way of deploy clojure files is using leiningen. But if we have no leiningen, or the scrip ...

  9. Linux 记录学习

  10. MySQL主从复制与Atlas读写分离

    配置主从复制 1. 增加主从配置 # 主库配置文件 server-id = 1 log-bin = /var/lib/mysql/mysql-bin expire_logs_days = 10 ski ...