多线程与并发

1.缓存一致性问题,如何解决?

当程序在运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速 缓存当中,那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中 写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
比如下面的这段代码: i = i+1
比如同时有 2 个线程执行这段代码,假如初始时 i 的值为 0,那么我们希望 两个线程执行完之后 i 的值变为 2。
可能存在下面一种情况:初始时,两个线程分别读取 i 的值存入各自所在的 CPU 的高速缓存当中,然后线程 1 进行加 1 操作,然后把 i 的最新值 1 写入到 内存。此时线程 2 的高速缓存当中 i 的值还是 0,进行加 1 操作之后,i 的值为 1,然后线程 2 把 i 的值写入内存。 最终结果 i 的值是 1,而不是 2。这就是著名的缓存一致性问题。通常称这 种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个 CPU 中都存在缓存(一般在多线程编程 时才会出现),那么就可能存在缓存不一致的问题。
解决的方法:
1)通过在总线加 LOCK#锁的方式
因为 CPU 和其他部件进行通信都是通过总线来进行的,如果对总线 加 LOCK#锁的话,也就是说阻塞了其他 CPU 对其他部件访问(如内存), 从而使得只能有一个 CPU 能使用这个变量的内存。比如上面例子中 如果一个 线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了 LCOK# 锁的信号,那么只有等待这段代码完全执行完毕之后,其他 CPU 才能从变量 i 所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
2)通过缓存一致性协议
当 CPU 向内存写入数据时,如果发现操作 的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其 他 CPU 将该变量的缓存行置为无效状态,因此当其他 CPU 需要读取这个变量 时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读 取。

2. 简述 volatile 关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰 之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行读取时的可见性,即一个线程修改 了某个变量的值,这新值对其他线程来说是立即可见的。(volatile 解决了 线程间共享变量的可见性问题)。
volatile 关键字会强制将修改的值立即写入主存;当线程 2 进行修改时,会导致线程 1 的 工作内存中缓存变量 stop 的缓存行无效;由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取。
2)禁止进行指令重排序,阻止编译器对代码的优化。
当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的 更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定 还没有进行;在进行指令优化时,不能把 volatile 变量前面的语句放在其后面执行, 也不能把 volatile 变量后面的语句放到其前面执行。

3.JAVA的内存模型

Java 内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内 存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。 只要有一个没有被保证,就有可能会导致程序运行不正确。

4. JAVA中的同步容器类和缺陷

同步容器主要包括 2 类:
1)Vector、HashTable。
2)Collections 类中提供的静态工厂方法创建的类。 Collections.synchronizedXXX()。
缺陷:
1.性能问题。
在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个 时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同 一把锁。
2.ConcurrentModificationException 异常。
在对 Vector 等容器进行迭代修改时,会报 ConcurrentModificationException 异常。但是在并发容器中(如 ConcurrentHashMap,CopyOnWriteArrayList 等)不会出现这个问 题。

5. 为什么说 ConcurrentHashMap 是弱一致性的?为何多 个 线 程 并 发 修 改 ConcurrentHashMap 时 不 会 报 ConcurrentModificationException?

1、ConcurrentHashMap#get():
正是因为 get 操作几乎所有时候都是一个无锁操作(get 中有一个 readValueUnderLock 调用,不过这句执行到的几率极小),使得同一个 Segment 实例上的 put 和 get 可以同时进行,这就是 get 操作是弱一致的根 本原因。
2、ConcurrentHashMap#clear():
因为没有全局的锁,在清除完一个 segment 之后,正在清理下一个 segment 的时候,已经清理的 segment 可能又被加入了数据,因此 clear 返回的时候,ConcurrentHashMap 中是可能存在数据的。因此,clear 方 法是弱一致的。
3、ConcurrentHashMap 中的迭代器:
在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出 ConcurrentModificationException 异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是 ConcurrentHashMap 迭代器弱一 致的表现。

总结,ConcurrentHashMap 的弱一致性主要是为了提升效率,是一致性 与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁, 这就与 Hashtable 和同步的 HashMap 一样了。

6. CopyOnWriteArrayList 的实现原理

写时复制容器,当向一个容器中添加元素时,不直接向其中添加,而是先将当前容器进行copy,复制出一个新的容器,然后新的容器添加元素,添加完元素后再将原容器的引用指向新的容器。好处是可以对copyonwrite容器进行并发读,不需要加锁,因为当前容器不会添加任何元素。是一种读写分离的思想,读和写不在同一个容器中,写的时候需要加锁。
在 CopyOnWriteArrayList 里处理写操作(包括 add、remove、set 等)是先将原始的 数据通过 Arrays.copyof()来生成一份新的数组,然后在新的数据对象上进行写,写完后 再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上。然后读 的时候就是在引用的当前对象上进行读(包括 get,iterator 等),不存在加锁和阻塞。
CopyOnWriteArrayList 中写操作需要大面积复制数组,所以性能肯定很差,但是读操 作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处 理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间, 这样读操作就很快很安全,适合在多线程里使用。
. 读 的 时 候 不 需 要 加 锁 , 如 果 读 的 时 候 有 线 程 正 在 向 CopyOnWriteArrayList 添加数据,读还是会读到旧的数据(在原容器中进行 读)。CopyOnWriteArrayList 在读上效率很高,由于,写的时候每次都要将源 数组复制到一个新组数中,所以写的效率不高。
该容器有两个问题:
(1)内存占用:可以通过将10进制转换为36或64进制,或改为其他并发容器。
(2)数据一致性:

7.Java中的堆和栈的区别

栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于 存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。
堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈。如果多个线程使用该变量就可能 引发问题,这时 volatile 变量就可以发挥作用了,它要求线程从主存中读取变 量的值。

8.Java 中的活锁,死锁,饥饿有什么区别?

死锁:两个或两个以上的进程因在执行的过程中,争夺资源而陷入互相等待的现象,若无外力作用,将无法继续进行下去,此时的系统称为死锁。
饥饿:例如有镀铬进程需要打印文件,系统按照短文件优先的策略排序,该策略具有平均等待时间短的优点,当短文件打印任务不断时,长文件的任务将被无限推迟,导致饥饿或饿死。
活锁:在忙式等待条件下发生的饥饿称为活锁。指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一 直重复尝试,失败,尝试,失败。

不进入等待状态的等待称为忙式等待。另一种等待方式是阻塞式等待,进 程得不到共享资源时将进入阻塞状态,让出 CPU 给其他进程使用。
忙等待和阻 塞式等待的相同之处在于进程都不具备继续向前推进的条件,不同之处在于处 于忙等待的进程不主动放弃 CPU ,尽管 CPU 可能被剥夺,因而是低效的;而 处于阻塞状态的进程主动放弃 CPU ,因而是高效的。

活锁的时候,进程是不会阻塞的,这会导致耗尽 CPU 资源,这是与死锁最 明显的区别。
活锁和死锁的区别:
处于活锁的实体是不断的改变状态,处于死锁的实体表现为等待;活锁有一定橘绿解开,死锁无法解开。

避免活锁的方法是使用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求封锁的先后次序对事务排队,数据对象上的锁 一旦释放就批准申请队列中第一个事务获得锁。

你可能感兴趣的:(多线程与并发,多线程,并发编程)