多线程知识分享

多线程知识分享

1、线程和进程的区别

进程:可以理解为运行的程序,如idea

线程:cpu调度的最小单位,一个进程包含多个线程。

2、并行与并发

并行:多核cpu运行 多线程时,真正的在同一时刻运行。

并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu,在同一个时间段内运行。

 

使用多线程的优点

优点:充分利用cpu资源,发挥多核cpu强大的能力。

多线程难点

  1. 多线程的执行结果不确定,受到cpu调度的影响
  2. 多线程的安全问题
  3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
  4. 多线程执行是动态的,同时的,难以追踪过程
  5. 多线程的底层是操作系统层面的,源码难度大

多线程的实现方式

  1. 继承Thread类 (可以说是 将任务和线程合并在一起)
  2. 实现Runnable接口 (可以说是 将任务和线程分开了)
  3. 实现Callable接口 (利用FutureTask执行任务)
  4. 使用线程池工具类,一般不推荐
  5. 自定义线程池

①和②一般不推荐,没有返回值。

关于多线程的实现,其实也可以从源码里看到具体的一些介绍

线程的六种状态

new:初始状态

running:运行中

blocked:阻塞状态

waiting:等待

timed-waiting:等待,可设置等待时间

terminated:终止状态

线程的生命周期

解决多线程安全问题,使用锁

synchronized和lock

synchronized是java里的关键字,是jvm级别的,可作用于方法和代码块上;synchronized不能显示的释放锁,不能响应中断,除非线程执行时发生异常;

synchronized的原理,可通过javac将java文件编译class文件,执行javap   -c   编译后的文件名,就可以看到字节码了。

借个图

lock是api级别的,作用于代码块;lock是一个接口,ReentrantLock是它的具体实现。ReentrantLock的实现依赖AQS,感兴趣的可以看看。

CountDownLatch、CyclicBarrier、Semaphore都是基于AQS实现的常用并发工具类(https://www.cnblogs.com/tuyang1129/p/12670014.html

https://mp.weixin.qq.com/s?__biz=MzkxNDEyOTI0OQ==&mid=2247484424&idx=1&sn=761eb25bd21dbfb4f8be0a458d528976&source=41#wechat_redirect

https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

ReentrantLock可以响应中断,可以知道有没有成功获取锁;Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,
因此使用 Lock 时需要在 finally 块中释放锁。

死锁

造成死锁的原因可以概括成三句话:

  • 当前线程拥有其他线程需要的资源
  • 当前线程等待其他线程已拥有的资源
  • 都不放弃自己拥有的资源

如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用!同步代码块最好仅被用于保护那些涉及共享状态的操作!

使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

使用tryLock()能够有效避免死锁问题

死锁检测

JDK提供了两种方式来给我们检测:

  • JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
  • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

提到多线程,就必须提到java内存模型

jmm 体现在以下三个方面

  1. 原子性 保证指令不会受到上下文切换的影响
  2. 可见性 保证指令不会受到cpu缓存的影响
  3. 有序性 保证指令不会受并行优化的影响

volatile

 volatile 关键字是 Java 虚拟机提供的的最轻量级的同步机制,它作为一个修饰
符出现,用来修饰变量。用来保证变量对所有线程可见性。

用 volatile 修饰变量时  
线程 1 对变量进行操作时,会把变量变化的值强制刷新的到主内存。当线程 2获取值时,会把自己的内存里的值过期掉,

之后从主内存中读取。所以每次都能获取最新的值。

volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,也就是四个位置,来保证重排序时不能把
内存屏障后面的指令重排序到内存屏障之前的位置。

记住:volatile 并不能解决原子性,如果需要解决原子性问题,需要使用
synchronzied 或者 lock或者Atomic包下的原子类。

 

无锁--CAS

CAS的全称是:⽐较并交换(Compare And Swap)。在CAS中,有这样三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)
⽐较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程
更新了V,则当前线程放弃更新,什么都不做。
所以这⾥的预期值E本质上指的是“旧值”。

CAS是⼀种原⼦操作。那么Java是怎样来使⽤CAS的呢?我们知道,在
Java中,如果⼀个⽅法是native的,那Java就不负责具体实现它,⽽是交给底层的
JVM使⽤c或者c++去实现。
在Java中,有⼀个  Unsafe  类,它在  sun.misc  包中。它⾥⾯是⼀些  native  ⽅法。

无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

cas是乐观锁的思想,sychronized是悲观锁的思想

cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

juc并发包下包含了实现了cas的原子类

AtomicInteger/AtomicBoolean/AtomicLong
AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
AtomicReference/AtomicStampedReference/AtomicMarkableReference
 

CAS存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

解决方式

AtomicStampedReference/AtomicMarkableReference

上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题
 

JUC并发集合

ConcurrentHashMap--对应的是单线程的HashMap

CopyOnWriteArrayList--对应的是单线程的ArrayList

ConcurrentLinkedQueue

ThreadLocal

ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,

但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。

ThreadLocal原理:

  1. 每个Thread维护着一个ThreadLocalMap的引用
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

关于ThreadLocal内存泄漏的问题

线程池

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

线程池的工作流程

当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a)  如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c)  如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。
e  当一个线程完成任务时,它会从队列中取下一个任务来执行。
f.  当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小。

线程池的参数

  1. 指定核心线程数量
  2. 指定最大线程数量
  3. 允许线程空闲时间
  4. 时间对象单位
  5. 阻塞队列
  6. 线程工厂
  7. 任务拒绝策略

corePoolSize:核心线程大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使
有其他空闲线程可以处理任务也会创新线程,等到工作的线程数大于核心线程数时就不会在创建了。如
果调用了线程池的 prestartAllCoreThreads 方法,线程池会提前把核心线程都创造好,并启动
maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且以创建的线程数小于最大线
程数,则线程池会再创建新的线程执行任务。如果我们使用了无界队列,那么所有的任务会加入队列,
这个参数就没有什么效果了
keepAliveTime:线程池的工作线程空闲后,保持存活的时间。如果没有任务处理了,有些线程会空
闲,空闲的时间超过了这个值,会被回收掉。如果任务很多,并且每个任务的执行时间比较短,避免线
程重复创建和回收,可以调大这个时间,提高线程的利用率
unit:keepAliveTIme的时间单位,可以选择的单位有天、小时、分钟、毫秒、微妙、千分之一毫秒和
纳秒。类型是一个枚举 java.util.concurrent.TimeUnit ,这个枚举也经常使用,有兴趣的可以看一
下其源码
workQueue:工作队列,用于缓存待处理任务的阻塞队列,常见的有4种,本文后面有介绍
threadFactory:线程池中创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的
名字
handler:饱和策略,当线程池无法处理新来的任务了,那么需要提供一种策略处理提交的新任务,默
认有4种策略

 

线程池中常见5种工作队列
任务太多的时候,工作队列用于暂时缓存待处理的任务,jdk中常见的5种阻塞队列:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按照先进先出原则对元素进行排

LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按照先进先出排序元素,吞吐量通
常要高于ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool 使用了这个队列。
SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另外一个线程调用移除操
作,否则插入操作一直处理阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法
Executors.newCachedThreadPool 使用这个队列
PriorityBlockingQueue:优先级队列,进入队列的元素按照优先级会进行排序

拒绝任务策略:

  • 直接抛出异常
  • 使用调用者的线程来处理
  • 直接丢掉这个任务
  • 丢掉最老的任务

jdk默认的是直接抛出异常

线程池的状态

ThreadPoolExecutor提供了shutdown()和shutdownNow()两个方法来关闭线程池

  • 调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP。
  • shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。

合理地配置线程池


要想合理的配置线程池,需要先分析任务的特性,可以从以下几个角度分析:
任务的性质:CPU密集型任务、IO密集型任务和混合型任务
任务的优先级:高、中、低
任务的执行时间:长、中、短
任务的依赖性:是否依赖其他的系统资源,如数据库连接。
性质不同任务可以用不同规模的线程池分开处理。CPU密集型任务应该尽可能小的线程,如配置cpu数
量+1个线程的线程池
由于IO密集型任务并不是一直在执行任务,不能让cpu闲着,则应配置尽可能多
的线程,如:cup数量*2
。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密
集型任务,只要这2个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐
量。可以通过 Runtime.getRuntime().availableProcessors() 方法获取cpu数量。优先级不同任务
可以对线程池采用优先级队列来处理,让优先级高的先执行。
使用队列的时候建议使用有界队列,有界队列增加了系统的稳定性,如果采用无解队列,任务太多的时
候可能导致系统OOM,直接让系统宕机。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值