Java同步锁解析

synchronized是用来控制线程同步的,是一个同步锁,防止在多线程的环境下,某些资源被多线程同时操作。将不能同时操作的资源用synchronized锁住,当某个线程要执行被synchronized关键字锁住的代码片段的时候,将首先检查锁是否可用,然后获取锁,执行代码,最后释放锁。synchronized可用于一段代码上(同步语句块),也可用于方法上(同步方法)。

synchronized锁住的是对象而不是某段代码,所以要将不能被同时访问的资源封装到一个对象里,然后将所有要访问这个资源的方法标记为synchronized。
如果某个线程处于一个对a对象中标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用该对象中任何标记为synchronized方法的线程都会被阻塞。所有对象都自动含有单一的锁,当在对象上调用起任意synchronized方法的时候,此对象就被加锁,这时该对象上其他synchronized方法只有等到前一个带有synchronized的方法调用完毕并释放锁之后才能被调用。例如:

1
2
3
4
5
public class A{
synchronized f1(){}
synchronized f2(){}
}
A a = new A();

当thread1调用了a.f1,当thread2要调用a.f2时,就会被阻塞,得等到thread1完成对a.f1的调用之后才能调用a.f2。

再看另一种情况:

1
2
3
4
5
6
7
public class A{
synchronized f1(){
f2();
}
synchronized f2(){}
}
A a = new A();

当thread1调用a.f1时,因为f1加了synchronized,则thread1获得了对象a的锁,而这时f1中又调用了f2,而f2是synchronized,也需要获得一个锁,因为此时的f1和f2都是A中的方法,所以当前线程thread1要获得的f2锁的对象也是a。由于当前线程thread1在执行f1时已经持有了a对象的锁,因此这个时候调用f2是没有任何影响的,相当于方法f2上没有加synchronized。

下面看个demo

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结束..

由结果看出带synchronized的test方法并没有锁住(其实不应该说没有锁住,而是这个锁并没有达到预计的目标)。回看代码,发现main线程起了3线线程,在线程的run方法中对test方法调用。在run中new了一个sync对象,这个sync对象每个线程都会new一个全新的,所以当调用sync.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
class Sync {
public synchronized void test() {
System.out.println("test开始.." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束.." + Thread.currentThread().getName());
}
}
class MyThread extends Thread {
private Sync sync;
public MyThread(Sync sync, String nameNo){
this.sync = sync;
this.setName(nameNo);
}
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++) {
// 3个线程都是同一个Sync对象
Thread thread = new MyThread(sync, "name" + i);
thread.start();
}
}
}

运行结果如下:

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

修改之后的代码中,多个线程操作的是同一个对象的sync.test方法,所以当name0调用sync.test时,其它线程被阻塞。

可见在这种情况下synchronized锁住的是对象而不是test方法中的代码。当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完并释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使不同的代码段,都要锁同一个对象,在调用该对象中这两个代码段时,这两个代码段也不能在多线程环境下同时运行

所以在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步的就不要在整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。(如果在function中只有A代码需要加同步锁synchronized,而在A代码之前会有一些额外的操作如准备工作,而这段工作又需要一些时间,那么你将整个function锁住,那其他线程会都阻塞在function方法上,而你如果只将A代码锁住,那其它线程可以先将A代码之前的工作执行完,阻塞在A代码处,这样减少了程序执行的时间,提高的代码的并发。)

下面看个同步语句块的demo

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
class Sync {
public void test() {
synchronized (this) {
System.out.println("test开始.." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束.." + Thread.currentThread().getName());
}
}
}
class MyThread extends Thread {
private Sync sync;
public MyThread(Sync sync, String nameNo){
this.sync = sync;
this.setName(nameNo);
}
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, "name" + i);
thread.start();
}
}
}

和上面的代码几乎一样,只是将synchronized从方法上换到了代码块上,synchronized(this)锁住也是对象本身。输出结果与上面的一样。

以上加锁的方法都是将同步锁作用于一个实例对象上,任何时刻都只有一个线程进入该对象的实例同步方法或实例同步语句块。当一个线程访问实例对象的一个实例同步方法或实例同步语句块时,其它线程对该实例中的其它所有实例同步方法和实例同步语句块的访问都被阻塞。但是其它线程可以访问该类的其它实例对象的实例同步方法和实例同步语句块

下面就来看一个在类中锁非本类的实例对象的demo

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
class Calulation{
public Calulation(long count){
this.count = count;
}
// 类中非本类的实例对象(也可以理解成一个专门的锁对象)
// 也可以专门声明一个锁对象
private Long count; // 这里是Long而不是long,synchronized锁住的是对象,不能用long int 。。
public void add(long value) throws InterruptedException {
synchronized (this.count) {
System.out.println("count 开始 : " + this.count + " name : "+ Thread.currentThread().getName());
Thread.sleep(10000);
this.count += value;
System.out.println("count 结束 : " + this.count + " name :" + Thread.currentThread().getName());
}
}
public void sub(long value) throws InterruptedException {
synchronized (this.count){
System.out.println("count 开始 : " + this.count + " name : "+ Thread.currentThread().getName());
Thread.sleep(1000);
this.count -= value;
System.out.println("count 结束 : " + this.count + " name :" + Thread.currentThread().getName());
}
}
}
public class CalculationTest {
public static void main(String[] args){
final Calulation cal = new Calulation(0);
for (long i=0; i<2; i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
try {
cal.add(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setName("add" + i);
thread.start();
}
for (long i=0; i<2; i++){
final long finalI = i;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
try {
cal.sub(finalI); // 这里必须传进去一个fianl类型的数据
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setName("sub" + i);
thread.start();
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
add0start...
count 开始 : 0 name : add0
add1start...
sub0start...
sub1start...
count 结束 : 5 name :add0
count 开始 : 5 name : sub1
count 结束 : 4 name :sub1
count 开始 : 4 name : sub0
count 结束 : 4 name :sub0
count 开始 : 4 name : add1
count 结束 : 9 name :add1

Calulation中有两个方法add和sub,这两个方法都对count加锁,那么如果某个线程调用需要得到count实例对象锁的某个方法时,那么其它线程调用需要得到该对象锁的任何一个方法都将阻塞。查看输出,add0线程start,调用cal.add,add中有sleep(模拟代码执行消耗的时间),sleep时,add1、sub0和sub1都相继start,但没有拿到count的锁,都被阻塞。

思考:synchronized(this)与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
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
class Calulation{
public Calulation(long count){
this.count = count;
}
private Long count; // 这里是Long而不是long,synchronized锁住的是对象,不能用long int 。。
public void add(long value) throws InterruptedException {
// 得到实例对象count的锁
synchronized (this.count) {
System.out.println("count 开始 : " + this.count + " name : "+ Thread.currentThread().getName());
Thread.sleep(10000);
this.count += value;
System.out.println("count 结束 : " + this.count + " name :" + Thread.currentThread().getName());
}
}
public void sub(long value) throws InterruptedException {
// 得到本类对象的锁
synchronized (this){
System.out.println("count 开始 : " + this.count + " name : "+ Thread.currentThread().getName());
Thread.sleep(1000);
this.count -= value;
System.out.println("count 结束 : " + this.count + " name :" + Thread.currentThread().getName());
}
}
}
public class CalculationTest {
public static void main(String[] args){
final Calulation cal = new Calulation(0);
for (long i=0; i<2; i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
try {
cal.add(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setName("add" + i);
thread.start();
}
for (long i=0; i<2; i++){
final long finalI = i;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
try {
cal.sub(finalI); // 这里必须传进去一个fianl类型的数据
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setName("sub" + i);
thread.start();
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
add1start...
add0start...
count 开始 : 0 name : add1
sub0start...
count 开始 : 0 name : sub0
sub1start...
count 结束 : 0 name :sub0
count 开始 : 0 name : sub1
count 结束 : -1 name :sub1
count 结束 : 4 name :add1
count 开始 : 4 name : add0
count 结束 : 9 name :add0

上述代码中add中是synchronized(this.count),锁住的是count对象,而sub中是synchronized(this),锁住的Calulation对象cal,则当add1.start时,得到了count的锁,将synchronized(this.count)的代码段锁住,则当add0 start之后无法得到count的锁,则add0阻塞。
然而sub0启动之后,调用的是cal.sub方法,在此方法中的同步代码块中需要的是对象cal的锁,而不是对象count的锁,所以并没有将sub0阻塞,但是sub0将cal锁住,导致sub1阻塞,同样对add也没有影响。

从上的实验可以得出,一般锁定一个属性进行实例范围的同步,任何时刻只有一个线程可以进入锁定该属性实例的代码块,其他线程访问锁定同一个属性实例的代码块将被阻塞,同时该属性实例中的实例同步方法和实例同步代码块也将组塞

上述几个例子都是对实例对象加锁,下面展示几个对类加锁的例子(也是一种全局锁)。

对类加锁是指将同步作用于一个类上,则该类所有的实例对象,任何时刻都只有一个线程进入某个实例的类同步方法或类同步语句块。常见的两种方法是:

  1. 静态方法上加synchronized关键字,声明这个静态方法是同步的
  2. 在同步语句块上添加synchronized(ClassName.class){}
  3. 同样也可以对某个属性进行类范围的同步synchronized(fieldName.getClass())

代码如下:

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
class Sync {
public void test() {
synchronized (Sync.class) {
System.out.println("test开始.." + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束.." + Thread.currentThread().getName());
}
}
}
class MyThread extends Thread {
public MyThread( String nameNo){
this.setName(nameNo);
}
public void run() {
// 每个线程都是一个新的Sync对象
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("name" + i);
thread.start();
}
}
}

输出如下:

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

代码中虽然在每个线程中都new了一个全新的Sync对象但是test中是对Sync.class类加锁,所以会阻塞其它线程对该类任何实例对象同步方法的调用

您的肯定,是我装逼的最大的动力!