答:引用来源: 百度文库《完全理解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
|
|