Java多线程并发(下篇)


前言

本章是介绍Java线程的原理和实现方式。


十、线程基本方法

在这里插入图片描述
Object类的线程相关方法:

方法名描述
wait()进入等待状态,只有等到另外的线程通知或中断才会返回,会释放对象锁。
notify()唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
notifyAll()唤醒在此对象监视器上的等待的所有线程。

Thread类的线程相关方法:

方法名描述
sleep(long time)使线程休眠N毫秒,会导致线程进入超时等待状态,不会释放当前占有锁。
yield()使当前线程让出CPU执行片,与其他线程一起重新竞争CPU时间片。
interrupt()中断一个线程。其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程不会因此而改变状态。
join()等待其他线程终止,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待cpu。
isAlive()判断一个线程是否存活
activeCount()程序中活跃的线程数
currentThread得到当前线程
isDaemon()一个线程是否为守护线程
setDaemon()设置为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName()为线程设置一个名称。

十一、线程上下文切换

巧妙地利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载,这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能。

11.1. 进程

一个程序运行的实例。在Linux系统中,线程就是能并行运行并且与它们的父进程(创建它们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

11.2. 上下文

是指某一时间点CPU寄存器和程序计数器的内容。

11.3. 寄存器

是CPU内部数量少但是速度很快的内存。

11.4. 程序计数器

是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

11.5. PCB-“切换帧”

上下文切换可以认为是内核在CPU上对进程进行切换,上下文切换过程中的信息是保存在进程控制块(PCB)中的。PCB还被称为切换帧,信息会一直保存到CPU的内存中,直到它们被再次使用。

11.6. 上下文切换活动

  1. 挂起一个进程,将这个进程在CPU的状态存储于内存中的某处。
  2. 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
  3. 跳转到程序计数器所指向的位置,以恢复该进程在程序中。

11.7. 引起上下文切换原因

  1. 当前执行任务的时间片用完之后,系统CPU正常调动下一个任务;
  2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一个任务;
  3. 多个任务强制锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一个任务;
  4. 用户代码挂起当前任务,让出CPU时间;
  5. 硬件中断。

十二、同步锁与死锁

12.1. 同步锁

当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况,需要保证线程互斥,即同一个时间只能运行一个线程访问共享数据。

12.2. 死锁

当多个线程同时被阻塞,它们中的一个或全部都在等待对方某个资源被释放。

十三、线程池原理

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

13.1. 线程复用

通过继承重写Thread类,在其start方法中添加不断循环调用传递过来的Runnable对象。这就是线程池的实现原理。循环方法中不断获取Runnable使用Queue实现,在获取下一个Runnable之前可以是阻塞的。

13.2. 线程池的组成

一般的线程池主要分为以下4个组成部分:

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

ThreadPoolExecutor的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:指定了线程池中的线程数量。
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。
unit:keepAliveTime的单位。
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

13.3. 拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也满了,再也塞不下新任务了。这是就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略:

  1. AbortPolicy:直接抛出异常,阻止系统正常运行。
  2. CallRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是任务提交线程的性能极有可能会急剧下降。
  3. DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  4. DIscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
  5. 扩展策略:实现RejectedExecutionHandler接口。

13.4. 工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就是队列里面有任务,线程池也不会立即执行它们。
  2. 当调用execute()方法添加一个任务时,线程池会做如下判断:
    a)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    b)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    c)如果这时候队列满了,而且正在运行的线程数量小于maximunPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d)如果队列满了,而且正在运行的线程数量大于maximunPoolSize,那么线程池会调用拒绝策略。
  3. 当一个线程完成任务时,他会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间,线程池会判断当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。

十四、Java阻塞队列原理

阻塞队列,关键字是阻塞,在阻塞队列中,线程阻塞有这样的两种情况:

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  2. 当队列在填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

14.1. 阻塞队列的主要方法

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用

14.2. Java中的阻塞队列

ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的无界阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

十五、并发工具

15.1. CountDownLatch

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这个功能。

15.2. CyclicBarrier

回环珊栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放后以后,CyclicBarrier可以被重用。我们暂且将这个状态叫做barrier,当调用await()方法之后,线程就处于barrier了。

15.3. Semaphore

Semaphore翻译为信号量,Semaphore可以控制同时访问的线程个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。

十六、volatile关键字

Java语言提供了一直稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。volatile变量具备两种特性,vvolatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  1. 变量可见性
    其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

  2. 禁止重排序
    volatile禁止了指令重排。

  3. 比synchronized更轻量的同步锁
    在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。

十七、多线程共享变量

Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个,可见性和有序性。Java内存模型解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方式:

  1. 将数据抽象成一个类,并将数据的操作作为这个类的方法。
  2. Runnable对象作为一个类的内部类。

总结

以上介绍了线程的原理和实现。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值