Java工程师知识树 / Java基础
synchronized的使用
JDK针对共享资源数据同步问题有一种方式为使用synchronized
关键字,synchronized
提供了一种排他锁机制,可以让程序在同一时间段内只有一个线程执行某些操作。
使用synchronized修饰执行内容后:
package com.thread.study;
public class TicketWindow implements Runnable {
public static int TICKET_NUM = 10;
@Override
public void run() {
while (true) {
synchronized (this) {
if (TICKET_NUM > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
} else {
return;
}
}
}
}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
Thread t1 = new Thread(ticketWindow, "1号售票窗口");
Thread t2 = new Thread(ticketWindow, "2号售票窗口");
Thread t3 = new Thread(ticketWindow, "3号售票窗口");
t1.start();
t2.start();
t3.start();
}
}
// 执行结果
1号售票窗口卖了票号为10的票
3号售票窗口卖了票号为9的票
2号售票窗口卖了票号为8的票
2号售票窗口卖了票号为7的票
2号售票窗口卖了票号为6的票
2号售票窗口卖了票号为5的票
2号售票窗口卖了票号为4的票
2号售票窗口卖了票号为3的票
2号售票窗口卖了票号为2的票
2号售票窗口卖了票号为1的票
将synchronized改为修改run()方法:
package com.thread.study;
public class TicketWindow implements Runnable {
public static int TICKET_NUM = 10;
@Override
public synchronized void run() {
while (true) {
if (TICKET_NUM > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
} else {
return;
}
}
}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
Thread t1 = new Thread(ticketWindow, "1号售票窗口");
Thread t2 = new Thread(ticketWindow, "2号售票窗口");
Thread t3 = new Thread(ticketWindow, "3号售票窗口");
t1.start();
t2.start();
t3.start();
}
}
//执行结果 不管执行多少次都是
1号售票窗口卖了票号为10的票
1号售票窗口卖了票号为9的票
1号售票窗口卖了票号为8的票
1号售票窗口卖了票号为7的票
1号售票窗口卖了票号为6的票
1号售票窗口卖了票号为5的票
1号售票窗口卖了票号为4的票
1号售票窗口卖了票号为3的票
1号售票窗口卖了票号为2的票
1号售票窗口卖了票号为1的票
通过上述两个例子对比,总结synchronized的使用:
- 由于
synchronized
关键字存在排他性,也就是说所有的线程必须串行地经过synchronized
保护的共享区域,如果synchronized
作用域越大,则代表着其效率越低,甚至还会丧失并发的优势。 -
synchronized
关键字应该尽可能地只作用于共享资源(数据)的读写作用域,或者说是synchronized
锁的是有增删改操作的对象。eg:
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {//synchronized锁的是有增删改操作的对象
list.add(Thread.currentThread().getName());
}
}, String.valueOf(i)).start();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
synchronized 关键字原理
synchronized说明:
synchronized 关键字是解决共享资源数据同步的常用解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前 Class 对象。
- 同步块,锁的是 {} 中的对象。
synchronized 实现原理:
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter
指令,在退出方法和可能发生异常处插入 monitor.exit
的指令。monitor指令是使用C++实现的。
其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit
之后才能尝试继续获取锁。
synchronized 特性:
- 互斥性(确保线程互斥的访问同步代码)
- 可见性(保证共享变量的修改能够及时可见)
- 有序性(有效解决重排序问题)
互斥性
可见性
有序性
流程图如下:
同步代码块
public class Synchronize{
public static void main(String[] args) {
synchronized (Synchronize.class){
System.out.println("Synchronize");
}
}
}
---------使用 javap-c 编译 Synchronize类 可以查看编译之后的具体信息。-----------
{
public com.thread.study.Synchronize();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/thread/study/Synchronize
2: dup
3: astore_1
4: monitorenter //同步方法调用前加入一个 monitor.enter 指令
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Synchronize
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit //退出的地方插入 monitor.exit 的指令
15: goto 23
18: astore_2
19: aload_1
20: monitorexit //异常可能出现的地方插入 monitor.exit 的指令 确保可以正常退出
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 5: 0
line 6: 5
line 7: 13
line 8: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Synchronize.java"
可以看到在同步块的入口和出口分别有 monitorenter
,monitorexit
指令。
同步方法
package com.thread.study;
public class TicketThread {
public static void main(String[] args) {
TicketRunnable ticketRunnable = new TicketThread.TicketRunnable();
Thread t1 = new Thread(ticketRunnable,"zhao");
Thread t2 = new Thread(ticketRunnable,"qian");
t1.start();
t2.start();
}
static class TicketRunnable implements Runnable{
private int TICKET_NUM = 10;
@Override
public synchronized void run() { // 同步方法
if (TICKET_NUM > 0) {
System.out.println(Thread.currentThread().getName() +TICKET_NUM);
TICKET_NUM --;
}
}
}
}
----------使用 javap-c 编译 TicketThread 可以查看编译之后的具体信息。主要看内部类TicketRunnable------------
{
com.thread.study.TicketRunnable();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 16: 0
public synchronized void run();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED标志
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field TICKET_NUM:I
3: ifle 45
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #4 // class java/lang/StringBuilder
12: dup
13: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
16: invokestatic #6 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
19: invokevirtual #7 // Method java/lang/Thread.getName:()Ljava/lang/String;
22: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: getstatic #2 // Field TICKET_NUM:I
28: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: getstatic #2 // Field TICKET_NUM:I
40: iconst_1
41: isub
42: putstatic #2 // Field TICKET_NUM:I
45: return
LineNumberTable:
line 20: 0
line 21: 6
line 22: 37
line 24: 45
StackMapTable: number_of_entries = 1
frame_type = 45 /* same */
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field TICKET_NUM:I
5: return
LineNumberTable:
line 17: 0
}
SourceFile: "TicketThread.java"
可以看出在synchronized修饰的同步方法的flags中会有ACC_SYNCHRONIZED标识
无论是monitorenter、 monitorexit,或者是ACC_SYNCHRONIZED,其都是基于Monitor机制实现的。
Monitor
monitor直译过来是监视器的意思,专业一点叫管程。Monitor机制一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果。无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒。
java则基于monitor机制实现了它自己的线程同步机制,就是synchronized内置锁。
基本元素
- 临界区
临界区是被synchronized包裹的代码块,可能是个代码块,也可能是个方法。
- monitor对象和锁
monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是通过mutex互斥锁实现的。
- 条件变量
条件变量和下方wait signal方法的使用有密切关系 。在获取锁进入临界区之后,如果发现条件变量不满足使用wait方法使线程阻塞,条件变量满足后signal唤醒被阻塞线程。 tips:当线程被signal唤醒之后,不是从wait那继续执行的,而是重新while循环一次判断条件是否成立
- 定义在monitor对象上的wait,signal操作