3.2.1 jQuery核心函数

获取jQuery对象

jQuery()函数是获取jQuery对象的重要途径。该函数主要有如下用法。

  1. jQuery(expression,[context]):该函数会将expression对应的DOM对象包装成的jQuery对象返回。其中expression既支持CSS1CSS3的选择器,也支持XPath语法,功能非常丰富。由于expression表达式可能对应单个DOM元素,也可能对应多个DOM元素,因此该方法可能返回将单个DOM对象包装成的jQuery对象,也可能返回将多个DOM对象包装成的jQuery对象。context是个可选参数,如果指定了该参数,则表明仅获取context的子元素。
  2. jQuery(elements):将一个或多个DOM元素包装为jQuery对象。elements既可以是单个的DOM对象,也可以是多个DOM对象。该方法返回包装这些DOM对象的jQuery对象。
  3. jQuery(html,[ownerDocument]):该函数根据html参数(该参数是个HTML字符串)创建一个或多个DOM 对象,返回包装这些DOM 对象的jQuery 对象。其中ownerDocument是可选参数,指定使用ownerDocument(document对象)来创建DOM对象。
  4. jQuery(html,props):该函数根据html参数(该参数是个HTML字符串)创建一个或多个DOM 对象,返回包装这些DOM对象的jQuery对象。其中props 是一个形如{prop:value,prop2:value}的对象,该对象指定的属性将被附加到根据HTML字符串所创建的DOM对象上。
  5. jQuery(object):把普通对象包装成jQuery对象。

程序示例

下面的代码示范了jQuery函数的几种用法。

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
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title> 使用jQuery()函数 </title>
</head>
<body>
<div id="lee"></div>
<div id="yeeku"></div>
<script type="text/javascript" src="../jquery-3.1.1.js">
</script>
<script type="text/javascript">
// 1.获取所有<div.../>标签对应的DOM对象
$("div").append("新增的内容");

// 2.直接将一个DOM对象包装成jQuery对象
$(document.getElementById('lee'))
.css("background-color", "#aaffaa")
.css("border", "1px solid black");

// 3.使用HTML字符串创建一个DOM对象,并将添加到body元素内
$("<input type='button' value='单击我'/>")
.appendTo(document.body);

// 4.使用HTML字符串创建一个DOM对象,并在创建时添加属性
$("<input/>",
{
type: "button",
value: "有惊喜",
click: function () { alert("惊喜时刻!"); }
}
).appendTo(document.body);
</script>
</body>
</html>

上面的代码在使用$()函数获取了jQuery 对象之后,还调用了jQuery 对象的appendTo()append()等方法,这些方法在后面会有更详细的介绍,此处不再赘述。
在页面中创建第二个按钮时为click属性指定了事件处理函数,如果单击该页面上”有惊喜”按钮,将可以看到如图3.3所示对话框。
值得指出的是,在jQuery 的第一种用法jQuery(expression,[context])中,需要指定一个expression,该表达式能支持的形式相当多,下一节将详细介绍这些用法。

3.2 获取jQuery对象

使用jQuery的第一步就是获取jQuery对象,从jQuery库中获取jQuery对象主要有如下两种方式:

  1. 使用$()函数或用jQuery对象提供的利用父子关系返回的jQuery对象。
  2. jQuery对象的调用方法改变自身后将再次返回该jQuery对象。

本节主要介绍第一种获取jQuery对象的方式,即便如此,使用$()函数获取jQuery对象的方式仍然很多,它既支持CSS 1CSS 3选择器来访问DOM元素,也支持使用XPath语法来访问DOM元素,$()函数会将这些DOM元素包装成jQuery对象后返回。

3.1.3 让jQuery与其他JavaScript库共存

美元函数冲突

如果需要让jQuery和其他JavaScript库(如Prototype)共存,则有一个小小的问题需要解决,就是$()函数。由于jQuery中的$()函数的功能很强大,而且返回的是一个jQuery对象,而其他JavaScript库中的$()函数返回的不是jQuery对象(如Prototype$()函数返回的是一个DOM对象),因此必然引起冲突。

如何解决美元函数冲突

为了解决jQuery中的$()函数和其他JavaScript库中的$()函数的冲突问题,需要取消jQuery中的$()函数,为此jQuery提供了如下方法:
jQuery.noConflict()
建议将上面的代码放在JavaScript代码的第一行,这行代码会取消了jQuery()函数的$()函数别名,因此依然可以使用jQuery()函数来代替原来的$()函数。

为jQuery函数写别名

除此之外,多次重复书写jQuery()也是很烦琐的事情,jQuery还允许开发者为jQuery()指定一个别名,如以下代码所示。

1
2
3
// 给jQuery()函数指定别名为lee
var lee = jQuery.noConflict();
var target = lee("#lee")

上面代码为jQuery函数指定别名为lee,这就允许后面的程序使用lee()函数来代替jQuery()函数了。通过这种方式,我们可以让jQuery和其他JavaScript库共存。

3.1.2 下载和安装jQuery

从官方站点下载

由于jQuery也是一个纯粹的JavaScript库,因此下载和安装jQuery与安装其他JavaScript库完全相同。登录jQuery官方站点,在该站点可下载开发中的jQuery的最新版本。

jQuery版本

目前jQuery依然在维护、升级的有3个版本,依次是:

  • 1.X版本,兼容IE 6IE 7IE 8,这是目前使用最广泛的jQuery。但jQuery官方对该版本的jQuery只做简单的Bug修复,不会增加新功能。如果开发者希望兼容早期的IE浏览器,则只能使用jQuery 1.X系列。
  • 2.X版本,不兼容IE 8及更早版本的IE浏览器,该版本的jQuery应用并不广泛。jQuery官方对该版本的jQuery只做简单的Bug修复,不会增加新功能。一般没必要使用该版本的jQuery
  • 3.X版本,这是目前主流的jQuery,不兼容IE 8及更早版本的IE浏览器,支持最新的浏览器特性。目前官方主要更新维护的就是该版本。

从GitHub下载

登录jQueryGitHub托管站点,然后单击页面上”releases“链接,即可根据需要下载任意版本的jQuery

jQuery库版本区别

下载完成后即可得到一个jQuery库的压缩zip包,解压该zip包后,即可在解压路径的dist目录下找到如下4个JavaScript库。

  • jquery.min.js:该版本是去除注释后的jQuery库,文件体积较小,实际项目运营时推荐使用该版本。
  • jquery.js:该版本的jQuery库没有压缩,而且保留了注释。学习jQuery及有兴趣研究jQuery源代码的读者可以使用该版本。
  • jquery.slim.min.js:该版本是jquery.min.js的”瘦身”版本,其实就是去除了Ajax支持等功能的jQuery库。
  • jquery.slim.js:该版本是jquery.js 的”瘦身”版本,同样去除了Ajax 支持等功能,同时保留了注释。

本书为便于读者学习,会详细介绍jQueryAjax支持,因此使用jquery.js这个版本。

jQuery在线文档

jQuery库的在线文档主要包括Get Start(快速入门)和jQuery API Reference(API 参考)两个部分,读者也可参考该文档来学习jQuery的用法。

jQuery库的安装

jQuery库的安装比较简单,只要在HTML页面中导入jQueryJavaScript文件即可。
一旦导入了jQuery库,开发者就可以在自己的脚本中使用jQuery库提供的功能了。为了导入jQuery类库,应在HTML页面的开始位置增加如下代码:
注意,src属性在不同的安装中可能会有小小的变化,如果jquery-3.1.1.js文件名被改变了,或者它与HTML页面并不是放在同一个路径下,则应该在上述代码的基础上做相应的修改,让src属性指向jquery-3.1.1.js脚本文件所在的位置。

3.1 jQuery入门

3.1.1 理解jQuery的设计

几乎每个初次学习jQuery的读者都会发现,jQuery提供了一个$()函数,该函数专门用于获取页面上的DOM元素。看下面的jQuery入门示
例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title> jQuery入门 </title>
</head>
<body>
<div id="lee"></div>
<script type="text/javascript" src="../jquery-3.1.1.js">
</script>
<script type="text/javascript">
var target = $("#lee")
target.html("我要学习jQuery")
.height(60)
.width(160)
.css("border", "2px solid black")
.css("background-color", "#ddddff")
.css("padding", 20);
</script>
</body>
</html>

这段代码中的第一行粗体字代码使用$()函数获取页面上idleeDOM对象,后面的粗体字代码依次调用heightwidthcss 等方法处理该对象,程序的运行结果如图3.1所示。
从这个运行结果可以发现,使用jQuery动态操作HTML页面非常简单。但读者很容易产生疑惑:程序中那些粗体字代码如何来理解?
程序中的target对象到底是什么?怎么会有height()width()css()这些方法呢?
事实是,上面这个程序已经体现了jQuery的优雅设计!程序中的target对象并不是DOM对象,而是一个jQuery对象,因此它可以调用height()、width()、css()等方法。由此可见,jQuery$()函数并非简单地获取DOM元素,$()函数不仅可以获取页面上一个或多个DOM元素,而且还要将这些DOM元素包装成jQuery对象,这样即可面向jQuery对象编程,调用jQuery对象的方法了。$()函数其实是jQuery()函数的简化别名
研究了上面这个程序后,即可明白jQuery的设计:使用jQuery之后,JavaScript操作的不再是HTML元素对应的DOM对象,而是包装DOM对象的jQuery对象。JavaScript通过调用jQuery对象的方法来改变它所包装的DOM对象的属性,从而实现动态更新HTML页面。由此可见,使用

jQuery动态更新HTML页面步骤

jQuery动态更新HTML页面只需如下两个步骤:

  1. 获取jQuery对象。jQuery对象通常是对DOM对象的包装。
  2. 调用jQuery对象的方法来改变自身。当jQuery对象被改变时,jQuery包装的DOM对象随之改变,HTML页面的内容也就随之更新了。

还有一点需要指出,jQuery很多改变自身属性的方法都有返回值,就是返回该对象本身,因此可以连续多次调用改变自身属性的方法。例如在上面的程序中连续调用height()width()css()方法来改变target对象。
以上就是使用jQuery的基本思路,开发者只要掌握两点即可:
①获取jQuery对象;
jQuery有哪些可用的方法,这也是本章将要详细介绍的。
下面将从jQuery的下载和安装开始讲起。

本文重点

  • jQuery提供了一个$()函数,该函数专门用于获取页面上的DOM元素
  • $()函数其实是jQuery()函数的简化别名
  • jQuery$()函数并非简单地获取DOM元素,$()函数不仅可以获取页面上一个或多个DOM元素,而且还要将这些DOM元素包装成jQuery对象,这样即可面向jQuery对象编程,调用jQuery对象的方法了
  • jQuery很多改变自身属性的方法都有返回值,就是返回该对象本身,因此可以连续多次调用改变自身属性的方法

jQuery动态更新HTML页面步骤

jQuery动态更新HTML页面只需如下两个步骤:

  1. 获取jQuery对象。jQuery对象通常是对DOM对象的包装。
  2. 调用jQuery对象的方法来改变自身。当jQuery对象被改变时,jQuery包装的DOM对象随之改变,HTML页面的内容也就随之更新了。

开发者只要掌握两点即可:
①获取jQuery对象;
jQuery有哪些可用的方法。

第3章 jQuery库详解

本章要点

  • 理解jQuery的优雅设计
  • 下载和安装jQuery
  • 获取jQuery的工具函数
  • jQuery访问DOM对象支持的选择器
  • jQuery访问类数组对象的工具方法
  • jQuery过滤类数组的工具方法
  • jQuery提供的类DOM导航的工具方法
  • jQuery命名空间包含的方法
  • jQuery数据存储的方法
  • jQuery操作通用属性的方法
  • jQuery操作CSS样式的方法
  • jQuery更新HTML页面内容的方法
  • jQuery提供的事件编程支持
  • jQuery提供的动画效果方法
  • jQuery提供的Callbacks对象
  • Callbacks对象与回调函数列表
  • 使用load方法发送异步请求
  • 使用jQuery.ajax控制异步请求的各种选项
  • 使用get/post方法发送异步请求
  • 使用Deferred对象来管理回调函数
  • 为普通对象增加Deferred接口
  • 扩展jQuery的方法

16.6 线程通信

16.6.1 传统的线程通信

假设现在系统中有两个线程,这两个线程分别代表存款者取钱者
现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。

线程通信方法wait notify notifyAll

为了实现这种功能,可以借助于Object类提供的wait()notify()notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object。但这三个方法必须由同步监视器对象来调用,这可分成以下两种情况。

  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized关键字后面括号里的对象,所以必须使用synchronized关键字后面括号里的对象调用这三个方法.
  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法.

线程通信方法介绍

关于这三个方法的解释如下:

Object类方法 描述
wait() 导致当前线程等待,直到其他线程调用该同步监视器notify()方法或notifyAll()方法来唤醒该线程。
wait()方法有三种形式:
  • 无时间参数的wait()一直等待,直到其他线程通知)、
  • 带毫秒参数的wait()和带毫秒、毫微秒参数的wait()这两种方法都是等待指定时间后自动苏醒)。
调用wait()方法的当前线程会释放对该同步监视器的锁定
notify() 唤醒在此同步监视器上等待的单个线程
如果所有线程都在此同步监视器上等待,则会任意选择唤醒其中的一个线程。
但要等到当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll() 唤醒在此同步监视器上等待的所有线程
但要等到当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程

程序示例 账户类 存钱取钱的协调

程序中可以通过一个旗标来标识账户中是否已有存款:

  • 当旗标为false时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为true,并调用notify(或notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让该线程等待。
  • 当旗标为true时,表明账户中已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调用notifynotifyAl()方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为false就调用wait()方法让该线程等待。

修改账户类

本程序为Account类提供draw()deposit()两个方法,分别对应该账户的取钱、存款等操作,因为这两个方法可能需要并发修改Account类的balance成员变量的值,所以这两个方法都使用synchronized修饰成同步方法。除此之外,这两个方法还使用了wait()notifyAll()来控制线程的协作

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
public class Account {
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
// 标识账户中是否已有存款的旗标
private boolean haveDeposit = false;

public Account() {
}

// 构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}

public String getAccountNo() {
return this.accountNo;
}

// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
// 取款功能
public synchronized void draw(double drawAmount) {
try {
// 如果flag为假,表明账户中还没有人存钱进去,我将取不到钱,先等别人存钱进去,取钱方法阻塞
if (!haveDeposit) {
wait();
}
// 如果旗标为真,表明有人存钱了,我就可以取钱
else {
// 执行取钱
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
haveDeposit = false;
// 唤醒其他线程
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}

// 存款功能
public synchronized void deposit(double depositAmount) {
try {
// 如果flag为真,表明账户中已有人存钱进去,不需要再存钱,等别人取走前我再存
if (haveDeposit) // ①
{
wait();
}
// 如果标记假,说明账户中的钱被取走了,那我就存钱进去
else {
// 执行存款
System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
haveDeposit = true;
// 唤醒其他线程
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}

public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

上面程序中使用wait()notifyAll()进行了控制:
对于存款者线程而言,当程序进入deposit()方法后,

  • 如果flagtrue,则表明账户中已有存款,暂时不需要我存款进去,程序调用wait()方法阻塞;
  • 如果flagfalse,则表示账户中没钱,需要存钱进去,程序向下执行存款操作,当存款操作执行完成后,将flag设为true,然后调用notifyAll()方法来唤醒其他被阻塞的线程.
    • 这样,如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到①号代码处时再次进入阻塞状态,
    • 只有执行draw()方法的取钱者线程才可以向下执行从而将钱取走。同理,取钱者线程的运行流程也是如此。

程序中的存款者线程循环100次重复存款,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用Account对象的deposit()draw()方法来实现。

取钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DrawThread extends Thread {
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;

public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}

// 重复100次执行取钱操作
public void run() {
for (int i = 0; i < 100; i++) {
account.draw(drawAmount);
}
}
}

存钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DepositThread extends Thread {
// 模拟用户账户
private Account account;
// 当前取钱线程所希望存款的钱数
private double depositAmount;

public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}

// 重复100次执行存款操作
public void run() {
for (int i = 0; i < 100; i++) {
account.deposit(depositAmount);
}
}
}

主程序

主程序可以启动任意多个存款线程和取钱线程,可以看到所有的取钱线程必须等存款线程存钱后才可以向下执行,而存款线程也必须等取钱线程取钱后才可以向下执行。主程序代码如下。

1
2
3
4
5
6
7
8
9
10
public class DrawTest {
public static void main(String[] args) {
// 创建一个账户
Account account = new Account("1234567", 0);
new DrawThread("取钱者", account, 800).start();
new DepositThread("存款者甲", account, 800).start();
new DepositThread("存款者乙", account, 800).start();
new DepositThread("存款者丙", account, 800).start();
}
}

运行效果

运行该程序,可以看到存款者线程、取钱者线程交替执行的情形,每当存款者向账户中存入800元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是800元,取钱结束后账户余额总是0元。运行该程序,会看到下所示的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
存款者甲 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
......
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0

可以看出,3个存款者线程随机地向账户中存款,只有1个取钱者线程执行取钱操作。只有当取钱者取钱后,存款者才可以存款;同理,只有等存款者存款后,取钱者线程才可以取钱。
程序最后被阻塞无法继续向下执行,这是因为3个存款者线程共有300次尝试存款操作,但1个取钱者线程只有100次尝试取钱操作,所以程序最后被阻塞.
这种阻塞并不是死锁,对于这种情况,取钱者线程已经执行结束,而存款者线程只是在等待其他线程来取钱而已,并不是等待其他线程释放冋步监视器。不要把死锁和程序阻塞等同起来。

16.5.6 死锁

什么时候会发生死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,

发生死锁会怎样

Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

程序 死锁示例

死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。

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
package thread;

class A
{
public synchronized void firstA(B b)
{
System.out.println(
Thread.currentThread().getName() + " 进入了A实例的firstA()方法 睡眠.zzz"); // ①
try
{
Thread.sleep(200);
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + " 企图调用B实例的lastB()方法");
// ③
b.lastB();
}
public synchronized void lastA()
{
System.out.println("进入了A类的lastA()方法内部");
}
}
class B
{
public synchronized void firstB(A a)
{
System.out.println(
Thread.currentThread().getName() + " 进入了B实例的firstB()方法 睡眠.zzz"); // ②
try
{
Thread.sleep(200);
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + " 企图调用A实例的lastA()方法"); // ④
a.lastA();
}
public synchronized void lastB()
{
System.out.println("进入了B类的lastB()方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的firstA方法
a.firstA(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的firstB方法
b.firstB(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

运行上面程序,将会看到如下效果,此时程序既无法向下执行,也不会抛出任何异常,就一直”僵持”着

1
2
3
4
5
主线程 进入了A实例的firstA()方法 睡眠.zzz
副线程 进入了B实例的firstB()方法 睡眠.zzz
主线程 企图调用B实例的lastB()方法
副线程 企图调用A实例的lastA()方法
......一直阻塞......

究其原因,是因为上面程序中A对象和B对象的方法都是同步方法,也就是A对象和B对象都是同步锁
程序中两个线程执行:

  • 一个线程的线程执行体是DeadLock类的run()方法,
  • 另一个线程的线程执行体是DeadLockinit()方法(主线程调用了init()方法)。

其中run()方法中让B对象调用firstB()方法,
init()方法让A对象调用firstA()方法。

发生死锁的过程分析

  • 从本次运行结果来看,主线程的init()方法先执行,调用了A对象的firstA方法,进入firstA方法之前,该线程会对A对象加锁,不过当程序执行到firstA方法中的①号代码时,主线程睡眠200毫秒,在睡眠期间,主线程继续持有A对象的锁。
  • 这时候CPU切换到执行另一个线程(副线程),所以看到副线程开始执行B实例的firstB方法,进入firstB方法之前,该线程会对B对象加锁,不过当程序执行到firstB方法中的②号代码时,副线程也睡眠200毫秒,在睡眠期间,副线程继续持有B对象的锁。
  • 接下来主线程会先醒过来,继续向下执行,执行到③号代码处时,要调用B对象的lastB()方法,但执行该方法之前必须先对B对象加锁,由于此时副线程正保持着B对象的锁,所以主线程无法加锁,主线程阻塞;
  • 接下来副线程醒过来,继续向下执行,执行到④号代码处时,要调用A对象的lastA()方法,但执行该方法之前必须先对A对象加锁,由于主线程还没有释放A对象的锁,副线程无法加锁,副线程也阻塞
  • 这就出现了主线程保持着A对象的锁,等待对B对象加锁,而副线程保持着B对象的锁,等待对A对象加锁,两个线程互相等待对方先释放,所以就出现了死锁。

由于Thread类的suspend方法也很容易导致死锁,所以Java不再推荐使用该方法来暂停线程的执行

如何写一个死锁

  • 先要有两个线程,设为主线程,副线程
  • 要有两个类,设为A类对象和B类对象
    • A类对象有两个同步方法,
      • A类对象的第一个方法(设置firstA)的参数是B类对象,
        • 执行该方法时,先睡眠一会,然后调用B类对象的第二个无参同步方法lastB
      • A类对象的第二个同步方法lastB是个无参方法。
    • B类对象有两个同步方法,
      • B类对象的第一个方法firstB的参数传入A类对象,
        -firstB方法中调用A类对象的第二个无参同步方法lastA
      • B类对象的第二个方法同样是无参方法。
  • 创建A类对象,创建B类对象.
  • 主线程的执行体中调用A类对象的第一个方法firstA,该方法传入B类对象作为参数.
  • 副线程的执行题中条用B类对象的第一个方法firstB,该方法传入A类对象作为参数.
  • 启动这两个线程,就会发生死锁.

16.5.5 同步锁Lock

使用Lock对象作为同步锁

Java 5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当

Lock的优点

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

锁接口和实现类

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。

LockReadWriteLockJava 5提供的两个根接口

Lock接口

  • JavaLock提供了ReentrantLock(可重入锁)实现类,

ReadWriteLock接口

ReadWriteLock(读写锁)允许对共享资源并发访问。

  • JavaReadWriteLock提供了ReentrantReadWriteLock实现类。
    • ReentrantReadWriteLock为读写操作提供了三种锁模式: WritingReadingOptimisticReading

StampedLock类

Java 8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock,

常用的 可重入锁 ReentrantLock

在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class X
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
...
//定义需要保证线程安全的方法
public void m()
{
//加锁
lock.lock();
try
{
//需要保证线程安全的代码
}
//使用 finally块来保证释放锁
finally
{
lock.unlock();
}
}
}

使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

程序示例 使用ReentrantLock改写Account

通过使用ReentrantLock对象,可以把Account类改为如下形式,它依然是线程安全的:

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
import java.util.concurrent.locks.*;

public class Account
{
// 1.定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
public void draw(double drawAmount)
{
// 2.加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 3.修改完成,释放锁
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

上面程序中的

  • 先定义了一个ReentrantLock对象,
  • 程序中实现draw()方法时,进入该方法后立即请求对ReentrantLock对象进行加锁,
  • 当执行完draw()方法的取钱逻辑之后,程序使用finally块来确保释放锁。

Lock和同步方法的异同

使用Lock与使用同步方法有点相似,

  • 只是使用Lock时显式使用Lock对象作为同步监视器,
  • 而使用同步方法时系统隐式使用当前对象作为同步监视器,
  • 同样都符合”加锁→修改→释放锁”的操作模式,而且使用Lock对象时,每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一个线程能进入临界区

同步代码块,同步方法 锁三者的区别

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

Lock提供了同步方法和同步代码块没有的功能

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。
Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long, TimeUnit)方法。

可重入性

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

16.5.4 释放同步监视器的锁定

何时释放同步监视器

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:

正常执行结束时

当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

break return时

当前线程在同步代码块中遇到break、同步方法中遇到return,当前线程将会释放同步监视器。

出现异常时

线程执行同步代码块或同步方法时出现了未处理的ErrorException,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。

wait方法

线程执行同步代码块或同步方法时调用了同步监视器对象的wait()方法时,则当前线程暂停,并释放同步监视器。

不会释放同步监视器的情况

在如下所示的情况下,线程不会释放同步监视器。

sleep yield

线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器

suspend

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用suspend()resumed()方法来控制线程。