心安

细说Java关键字synchronized(二) -- 详解synchronized的用法

字数统计: 2.1k阅读时长: 9 min
2019/02/26 Share

前言

在上一篇入门案例的文章中,我们简单的演示了synchronized的作用,本篇文章我们就来详细的讲解一下synchronized关键字的用法。

synchronized的两个用法

  1. 对象锁
    对象锁包括了方法锁和同步代码块锁。其中方法锁默认的锁对象是当前的实例对象,而同步代码块锁可以手动指定锁对象。

  2. 类锁
    类锁包括静态方法锁和锁对象为Class对象

对象锁

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
25
26
27
28
// 示例一:
public class SynchronizedDemo implements Runnable {

private static SynchronizedDemo s = new SynchronizedDemo();

private static int i = 0;

@Override
public void run() {
synchronized (this) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);

t1.start();
t2.start();

t1.join();
t2.join();
System.out.println(i);
}
}

上面就是代码块锁,这里不仅可以使用this对象,还可以是任意对象。比如下面:

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
// 示例二:
public class SynchronizedDemo implements Runnable {

private static SynchronizedDemo s = new SynchronizedDemo();

private static int i = 0;

private final Object lock = new Object();

@Override
public void run() {
synchronized (lock) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);

t1.start();
t2.start();

t1.join();
t2.join();
System.out.println(i);
}
}

这里加final是为了保证每个线程的锁都是同一个对象。

2. 方法锁

使用synchronized关键字修饰普通方法,默认的锁对象是this。注意,这里不是静态方法。静态方法下面会单独讲解。

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
// 示例三:
public class SynchronizedObjectMethodDemo implements Runnable {

private static SynchronizedObjectMethodDemo s = new SynchronizedObjectMethodDemo();

@Override
public void run() {
fun();
}

private synchronized void fun() {
System.out.println("method run: " + Thread.currentThread().getName());
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println("stop..");
}
}

类锁

上面说的对象锁,不管何种形式,总之它们用到的锁都是一个对象,而接下来讲解的都是类对象作为锁对象。
类对象具有很大的特殊性,那就是Java类可能有多个对象,但是只有一个Class对象,就是说这个锁是唯一的。

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
25
26
27
28
29
30
31
// 示例四:
public class SynchronizedClassCodeBlockDemo implements Runnable {

private static SynchronizedClassCodeBlockDemo d1 = new SynchronizedClassCodeBlockDemo();

private static SynchronizedClassCodeBlockDemo d2 = new SynchronizedClassCodeBlockDemo();

@Override
public void run() {
// 请尝试将这里的锁换成this锁
synchronized (SynchronizedClassCodeBlockDemo.class) {
System.out.println(Thread.currentThread().getName() + "开始运行");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}

public static void main(String[] args) throws InterruptedException {
// 第二个参数是为线程设置名称
Thread t1 = new Thread(d1, "线程A");
Thread t2 = new Thread(d2, "线程B");
t1.start();
t2.start();
t1.join();
t2.join();
}
}

这里请注意一下,在main()方法中我们创建两个线程使用的是两个不同的对象,这个和我们前面的不同,前面使用的是同一个对象。
然后首先我们仍然使用this作为锁,看一下运行效果,可以看到,两个线程几乎同时开启也几乎同时结束,也就是说这里this锁是无效的。
这是因为我们创建线程用到的是不同的对象,那么这里的this指代的就不是同一个对象,也就是不是同一把锁。
两个线程竞争的不是同一把锁,肯定不是同步的。

2. 静态方法锁

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
// 示例五:
public class SynchronizedClassMethodDemo implements Runnable {

private static SynchronizedClassMethodDemo d1 = new SynchronizedClassMethodDemo();

private static SynchronizedClassMethodDemo d2 = new SynchronizedClassMethodDemo();

@Override
public void run() {
fun();
}

private static synchronized void fun() {
System.out.println(Thread.currentThread().getName() + "开始运行");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(d1, "线程1");
Thread t2 = new Thread(d2, "线程2");

t1.start();
t2.start();

t1.join();
t2.join();
}
}

这里和上面方法锁唯一的区别就是这里同步的方法是静态的,要知道,静态方法并不需要对象来调用,所以这里的锁当然不会是对象锁,这里用的是类锁。

常见问题

  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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SynchronizedDiffMethodDemo implements Runnable {

private static SynchronizedDiffMethodDemo d = new SynchronizedDiffMethodDemo();

@Override
public void run() {
if ("线程AA".equals(Thread.currentThread().getName())) {
fun1();
} else {
fun2();
}
}

private synchronized void fun1() {
System.out.println("同步方法1运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("同步方法1运行结束");
}

private synchronized void fun2() {
System.out.println("同步方法2运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("同步方法2运行结束");
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(d, "线程AA");
Thread t2 = new Thread(d, "线程BB");
t1.start();
t2.start();
t1.join();
t2.join();
}
}

上面的代码使用同一个对象创建了两个线程,并且两个线程分别执行方法1和方法2,结果发现两个线程产生了竞争。
所以结论是如果由同一个对象创建的两个线程同时访问两个同步方法,会产生锁竞争
注意,这里是同一个对象创建的两个线程,如果是两个对象创建了两个线程,则不会产生竞争。

  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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SynchronizedStaticAndNormalDemo implements Runnable {

private static SynchronizedStaticAndNormalDemo d = new SynchronizedStaticAndNormalDemo();

@Override
public void run() {
if ("线程AAA".equals(Thread.currentThread().getName())) {
fun1();
} else {
fun2();
}
}

private synchronized static void fun1() {
System.out.println("静态同步方法1运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("静态同步方法1运行结束");
}

private synchronized void fun2() {
System.out.println("同步方法2运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("同步方法2运行结束");
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(d, "线程AAA");
Thread t2 = new Thread(d, "线程BBB");
t1.start();
t2.start();
t1.join();
t2.join();
}
}

结果发现,没有产生竞争。因为普通方法是this锁,静态方法是类锁。

  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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SynchronizedExceptionDemo implements Runnable {
private static SynchronizedExceptionDemo d = new SynchronizedExceptionDemo();

@Override
public void run() {
if ("A".equals(Thread.currentThread().getName())) {
fun1();
} else {
fun2();
}
}

private synchronized void fun1() {
System.out.println("同步方法1运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
}

private synchronized void fun2() {
System.out.println("同步方法2运行开始");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("同步方法2运行结束");
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(d, "A");
Thread t2 = new Thread(d, "B");
t1.start();
t2.start();
t1.join();
t2.join();
}
}

运行上面的代码可以发现,线程A获取到锁,然后执行方法1,然后抛出异常,方法二正常运行,这也就意味着方法抛出异常后会自动释放锁。

上面的简单的示例只是为了说明一个问题,不管是对象锁还是类锁,两个线程直接有没有产生锁竞争,必须要知道他们所用的锁是否为同一把锁。

总结

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待。

  2. 每个实例都对应有自己的一把锁(对象锁),不同的实例之间不产生锁竞争。

  3. 锁对象是*.class以及被synchronized关键字修饰的静态方法使用的是类锁,所有的线程竞争的都是同一把锁。

  4. 无论方法是否抛出了异常,都会自动释放锁。

关于多线程和锁的知识很零散而且枯燥乏味,笔者实力有限加上表达能力不够好,博文写的可能有很多瑕疵。
但是还是希望想要学习多线程及并发的同学仔细阅读本系列文章内容,相信你还是会有收获的。
另外笔者最近面试遇到了太多太多的线程和并发相关的题目,这也说明了现在的java开发面试越来越偏向了多线程编程,所以希望大家好好学习相关知识,技术越来越来。
最后感谢大家的阅读。在阅读的过程中发现任何错误或者问题,都欢迎在下方留言来和笔者沟通交流。

原文作者:XinAnzzZ

原文链接:https://www.yuhangma.com/2019/concurrent/2019-02-26-concurrent-synchronized-02/

发表日期:February 26th 2019, 12:00:00 am

更新日期:September 26th 2019, 10:46:42 am

版权声明:(转载本站文章请注明作者和出处 心 安 – XinAnzzZ ,请勿用于任何商业用途)

CATALOG
  1. 1. 前言
  2. 2. synchronized的两个用法
    1. 2.1. 对象锁
      1. 2.1.1. 1. 代码块锁
      2. 2.1.2. 2. 方法锁
    2. 2.2. 类锁
      1. 2.2.1. 1. 代码块使用类锁
      2. 2.2.2. 2. 静态方法锁
  3. 3. 常见问题
  4. 4. 总结