Java 多线程、线程同步、线程间通信

[TOC]

本文记录对 Java多线程相关技术的理解,读完将了解以下知识点:

  1. 线程是什么?与进程有什么关系?
  2. Java 多线程怎么实现?
  3. 多线程并行为什么要做同步以及怎么同步?
  4. 多线程间如何通信

线程 & 进程

  • 定义

    通俗一点理解,线程是操作系统中执行代码的一条路径。一个线程从创建到结束期间的任务就是在指定的代码路径上执行一次。

    线程是运行在进程中的一个实体,一个进程可以有多个线程。

    进程是软件中应用程序一次运行的一个实例,一个软件中可以有多个进程。

  • 线程和进程的不同

    1. 线程是CPU调度的基本单位,多个线程共享进程的资源,线程不单独拥有操作系统资源(除了线程栈空间)。
    2. 进程是操作系统分配系统资源的基本单位,进程拥有独立的运行环境。
    3. 进程的创建开销大,线程更轻量级。
    4. 进程和线程都是并发技术的一种载体

Java 多线程实现

Java 实现多线程有多种方法。

Thread & Runnable

通过创建 Thread 对象,实现其 run() 方法,并通过 Thread.start() 方法启动线程。

Thread t = new Thread() {
    @Override
    public void run(){
        ...
    }
};
t.start();

还可以通过 Runnable 对象创建 Thread 对象,并通过 Thread.start() 方法启动线程。

Runnable r = new Runnable() {
    @Override
    public void run() {
    ...
    }
};
Thread t = new Thread(r);
t.start();

FutureTask & Callable

FutureTask + Callable 实现多线程与 Thread + Runnable 的区别是前者有返回值。

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "Done";
    }
};
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
try {
    Thread.sleep(2000);
    System.out.println(task.get());
} catch (Exception e) {
}

ThreadFactory

线程工厂,通过传入 Runnable 对象创建线程对象。

ThreadFactory f = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r);
    }
};
Thread t= f.newThread(() -> System.out.println("New Thread"));
t.start();

Excutors 线程池

Excutors 是 Java 提供的线程池工具类。

  • 创建指定大小的线程池

    public static ExecutorService newFixedThreadPool(int nThreads)
    
  • 创建无限大小的线程池,初始大小为0

    public static ExecutorService newCachedThreadPool()
    
  • 创建无限大小的线程池,初始大小指定

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    
  • 创建只有一个线程的线程池

    public static ExecutorService newSingleThreadExecutor()
    

Excutors 创建线程池的方法最终都是通过 ThreadPoolExcutor 创建出来的。

image-20210822102453517.png

ThreadPoolExcutor 构造函数的五个参数决定了创建的线程池是哪个类型。

  • corePoolSize 线程池始终活跃的线程数。
  • maximumPoolSize 线程池最大能创建的线程数。
  • keepAliveTime 始终活跃线程之外的线程空闲之后存活的最长时间。
  • unit 存活时间单位。
  • workQueue 当线程池工作饱和时,新来的任务存放的队列。

线程同步

多线程环境下为什么要做同步

多线程运行环境下,多个线程共享进程的资源。在多个线程访问同一个资源时,某一个线程对资源进行写操作的过程中,其他线程对这个写了一半的资源进行了读操作,或者对这个写了一半的资源进行了写操作,则会导致数据不一致,即数据错误。

多线程同步则是对共享资源的访问进行控制,让同一时间只有一个线程可以访问资源,保证数据的一致性。

Java 多线程同步机制

Java 实现多线程同步有三种常用的方式。

synchronized

  • synchronized 可以修饰代码块、实例方法、静态方法,对应同步的粒度不一样。

  • synchronized 本质是通过控制同步代码在同一时间内只有一个线程能访问,这样就保证了同步代码涉及的资源在同一时间内只有一个线程能访问,Java 中将这个控制线程的单元是称为 monitor,monitor 其实也是一段具体的代码,实现了互斥访问和访问缓存队列。

  • 任何线程在获得 monitor 对象的第一时间,会将共享内存中的资源复制到自己的缓存中;在释放 monitor 的第一时间,会将缓存中的资源复制到共享内存中。这样下一个请求获得 monitor 的线程就可以在共享内存中取到正确的数据。

volatile

  • volatile 能保证基本类型的赋值操作和对象的引用赋值操作的同步性,但不能保证对象内容的修改同步。
  • volatile 不能解决 ++ 的原子性问题。
  • volatile 基于禁止指令重排序、写后缓存失效、写后立即刷新内存机制实现了数据修改对多线程的可见性。
  • Java 在 java.util.concurrent.atomic 包下封装了基本类型的 volatile 同步实现。

ReentrantReadWriteLock

  • ReentrantReadWriteLock 是 Java 中封装的读写锁,通过 ReentrantReadWriteLock.readLock() 拿到读锁,ReentrantReadWriteLock.writeLock() 拿到写锁,读锁和写锁分别用于对共享资源的读同步和写同步。

  • 读写锁有配套的 lock() 和 unlock() 方法,注意在异常分支调用 unlock() 释放锁对象,否则可能出现死锁的情况。

锁的优化

锁升级

Java 中锁有四种状态,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

  • 偏向锁
    • 由于大多数情况下,多线程间锁竞争是不发生的,往往都是同一个线程多次请求同一个锁,这样每次竞争锁都会增加不必要的资源消耗,为了降低竞争锁的资源消耗,引入了偏向锁。
    • 偏向锁不会主动释放锁,当线程 A 首次访问同步代码块并获取锁对象时,会在 java 对象头和栈帧中记录偏向锁的线程 id。下一次线程 A 再次获取锁的时候,只需要比较当前线程 id 和 java 对象头中的线程 id 是否一致,如果一致,则不用使用 CAS 来加锁、解锁;如果不一致(非线程A),则会去查询记录的线程 id 对应的线程是否还存活,如果没有存活,那么锁对象被标记为无锁状态,其他线程可以获取并将其重新设置为偏向锁;如果存活,则会去该线程的栈帧信息中查询此线程是否还需要继续持有这个锁对象,如果不需要则和线程不存活处理一致;如果仍然需要,则会暂停此线程,撤销偏向锁,升级为轻量级锁。
  • 轻量级锁
  • 在大多数情况下,除了锁竞争的现象不发生,还有线程持有锁的时间一般也不长。阻塞线程需要将 CPU 从用户态切换到内核态,会消耗资源,如果阻塞不久立即被唤醒,阻塞线程带来的资源消耗就有点得不偿失,因此这种状况下,还不如让线程自旋着等待锁释放,这就是轻量级锁。
  • 当线程 A 持有的偏向锁时,线程 B 尝试竞争锁,这时候线程 B 就会自旋在那等待线程 A 释放锁,如果自旋次数达到了设定的阈值线程 A 还没有释放锁,或者是线程 B 在自旋的过程中,又有线程 C 尝试竞争这个锁对象,这个时候轻量级锁就会升级为重量级锁。
  • 重量级锁
    • 重量级锁会把除了拥有锁的线程之外的其他线程全部阻塞,防止 CPU 空旋。

锁粗化

  • 一般来说,锁的粒度应该尽可能小,这样就能缩短其他线程的阻塞时间,等待的线程能尽快竞争到锁资源。但是加锁和解锁需要额外的资源消耗,如果锁粒度过小,则会导致一系列的加锁解锁操作,可能会导致不必要的资源消耗。
  • 锁粗化就是将多个连续的加锁、解锁操作连接到一起,扩展成一个更大的锁,避免频繁的加锁和解锁操作。

锁消除

  • Java 虚拟机在 JIT 编译时,通过对上下文进行扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁。## 多线程间的通信

线程间的通信指的是两个线程之间的交互,如启动、终止、等待/唤醒。

  • 启动

    启动一个线程的方法有多种,参考 Java 多线程实现章节,这里每一种实现方法都是由一个线程启动另外一个线程的方法。

  • 终止

    • Thread.stop()

      暴力停止一个线程,线程的执行会立即停止掉。

    • Thread.interrupt()

      标记线程为终止状态,需要配合Thread.interrupt() 或 isInterrupted() 使用来终止线程。

  • 等待/ 唤醒

    • wait()

      一个线程需要等待另外一个线程执行完成,调用 wait() 可以使得当前线程进入到当前同步块 monitor 对象的等待唤醒队列。

    • notify() / notifyAll()

      notify() 用于唤醒 monitor 等待唤醒队列中的一个线程;notifyAll() 用于唤醒 monitor 等待唤醒队列中的所有线程。

    • join()

      等待调用线程执行完成,再继续往下执行。

    • yeild()

      暂时释放自身资源给同优先级线程使用。

总结

读完应该理解:

  1. 线程、进程是什么,有什么特点,为什么会将这两个联系到一起。
  2. Java 实现多线程的几种方式。
  3. 多线程执行环境下会有什么问题,Java 实现同步的几种方法。
  4. 线程间通信的几种手段和原理。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,074评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,181评论 3 410
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 173,579评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,741评论 1 304
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,675评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,144评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,344评论 3 433
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,429评论 0 282
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,984评论 1 328
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,960评论 3 351
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,057评论 1 359
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,658评论 5 352
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,349评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,757评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,943评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,670评论 3 384
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,116评论 2 368