原计划写Java中的锁,但篇幅可能会过长,降低阅读性,所以先从synchronized这个关键字开始。
这是一个最好的时代,我们拥有自由,可以去做很多想做的事,我们要珍惜。
在并发编程中,当需要只能有一个线程执行某段代码时,一般将代码放到synchronized的代码块里,或者用synchronized修饰方法,以此来保证同一时刻只会有一个进程执行对应的代码,那么为什么加个synchronized就可以,这其中的原理是什么呢?这还是要从Object类说起。
1. Object
Object中一共声明了11个方法,作为所有类的父类,那么这些方法一定是所有类都会用到的,其中有5个,就是和并发编程相关的通知和等待系列方法,
1 | void notify(); |
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 | public final join() |
最终,join()和join(long, int)都会走到join(long)方法里,看到这是不是这一幕有些熟悉了?没错,这和wait系列方法的设计是一样的,而且,join(long)里最终调用的就是wait方法,我们来分析一下。
看到join(long)方法前synchronized修饰符就可以知道,只要能进入到join方法,该线程便拥有监视器锁,方法内最终又调用了wait方法,便会释放监视器锁,并阻塞挂起等待,那么是谁,又是在什么时候,调用了共享对象的notify来通知调用线程,来打破这挂起状态呢?
我们知道,在创建线程对象后,要调用start方法来启动线程执行,那么就来看看这个方法吧
1 | // Thread.java |
这里面啥都没做,就是调用了start0方法,而start0方法由是个本地方法,那就再去找找cpp的代码吧,
1 | void JavaThread::exit(bool destroy_vm, ExitType exit_type) { |
我没下CPP源码,这是从网上找的。上面这段是thread.cpp中,线程退出时的代码,可以看到有用的只有一句ensure_join函数的调用,代码注释里写着唤醒处于等待的线程对象,感觉对头,看看这个里面做了什么
1 | static void ensure_join(JavaThread* thread) { |
看到了吧,在这里调用了notifyAll,挂起的地方就会打破阻塞,继续执行了。所以,总的来看,线程的阻塞和唤醒,用的就是synchronized,调用join来挂起等待监视器锁,线程结束时通知唤醒所有阻塞的线程。