Java多线程:同步和锁

为何需要同步和锁


看下面一段代码:

1
2
3
4
5
6
7
8
9
10
public class UnsafeSequence{
private int value;
/**
* 返回一个唯一的数值
*/
public int getNext(){
return value++;
}
}

这个类存在的问题是如果执行时机不对,那么两个线程在调用getNext方法时候会得到相同的值。具体过程如下:

当执行某个方法的正确性取决于多个线程的交替执行时序时候,那么就会发生竞态条件

而线程的同步和锁就是为了解决竞态条件而出现的,它可以防止多个线程访问同一个数据对象时候对数据造成的破坏。

要点


每一个Java对象都有一个可重入的内置锁,这里的可重入不是Linux里面的函数重入,而是以线程为粒度,线程自身重入。

当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

  1. 只能同步方法,而不能同步变量和类;
  2. 每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
  3. 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
  4. 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
  5. 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
  6. 线程睡眠时,它所持的任何锁都不会释放。
  7. 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
  8. 同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
  9. 在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。

同步方法和同步块


同步方法如下:

1
2
3
public synchronized int getX() {
return x++;
}

Java提供了粒度更加细的同步块,可以实现和同步方法一样的效果,例如:

1
2
3
4
5
public int getX() {
synchronized (this) {
return x++;
}
}

同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。
例如:

1
2
3
public static synchronized int setName(String name){
Xxx.name = name;
}

等价于

1
2
3
4
5
public static int setName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}

当考虑阻塞时,一定要注意哪个对象正被用于锁定:

  1. 调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。

  2. 调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

  3. 静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

  4. 对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

无论何时都要记住synchronized锁住的总是对象而不是代码,例如:

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
class Sync {
public synchronized void test() {
System.out.println("test开始..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束..");
}
}
class MyThread extends Thread {
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new MyThread();
thread.start();
}
}
}

运行结果:

1
2
3
4
5
6
test开始..
test开始..
test开始..
test结束..
test结束..
test结束..

明显结果不是想要的,那么应该改成:

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
class Sync {
public void test() {
synchronized (Sync.class) {
System.out.println("test开始..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束..");
}
}
}
class MyThread extends Thread {
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new MyThread();
thread.start();
}
}
}
/***
*
或者改为让三个线程使用同一个Sync的对象
class MyThread extends Thread {
private Sync sync;
public MyThread(Sync sync) {
this.sync = sync;
}
public void run() {
sync.test();
}
}
public class Main {
public static void main(String[] args) {
Sync sync = new Sync();
for (int i = 0; i < 3; i++) {
Thread thread = new MyThread(sync);
thread.start();
}
}
}
*
*/

这里使用synchronized(Sync.class)实现了全局锁对象的效果

运行结果:

1
2
3
4
5
6
test开始..
test结束..
test开始..
test结束..
test开始..
test结束..

同步机制


Java最初提供了wait()notify()notifyAll()方法来进行同步机制。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。

1
2
3
4
5
6
7
8
9
10
void notify()
唤醒在此对象监视器上等待的单个线程。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

wait()notify()notifyAll()都是Object的实例方法。

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。

从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行

相应的notify()就是对对象锁的唤醒操作

但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。

这样就提供了在线程间同步、唤醒的操作。

Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制

另外需要注意的是直接在线程中调用wait(),notify()方法,那么这个对象就是当前的线程,因此在一个线程中不能自己wait()和notify()。

例子

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
/***
* 同步机制:wait和notify
*
*/
public class Wait {
public static void main(String[] args) {
Object a = new Object();
ThreadB b = new ThreadB(a);
// 启动计算线程
b.start();
//线程A拥有b对象上的锁。
//线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者
synchronized (a) { // 这里也可以是b,然后ThreadB里面直接notify即可
try {
System.out.println("等待对象b完成计算。。。");
// 当前线程A等待
a.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b对象计算的总和是:" + b.total);
}
}
}
/**
* 计算1+2+3 ... +100的和
*
*/
class ThreadB extends Thread {
int total;
private Object a;
public ThreadB(Object a) {
this.a = a;
}
public void run() {
synchronized (a) {
for (int i = 0; i < 101; i++) {
total += i;
}
// (完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒
// 注意,这里一定是a,除非锁的是 ThreadB b ,此时可以是notify()
a.notify();
}
}
}

输出结果:

1
2
等待对象b完成计算。。。
b对象计算的总和是:5050