多线程
- 多线程就是指一个进程中同时有多个线程正在执行。
- 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。
场景:
- 并发处理文件
- 并发下载工具
- 不同公式计算
多线程要注意的问题主要有以下几个:并发问题、安全问题、效率问题
一、效率问题:
- 可以依据CPU密集型还是IO密集型来进行分析优化
二、并发问题:
- 并发中问题会导致死锁,也会导致安全性问题,涉及到安全性问题的先不讨论,先来说一下死锁。
死锁:
- 1.定义:指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。
(死锁不仅存在线程之间会发生,存在资源独占的进程之间也可能会发生)
- 2.死锁是怎么产生的?(产生的原因)
(1)系统资源的竞争:
系统拥有不可剥夺的资源,且资源数量不能满足多个进程/线程运行的需要,因此系统中的进程/线程在运行过程中会进行资源的争夺,从而陷入一种僵局。如磁盘、打印机等资源。
(2)线程/进程推进顺序非法:
进程/线程在运行过程中请求和释放资源的顺序不当,也同样会导致死锁。
如并发进程中P1、P2分别都请求和保持R1、R2资源,一开始进程P1请求R1资源,进程P2请求R2资源,刚开始没什么事,接着进程P1请求R2资源,进程P2请求R1资源,两者都会因为资源被占用而陷入等待获取,死锁就产生了。
-
3.产生死锁的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用。

(2)不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺,只能在进程使用完时自己释放。

(3)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

-
4.避免死锁的方法:(避免死锁也主要是从死锁的四个必要条件入手。)
(1)破坏“互斥”条件:若一个资源不被一个进程独占使用,那死锁就不会发生,但这种一般无法实现,只能通过破坏其他三个死锁必要条件入手。
(2)破坏“不可剥夺”条件:允许对资源实行抢夺。
有两种方案:一是当进程请求资源失败时,释放它之前所获取的所有资源(如有必要可再次请求)。二是当进程请求的资源被另一个进程占用,要求另一个进程释放该资源(只有连个进程优先级都不相等的情况下能使用)。
(3)破坏“请求与保持”条件:不允许在已获得某种资源的情况下,申请其他资源。即阻止进程持有资源的同时申请其他资源。
方案:所有进程在运行之前,一次性申请在运行中所需要的全部资源。优点:简单、易行且安全。缺点:资源被严重浪费,严重恶化了资源的利用率。
(4)破坏循环等待条件:将系统中的资源进行统一编号,进程可以在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出,这样做可以保证系统不出现死锁。(编好加锁的顺序)
- 5.死锁发生时怎么解决
死锁的的解除主要有两种方法:
1)抢占到资源:从一个或多个进程中抢占到足够数量的资源,分配给死锁进程,以解除死锁状态。
2)终止进程/撤销进程:终止/撤销系统中一个或多个死锁进程,直到打破循环环路,使系统从死锁状态中解脱出来。
三、安全问题
- 主要是防止线程安全问题,确保数据的一致性。
线程安全:指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。
如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。
1 . 分析为什么多线程容易导致线程不安全现象?(即为什么多线程情况下,访问的数据以及计算出的结果不一致。)
(1)首先,JVM中重新排序优化,也会导致有序性问题。在执行程序时,编译器和处理器为了提高并行度、为了提高性能,编译器和处理器常常会对指令做重新排序。
不管怎么重排,编译器不会对数据依赖性的程序指令进行重排,而且重排不会导致执行结果的改变(遵守as-if-serial语义),
但重排对数据依赖性以及不影响执行结果的情况,仅仅是在单线程情况下才有效;多线程并发情况下,此规则将失效。
比如:
public void write(){
a = 1;
a = a + 1;
b = 2;
flag = true;
}
public void doSomething(){
if(flag){
c = a + b;
}
}
我们分别讨论下单线程和多线程下执行write() -->doSomething()会产生什么影响

因此,虽然重新排序在单线程情况下能保证运行结果数据的一致性,但不能保证多线程情况下运行数据的一致性。
(2)接着从java内存模型入手:
Java内存模型规定:所有的变量都存放在主存中,每个线程都还有自己的工作内存,线程的工作内存中还保存了该线程所使用到的变量(这些变量都是从主内存中拷贝而来)。
线程对变量的所有操作(读取/赋值)都必须在工作内存中进行,不同线程之间也无法直接访问对方工作内存中的变量,线程访问变量值的传递均需要通过主内存来完成。

由上面的java内存模型我们可以看出:
每个线程都有一个工作内存(可以理解为每个线程的变量缓存),
当主存变量改变时,每个线程的工作内存都还没及时作出相应的调整(可见性问题),
或者线程正在执行该变量,结果主存中改了该变量的值,导致问题的发生(原子性问题)。
因此这种内存内存模型在多线程操作情况下容易出现两个问题:
1)原子性问题:多个线程执行相同的逻辑操作,对同一个变量进行操作,执行结果也会不一致。
如a++ 里面就包含三个步骤:取a的值、a加1、将a写到主存中。10个线程都对a变量执行自增100次操作,执行结果都小于10*1000.
2)可见性问题:当一个线程某个变量的值,其他线程不能立马看到修改的值。
四、Java并发容器类
4-1、并发 Map
ConcurrentHashMap
1)JDK 7 中的实现:分段锁(Segment)
在 JDK 7 中,ConcurrentHashMap 采用分段锁(Segment)机制。它将整个哈希表分成多个段(Segment),每个段相当于一个小的 Hashtable,并且每个段都有自己的锁。不同的线程可以同时访问不同的段,从而提高了并发性能。每个段内部的数据结构是数组 + 链表。
2)JDK 8 及以后的实现:CAS + synchronized
从 JDK 8 开始,ConcurrentHashMap 摒弃了分段锁机制,采用 CAS(Compare - And - Swap)和 synchronized 来保证并发操作的线程安全。其底层数据结构为数组 + 链表 + 红黑树。当链表长度超过一定阈值(默认为 8)且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率。
4-2、并发 List
CopyOnWriteArrayList
ReentrantLock 锁用来保证修改操作的线程安全,array 的 Object[] 数组是被 volatile 修饰的,可以保证数组的可见性,存储元素的数组。
CopyOnWrite 的思想:写操作是在原来容器的拷贝上进行的,并且在读数据的时候不会锁住 list。而且可以看到,如果对容器拷贝操作的过程中有新的读线程进来,那么读到的还是旧的数据,因为在那个时候对象的引用还没有被更改。
1)add() 方法
public boolean add(E e) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 得到原数组的长度和元素
Object[] elements = getArray();
int len = elements.length;
// 复制出一个新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加时,将新元素添加到新数组中
newElements[len] = e;
// 将volatile Object[] array 的指向替换成新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2)get() 方法
get 相关的操作没有加锁,保证了读取操作的高速
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
3)remove() 方法
/**
* 删除此列表中指定位置的元素,会将后续元素向左移动
*/
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取原数组
Object[] elements = getArray();
int len = elements.length;
// 获取index下标的元素
E oldValue = get(elements, index);
// 计算需要移动的数量
int numMoved = len - index - 1;
if (numMoved == 0)
// 删除的是末尾,将原数组减1
setArray(Arrays.copyOf(elements, len - 1));
else {
// 开辟新数组
Object[] newElements = new Object[len - 1];
// 复制前面
System.arraycopy(elements, 0, newElements, 0, index);
// 复制后面
System.arraycopy(elements, index + 1, newElements, index, numMoved);
// 修改原数组
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
4-3)并发 Queue
1)ArrayBlockingQueue:有界数组 + 单 ReentrantLock(双条件变量)。
2)LinkedBlockingQueue:链表 + 双锁分离(takeLock 和 putLock),提高吞吐量。
3)PriorityBlockingQueue:优先级堆 + ReentrantLock,自动扩容。
4)SynchronousQueue:无缓冲队列,直接传递任务(匹配生产者/消费者线程)。非阻塞队列。
5)ConcurrentLinkedQueue:无界链表 + CAS 操作(无锁),通过 UNSAFE.compareAndSwapObject 更新头/尾节点。
4-4)并发 Set
1)ConcurrentSkipListSet:基于 ConcurrentSkipListMap 实现,支持排序。
2)CopyOnWriteArraySet:基于 CopyOnWriteArrayList,适用读多写少场景。
4-5)其他容器
1)ConcurrentSkipListMap:跳表(SkipList) + CAS。分层链表结构,查询复杂度 O(log n)。
插入时仅锁定局部节点,减少锁竞争。

2)ConcurrentLinkedDeque:双端队列 + CAS 无锁算法。
备注:
Java并发的三大特性:
1)有序性:指程序执行的顺序按照代码的先后顺序执行。
2)原子性:指一个操作是不可中断的,要么全部执行成功要么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
3)可见性:指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
2 . 用什么方法可以解决线程安全问题,确保数据一致性呢?
一般分两种情况:
(1)一种是对变量只读不写:这种情况下用final类型修饰变量即可,因为被final修饰的变量,除了初始化能赋值,其他时刻都是不能修改变量的值,对于读的时候,能很好的保证数据的一致性。
(2)另一种是对变量既读又写:
1)JUC(java.util.concurrent):
①Lock:基本的锁的实现,最终要的是AQS框架和LockSupport。(同时还有ReentrantLock、ReentrantReadWriteLock等)(原子性)
②Atomic:原子数据的构建(如AutomicInteger、AutomicBoolean)(原子性)
③Concurrent:构建的一些高级的工具,如线程池、并发队列等。
2)volatile修饰符(可见性、有序性)
3)synchronize修饰符,用于同步方法或者同步代码块(原子性、可见性、有序性)
备注:
1)JUC(java.util.concurrent)中都用到了CAS(Compare-and-swap)操作
2)JUC中Concurrent里面有两个比较重要的:
2-1)BlockingQueue阻塞队列:queue是单向队列,可在队列头添加元素和在队尾删除或取出元素。特别适用于先进先出策略的一些应用场景。
2-2)ConcurrentHashMap:搞笑的线程安全哈希map。
3)多个线程同时读写,读线程的数量远远大于写线程,你认为应该如何解决并发的问题?你会选择加什么样的锁?
3-1)选择读写锁(ReadWriteLock):读操作频繁,写操作较少,且要求强一致性。
3-2)实现方式:使用 ReentrantReadWriteLock,允许多个读线程共享读锁,写线程独占写锁。读锁与读锁兼容,读锁与写锁互斥。
总结:
并发问题中主要包含死锁问题(多半自身原因。当然并发不止这个问题,跟安全性问题也是耦合的)。
安全问题中主要是通过“同步方法”这过程--->来确保--->多线程中“数据一致性”的结果。
更多java基础总结(适合于java基础学习、java面试常规题):
总结篇(9)---字符串及基本类 (1)字符串及基本类之基本数据类型
总结篇(10)---字符串及基本类 (2)字符串及基本类之java中公共方法及操作
总结篇(12)---字符串及基本类 (4)Integer对象
总结篇(14)---JVM(java虚拟机) (1)JVM虚拟机概括
总结篇(15)---JVM(java虚拟机) (2)类加载器
总结篇(16)---JVM(java虚拟机) (3)运行时数据区
总结篇(17)---JVM(java虚拟机) (4)垃圾回收
总结篇(18)---JVM(java虚拟机) (5)垃圾回收算法
总结篇(19)---JVM(java虚拟机) (6)JVM调优
总结篇(24)---Java线程及其相关(2)多线程及其问题
总结篇(25)---Java线程及其相关(3)线程池及其问题
总结篇(26)---Java线程及其相关(4)ThreadLocal
总结篇(27)---Java并发及锁(1)Synchronized
总结篇(31)---JUC工具类(1)CountDownLatch
本文详细探讨了多线程的基础概念,分析了多线程环境下可能出现的效率、并发及安全问题,特别是针对死锁产生的原因及解决方案进行了深入剖析,并介绍了如何通过多种手段确保线程安全。
214





