您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

还不懂 Java 中的多线程?

时间:2019-07-08 15:02:52  来源:  作者:
还不懂 Java 中的多线程?

 

作者 | 纳达丶无忌

原文 | jianshu.com/p/40d4c7aebd66

前言

由于此订阅号换了个皮肤,导致部分用户接受文章不及时。可以打开本订阅号,选择置顶(标星)公众号,重磅干货,第一时间送达!

正文

如果对什么是线程、什么是进程仍存有疑惑,请先 google 之,因为这两个概念不在本文的范围之内。

用多线程只有一个目的,那就是更好的利用 CPU 的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。

很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程

  • 并行与并发:

  • 并行:多个 CPU 实例或者多台机器同时执行一段处理逻辑,是真正的同时。

  • 并发:通过 CPU 调度算法,让用户看上去同时执行,实际上从 CPU 操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用 TPS 或者 QPS 来反应这个系统的处理能力。

还不懂 Java 中的多线程?

并发与并行

  • 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,CPU 是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

void transferMoney(User from, User to, float amount){ to.setMoney(to.getBalance + amount); from.setMoney(from.getBalance - amount);}
  • 同步:JAVA 中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入 @synchronized 关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。



  •  

好了,让我们开始吧。我准备分成几部分来总结涉及到多线程的内容:

1. 扎好马步:线程的状态

2. 内功心法:每个对象都有的方法(机制)

3. 太祖长拳:基本线程类

4. 九阴真经:高级多线程控制类

扎好马步:线程的状态

先来两张图:

还不懂 Java 中的多线程?

线程状态

还不懂 Java 中的多线程?

线程状态转换

各种状态一目了然,值得一提的是 "Blocked" 和 "Waiting" 这两个状态的区别:

  • 线程在 Running 的过程中可能会遇到阻塞 (Blocked) 情况

    对 Running 状态的线程加同步锁 (Synchronized) 使其进入 (lock blocked pool),同步锁被释放进入可运行状 (Runnable)。从 jdk 源码注释来看,blocked 指的是对 monitor 的等待(可以参考下文的图)即该线程位于等待区。

  • 线程在 Running 的过程中可能会遇到等待(Waiting)情况线程可以主动调用 object.wait 或者 sleep,或者 join(join内部调用的是 sleep ,所以可看成 sleep 的一种)进入。从 jdk 源码注释来看,Waiting 是等待另一个线程完成某一个操作,如 join 等待另一个完成执行,object.wait 等待object.notify 方法执行。

Waiting 状态Blocked 状态有点费解,我个人的理解是:Blocked 其实也是一种 wait ,等待的是 monitor ,但是和Waiting 状态不一样,举个例子,有三个线程进入了同步块,其中两个调用了 object.wait,进入了 Waiting 状态,这时第三个调用了 object.notifyAll ,这时候前两个线程就一个转移到了 Runnable,一个转移到了 Blocked。

从下文的 monitor 结构图来区别:每个 Monitor 在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set” 和 “Wait Set” 里面等候。在 “Entry Set” 中等待的线程状态 Blocked,从 jstack 的dump 中来看是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的线程状态是 Waiting,表现在 jstack 的 dump 中是 “in Object.wait”。

此外,在 runnable 状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread 类中的 yield 方法可以让一个 running 状态的线程转入 runnable。

内功心法:每个对象都有的方法(机制)

synchronized, wait, notify 是任何对象都具有的同步工具。让我们先来了解他们

还不懂 Java 中的多线程?

monitor

他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确 monitor 的概念,Java 中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在 synchronized 范围内,监视器发挥作用。

wait/notify 必须存在于 synchronized 块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着 wait之后,其他线程可以进入同步块执行。

当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去 wait 或 notify,会抛出java.lang.IllegalMonitorStateException。

也包括在 synchronized 块中去调用另一个对象的 wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

再讲用法:

  • synchronized 单独使用:

  • 代码块:如下,在多线程环境下,synchronized 块中的方法获取了 lock 实例的 monitor,如果实例相同,那么只有一个线程能执行该块内容


 

 

public class Thread1 implements Runnable { Object lock; public void run { synchronized(lock){ ..do something } } }

  • 直接用于方法:相当于上面代码中用 lock 来锁定的效果,实际获取的是 Thread1 类的 monitor。更进一步,如果修饰的是 static 方法,则锁定该类所有实例


 

 

public class Thread1 implements Runnable { public synchronized void run { ..do something }}

  • synchronized, wait, notify 结合:典型场景生产者消费者问题


 

 

/** * 生产者生产出来的产品交给店员 */ public synchronized void produce { if(this.product >= MAX_PRODUCT) { try { wait; System.out.println("产品已满,请稍候再生产"); } catch(InterruptedException e) { e.printStackTrace ; } return; }

this.product++; System.out.println("生产者生产第" + this.product + "个产品."); notifyAll; //通知等待区的消费者可以取出产品了 } /** * 消费者从店员取产品 */ public synchronized void consume { if(this.product <= MIN_PRODUCT) { try { wait; System.out.println("缺货,稍候再取"); } catch (InterruptedException e) { e.printStackTrace; } return; } System.out.println("消费者取走了第" + this.product + "个产品."); this.product--; notifyAll; //通知等待去的生产者可以生产产品了 }

volatile

多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存 load 到本地栈,完成操作后再 save 回去 (volatile 关键词的作用:每次针对该变量的操作都激发一次 load and save) 。

还不懂 Java 中的多线程?

volatile

针对多线程使用的变量如果不是 volatile 或者 final 修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile 就是不去缓存,直接取值。在线程安全的情况下加 volatile 会牺牲性能。

太祖长拳:基本线程类

基本线程类指的是 Thread 类,Runnable 接口,Callable 接口

Thread 类实现了 Runnable 接口,启动一个线程的方法:


 

 

MyThread my = new MyThread; my.start;

Thread类相关方法


 

 

//当前线程可转让 cpu 控制权,让别的就绪状态线程运行(切换)public static Thread.yield//暂停一段时间public static Thread.sleep //在一个线程中调用 other.join,将等待other执行完后才继续本线程。 public join//后两个函数皆可以被打断public interrupte

关于中断:它并不像 stop 方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为 true )。终端只会影响到 wait 状态、sleep 状态和 join 状态。被打断的线程会抛出 InterruptedException。

Thread.interrupted 检查当前线程是否发生中断,返回boolean

synchronized 在获锁的过程中是不能被中断的。

中断是一个状态!interrupt方法只是将这个状态置为 true 而已。所以说正常运行的程序不去检测状态,就不会终止,而 wait 等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted) ,则同样可以在中断后离开代码体

Thread类最佳实践:

写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。

如何获取线程中的异常

还不懂 Java 中的多线程?

不能用try,catch来获取线程中的异常

Runnable

与 Thread 类似

Callable

future 模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是 isDone 和 get。其中 Future 对象用来存放该线程的返回值以及状态


 

 

ExecutorService e = Executors.newFixedThreadPool(3);//submit 方法有多重参数版本,及支持 callable 也能够支持runnable 接口类型. Future future = e.submit(new myCallable);future.isDone //return true,false 无阻塞 future.get // return 返回值,阻塞直到该线程运行结束

九阴真经:高级多线程控制类

以上都属于内功心法,接下来是实际项目中常用到的工具了,Java1.5 提供了一个非常高效实用的多线程包: java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的 Java 多线程程序。

1.ThreadLocal类

用处:保存线程的独立变量。对一个线程类(继承自 Thread )当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录 session 信息。

实现:每个Thread 都持有一个 TreadLocalMap 类型的变量(该类是一个轻量级的 Map,功能与 map 一样,区别是桶里放的是 entry 而不是 entry 的链表。功能还是一个 map 。)以本身为 key,以目标为 value。主要方法是 get 和 set(T a),set 之后在 map 里维护一个threadLocal -> a,get 时将 a 返回。ThreadLocal 是一个特殊的容器。

2.原子类(AtomicInteger、AtomicBoolean……)

如果使用 atomic wrApper class 如 atomicInteger,或者使用自己保证原子的操作,则等同于 synchronized


 

 

//返回值为 booleanAtomicInteger.compareAndSet(int expect,int update)

该方法可用于实现乐观锁,考虑文中最初提到的如下场景:a 给 b 付款10元,a 扣了 10 元,b 要加 10 元。此时 c 给 b 2 元,但是 b的加十元代码约为:


 

if(b.value.compareAndSet(old, value)){ return ;}else{ //try again // if that fails, rollback and log}

AtomicReference

对于 AtomicReference 来讲,也许对象会出现,属性丢失的情况,即 oldObject == current,但是 oldObject.getPropertyA != current.getPropertyA。这时候,AtomicStampedReference 就派上用场了。这也是一个很常用的思路,即加上版本号

3.Lock类

lock: 在 java.util.concurrent 包内。共有三个实现:

  • ReentrantLock

  • ReentrantReadWriteLock.ReadLock

  • ReentrantReadWriteLock.WriteLock

主要目的是和 synchronized 一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

区别如下:

1.lock 更灵活,可以自由定义多把锁的枷锁解锁顺(synchronized 要按照先加的后解顺序)

2.提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有 trylock 的带超时时间版本

3.本质上和监视器锁(即 synchronized 是一样的)

4.能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。

5.和 Condition 类的结合。

6.性能更高,对比如下图:

还不懂 Java 中的多线程?

synchronized和Lock性能对比

ReentrantLock

可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。

使用方法是:

1.先 new 一个实例


 

 

static ReentrantLock r=new ReentrantLock;

2.加锁


 

r.lock或 r.lockInterruptibly;

此处也是个不同,后者可被打断。当 a 线程 lock 后,b 线程阻塞,此时如果是 lockInterruptibly,那么在调用 b.interrupt 之后,b 线程退出阻塞,并放弃对资源的争抢,进入 catch 块。(如果使用后者,必须 throw interruptable exception 或 catch)

3.释放锁


 

 

r.unlock

必须做!何为必须做呢,要放在 finally 里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally 是可以信任的:经过测试,哪怕是发生了 OutofMemoryError ,finally 块中的语句执行也能够得到保证。

ReentrantReadWriteLock

可重入读写锁(读写锁的一个实现)


 

 

ReentrantReadWriteLock lock = new ReentrantReadWriteLockReadLock r = lock.readLock; WriteLock w = lock.writeLock;

两者都有 lock,unlock 方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码

4.容器类

这里就讨论比较常用的两个:

  • BlockingQueue

  • ConcurrentHashMap

BlockingQueue

阻塞队列。该类是 java.util.concurrent 包下的重要类,通过对 Queue 的学习可以得知,这个 queue 是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管道,特别适用于先进先出策略的一些应用场景。普通的 queue 接口主要实现有 PriorityQueue(优先队列),有兴趣可以研究

BlockingQueue 在队列的基础上添加了多线程协作的功能:

还不懂 Java 中的多线程?

BlockingQueue

除了传统的 queue 功能(表格左边的两列)之外,还提供了阻塞接口 put 和 take,带超时功能的阻塞接口 offer 和 poll。put 会在队列满的时候阻塞,直到有空间时被唤醒;take 在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。

常见的阻塞队列有:

  • ArrayListBlockingQueue

  • LinkedListBlockingQueue

  • DelayQueue

  • SynchronousQueue

** ConcurrentHashMap**

高效的线程安全哈希 map。请对比 hashTable , concurrentHashMap, HashMap

5.管理类

管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。

了解到的值得一提的管理类:ThreadPoolExecutor 和 JMX框架下的系统级管理类 ThreadMXBean

ThreadPoolExecutor

如果不了解这个类,应该了解前面提到的 ExecutorService,开一个自己的线程池非常方便


 

 

ExecutorService e = Executors.newCachedThreadPool; ExecutorService e =Executors.newSingleThreadExecutor; ExecutorService e = Executors.newFixedThreadPool(3); // 第一种是可变大小线程池,按照任务数来分配线程, // 第二种是单线程池,相当于 FixedThreadPool(1) // 第三种是固定大小线程池。 // 然后运行 e.execute(new MyRunnableImpl);

该类内部是通过 ThreadPoolExecutor 实现的,掌握该类有助于理解线程池的管理,本质上,他们都是 ThreadPoolExecutor 类的各种实现版本。请参见 javadoc:

还不懂 Java 中的多线程?

ThreadPoolExecutor参数解释

翻译一下:

corePoolSize: 池内线程初始值与最小值,就算是空闲状态,也会保持该数量线程。

maximumPoolSize: 线程最大值,线程的增长始终不会超过该值。

keepAliveTime: 当池内线程数高于 corePoolSize 时,经过多少时间多余的空闲线程才会被回收。回收前处于 wait 状态

unit

时间单位,可以使用 TimeUnit 的实例,如 TimeUnit.MILLISECONDS

workQueue: 待入任务(Runnable)的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死 (starving)

threadFactory: 线程工厂类,有默认实现,如果有自定义的需要则需要自己实现 ThreadFactory 接口并作为参数传入。

请注意:该类十分常用,作者80%的多线程问题靠他。



Tags:多线程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一文看懂"async"和“await”关键词是如何简化了C#中多线程的开发过程当我们使用需要长时间运行的方法(即,用于读取大文件或从网络下载大量资源)时,在同步的应用程序中,应用程序本...【详细内容】
2021-12-01  Tags: 多线程  点击:(24)  评论:(0)  加入收藏
相关概念 并发和并行 并发:指一个时间段内,在一个CPU(CPU核心)能运行的程序的数量。 并行:指在同一时刻,在多个CPU上运行多个程序,跟CPU(CPU核心)数量有关。因为计算机CPU(CPU核心)在同...【详细内容】
2021-11-30  Tags: 多线程  点击:(18)  评论:(0)  加入收藏
一、现代计算机理论模型与工作方式现代计算机模型是基于-冯诺依曼计算机模型。计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进...【详细内容】
2021-07-19  Tags: 多线程  点击:(87)  评论:(0)  加入收藏
public class SReactorSThread { private Selector selector; private ServerSocketChannel serverSocketChannel; private int PORT = 6666; public SReactorS...【详细内容】
2021-05-25  Tags: 多线程  点击:(779)  评论:(0)  加入收藏
并发与并行分布式系统的一个重要特征就是计算能力是可以并发或者并行的。在分布式系统中,往往会将一个大任务进行分解,而后下发给不同的节点去计算,从而节省整个任务的计算时间...【详细内容】
2021-04-15  Tags: 多线程  点击:(235)  评论:(0)  加入收藏
年轻人,醒醒吧!此时不搏何时搏!本文主要讲一下常见的CAS理论。再者就是说一下锁的分类,什么乐观锁啊,悲观锁、重入锁等等。这篇文章要一网打尽,都介绍一下。把CAS按在地上摩擦中文...【详细内容】
2020-12-08  Tags: 多线程  点击:(100)  评论:(0)  加入收藏
你真的了解多线程吗?如果问你“为什么多线程可以提高程序运行效率?”,想必你会说“计算机并行执行任务啊,当然效率高!” 这显然不是一个内行该给出的答案。要知道,一个 CPU 在任何...【详细内容】
2020-11-10  Tags: 多线程  点击:(142)  评论:(0)  加入收藏
最近定位了在一个多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题。定位这个问题历经曲折,尝试了各种内存调试的办法。往...【详细内容】
2020-10-27  Tags: 多线程  点击:(152)  评论:(0)  加入收藏
一、什么是分布式?分布式更多的是一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段,该领域需要解决的问题极多,在不同的技术层面上,又包括:分布式文件系统...【详细内容】
2020-10-14  Tags: 多线程  点击:(91)  评论:(0)  加入收藏
作为一个程序员,线程,进程,协程这些是我们必须要掌握的最基础的知识,这就好比数学家必须要学习的基本几何原理一样,没有它们,我们在编程的世界里寸步难行。并发和并行并发和并行是...【详细内容】
2020-10-09  Tags: 多线程  点击:(97)  评论:(0)  加入收藏
▌简易百科推荐
面向对象的特征之一封装 面向对象的特征之二继承 方法重写(override/overWrite) 方法的重载(overload)和重写(override)的区别: 面向对象特征之三:多态 Instanceof关键字...【详细内容】
2021-12-28  顶顶架构师    Tags:面向对象   点击:(2)  评论:(0)  加入收藏
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  CF07    Tags:Java   点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Java架构师之路    Tags:JAVA   点击:(5)  评论:(0)  加入收藏
大家好!我是老码农,一个喜欢技术、爱分享的同学,从今天开始和大家持续分享JVM调优方面的经验。JVM调优是个大话题,涉及的知识点很庞大 Java内存模型 垃圾回收机制 各种工具使用 ...【详细内容】
2021-12-23  小码匠和老码农    Tags:JVM调优   点击:(12)  评论:(0)  加入收藏
前言JDBC访问Postgresql的jsonb类型字段当然可以使用Postgresql jdbc驱动中提供的PGobject,但是这样在需要兼容多种数据库的系统开发中显得不那么通用,需要特殊处理。本文介绍...【详细内容】
2021-12-23  dingle    Tags:JDBC   点击:(13)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  JAVA小白    Tags:Java   点击:(11)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  小西学JAVA    Tags:JAVA并发   点击:(11)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  唯一浩哥    Tags:Java基础   点击:(17)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  软件老王    Tags:logback   点击:(19)  评论:(0)  加入收藏
本篇文章我们以AtomicInteger为例子,主要讲解下CAS(Compare And Swap)功能是如何在AtomicInteger中使用的,以及提供CAS功能的Unsafe对象。我们先从一个例子开始吧。假设现在我们...【详细内容】
2021-12-14  小西学JAVA    Tags:JAVA   点击:(22)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条