总结篇(24)---Java线程及其相关(2)多线程及其问题

本文详细探讨了多线程的基础概念,分析了多线程环境下可能出现的效率、并发及安全问题,特别是针对死锁产生的原因及解决方案进行了深入剖析,并介绍了如何通过多种手段确保线程安全。

多线程

  • 多线程就是指一个进程中同时有多个线程正在执行。
  • 用多线程只有一个目的,那就是更好的利用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面试常规题):

总结篇(1)---复用类

总结篇(2)---多态

总结篇(3)---内部类 (1)内部类的基本概念

总结篇(4)---内部类 (2)内部类之静态内部类

总结篇(5)---内部类 (3)内部类之成员内部类

总结篇(6)---内部类 (4)内部类之局部内部类

总结篇(7)---内部类 (5)内部类之匿名内部类

总结篇(8)---序列化

总结篇(9)---字符串及基本类 (1)字符串及基本类之基本数据类型

总结篇(10)---字符串及基本类 (2)字符串及基本类之java中公共方法及操作

总结篇(11)---字符串及基本类 (3)String对象

总结篇(12)---字符串及基本类 (4)Integer对象

总结篇(13)--- Java注解及元注解

总结篇(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调优

总结篇(20)---反射

总结篇(21)---Java IO

总结篇(22)---Java 进程

总结篇(23)---Java线程及其相关(1)线程介绍

总结篇(24)---Java线程及其相关(2)多线程及其问题

总结篇(25)---Java线程及其相关(3)线程池及其问题

总结篇(26)---Java线程及其相关(4)ThreadLocal

总结篇(27)---Java并发及锁(1)Synchronized

总结篇(28)---Java并发及锁(2)Volatile

总结篇(29)---Java并发及锁(3)Lock

总结篇(30)---Java并发及锁(4)常见锁及分类

总结篇(31)---JUC工具类(1)CountDownLatch

总结篇(32)---JUC工具类(2)CyclicBarrier

总结篇(33)---JUC工具类(3)Semaphore

总结篇(34)---JUC工具类(4)Exchanger

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sun cat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值