Java基础-多线程

线程的6种状态

  1. New

创建后尚未启动的线程处于这种状态。

  1. Runnable

包括了操作系统线程状态中的 Running 和 Ready ,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。

  1. Blocked

阻塞状态是指线程因为某种原因放弃了cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。

  • 同步阻塞:运行(running)的线程进入了一个 synchronized 方法,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
  • 其他阻塞:运行(running)的线程发出了I/O请求时,JVM会把该线程置为阻塞状态。当I/O处理完毕时,线程重新转入可运行(runnable)状态。
  1. Waiting

处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。

  • Object.wait() 方法,并且没有使用 timeout 参数;
  • Thread.join() 方法,没有使用 timeout 参数;
  • LockSupport.park() 方法;
  • Conditon.await() 方法。
  1. Timed Waiting

处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由操作系统自动唤醒。

  • Thread.sleep() 方法;
  • Object.wait() 方法,带有时间;
  • Thread.join() 方法,带有时间;
  • LockSupport.parkNanos() 方法,带有时间。
  1. Terminated

已终止的线程状态,线程已经结束执行。

线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
public ThreadPoolExecutor(
    int corePoolSize, //线程池核心线程数最大值
    int maximumPoolSize, //线程池最大线程数大小
    long keepAliveTime, // 线程池中非核心线程空闲存活时间大小
    TimeUnit unit, //线程空闲存活时间单位
    BlockingQueue<Runnable> workQueue, //存放任务的阻塞队列
    ThreadFactory threadFactory, //用于设置创建线程的工厂
    RejectedExecutionHandler handler) // 线城池的饱和策略事件,主要有四种类型。

newCachedThreadPool(可缓存线程的线程池)

  • 核心线程数为0。
  • 最大线程数为 Integer.MAX_VALUE。
  • 阻塞队列是 SynchronousQueue。
  • 非核心线程空闲存活时间为60秒。

newFixedThreadPool(固定数目线程的线程池)

  • 核心线程数和最大线程数大小一样。
  • 没有所谓的非空闲时间,即 keepAliveTime 为0。
  • 阻塞队列为无界队列 LinkedBlockingQueue。

newScheduledThreadPool(定时及周期执行的线程池)

  • 最大线程数为 Integer.MAX_VALUE。
  • 阻塞队列是 DelayedWorkQueue。
  • keepAliveTime 为0。

newSingleThreadExecutor(单线程的线程池)

  • 核心线程数为1。
  • 最大线程数也为1。
  • 阻塞队列是 LinkedBlockingQueue。
  • keepAliveTime 为0。

多线程常见问题

上下文切换

多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 CPU 通过给每个线程分配一定的时间片,由于时间非常短通常是几十毫秒,所以 CPU 可以不停的切换线程执行任务从而达到了多线程的效果。但是由于在线程切换的时候需要保存本次执行的信息,在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。上下文切换是非常耗效率的。

通常有以下解决方案:

  • 采用无锁编程,比如将数据按照 Hash(id) 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。
  • 采用 CAS(compare and swap) 算法,如 Atomic 包就是采用 CAS 算法。
  • 合理的创建线程,避免创建了一些线程但其中大部分都是处于 waiting 状态,因为每当从 waiting 状态切换到 running 状态都是一次上下文切换。

死锁

死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。

常用的解决方案如下:

  • 尽量一个线程只获取一个锁。
  • 一个线程只占用一个资源。
  • 尝试使用定时锁,至少能保证锁最终会被释放。

资源限制

当在带宽有限的情况下一个线程下载某个资源需要 1M/S,当开 10 个线程时速度并不会乘 10 倍,反而还会增加时间,毕竟上下文切换比较耗时。如果是受限于资源的话可以采用集群来处理任务,不同的机器来处理不同的数据,就类似于开始提到的无锁编程。

多线程三大核心

  1. 原子性

Java 的原子性就和数据库事务的原子性差不多,一个操作中要么全部执行成功或者失败。

  1. 可见性

现代计算机中,由于 CPU 直接从主内存中读取数据的效率不高,所以都会对应的 CPU 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。

volatile 关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。使用 volatile 关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。

synchronized 和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 volatile 相比开销较大。

  1. 顺序性

有时 JVM 为了提高整体的效率在保证最终结果和代码顺序执行结果一致的情况下会进行指令重排。重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。

Java 中可以使用 volatile 来保证顺序性,synchronized 和 lock 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。

synchronized 关键字原理

synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式:

  • 同步普通方法,锁的是当前对象。
  • 同步静态方法,锁的是当前 Class 对象。
  • 同步块,锁的是 () 中的对象。

实现原理: JVM 是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的。

具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

private Object lock = new Object();
synchronized (lock) {
    if / while () {
        lock.wait();
    }
    lock.notifyAll();
}

锁优化

synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。

ReentrantLock

ReentrantLock 是一个普通的类,是基于 AQS(Java 并发包里实现锁、同步的一个重要的基础框架)来实现的。

synchronized 和 ReentrantLock 都是可重入锁(一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况)。

ReentrantLock 分为公平锁和非公平锁:

  • 公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。
  • 而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。

可以通过构造方法来指定具体类型:

//默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

//公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制,因此非公平锁的效率和吞吐量都比公平锁高的多,默认一般使用非公平锁。

通常的使用方式如下:

private ReentrantLock lock = new ReentrantLock();
public void run() {
    lock.lock();
    try {
    } catch (InterruptedException e) {
    } finally {
        lock.unlock();
    }
}

锁的种类

  • 可重入锁

如果锁具备可重入性(表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配),则称作为可重入锁。

其表现为:
当前线程可以反复加锁,但也需要释放同样加锁次数的锁。
同一个线程再次进入同步代码时,可以使用自己已经获取到的锁

Synchronized 和 ReentrantLock 都是可重入锁。

  • 读写锁

读写锁将对一个资源的访问分成了2个锁:一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口,可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。

  • 可中断锁

即可以中断的锁。在 Java 中,Synchronized 不是可中断锁,而 Lock 是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

Lock 接口中的 lockInterruptibly() 方法就体现了 Lock 的可中断性。

  • 公平锁

公平锁即尽量以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。

Synchronized 是非公平锁,它无法保证等待的线程获取锁的顺序。对于 ReentrantLock 和 ReentrantReadWriteLock,默认情况下是非公平锁,但是可以设置为公平锁。

Synchronized 和 Lock 的区别

  • Lock 是一个接口,而 Synchronized 是 Java 中的关键字,Synchronized 是内置的语言实现;
  • Synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  • Lock 可以让等待锁的线程响应中断,而 Synchronized 却不行,使用 Synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • Lock可以提高多个线程进行读操作的效率。

sleep 和 wait 的区别

  • 使用上:sleep 是 Thread 线程类的方法,而 wait 是 Object 顶级类的方法。sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步块中使用。
  • CPU及资源锁释放:sleep 和 wait 调用后都会暂停当前线程并让出cpu的执行时间,但不同的是 sleep 不会释放当前持有的对象的锁资源,到时间后会继续执行,而 wait 会放弃所有锁并需要 notify/notifyAll 后重新获取到对象锁资源后才能继续执行。
  • 异常捕获:sleep 需要捕获或者抛出异常,而 wait/notify/notifyAll 不需要。

ThreadLocal

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

Android中的应用:Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。

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