Java多线程下的协同控制,这些你都知道了吗?

Wesley13
• 阅读 355

协同控制是并发程序必不可少的重要手段。主要分为两大控制方法,一个是JDK提供的最基础的协同控制方法,一个是java.util.concurrent包下的拓展类控制,接下来我们将会介绍这两种方法有哪些操作可以进行同步控制。

一、基础的协同控制

线程基础知识

因为加锁涉及到多线程,所以有必要先说一下线程的基础知识(定义那些就不必多说了吧~~)。

    首先线程是有生命周期的,在Java中它有6个状态来表示,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

  • 新建(NEW):创建后尚未启动的线程的状态

  • 运行(RUNNABLE):正在运行或在准备的线程,包含Running和Ready

  • 无限期等待(WAITING):不会被分配CPU执行时间,需要显式被唤醒

  • 限期等待(TIMED_WAITING):在一定时间后会由系统自动唤醒

  • 阻塞(BLOCKED):等待获取排它锁

  • 结束(TERMINATED):已终止线程的状态,线程已经结束执行,并且不可再次唤醒。

Java多线程下的协同控制,这些你都知道了吗?

    线程的启动是由start()方法启动的,至于结束stop()方法可以关闭,但是它是强制性关闭,也就是说你不管你线程的任务有没有执行完都立马停止,不推荐这种方法,取而代之的是interrupt()方法,它的原理就是多加了一个中断标志位,在线程执行中不断去判断是否中断,当中断设置为true时,当前任务执行完之后就结束线程。

synchronized

    如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized关键字就可以实现线程间的同步。它可以保证线程的原子性、可见性、有序性。

synchronized的用法大致有三种:

  • 对象加锁:对指定对象加锁,进入同步代码钱要获得给定对象的锁。

  • 实例方法加锁:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

  • 静态方法加锁:相当于对当前类加锁,进入同步代码前要获得当前的锁。

其中对实例方法加锁时容易出现错误的使用,比如下面的伪代码:

// 假设SyncClass中的非静态increase方法加了同步锁

因为在运行的时候因为不是同一个实例,每new一个对象就是一个新的实例,锁对方法的同步并未作用到同一个实例,所以加锁无效。有效的加锁方法如下:

// 假设SyncClass中的非静态increase方法加了同步锁

等待与通知

    为了保证多个线程之间的协作,JDK提供了两个重要方法可以修改线程的状态:wait()和notify()。这两个类是Object类的方法,意味着任何对象都可以调用,但这两个方法必须在同步块中调用。

    在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,而转为等待状态,进入等待池(或等待队列)。

    等待何时结束呢?要么在wait方法调用时传入等待时间,那么它就会进入TIMED_WAITING状态,然后经过等待时间后自动唤醒,如果没有传入等待时间就会进入WAITING状态,只有调用notify()或notifyAll()方法时才会被唤醒(两个方法区别后面介绍)。

    如果线程所操作的资源被synchronized加了锁,并且此时锁被其他线程占用,那么该线程就会转为BLOCKED状态,进入锁池(或阻塞队列)。只有占用锁的这个线程释放了锁并且当前线程抢占到了这个锁,才会转为RUNNABLE状态继续运行任务。

notify()和notifyAll()的区别是什么?

    notify()方法会从等待池(或等待队列)中随机选择一个线程进行唤醒,当然最恶劣的情况发生就是某一个线程运气很不好,每次都没有被选中,这样就容易造成线程饥饿,当然这种情况发生的可能性还是很小的。而notifyAll()方法会将等待池(等待队列)中所有的线程全部唤醒,进而抢占资源。

下面我简单画了一个示意图帮助大家理解

Java多线程下的协同控制,这些你都知道了吗?

等待线程结束

    在一些情况下,线程之间的协作可能会存在依赖关系,比如线程B需要线程A的输出才能继续执行,那么就必须等待线程A运行完,此时就可以使用join来实现。

public final void join() throws InterruptedException 

join方法调用时可以传入等待时间参数,如果不传入任何参数表示无限等待,它会一直阻塞当前线程,知道目标线程执行完毕。如果传入等待时间,则在超过这个时间之后就停止等待,继续往下执行。

Thread threadA = new Thread();

线程谦让

    在实际应用中有些任务是重要任务,有些任务的重要程度可能低一点,那么在程序运行中有可能存在优先执行重要任务而延后执行次要任务的需求。那么要实现这样的操作就要对线程优先级进行操作了,大家都知道线程是有优先级的,优先级高的就有可能优先执行,为什么是可能而不是一定呢?因为现在计算机的运行速度非常快,而且大多数都是多核心的,就算优先级低的线程也可能优先被CPU执行。

    如果在运行中想手动让某个线程让出CPU让其他线程优先执行的话,就需要使用yield()方法了。该方法会让出CPU但是不会让出锁,但也不一定调用之后就会让出CPU,因为它只是给一个暗示,告诉其他线程我可以晚点执行,你们先执行吧!但是CPU可能会忽视这个暗示,在JDK中的注释有所说明:

/**

小小总结

对一些常用的线程协同控制方法进行小小总结:

  • wait():Object类的方法,必须在synchronized同步块中调用,会让出CPU,并让出锁,不传入时间则无限等待,该对象进入等待池中,除非被notify唤醒。

  • sleep():Thread类的方法,可以在任意处调用,目的在让线程休眠,会让出CPU,但不会让出锁。

  • notify():Object类的方法,必须在synchronized同步块中调用,从等待池中随机唤醒一个线程进入锁池去竞争锁。

  • notifyAll():将等待池中所有线程唤醒,全部进入锁池竞争锁。

  • yield():暗示让出CPU的使用权,但是调度器可能会无视该暗示,并不会让出锁。

  • stop():强制停止一个线程(不推荐使用)。

  • interrupt():通知线程应该被中断。如果线程处于阻塞状态就会抛出InterruptException异常;如果线程正常运行就会将中断标志设置为true,线程运行完当前任务之后结束,保证原子性。

二、JUC包提供的协同控制拓展

    java.util.concurrent包提供了很多并发控制工具,它们几乎可以完全替代前面介绍的基础控制方法,而且可以做到更细粒度化,甚至有些操作上面的方法不能做到,而JUC包下的类可以。由于篇幅的原因这里仅仅介绍一些类方法的作用,实际使用及原理不包含在本文中。

可重入锁ReentrantLock

    可重入锁可以完全替代synchronized关键字。在JDK5.0的版本重入锁的性能远远超出于synchronized,但从jdk6.0开始,JDK在synchronized关键字做了大量优化(具体优化会在后面我的JVM系列文章介绍,欢迎关注哦~),使得两者性能差距不大。

    ReentrantLock实现了Lock接口,lock()方法进行加锁,unlock()方法释放锁,一次原子操作完之后必须调用unlock()。

中断响应

    对于synchronized来说,如果一个线程在等待锁,那么结果只有要么一直等要么获得锁继续执行。其中不能手动中断,而可重入锁赋予了这个能力,程序可以根据需求取消对锁的请求,某些时候这样做也可以很好地避免用锁时最不希望发生的事情——死锁。

    对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行相应的锁申请动作,即在等待锁的过程中,可以响应中断。

void lockInterruptibly() throws InterruptedException;

锁申请等待限时

    除了中断响应可以避免死锁外还可以使用可重入锁的tryLock()方法,这是一个对申请锁时间进行限制的方法,在限定时间内会自旋式地重复申请锁,直到申请成功返回true,当超出限定时间还未获取到锁就会返回false。当然如果不传入限制时间则只会尝试申请一次,如果这一次未申请成功就返回false。

boolean tryLock();

实现公平锁

    synchronized锁抢占是随机的,哪个线程抢占到锁就具有锁的使用权,而这个抢占过程是不公平的,也就是说有可能造成某个线程一直抢不过其他线程,而一直处于阻塞状态,那么这个过程就是不公平的获取锁。为了不让这种现象发生可以使用公平锁让每个线程都有机会获得锁,而且是公平的。

    在ReentrantLock实例化时可传入一个boolean类型的参数fair,它就代表是否实现公平锁,传入true就表示公平锁,false代表非公平锁。当然如果什么都不传就是非公平锁。

public ReentrantLock() {

    ReentrantLock中还提供了其他方法可以获取锁的状态以及等待线程队列等,这里就不一一介绍了,总而言之ReentrantLock是把锁对象化,能更细粒度地操作锁。

Condition

    一般和ReentrantLock一起使用的还有Condition,它是一个接口,它的作用和wait()、notify()方法的作用是大致相同的。区别就是wait和notify是配合synchronized使用的,Condition是配合Lock接口使用的。

其方法含义如下:

  • await():使当前线程等待,同时释放当前锁,当其他线程调用signal()或者signalAll()时,线程会重新获得锁继续执行。或者当线程被中断时,也能跳出等待。

  • awaitUninterruptibly():该方法与await()方法基本相同,但是它不会在等待过程中响应中断。

  • signal():唤醒一个在等待的线程。在调用前要先获得锁

  • signalAll():唤醒所有在等待的线程。在调用前要先获得锁

    其实在ArrayBlockingQueue源码里面就是使用的Condition来控制队列满和空时的操作限制的。

Semaphore

    信号量Semaphore为多线程协作提供了更多强大的控制方法。它是锁的扩展,无论是synchronized还是ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问一个资源

public Semaphore(int permits) {

在构造信号量对象时,必须指定准入数(同时最多允许多少个线程访问资源),同时可以指定是否公平。其主要方法如下:

// 尝试获得一个准入的许可。若无法获得线程就会等待,直到有线程释放一个许可或当前线程被中断

CountDownLatch

    倒计时器CountDownLatch是一个非常实用的多线程控制工具类。它通常用来控制线程等待,可以让某一个线程等待直到倒计时结束,再开始执行。

public CountDownLatch(int count)

在实例化对象时可传入计时线程数,当一个线程完成任务时可用countDown()方法申明该项任务已完成,用await等待全部任务完成之后才会继续执行,当然该方法也可以限定等待时间。

public void countDown()

CyclicBarrier

    这是一个比CountDownLatch功能更强大更复杂的工具类,称之为循环栅栏。栅栏是一种障碍物,用来组织线程继续执行,要求线程在栅栏处等待。循环一词便表示了它可以反复地进行倒计时。比如,假设我们将倒计时数设为10,那么10个线程任务都完成之后,计数器就会清零,然后再计算下一批10个任务。

ReadWriteLock

    ReadWriteLock看名字就知道是一种读写锁,其实它和数据库中的锁实现非常类似,将对数据操作的锁分为两类锁:读锁和写锁。这两类锁之间遵循一定的规则:

  • 读-读不互斥

  • 读-写互斥

  • 写-写互斥

了解数据库中锁的同学就很容易理解了,这里因为篇幅原因就不过多地介绍,简单的把获取锁的伪代码放出来吧:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

OK,对于线程协同控制基本上就介绍这些了,如果能熟练地运用上面的这些方法,相信你对多线程的理解又会进入更深的层次。

欢迎关注我的微信公众号“北风IT之路”,一起分享有趣的编程知识!

Java多线程下的协同控制,这些你都知道了吗?

如果你喜欢这篇文章,关注,在看,转发

(づ ̄3 ̄)づ╭❤~

本文分享自微信公众号 - 北风IT之路(beifengtz)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Noark入门之协议映射
0x00消息控制器消息控制器,主要作用就是为每个模块提供消息处理的入口.这里的消息不仅仅是协议,还有内部指令,事件等等逻辑入口,这也是为了响应线程模型作出的一种支撑,只要入口在此消息控制器内,那必然走期望的线程调度。@Controller用于标识一个类为当前模块的消息控制器入口.@Controller(threadGroup
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这