learn.lianglianglee.com/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html
2022-08-14 03:40:33 +08:00

447 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>答疑课堂:模块三热点问题解答.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Java并发编程实战/00 开篇词你为什么需要学习并发编程?.md.html">00 开篇词你为什么需要学习并发编程?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/01 如何制定性能调优标准?.md.html">01 如何制定性能调优标准?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/02 如何制定性能调优策略?.md.html">02 如何制定性能调优策略?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/03 字符串性能优化不容小觑百M内存轻松存储几十G数据.md.html">03 字符串性能优化不容小觑百M内存轻松存储几十G数据</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/04 慎重使用正则表达式.md.html">04 慎重使用正则表达式</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/05 ArrayList还是LinkedList使用不当性能差千倍.md.html">05 ArrayList还是LinkedList使用不当性能差千倍</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/06 Stream如何提高遍历集合效率.md.html">06 Stream如何提高遍历集合效率</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/07 深入浅出HashMap的设计与优化.md.html">07 深入浅出HashMap的设计与优化</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/08 网络通信优化之IO模型如何解决高并发下IO瓶颈.md.html">08 网络通信优化之IO模型如何解决高并发下IO瓶颈</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/09 网络通信优化之序列化避免使用Java序列化.md.html">09 网络通信优化之序列化避免使用Java序列化</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/10 网络通信优化之通信协议如何优化RPC网络通信.md.html">10 网络通信优化之通信协议如何优化RPC网络通信</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/11 答疑课堂深入了解NIO的优化实现原理.md.html">11 答疑课堂深入了解NIO的优化实现原理</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/12 多线程之锁优化深入了解Synchronized同步锁的优化方法.md.html">12 多线程之锁优化深入了解Synchronized同步锁的优化方法</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/13 多线程之锁优化深入了解Lock同步锁的优化方法.md.html">13 多线程之锁优化深入了解Lock同步锁的优化方法</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/14 多线程之锁优化(下):使用乐观锁优化并行操作.md.html">14 多线程之锁优化(下):使用乐观锁优化并行操作</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/15 多线程调优(上):哪些操作导致了上下文切换?.md.html">15 多线程调优(上):哪些操作导致了上下文切换?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/16 多线程调优(下):如何优化多线程上下文切换?.md.html">16 多线程调优(下):如何优化多线程上下文切换?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/17 并发容器的使用:识别不同场景下最优容器.md.html">17 并发容器的使用:识别不同场景下最优容器</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/18 如何设置线程池大小?.md.html">18 如何设置线程池大小?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/19 如何用协程来优化多线程业务?.md.html">19 如何用协程来优化多线程业务?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型.md.html">20 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/21 深入JVM即时编译器JIT优化Java编译.md.html">21 深入JVM即时编译器JIT优化Java编译</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/22 如何优化垃圾回收机制?.md.html">22 如何优化垃圾回收机制?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/23 如何优化JVM内存分配.md.html">23 如何优化JVM内存分配</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/24 内存持续上升,我该如何排查问题?.md.html">24 内存持续上升,我该如何排查问题?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/25 答疑课堂:模块四热点问题解答.md.html">25 答疑课堂:模块四热点问题解答</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/26 单例模式:如何创建单一对象优化系统性能?.md.html">26 单例模式:如何创建单一对象优化系统性能?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/27 原型模式与享元模式:提升系统性能的利器.md.html">27 原型模式与享元模式:提升系统性能的利器</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/28 如何使用设计模式优化并发编程?.md.html">28 如何使用设计模式优化并发编程?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/29 生产者消费者模式:电商库存设计优化.md.html">29 生产者消费者模式:电商库存设计优化</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/30 装饰器模式:如何优化电商系统中复杂的商品价格策略?.md.html">30 装饰器模式:如何优化电商系统中复杂的商品价格策略?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/31 答疑课堂:模块五思考题集锦.md.html">31 答疑课堂:模块五思考题集锦</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/32 MySQL调优之SQL语句如何写出高性能SQL语句.md.html">32 MySQL调优之SQL语句如何写出高性能SQL语句</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/33 MySQL调优之事务高并发场景下的数据库事务调优.md.html">33 MySQL调优之事务高并发场景下的数据库事务调优</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/34 MySQL调优之索引索引的失效与优化.md.html">34 MySQL调优之索引索引的失效与优化</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/35 记一次线上SQL死锁事故如何避免死锁.md.html">35 记一次线上SQL死锁事故如何避免死锁</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/36 什么时候需要分表分库?.md.html">36 什么时候需要分表分库?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/37 电商系统表设计优化案例分析.md.html">37 电商系统表设计优化案例分析</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/38 数据库参数设置优化,失之毫厘差之千里.md.html">38 数据库参数设置优化,失之毫厘差之千里</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/39 答疑课堂MySQL中InnoDB的知识点串讲.md.html">39 答疑课堂MySQL中InnoDB的知识点串讲</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/41 如何设计更优的分布式锁?.md.html">41 如何设计更优的分布式锁?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/42 电商系统的分布式事务调优.md.html">42 电商系统的分布式事务调优</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/43 如何使用缓存优化系统性能?.md.html">43 如何使用缓存优化系统性能?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/44 记一次双十一抢购性能瓶颈调优.md.html">44 记一次双十一抢购性能瓶颈调优</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 什么是数据的强、弱一致性?.md.html">加餐 什么是数据的强、弱一致性?</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">加餐 推荐几款常用的性能测试工具</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java并发编程实战/答疑课堂:模块三热点问题解答.md.html">答疑课堂:模块三热点问题解答</a>
</li>
<li>
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.md.html">结束语 栉风沐雨,砥砺前行!</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>答疑课堂:模块三热点问题解答</h1>
<p>你好,我是刘超。</p>
<p>不知不觉“多线程性能优化“已经讲完了,今天这讲我来解答下各位同学在这个模块集中提出的两大问题,第一个是有关监测上下文切换异常的命令排查工具,第二个是有关 blockingQueue 的内容。</p>
<p>也欢迎你积极留言给我,让我知晓你想了解的内容,或者说出你的困惑,我们共同探讨。下面我就直接切入今天的主题了。</p>
<h2>使用系统命令查看上下文切换</h2>
<p>在第 15 讲中我提到了上下文切换,其中有用到一些工具进行监测,由于篇幅关系就没有详细介绍,今天我就补充总结几个常用的工具给你。</p>
<h3>1. Linux 命令行工具之 vmstat 命令</h3>
<p>vmstat 是一款指定采样周期和次数的功能性监测工具,我们可以使用它监控进程上下文切换的情况。</p>
<p><img src="assets/13eeee053c553863b3bdd95c07cb3b71.jpg" alt="img" /></p>
<p>vmstat 1 3 命令行代表每秒收集一次性能指标,总共获取 3 次。以下为上图中各个性能指标的注释:</p>
<ul>
<li><strong>procs</strong>
r等待运行的进程数
b处于非中断睡眠状态的进程数</li>
<li><strong>memory</strong>
swpd虚拟内存使用情况
free空闲的内存
buff用来作为缓冲的内存数
cache缓存大小</li>
<li><strong>swap</strong>
si从磁盘交换到内存的交换页数量
so从内存交换到磁盘的交换页数量</li>
<li><strong>io</strong>
bi发送到快设备的块数
bo从块设备接收到的块数</li>
<li><strong>system</strong>
in每秒中断数
cs每秒上下文切换次数</li>
<li><strong>cpu</strong>
us用户 CPU 使用事件
sy内核 CPU 系统使用时间
id空闲时间
wa等待 I/O 时间
st运行虚拟机窃取的时间</li>
</ul>
<h3>2. Linux 命令行工具之 pidstat 命令</h3>
<p>我们通过上述的 vmstat 命令只能观察到哪个进程的上下文切换出现了异常,那如果是要查看哪个线程的上下文出现了异常呢?</p>
<p>pidstat 命令就可以帮助我们监测到具体线程的上下文切换。pidstat 是 Sysstat 中一个组件,也是一款功能强大的性能监测工具。我们可以通过命令 yum install sysstat 安装该监控组件。</p>
<p>通过 pidstat -help 命令,我们可以查看到有以下几个常用参数可以监测线程的性能:</p>
<p><img src="assets/7a93cba1673119e4c9162a29e9875dd1.jpg" alt="img" /></p>
<p>常用参数:</p>
<ul>
<li>-u默认参数显示各个进程的 cpu 使用情况;</li>
<li>-r显示各个进程的内存使用情况</li>
<li>-d显示各个进程的 I/O 使用情况;</li>
<li>-w显示每个进程的上下文切换情况</li>
<li>-p指定进程号</li>
<li>-t显示进程中线程的统计信息</li>
</ul>
<p>首先,通过 pidstat -w -p pid 命令行,我们可以查看到进程的上下文切换:</p>
<p><img src="assets/3e6cee25e85826aa5d4f8f480535234f.jpg" alt="img" /></p>
<ul>
<li>cswch/s每秒主动任务上下文切换数量</li>
<li>nvcswch/s每秒被动任务上下文切换数量</li>
</ul>
<p>之后,通过 pidstat -w -p pid -t 命令行,我们可以查看到具体线程的上下文切换:</p>
<p><img src="assets/728b1634e3e9971307264b5736cb1c6f.jpg" alt="img" /></p>
<h3>3. JDK 工具之 jstack 命令</h3>
<p>查看具体线程的上下文切换异常,我们还可以使用 jstack 命令查看线程堆栈的运行情况。jstack 是 JDK 自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中的线程堆栈信息。</p>
<p>jstack 最常用的功能就是使用 jstack pid 命令查看线程堆栈信息,通常是结合 pidstat -p pid -t 一起查看具体线程的状态,也经常用来排查一些死锁的异常。</p>
<p><img src="assets/0e61a2f4eb945f5a26bd7987d0babd1d.jpg" alt="img" /></p>
<p>每个线程堆栈的信息中,都可以查看到线程 ID、线程状态wait、sleep、running 等状态)以及是否持有锁等。</p>
<p>我们可以通过 jstack 16079 &gt; /usr/dump 将线程堆栈信息日志 dump 下来,之后打开 dump 文件,通过查看线程的状态变化,就可以找出导致上下文切换异常的具体原因。例如,系统出现了大量处于 BLOCKED 状态的线程,我们就需要立刻分析代码找出原因。</p>
<h2>多线程队列</h2>
<p>针对这讲的第一个问题,一份上下文切换的命令排查工具就总结完了。下面我来解答第二个问题,是在 17 讲中呼声比较高的有关 blockingQueue 的内容。</p>
<p>在 Java 多线程应用中特别是在线程池中队列的使用率非常高。Java 提供的线程安全队列又分为了阻塞队列和非阻塞队列。</p>
<h3>1. 阻塞队列</h3>
<p>我们先来看下阻塞队列。阻塞队列可以很好地支持生产者和消费者模式的相互等待,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线程会阻塞直到队列不满。</p>
<p>在 Java 线程池中,也用到了阻塞队列。当创建的线程数量超过核心线程数时,新建的任务将会被放到阻塞队列中。我们可以根据自己的业务需求来选择使用哪一种阻塞队列,阻塞队列通常包括以下几种:</p>
<ul>
<li>**ArrayBlockingQueue**一个基于数组结构实现的有界阻塞队列,按 FIFO先进先出原则对元素进行排序使用 ReentrantLock、Condition 来实现线程安全;</li>
<li>**LinkedBlockingQueue**一个基于链表结构实现的阻塞队列,同样按 FIFO (先进先出) 原则对元素进行排序,使用 ReentrantLock、Condition 来实现线程安全,吞吐量通常要高于 ArrayBlockingQueue</li>
<li>**PriorityBlockingQueue**一个具有优先级的无限阻塞队列,基于二叉堆结构实现的无界限(最大值 Integer.MAX_VALUE - 8阻塞队列队列没有实现排序但每当有数据变更时都会将最小或最大的数据放在堆最上面的节点上该队列也是使用了 ReentrantLock、Condition 实现的线程安全;</li>
<li>**DelayQueue**一个支持延时获取元素的无界阻塞队列,基于 PriorityBlockingQueue 扩展实现,与其不同的是实现了 Delay 延时接口;</li>
<li>**SynchronousQueue**一个不存储多个元素的阻塞队列,每次进行放入数据时, 必须等待相应的消费者取走数据后,才可以再次放入数据,该队列使用了两种模式来管理元素,一种是使用先进先出的队列,一种是使用后进先出的栈,使用哪种模式可以通过构造函数来指定。</li>
</ul>
<p>Java 线程池 Executors 还实现了以下四种类型的 ThreadPoolExecutor分别对应以上队列详情如下</p>
<p><img src="assets/59e1d01c8a60fe722aae01db86a913da.jpg" alt="img" /></p>
<h3>2. 非阻塞队列</h3>
<p>我们常用的线程安全的非阻塞队列是 ConcurrentLinkedQueue它是一种无界线程安全队列 (FIFO),基于链表结构实现,利用 CAS 乐观锁来保证线程安全。</p>
<p>下面我们通过源码来分析下该队列的构造、入列以及出列的具体实现。</p>
<p>**构造函数:**ConcurrentLinkedQueue 由 head 、tair 节点组成每个节点Node由节点元素item和指向下一个节点的引用 (next) 组成,节点与节点之间通过 next 关联,从而组成一张链表结构的队列。在队列初始化时, head 节点存储的元素为空tair 节点等于 head 节点。</p>
<pre><code>public ConcurrentLinkedQueue() {
head = tail = new Node&lt;E&gt;(null);
}
private static class Node&lt;E&gt; {
volatile E item;
volatile Node&lt;E&gt; next;
.
.
}
</code></pre>
<p>**入列:**当一个线程入列一个数据时,会将该数据封装成一个 Node 节点,并先获取到队列的队尾节点,当确定此时队尾节点的 next 值为 null 之后,再通过 CAS 将新队尾节点的 next 值设为新节点。此时 p != t也就是设置 next 值成功,然后再通过 CAS 将队尾节点设置为当前节点即可。</p>
<pre><code>public boolean offer(E e) {
checkNotNull(e);
// 创建入队节点
final Node&lt;E&gt; newNode = new Node&lt;E&gt;(e);
//tp 为尾节点,默认相等,采用失败即重试的方式,直到入队成功
for (Node&lt;E&gt; t = tail, p = t;;) {
// 获取队尾节点的下一个节点
Node&lt;E&gt; q = p.next;
// 如果 q 为 null则代表 p 就是队尾节点
if (q == null) {
// 将入列节点设置为当前队尾节点的 next 节点
if (p.casNext(null, newNode)) {
// 判断 tail 节点和 p 节点距离达到两个节点
if (p != t) // hop two nodes at a time
// 如果 tail 不是尾节点则将入队节点设置为 tail。
// 如果失败了,那么说明有其他线程已经把 tail 移动过
casTail(t, newNode); // Failure is OK.
return true;
}
}
// 如果 p 节点等于 p 的 next 节点,则说明 p 节点和 q 节点都为空,表示队列刚初始化,所以返回
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t &amp;&amp; t != (t = tail)) ? t : q;
}
}
</code></pre>
<p>**出列:**首先获取 head 节点,并判断 item 是否为 null如果为空则表示已经有一个线程刚刚进行了出列操作然后更新 head 节点;如果不为空,则使用 CAS 操作将 head 节点设置为 nullCAS 就会成功地直接返回节点元素,否则还是更新 head 节点。</p>
<pre><code> public E poll() {
// 设置起始点
restartFromHead:
for (;;) {
//p 获取 head 节点
for (Node&lt;E&gt; h = head, p = h, q;;) {
// 获取头节点元素
E item = p.item;
// 如果头节点元素不为 null通过 cas 设置 p 节点引用的元素为 null
if (item != null &amp;&amp; p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 如果 p 节点的下一个节点为 null则说明这个队列为空更新 head 结点
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 节点出队失败,重新跳到 restartFromHead 来进行出队
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
</code></pre>
<p>ConcurrentLinkedQueue 是基于 CAS 乐观锁实现的,在并发时的性能要好于其它阻塞队列,因此很适合作为高并发场景下的排队队列。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java并发编程实战/加餐 推荐几款常用的性能测试工具.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java并发编程实战/结束语 栉风沐雨,砥砺前行!.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709971c5afec3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>