diff --git "a/week_03/49/Java\345\206\205\345\255\230\346\250\241\345\236\213" "b/week_03/49/Java\345\206\205\345\255\230\346\250\241\345\236\213" new file mode 100644 index 0000000000000000000000000000000000000000..ec47cebfc07adc0338b7b9d6f9d099c1a2763462 --- /dev/null +++ "b/week_03/49/Java\345\206\205\345\255\230\346\250\241\345\236\213" @@ -0,0 +1,90 @@ +一、Java内存模型介绍 +Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。 +如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。 +原始的Java内存模型效率并不是很理想,因此Java1.5版本对其进行了重构,现在的Java8仍沿用了Java1.5的版本。 + +二、Java内存模型的目的及实现方式: +java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都可能达到一致的内存访问效果。 +在此之前,C语言/C++直接使用物理硬件和操作系统的内存模型,所以就会出现在一套平台上并发访问正常,但是在另一套平台上却有问题,平台兼容性相对较差。 +JMM的主要目标是定义程序中的各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出变量这样的底层细节。 +此处的变量与java变成中变量有所区别,它包括了实例字段,静态字段和构成数据对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享, +自然就不会存在竞争问题。为了更好地性能,java内存模型并没有限制执行引擎使用处理器的特定寄存器和缓存来和主内存进行交互, +也没有限制即时编译器进行调整代码执行顺序这类优化。 +JMM规定所有的变量都存贮在主内存(虚拟机内存的一部分)中,每条线程还有自己的工作内存, +线程的工作内存中保存了该线程使用到的主内存中的变量的副本(注意:一个对像如果10M,是不是会把这个10M的内存复制一份到工作内存呢?显然是不会的, +但是这个对像的引用,对像中的某个在线程中访问到的字段是有可能会复制到工作能存中的,但是不会把整个对象复制一份), +线程对变量的所有操作(读取,赋值等)都需要在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也是无法直接访问对方工作内存中的变量, +线程间变量值的传递均需要通过主内存来完成。 +另外要注意,这里所说的主内存、工作内存和java内存区域中的java堆,栈,方法区等并不是一个层次的内存划分,这两者没有任何关系,如果非要勉强对应的话, +主内存主要对应于java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就是直接对应于物理硬件的内存, +而为了获取更好的运行速度,虚拟机可能会让工作内存有限存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存 + +三、主内存和工作内存之间的交互 + +Java内存模型定义了8种操作来完成关于主内存和工作内存之间具体的交互,这些操作都是原子的,不可分割(long double类型除外)。这8种操作如下所示: + +1) lock(锁定) 作用于主内存的变量,它把一个变量标志为一条线程独占的状态 +2) unlock(解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定 +3) read(读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 +4) load(载入) 作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中 +5) use(使用) 作用于工作内存的变量,它把变量副本的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时,将会执行这个操作。 +6) assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作副本变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 +7) store(存储) 作用于工作内存的变量,将工作副本变量的值传输给主内存,以便随后的write操作使用 +8) write(写入) 作用于主内存的变量, 它把store操作从工作内存得到的变量的值放入主内存的变量 + +如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,那就要顺序地执行store和write操作。 +注意,Java内存模型只要求上述两个操作必须按顺序地执行,而没有保证必须是连续执行,也就是说read和load之间,store和write之间是可以插入其它指令的, +如对内存中的变量a,b进行访问时,一种可能出现的顺序是read a, read b, load b, load a。 + +除此之外,java内存模型还规定了在执行上述8中基本操作时必须满足以下规则。 + +1.不允许read和load,store和write操作之一单独出现,即不允许一个变量从主内存读取了但是工作内存不接受,或者从工作内存中发起了回写了但是主内存不接受的情况出现。 + +2.不允许一个县城丢弃它的最近的assign操作,即变量在工作内存中改变后必须把该变化同步回主内存。 + +3.不允许一个线程无原因的(没有发生过任何assign操作)吧数据从线程的工作内存同步回主内存中。 + +4.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。换就话说,就是对一个变量实施use,store操作之前,必须先执行过了assign和load操作。 + +5.一个变量统一时刻只允许一个线程对其进行lock操作,但是lock操作可以被同一个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁 + +6.如果对一个变量执行lock操作,那将会情况巩固走内存中次变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。 + +7.如果一个变量事前没有被lock操作锁定,那就不允许对她执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。 + +8.对一个变量执行unlock之前,必须先把变量同步回主内存中(执行store,write操作)。 + +通过这8中内存访问操作及其相关的规定,再加上volatile的一些特殊规定,就完全可以确定哪些内存访问操作在并发下是安全的。 +由于这种定义相当严谨但又十分的繁琐,实践起来很是麻烦,所以java虚拟机提供了一个等效判断原则--先行发现原则。 + +四、内存模型三大特性 +1. 原子性 +Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。 +但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行, +即 load、store、read 和 write 操作可以不具备原子性。 +PS:有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。 + +AtomicInteger 能保证多个线程修改的原子性 +除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。 +它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 + +2. 可见性 +可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存, +在变量读取前从主内存刷新变量值来实现可见性的。JMM 内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证, +也就是实现了各种 happen-before 规则。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。 + +主要有有三种实现可见性的方式: + +volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。 +synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。 +final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象), +那么其它线程就能看见 final 字段的值。 + + +3. 有序性 +有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。 +在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 + +volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 + +也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码 \ No newline at end of file diff --git "a/week_03/49/\345\210\206\345\270\203\345\274\217\351\224\201\344\273\213\347\273\215" "b/week_03/49/\345\210\206\345\270\203\345\274\217\351\224\201\344\273\213\347\273\215" new file mode 100644 index 0000000000000000000000000000000000000000..0244dd7cd3311eede5d173b64b980433494befe2 --- /dev/null +++ "b/week_03/49/\345\210\206\345\270\203\345\274\217\351\224\201\344\273\213\347\273\215" @@ -0,0 +1,48 @@ +一、锁的介绍: +在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。 +而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到, +当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。 +不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记, +Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改, +linux 内核中也是利用互斥量或信号量等内存数据做标记。 +除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。 +只需要满足在对标记进行修改能保证原子性和内存可见性即可。 + +二、分布式锁概述: +为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁 + +三、分布式锁应该具备哪些条件 +在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行 +高可用的获取锁与释放锁 +高性能的获取锁与释放锁 +具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误) +具备锁失效机制,防止死锁 +具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败 + +四、分布式锁的实现有哪些 +1、Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。 +2、Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。 +基于 REDIS 的 SETNX()、EXPIRE() 方法做分布式锁: +setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。 +expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。 + +3、Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。 +ZOOKEEPER 锁相关基础知识: +zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。 +zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。 +子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。 +Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件 + +ZK 基本锁 +原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。 +缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。 +ZK 锁优化 +原理:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。 + +4、基于数据库排他锁做分布式锁。 +在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候, +只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引, +值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。 +当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。 + +