并发与多线程相关知识点梳理

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发与多线程相关知识点梳理相关的知识,希望对你有一定的参考价值。

文章目录

本文总结了并发与多线程相关知识点,做一个小结。

并发和并行的概念

首先从并发( Concurrency )与并行( Parallelism )说起。

并发是指在某个时间段内,多任务交替处理的能力。所谓不患寡而患不均,每个 CPU 不可能只顾着执行某个进程,让其他进程一直处于等待状态。所以, CPU 把可执行时间均匀地分成若干份,每个进程执行一段时间后,记录当前的工作状态,释放相关的执行资源并进入等待状态,让其他进程抢占 CPU 资源。

并行是指同时处理多任务的能力。目前, CPU 已经发展为多核,可以同时执行多个互不依赖的指令及执行块。

并发与并行两个概念非常容易混淆,它们的核心区别在于进程是否同时执行。

以 KTV 唱歌为例,并行指的是有多少人可以使用话筒同时唱歌,并发指的是同一个话筒被多个人轮流使用。

如何保证线程安全

线程安全问题只在多线程环境下才出现,单纯程串行执行不存在此问题。保证高并发场景下的线程安全,可以从以下四个维度考量:

1. 数据单线程内可见

单线程总是安全的。通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程毫无瓜葛。 ThreadLocal 就是采用这种方式来实现线程安全。

2. 只读对象

只读对象总是安全的。它的特性是允许复制、拒绝写入。最典型的只读对象有 String Integer 等。一个对象想要拒绝任何写入,必须要满足以下条件,使用 final 关键字修饰类,避免被继承,使用 private final 关键字避免属性被中途修改;没有任何更新方法;返回值不能可变对象为引用。

3. 线程安全类

某些线程安全类的内部有非常明确的结程安全机制。比如 StringBuffer 就是一个线程安全类,它采用 synchronized 关键字来修饰相关方法。

4. 同步与锁机制

如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要开发工程师在代码中实现安全的同步机制。虽然这个机制支持的并发场景很有价值,但非常复杂且容易出现问题。

线程安全的核心理念就是“要么只读,要么加锁”。合理利用好 JDK 提供的并发包,往往能化腐朽为神奇。并发包主要分成以下几个类族:

  • 线程同步类

    这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用 Object wait()和 notify () 进行同步的方式。主要代表为 CountDownLatch、 Semaphore、 CyclicBarrier 等。

  • 并发集合类

    集合并发操作的要求是执行速度快,提取数据准。最著名的类非 ConcurrentHashMa 莫属,它不断地优化,由刚开始的锁分段到后来的 CAS,不断地提升并发性能。其他还有 ConcurrentSkipListMap、CopyOnWriteArrayList、BlockingQueue 等。

  • 线程管理类

    虽然 Thread 和 ThreadLocal JDK 1.0 就已经引入,但是真正把 Thread 发扬光大的是线程池。根据实际场景的需要,提供了多种创建线程池的快捷方式,如使用 Executors 静态工厂或者使用 ThreadPoolExecutor 等。另外,通过 ScheduledExecutorService 来执行定时任务。

  • 锁相关类

    锁以 Lock 接口为核心,派生出在一些实际场景中进行互斥操作的锁相关类。最有名的是 ReentrantLock。锁的很多概念在弱化,是因为锁的实现在各种场景中已经通过类库封装进去了。

    并发包中的类族有很多,差异比较微妙,开发工程师需要有很好的 Java 基础、逻辑思维能力,还需要有定的数据结构基础,才能够彻底分清各个类族的优点、缺点及差异点。

什么是锁

计算机的锁也是从开始的悲观锁,发展到后来的乐观锁、偏向锁、分段锁等。锁主要提供了两种特性 互斥性和不可见性。因为锁的存在,某些操作对外界来说是黑箱进行的,只有锁的持有者才知道对变量进行了什么修改。

Java 中常用锁实现的方式有两种:

  • 用并发包同步锁类

  • 利用同步代码块

同步代码块一般使用 Java synchronized 关键字来实现,有两种方式对方法进行加锁操作第 ,在方法签名处加 synchronized 关键字;第二,使用 synchronized (对象或类)进行同步。

这里的原则是锁的范围尽可能小,锁的时间尽可能短,即能锁对象,就不要锁类,能锁代码块,就不要锁方法。

同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

synchronized 关键字是用于保证线程安全的,是阻塞式的解决方案。

当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断.

// 加在方法上 实际是对this对象加锁
private synchronized void a() 


// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b()
    synchronized (this)
    


// 加在静态方法上 实际是对类对象加锁
private synchronized static void c() 

// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d()
    synchronized (TestSynchronized.class)

    

线程同步

资源共享的两个原因是 源紧缺 共建需求。线程共享 CPU 是从 紧缺度来考虑的 而多线程共享同一变量 通常是从共建需求的维度来考虑的。在多个线程对同一变量进行写操作时,如果操作没有原子性,就可能产生脏数据。

所谓原子性是指不可分割的一系列操作指令 在执行完毕前不会被任何其他操作中断 要么全部执行 要么全部不执行。如果每个线程的修改都是原子操作 就不存在线程同步问题。

有些看似非常简单的操作其实不具备原子性 典型的就是 i++操作 它需要分为三步,即 ILOAD IINC IS TORE。另一方面 ,更加复杂的 CAS ( Compare and Swap )操作却具有原子性。

线程 步现象在实际生活随处可见。 如乘客在火车站排队打车 每个人都是一线程 管理员每次放 10 个人进来 为了保证安全,等全部离开后,再放下一批人进来。如果没有协调机制,场面一定是混乱不堪的 ,人们会一 窝蜂地上去抢车, 存在严重的安全隐患。计算机的线程同步, 就是线程之间接某种机制协调先后次序执行。 当有个线程在对内存进行操作时, 其他线程都不可以对这个内存地址进行操作,直到该线程完成操作。实现线程同步的方式有很多 比如同步方法、锁、阻塞队列等。

  • Volatile

    当使用volatile修饰变量时, 意昧着任何对此变量的操作都会在内存中进行, 不会产生副本 ,以保证共享变量的可见性局部阻止了指令重排的发生。

  • 信号量同步

    信号量同步是指在不同的线程之间,通过传递同步信号量来协调线程执行的先后次序。这里重点分析基于时间维度和 信号维度的两个类: CountDownLatch、Semaphore。

小结:无论从性能还是安全性上考虑 ,我们尽量使用并发包中提供的信号同步类, 避免使用对象的 wait()和 notify() 方式来进行同步。

扩展:

Semaphore有什么作用

  • Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数
  • Semaphore 有一个构造函数,可以传入一个 int 型整数 n ,表示某段代码最多只有 n 个线程可以访问
  • 如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入
  • 由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n = 1,相当于变成了一个 synchronized 了。

Semaphore 源码分析:

/**
 * Creates a @code Semaphore with the given number of
 * permits and nonfair fairness setting.
 *
 * @param permits the initial number of permits available.
 *        This value may be negative, in which case releases
 *        must occur before any acquires will be granted.
 */
// 参数 permits 表示许可数目,即同时可以允许多少线程进行访问  
public Semaphore(int permits) 
    sync = new NonfairSync(permits);


/**
 * Creates a @code Semaphore with the given number of
 * permits and the given fairness setting.
 *
 * @param permits the initial number of permits available.
 *        This value may be negative, in which case releases
 *        must occur before any acquires will be granted.
 * @param fair @code true if this semaphore will guarantee
 *        first-in first-out granting of permits under contention,
 *        else @code false
 */
// 参数 fair 表示是否是公平的,即等待时间越久的越先获取许可
public Semaphore(int permits, boolean fair) 
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);

Semaphore 相关重要方法:

  1. acquire() 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
  2. release() 用来释放许可。注意,在释放许可之前,必须先获获得许可。

使用示例:

假若一个工厂有 5 台机器,但是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们用 Semaphore 来实现,(Kotlin)代码如下:

fun main(args: Array<String>) 
    val workerNum = 8
    val semaphore = Semaphore(5)

    for (i in 0 until workerNum) 
        Worker(i, semaphore).start()
    

internal class Worker(private val num: Int, private val semaphore: Semaphore) : Thread() 
    override fun run() 
        try 
            semaphore.acquire()
            println("第 $num 获取机器开始生产")
            sleep(2000)
            println("第 $num 释放机器")
            semaphore.release()
         catch (e: InterruptedException) 
            e.printStackTrace()
        
    

执行结果:

第一张图为首次执行的 5 个线程,后面为停顿 2 秒后的执行情况。

引用类型

  • 强引用 StrongReference

    只要对象有强引用指向,并且 GC Roots 可达,Java内存回收时,内存耗尽,也不会回收该对象。

    使用场景:任何场景

  • 软引用 SoftReference

    在即将 OOM 之前,GC回收,使程序获取更多的内存空间,让程序能健康运行下去。
    使用示例:

    ImageView imageView = findViewById(R.id.iv_soft_reference);
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),    R.mipmap.ic_launcher_round);
        // 创建软引用实例,将 bitmap 存入到软引用中
       SoftReference<Bitmap> softReference = new SoftReference<>(bitmap);
            if (softReference.get() != null) 
            imageView.setImageBitmap(softReference.get())
            
    

    通过软引用的 get() 方法获取强 Bitmap 对象实例的引用,如果对象未被回收那么设置图片到控件上。

    如果内存紧张,系统就会 GC,softReference.get() 就不会返回 Bitmap 对象,而是返回 null,

    这里也需要把软引用获取的引用做空判断,避免空指针。

    使用场景:比较少使用,已被 LruCache 替代

    LruCache:

    LruCache是一个泛型类,它内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象,它提供了 get 和 put 方法来完成缓存的获取和添加操作,当缓存满时,LruCache 会移除较早使用的缓存对象,然后再添加新的缓存对象。

  • 弱引用 WeakReference

    不管内存足不足,只要GC了都能回收弱引用指向的对象。

    弱引用主要用于指向某个易消失 的对象,调用 WeakReference.get() 可能返回为 null , 需要注意。

    使用场景:常用于避免内存泄漏。

  • 虚引用

    随时都能回收,也称幽灵引用,相当于没有指向任何实例引用。

ThreadLocal

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之 间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

这个理解起来可能不好理解,容易忘记,需要结合平时的开发使用场景多角度理解,可以再看下这篇。

扩展:Android ThreadLocal理解及应用场景

LeetCode 相关线程的练习

算法多线程leetcode题目总结

参考:

  1. 《码出高效:Java开发手册》
  2. 万字图解Java多线程
  3. Android性能优化之三级缓存-内存缓存详解面试题

以上是关于并发与多线程相关知识点梳理的主要内容,如果未能解决你的问题,请参考以下文章

并发与多线程相关知识点梳理

太好了, 终于梳理清楚Python多线程与多进程

一篇文章梳理清楚 Python 多线程与多进程

c++11多线程入门<学习记录;

Python 多线程与多进程

浅谈RxJava与多线程并发