答:引用来源: 百度文库《完全理解Java中生产者和消费者模型》http://wenku.baidu.com/view/cfdf0f89cc22bcd126ff0c3e.html
在平时的编程中,经常遇到一个线程要产生数据,而另一个线程要处理产生出来的数据,这其实就是生产者和消费者的关系。生产者在产生数据后可以直接调用消费者处理数据;也可以把数据放在一个缓冲区中,让消费者从缓冲区中取出数据处理,两种方式从调用方式上来说,第一种可是说是同步的,即生产者在生产出数据后要等待消费者消耗掉后才能生产下一个数据,等待时间的长短取决于消费者处理数据的能力;第二种方式是异步的,生产者只管生产数据,然后扔到一个缓冲区内,不管数据是否被立即处理了,消费者则从缓冲区中依次取出数据进行自己节奏的处理。从线程模型角度来说,第一种是单线程的,而第二种则是多线程的。多线程必须要考虑的一个问题是线程之间的协作,协作即协调合作,不要乱套,以生产者和消费者模型而言,就是当缓冲区里没有数据时消费者要等待,等待生产者生产数据,当缓冲区满的时候生产者要等待,等待消费者消耗掉一些数据空出位置好存放数据。
java中为了实现多线程之间的协助,需要用到几个特性:wait(),notify(),notifyAll(),synchronized,synchronized相当于操作系统里的临界区或者锁的概念,所谓临界区就是说一次只能有一个线程进去,其他想进入的线程必须等待,加了synchronized锁后,才能调用wait(),notify()和notifyAll()操作,wait方法被调用后,当前线程A(举例)进入被加锁对象的线程休息室,然后释放锁,等待被唤醒。释放的锁谁来获取?当然是由先前等待的另一个线程B得到,B在获得锁后,进行某种操作后通过notify或者notifyAll把A从线程休息室唤醒,然后释放锁,A被唤醒后,重新获取锁定,进行下一语句的执行。
再回到生产者和消费者模型,如果引入了缓冲区的话就需要处理生产者线程和消费者线程之间的协作,缓冲区可以有这几种,队列缓冲区,比如队列或者栈,队列缓冲区的特点是其长度是动态增长的,这就意味着内存的动态分配带来的性能开销,同时队列缓冲区还会产生因为多线程之间的同步和互斥带来的开销。环形缓冲区可以解决内存分配带来开销的问题,因为环形缓冲区长度是固定的。但是环形缓冲区还是无法解决同步互斥带来的多线程切换的开销,如果生产者和消费者都不止一个线程,带来的开销更大,终极解决办法是引入双缓冲区,何为双缓冲区?双缓冲区顾名思义是有两个长度固定的缓冲区A B,生产者和消费者只使用其中一个,当两个缓冲区都操作完成后完成一次切换,开始时生产者开始向A里写数据,消费者从B里读取数据,当A写满同时B也读完后,切换一下,这时消费者从A里取数据,生产者向B写数据,由于生产者和消费者不会同时操作同一个缓冲区,所以不会发生冲突。
生产者和消费者模型不止是用在多线程之间,不同进程之间也可以有。线程和进程到底有什么区别?这是很多程序员搞不清的问题,其实很简单,进程有自己的地址空间和上下文,线程是在一个进程上并发执行的代码段。其实在win32系统中进程只是占用一定长度的地址空间,进程中总是有一个主线程来运行。消费者和生产者模型应用于进程间通信的典型例子是分布式消息处理,消息的消费者进程需要一个缓冲区缓冲收到的消息,消息的生产者进程也需要一个缓冲区缓冲将要发送的消息,这样可以一定程度上减少因为网络断开引起的消息丢失。
对于此模型,应该明确一下几点:
1,生产者仅仅在仓储未满时生产,仓满则停止生产。
2, 消费 者仅仅在仓储有产品时才能消费,仓空则等待。
3, 当消费者发现仓储没有产品的时候会通知生产者生产。
4, 生产者在生产出可消费产品的时候,应该通知等待的消费者去消费。 以下是它的具体实现:
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
|
public
class
ProducerConsumer {
public
static
void
main(String []args) {
SyncStack ss=
new
SyncStack();
Producer p=
new
Producer(ss);
Consumer c=
new
Consumer(ss);
new
Thread(p).start();
new
Thread(c).start();
}
}
class
WoTou {
int
id;
WoTou(
int
id) {
this
.id=id;
}
public
String toString() {
return
"WoTou : "
+id;
}
}
class
SyncStack {
int
index=
0
;
WoTou[] arrWT=
new
WoTou[
6
];
public
synchronized
void
push(WoTou wt) {
while
(index==arrWT.length) {
try
{
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
this
.notify();
arrWT[index]=wt;
index++;
}
public
synchronized
WoTou pop() {
while
(index==
0
) {
try
{
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
this
.notify();
index--;
return
arrWT[index];
}
}
class
Producer
implements
Runnable {
SyncStack ss=
null
;
Producer(SyncStack ss) {
this
.ss=ss;
}
public
void
run() {
for
(
int
i=
0
;i<
20
;i++) {
WoTou wt=
new
WoTou(i);
ss.push(wt);
System.out.println(
"生产了:"
+wt);
try
{
Thread.sleep((
int
)(Math.random()*
2
));
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
class
Consumer
implements
Runnable {
SyncStack ss=
null
;
Consumer(SyncStack ss) {
this
.ss=ss;
}
public
void
run() {
for
(
int
i=
0
;i<
20
;i++) {
WoTou wt=ss.pop();
System.out.println(
"消费了:"
+wt);
try
{
Thread.sleep((
int
)(Math.random()*
1000
));
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
|
附:1程序、进程和线程
程序,就是一段静态的可执行的代码。 进程,就是程序的一次动态的执行过程。
线程,是程序 从头到尾的执行路线,也称为轻量级的进程。一个进程在执行过程中,可以产生多个线程,形成多个执行路线。但线程间是彼此相互独立的。各个线程可以共享相同的内存空间,并利用共享内存来完成数据交换、实时通信和一些同步的工作。而进程都占有不同的内存空间。
单线程是指一个 程序只有一条从开始到结束的顺序的执行路线。 多线程是多个彼此独立的线程,多条执行路线。
2.wait()、notify()可以在任何位置调用,suspend()、resume()只能在synchronized()方法或代码块中调用。 3线程同步
当多个用户线程在并发运行中,可能会因为同时访问一些内容而产生错误问题。例如,同一时刻,一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没有等到读取数据的线程读取完毕就去处理数据,必然得到错误的结果。
二、Java参数传递和引用传递的区别?
答:引用来源: 《java中的值传递和引用传递》,http://blog.csdn.net/wyzsc/article/details/6341107
Java中没有指针,所以也没有引用传递了,仅仅有值传递 不过可以通过对象的方式来实现引用传递 类似java没有多继承 但可以用多次implements 接口实现多继承的功能
值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参
数的值。
引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
面试题:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:是值传递。Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。
-------------------------------------------------------------
在 Java 应用程序中永远不会传递对象,而只传递对象引用。因此是按引用传递对象。但重要的是要区分参数是如何传递的,这才是该节选的意图。Java 应用程序按引用传递对象这一事实并不意味着 Java 应用程序按引用传递参数。参数可以是对象引用,而 Java 应用程序是按值传递对象引用的。
Java 应用程序中的变量可以为以下两种类型之一:引用类型或基本类型。当作为参数传递给一个方法时,处理这两种类型的方式是相同的。两种类型都是按值传递的;没有一种按引用传递。
按 值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本。因此,如果函数修改了该参数,仅改变副本,而原始值保持不变。按引用传递意味 着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本。因此,如果函数修改了该参数的值,调用代码中的原始值也随之改变。如果 函数修改了该参数的地址,调用代码中的原始值不会改变.
当传递给函数的参数不是引用时,传递的都是该值的一个副本(按值传递)。区别在于引用。在 C++ 中当传递给函数的参数是引用时,您传递的就是这个引用,或者内存地址(按引用传递)。在 Java 应用程序中,当对象引用是传递给方法的一个参数时,您传递的是该引用的一个副本(按值传递),而不是引用本身。
Java 应用程序按值传递参数(引用类型或基本类型),其实都是传递他们的一份拷贝.而不是数据本身.(不是像 C++ 中那样对原始值进行操作。)
例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//在函数中传递基本数据类型,
public
class
Test {
public
static
void
change(
int
i,
int
j) {
int
temp = i;
i = j;
j = temp;
}
public
static
void
main(String[] args) {
int
a =
3
;
int
b =
4
;
change(a, b);
System.out.println(
"a="
+ a);
System.out.println(
"b="
+ b);
}
}
结果为:
a=
3
b=
4
原因就是 参数中传递的是 基本类型 a 和 b 的拷贝,在函数中交换的也是那份拷贝的值,
而不是数据本身;
|
例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//传的是引用数据类型
public
class
Test {
public
static
void
change(
int
[] counts) {
counts[
0
] =
6
;
System.out.println(counts[
0
]);
}
public
static
void
main(String[] args) {
int
[] count = {
1
,
2
,
3
,
4
,
5
};
change(count);
}
}
在方法中 传递引用数据类型
int
数组,实际上传递的是其引用count的拷贝,他们都指向数组对象,
在方法中可以改变数组对象的内容。即:对复制的引用所调用的方法更改的是同一个对象。
|
例3:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//对象的引用(不是引用的副本)是永远不会改变的
class
A {
int
i =
0
;
}
public
class
Test {
public
static
void
add(A a) {
a =
new
A();
a.i++;
}
public
static
void
main(String args[]) {
A a =
new
A();
add(a);
System.out.println(a.i);
}
}
输出结果是
0
在该程序中,对象的引用指向的是A ,而在change方法中,传递的引用的一份副本则指向了一个新的
OBJECT,并对其进行操作。而原来的A对象并没有发生任何变化。 引用指向的是还是原来的A对象。
|
例4:
String 不改变,数组改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
Example {
String str =
new
String(
"good"
);
char
[] ch = {
'a'
,
'b'
,
'c'
};
public
static
void
main(String args[]) {
Example ex =
new
Example();
ex.change(ex.str, ex.ch);
System.out.print(ex.str +
" and "
);
System.out.println(ex.ch);
}
public
void
change(String str,
char
ch[]) {
str =
"test ok"
;
ch[
0
] =
'g'
;
}
}
程序
3
输出的是 good and gbc.
String 比较特别,看过String 代码的都知道, String 是
final
的。所以值是不变的。
函数中String对象引用的副本指向了另外一个新String对象,而数组对象引用的副本没有改变,
而是改变对象中数据的内容.对于对象类型,也就是Object的子类,如果你在方法中修改了它的
成员的值,那个修改是生效的,方法调用结束后,它的成员是新的值,但是如果你把它指向一个
其它的对象,方法调用结束后,原来对它的引用并没用指向新的对象。
|
Java参数,不管是原始类型还是引用类型,传递的都是副本(有另外一种说法是传值,但是说传副本更好理解吧,传值通常是相对传址而言)。
如果参数类型是原始类型,那么传过来的就是这个参数的一个副本,也就是这个原始参数的值,这个跟之前所谈的传值是一样的。如果在函数中改变了副本的 值不会改变原始的值.
如果参数类型是引用类型,那么传过来的就是这个引用参数的副本,这个副本存放的是参数的地址。如果在函数中没有改变这个副本的地址,而是改变了地址中的 值,那么在函数内的改变会影响到传入的参数。如果在函数中改变了副本的地址,如new一个,那么副本就指向了一个新的地址,此时传入的参数还是指向原来的 地址,所以不会改变参数的值。
( 对象包括对象引用即地址和对象的内容)
a.传递值的数据类型:八种基本数据类型和String(这样理解可以,但是事实上String也是传递的地址,只是string对象和其他对 象是不同的,string对象是不能被改变的,内容改变就会产生新对象。那么StringBuffer就可以了,但只是改变其内容。不能改变外部变量所指 向的内存地址)。
b.传递地址值的数据类型:除String以外的所有复合数据类型,包括数组、类和接口
下面举例说明:
在 Java 应用程序中永远不会传递对象,而只传递对象引用。因此是按引用传递对象。但重要的是要区分参数是如何传递的,这才是该节选的意图。 Java 应用程序按引用传递对象这一事实并不意味着 Java 应用程序按引用传递参数。参数可以是对象引用,而 Java 应用程序是按值传递对象引用的。
Java 应用程序中的变量可以为以下两种类型之一:引用类型或基本类型。当作为参数传递给一个方法时,处理这两种类型的方式是相同的。两种类型都是按值传递的;没有一种按引用传递。
按 值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本。因此,如果函数修改了该参数,仅改变副本,而原始值保持不变。按引用传递意味 着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本。因此,如果函数修改了该参数,调用代码中的原始值也随之改变。
当传递给函数的参数不是引用时,传递的都是该值的一个副本(按值传递)。区别在于引用。在 C++ 中当传递给函数的参数是引用时,您传递的就是这个引用,或者内存地址(按引用传递)。在 Java 应用程序中,当对象引用是传递给方法的一个参数时,您传递的是该引用的一个副本(按值传递),而不是引用本身。
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
|
class
Test
{
public
static
void
main(String args[])
{
int
val;
StringBuffer sb1, sb2;
val =
10
;
sb1 =
new
StringBuffer (
"apples"
);
sb2 =
new
StringBuffer (
"pears"
);
System .out.println(
"val is "
+ val);
System .out.println(
"sb1 is "
+ sb1);
System .out.println(
"sb2 is "
+ sb2);
System .out.println(
""
);
System .out.println(
"calling modify"
);
// 按值传递所有参数
modify(val, sb1, sb2);
System .out.println(
"returned from modify"
);
System .out.println(
""
);
System .out.println(
"val is "
+ val);
System .out.println(
"sb1 is "
+ sb1);
System .out.println(
"sb2 is "
+ sb2);
}
public
static
void
modify(
int
a, StringBuffer r1,
StringBuffer r2)
{
System .out.println(
"in modify..."
);
a =
0
;
r1 =
null
;
//1
r2.append(
" taste good"
);
System .out.println(
"a is "
+ a);
System .out.println(
"r1 is "
+ r1);
System .out.println(
"r2 is "
+ r2);
}
}
|
Java 应用程序的输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
val is
10
sb1 is apples
sb2 is pears
calling modify
in modify...
a is
0
r1 is
null
r2 is pears taste good
returned from modify
val is
10
sb1 is apples
sb2 is pears taste good
|
这段代码声明了三个变量:一个整型变量和两个对象引用。设置了每个变量的初始值并将它们打印出来。然后将所有三个变量作为参数传递给 modify 方法。
modify 方法更改了所有三个参数的值:
将第一个参数(整数)设置为 0 。
将第一个对象引用 r1 设置为 null 。
保留第二个引用 r2 的值,但通过调用 append 方法更改它所引用的对象(这与前面的 C++ 示例中对指针 p 的处理类似)。
当执行返回到 main 时,再次打印出这三个参数的值。正如预期的那样,整型的 val 没有改变。对象引用 sb1 也没有改变。如果 sb1 是按引用传递的,正如许多人声称的那样,它将为 null 。但是,因为 Java 编程语言按值传递所有参数,所以是将 sb1 的引用的一个副本传递给了 modify 方法。当 modify 方法在 //1 位置将 r1 设置为 null 时,它只是对 sb1 的引用的一个副本进行了该操作,而不是像 C++ 中那样对原始值进行操作。
另外请注意,第二个对象引用 sb2 打印出的是在 modify 方法中设置的新字符串。即使 modify 中的变量 r2 只是引用 sb2 的一个副本,但它们指向同一个对象。因此,对复制的引用所调用的方法更改的是同一个对象。
传值---传递基本数据类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
class
PassValue{
static
void
exchange(
int
a,
int
b){
//静态方法,交换a,b的值
int
temp;
temp = a;
a = b;
b = temp;
}
public
static
void
main(String[] args){
int
i =
10
;
int
j =
100
;
System.out.println(
"before call: "
+
"i="
+ i +
"/t"
+
"j = "
+ j);
//调用前
exchange(i, j);
//值传递,main方法只能调用静态方法
System.out.println(
"after call: "
+
"i="
+ i +
"/t"
+
"j = "
+ j);
//调用后
}
}
|
运行结果:
1
2
|
before call: i =
10
j =
100
after call: i =
10
j =
100
|
说明:调用exchange(i, j)时,实际参数i,j分别把值传递给相应的形式参数a,b,在执行方法exchange()时,形式参数a,b的值的改变不影响实际参数i和j的值,i和j的值在调用前后并没改变。
引用传递---对象作为参数
如果在方法中把对象(或数组)作为参数,方法调用时,参数传递的是对象的引用(地址),即在方法调用时,实际参数把对对象的 引用(地址)传递给形式参数。这是实际参数与形式参数指向同一个地址,即同一个对象(数组),方法执行时,对形式参数的改变实际上就是对实际参数的改变, 这个结果在调用结束后被保留了下来。
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
|
class
Book{
String name;
private
folat price;
Book(String n,
float
){
//构造方法
name = n;
price = p;
}
static
void
change(Book a_book, String n,
float
p){
//静态方法,对象作为参数
a_book.name = n;
a_book.price = p;
}
public
void
output(){
//实例方法,输出对象信息
System.out.println(
"name: "
+ name +
"/t"
+
"price: "
+ price);
}
}
public
class
PassAddr{
public
static
void
main(String [] args){
Book b =
new
Book(
"java2"
,
32
.5f);
System.out.print(
"before call:/t"
);
//调用前
b.output();
b.change(b,
"c++"
,
45
.5f);
//引用传递,传递对象b的引用,修改对象b的值
System.out.print(
"after call:/t"
);
//调用后
b.output();
}
}
|
运行结果:
1
2
|
before call: name:java2 price:
32.5
after call: name:c++ price:
45.5
|
说明:调用change(b,"c++",45.5f)时,对象b作为实际参数,把引用传递给相应的形式参数a_book,实际上a_book也指向同一 个对象,即该对象有两个引用名:b和a_book。在执行方法change()时,对形式参数a_book操作就是对实际参数b的操作。
三、什么是数据库事务,解释数据库锁及其特性。
答:引用来源: 《数据库中的事务和锁》,http://blog.csdn.NET/zztfj/article/details/7879613
一、事务的ACID特性
1、A (Atomicity) 原子性
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。
2、C (Consistency)一致性
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B树索引或双向链表)都必须是正确的。
3、I (Isolation) 隔离性
并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。
4、D (Durability) 持久性
事务完成之后,它对于系统的影响是永久性的。该修改即使出现系统故障也将一直保持。
二、完整的事务
BEGIN a transaction: 设置事务的起始点
COMMIT a transaction: 提交事务,使事务提交的数据成为持久,不可更改的部分.
ROLLBACK a transaction:撤消一个事务,使之成为事务开始前的状态.
SAVE a transaction:建立一个标签,做为部分回滚时使用,使之恢复到标签初的状态.
事务的语法:
BEGIN TRAN[SACTION] [
COMMIT [TRAN[SACTION] [
ROLLBACK TRAN[SACTION] [
SAVE TRAN[SACTION] [
事务完整的例子:
1
|
|
三、锁
锁定是 Microsoft SQL Server数据库引擎用来同步多个用户同时对同一个数据块的访问的一种机制。
在事务获取数据块当前状态的依赖关系(比如通过读取或修改数据)之前,它必须保护自己不受其他事务对同一数据进行修改的影响。事务通过请求锁定数据块来达到此目的。锁有多种模式,如共享或独占。锁模式定义了事务对数据所拥有的依赖关系级别。如果某个事务已获得特定数据的锁,则其他事务不能获得会与该锁模式发生冲突的锁。如果事务请求的锁模式与已授予同一数据的锁发生冲突,则数据库引擎实例将暂停事务请求直到第一个锁释放。
四大冲突问题
1、脏读
某个事务读取的数据是另一个事务正在处理的数据。而另一个事务可能会回滚,造成第一个事务读取的数据是错误的。
2、不可重复读
在一个事务里两次读入数据,但另一个事务已经更改了第一个事务涉及到的数据,造成第一个事务读入旧数据。
3、幻读
幻读是指当事务不是独立执行时发生的一种现象。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
4、更新丢失
多个事务同时读取某一数据,一个事务成功处理好了数据,被另一个事务写回原值,造成第一个事务更新丢失。
锁模式:
1、共享锁
共享锁(S 锁)允许并发事务在封闭式并发控制下读取 (SELECT)资源。有关详细信息,请参阅并发控制的类型。资源上存在共享锁(S锁)时,任何其他事务都不能修改数据。读取操作一完成,就立即释放资源上的共享锁(S锁),除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留共享锁(S锁)。
2、更新锁(U锁)
更新锁在共享锁和排他锁的杂交。更新锁意味着在做一个更新时,一个共享锁在扫描完成符合条件的数据后可能会转化成排他锁。
这里面有两个步骤:
1) 扫描获取Where条件时。这部分是一个更新查询,此时是一个更新锁。
2) 如果将执行写入更新。此时该锁升级到排他锁。否则,该锁转变成共享锁。
更新锁可以防止常见的死锁。
3、排他锁
排他锁(X 锁)可以防止并发事务对资源进行访问。排他锁不与其他任何锁兼容。使用排他锁(X锁)时,任何其他事务都无法修改数据;仅在使用 NOLOCK提示或未提交读隔离级别时才会进行读取操作。
事务隔离级别
SQL Server通过SET TRANSACTION ISOLATION LEVEL语句设置事务隔离级别:
SET TRANSACTION ISOLATION LEVEL
{ READ UNCOMMITTED
| READ COMMITTED
| REPEATABLE READ
| SNAPSHOT
| SERIALIZABLE
}
[ ; ]
Read Committed是SQL Server和Oracle的预设隔离等级。
1、READ UNCOMMITTED
Read UnCommitted事务可以读取事务已修改,但未提交的的记录。
Read UnCommitted事务会产生脏读(Dirty Read)。
Read UnCommitted事务与select语句加nolock的效果一样,它是所有隔离级别中限制最少的。
2、READ COMMITTED
一旦创建共享锁的语句执行完成,该锁顶便释放。
Read Committed是SQL Server的预设隔离等级。
Read Committed只可以防止脏读。
1
|
|
3、REPEATABLE READ
REPEATABLE READ事务不会产生脏读,并且在事务完成之前,任何其它事务都不能修改目前事务已读取的记录。
其它事务仍可以插入新记录,但必须符合当前事务的搜索条件——这意味着当前事务重新查询记录时,会产生幻读(Phantom Read)。
4、SERIALIZABLE
SERIALIZABLE可以防止除更新丢失外所有的一致性问题,即:
1.语句无法读取其它事务已修改但未提交的记录。
2.在当前事务完成之前,其它事务不能修改目前事务已读取的记录。
3.在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。
5、SNAPSHOT
Snapshot事务中任何语句所读取的记录,都是事务启动时的数据。
这相当于事务启动时,数据库为事务生成了一份专用“快照”。在当前事务中看到不其它事务在当前事务启动之后所进行的数据修改。
Snapshot事务不会读取记录时要求锁定,读取记录的Snapshot事务不会锁住其它事务写入记录,写入记录的事务也不会锁住Snapshot事务读取数据。
四、悲观锁和乐观锁
1、悲观锁
悲观锁是指假设并发更新冲突会发生,所以不管冲突是否真的发生,都会使用锁机制。
悲观锁会完成以下功能:锁住读取的记录,防止其它事务读取和更新这些记录。其它事务会一直阻塞,直到这个事务结束.
悲观锁是在使用了数据库的事务隔离功能的基础上,独享占用的资源,以此保证读取数据一致性,避免修改丢失。
悲观锁可以使用Repeatable Read事务,它完全满足悲观锁的要求。
2、乐观锁
乐观锁不会锁住任何东西,也就是说,它不依赖数据库的事务机制,乐观锁完全是应用系统层面的东西。
如果使用乐观锁,那么数据库就必须加版本字段,否则就只能比较所有字段,但因为浮点类型不能比较,所以实际上没有版本字段是不可行的。
3、死锁
当二或多个工作各自具有某个资源的锁定,但其它工作尝试要锁定此资源,而造成工作永久封锁彼此时,会发生死锁。例如:
1. 事务 A取得数据列 1 的共享锁定。
2. 事务B取得数据列 2 的共享锁定。
3. 事务A现在要求数据列 2 的独占锁定,但会被封锁直到事务B 完成并释出对数据列 2 的共享锁定为止。
4. 事务B现在要求数据列 1 的独占锁定,但会被封锁直到事务A 完成并释出对数据列 1 的共享锁定为止。
等到事务B 完成后,事务A 才能完成,但事务B被事务A 封锁了。这个状况也称为「循环相依性」(Cyclic Dependency)。事务A相依于事务B,并且事务B也因为相依于事务A 而封闭了这个循环。
SQL Server遇到死锁时会自动杀死其中一个事务,而另一个事务会正常结束(提交或回滚)。
SQL Server对杀死的连接返回错误代码是1205,异常提示是:
Your transaction (process ID #52) was deadlocked on {lock | communication buffer | thRead} resources with another process and has been chosen as the deadlock victim. Rerun your transaction.
例如以下操作就会产生死锁,两个连接互相阻塞对方的update。
1
|
|