管程(Monitor)
管程(Monitor),Java 中被翻译为监视器,管程是它在操作系统层面的名称,所谓管程,即管理共享变量及对其操作过程,让它支持并发访问,在 Java 中可以理解为管理类的成员变量和方法,从而达到线程安全的目的
共享带来的问题
假设此时有两个线程同时对初始值为0
的静态变量做相同次数的自增和自减:
1 | public static void main(String[] args) throws InterruptedException { |
按常理,这个加一次减一次,最后的结果应该是0
吧,让我们来看看运行结果:
1 | 16:01:50 [main] c.Test1 - is -1768 |
显然,这个程序的运行结果不是0
,为什么会出现这个问题呢?我们需要从字节码来进行分析
对于i++
操作来说,实际上会产生四行字节码指令getstatic i
、iconst_1
、iadd
、putstatic
,而在 Java 内存模型中,完成静态变量的自增自减,需要在主存和工作内存中进行数据交换:
1 | 《---》 线程 1 i++ |
由于线程占用 cpu 资源的顺序是随机的,所以i++
和i--
这两行代码所对应的8
行字节码指令,可能会出现交错运行的情况:线程 1 从主存中获取了 i
的值为0
,还没进行++
操作时,线程 2 也取到了i = 0
,这时线程 1 和线程 2 分别进行++
和--
操作后,线程 1 先将i = 1
返回给主存,随后线程 2 又将i = -1
返回给主存,这时主存中的i
值即为-1
,在这种情况多次重复运行后,i
的值根本无法预测
如何解决在并发情况下的这种问题呢?这时要引入临界区(Critical Section)的概念
临界区(Critical Section)
上述程序中,一个程序运行多个线程本身是没有问题的,问题出现在多个线程同时访问共享资源,而多个线程访问共享资源本身也是没有问题的,问题的根本在于:在多个线程对共享资源进行读写操作时发生了指令交错,而当一段代码块内如果存在对共享资源的多线程读写操作,我们就称这段代码块为临界区
很明显,上一个例子中的临界区即为counter++
和counter--
这两行代码,多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件,为了避免临界区的竞态条件发生,可以使用:
- 阻塞时解决方案:synchronized、Lock
- 非阻塞式解决方案:原子变量
## synchronized 解决方案
synchronized
,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程持有【对象锁】,其他线程再想获取【对象锁】时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换
synchronized 用法
【对象锁】在使用时,只需要将临界区代码放在synchronized
代码块内即可,即:
1 | static Object room = new Object(); |
这种写法就保证了