oynix

于无声处听惊雷,于无色处见繁花

Java之synchronized关键字

原计划写Java中的锁,但篇幅可能会过长,降低阅读性,所以先从synchronized这个关键字开始。

这是一个最好的时代,我们拥有自由,可以去做很多想做的事,我们要珍惜。

在并发编程中,当需要只能有一个线程执行某段代码时,一般将代码放到synchronized的代码块里,或者用synchronized修饰方法,以此来保证同一时刻只会有一个进程执行对应的代码,那么为什么加个synchronized就可以,这其中的原理是什么呢?这还是要从Object类说起。

1. Object

Object中一共声明了11个方法,作为所有类的父类,那么这些方法一定是所有类都会用到的,其中有5个,就是和并发编程相关的通知和等待系列方法,

1
2
3
4
5
void notify();
void notifyAll();
void wait();
void wait(long timeout);
void wait(long timeout, int nanos);

2. wait系列方法

看下Object.java源码就知道,wait()和wait(long, int)最终都会调用wait(long),所以这三个重载方法,本质上就是一个方法,我们只看这一个就好。

源码里关于这个方法,写了长长的一串方法说明,具体意思就是在说:当一个线程调用一个共享变量的wait方法时,该调用线程会被阻塞挂起(注意这里是调用线程),直到发生了下面这4件事中的一件事才会返回,

  • 其他线程调用了该共享对象的notify方法,并且该线程被选中成为被唤醒的线程
  • 其他线程调用了该共享对象的notifyAll方法
  • 其他线程调用了该线程的interrupt方法
  • 到达了传入的timeout,如果没传则默认是0,0就会一直等待notify了

3. 对象的监视器锁

调用wait方法或者notify方法时,如果没有获取到该对象的监视器锁,那么就会抛出异常IllegalMonitorStateException.

那么如何才能获取到一个共享对象的监视器锁呢?具体的,源码中notify的方法说明中有着详尽的说明,一共有3种方式,现在终于轮到synchronized登场了~

  • 调用这个对象中被synchronized修饰的方法
  • 调用在这个对象上的synchronized的代码块
  • 上面两种都是同一对象,如果对于同一类型的多个对象,可调用synchronized修饰的静态方法

综合来看wait和notify,可以看出,在调用时需要事先占有对象的监视器锁,调用wait就是释放监视器锁,阻塞挂起等待,调用notify就是释放监视器锁,并通知所有在等着这把锁的线程。

4. 总结

上面说了这么多,主要是为了说明对象的监视器锁,而synchronized则可以获取该锁,通过独占锁来保证不会有多个线程并发执行。

附录一:进程和线程

线程是进程中的一个实体,不会独立存在。而进程,是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程资源。除去CPU资源是分配到线程,操作系统分配资源时,都将分配到进程,在Java中启动main方法时便是启动了一个JVM进程,而main方法所在的线程就是进程中的一个线程,也成主线程,一个进程只有一个主线程,这就是进程和线程的关系。

每个线程有自己的程序计数器和栈,程序计数器是一块内存区域,用来记录线程要执行的指令地址,因为线程是占用CPU执行的基本单位,CPU按照时间片轮转的方式分配到各个线程,程序计数器就是为了记录线程让出CPU时的执行地址,等到再次分配占用到CPU时就可以从计数器的指定位置继续执行,所以程序计数器是线程私有。此外,如果执行的native方法,pc计数器记录的是undefined地址,执行Java代码时pc计数器记录的是下一条指令的地址。除了程序计数器,线程还有私有的栈资源,用于存储线程的局部变量,其他线程无法访问,此外还可以存放线程的调用栈帧。

操作系统在创建进程后,会给其分配堆和方法区。堆是进程中最大的一块内存,被进程中的所有线程共享,主要存放new出的对象。方法区则是用来存放JVM加载的类、常量及静态等信息,也是线程共享的。

附录二:Thread.join

我们都知道,启动一个线程T之后,如果调用这个线程T的join方法,那么调用线程就会阻塞挂起,直到T线程执行完毕,便会恢复,以此可以保证多线程的有序执行,那么这里面是怎么做到的呢?打开Thread.java的源码,就可以看到,join一共有3个重载方法,

1
2
3
public final join()
public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)

最终,join()和join(long, int)都会走到join(long)方法里,看到这是不是这一幕有些熟悉了?没错,这和wait系列方法的设计是一样的,而且,join(long)里最终调用的就是wait方法,我们来分析一下。

看到join(long)方法前synchronized修饰符就可以知道,只要能进入到join方法,该线程便拥有监视器锁,方法内最终又调用了wait方法,便会释放监视器锁,并阻塞挂起等待,那么是谁,又是在什么时候,调用了共享对象的notify来通知调用线程,来打破这挂起状态呢?

我们知道,在创建线程对象后,要调用start方法来启动线程执行,那么就来看看这个方法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Thread.java
public synchronized void start() {
...
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {

}
}
}

private native void start0();

这里面啥都没做,就是调用了start0方法,而start0方法由是个本地方法,那就再去找找cpp的代码吧,

1
2
3
4
5
6
7
8
9
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
assert(this == JavaThread::current(), "thread consistency check");
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
assert(!this->has_pending_exception(), "ensure_join should have cleared");
...

我没下CPP源码,这是从网上找的。上面这段是thread.cpp中,线程退出时的代码,可以看到有用的只有一句ensure_join函数的调用,代码注释里写着唤醒处于等待的线程对象,感觉对头,看看这个里面做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
//这里是清除native线程,这个操作会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);//注意这里
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}

看到了吧,在这里调用了notifyAll,挂起的地方就会打破阻塞,继续执行了。所以,总的来看,线程的阻塞和唤醒,用的就是synchronized,调用join来挂起等待监视器锁,线程结束时通知唤醒所有阻塞的线程。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/04/b6a48f964362/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道