起因:为什么需要锁
由于线程通过共享内存来通信,多个线程在同一个内存上进行操作,往往会“打成一片”,为避免如此,必须用锁保证某一时间仅有一个线程可以访问内存上的数据(互斥)。
进程与线程
在多任务分时操作系统中,为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户的各个任务使用。CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态,这就是上下文切换。
而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。
在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源。
计算机内存模型
CPU和内存之间是有缓存的,甚至是多级缓存,每一级缓存中所储存的全部数据都是下一级缓存的一部分。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。且目前以多核CPU为主。
- 单线程:cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。
- 单核CPU + 多线程:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。
- 多核CPU + 多线程:每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
竞态条件
在上述计算机内存模型的机制下,多线程并发执行i++的例子就是竞态条件的形象解释
锁用多了也不好:死锁
如果极端一些,为了保证安全,让多线程代码安全运行的方法只能是让所有的方法都同步。
然而,这也会带来问题:
一是效率低下,如果每个方法都同步,大多数线程会频繁阻塞,失去了并发的意义
二是当使用多把锁时(Java中每一个对象都有自己的内置锁,线程之间可能发生死锁
除了缓存一致性还存在什么问题
程序语句的执行顺序并不如所见的那样,乱序执行完全有可能发生,以下所述均为事实:
- 编译器的静态优化可以打乱代码的执行顺序
- JVM的动态优化也会打乱代码的执行顺序
- 硬件可以通过乱序执行来优化其性能
关于并发编程,主要存在三大问题:原子性问题,可见性问题和有序性问题:
- 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
- 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性即程序执行的顺序按照代码的先后顺序执行。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
Java内存模型
前面介绍过了计算机内存模型,这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢,不同的编程语言,在实现上可能有所不同。
我们知道,Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
我们知道,Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。
特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。
《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。
再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
Java内存模型的实现
在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。
在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。
所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。
synchronized
synchronized修饰同步方法时,JVM采用ACC_SYNCHRONIZED(字节码)标记符来实现同步;
synchronized修饰同步代码块时,JVM采用monitorenter、monitorexit两个指令来实现同步;
因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
volatile
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存;
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量;
因此,可以使用volatile来保证多线程操作时变量的可见性。
(除了volatile,Java中的synchronized和final两个关键字也可以实现可见性)
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作,都可以保证多线程之间操作的有序性
==>锁的内存语义
==>....