每天进步一点点,大家好,我是大龄码农。
上次讲解了,内容非常多,大家一定要把基础学扎实了。今天讲解线程同步与通信,我们先引入两个概念:并行与并发
并行指同一时刻处理器同时处理不同任务的能力,并发是指一个处理器在单位时间内同时处理多个任务的能力。举个吃包子的例子,并行是指两个人同时在吃包子,并发是指一个人同时在吃两个包子。
一般来说,目前普遍使用的还是具有多核心的单CPU,底层其实是CPU在分配时间片让每个进程都可以得到执行,所以也就出现了进程和线程的切换问题
我们知道,线程是计算机执行任务的最小单元,那么是不是线程越多越好呢?不一定,这个得根据具体的使用场景来分析,一般来说,计算机的任务主要有两类,一种主要用于计算,被称为CPU密集型,另一种主要用于读写,被称为I/O密集型。
CPU密集型
CPU有大量运算要处理,较高的CPU占用率,几乎无I/O阻塞,通常线程数只需要设置为CPU核心数的线程个数就可以
注意:单核是没有意义的
I/O密集型
CPU在等待I/O (硬盘/内存) 的读写操作,即使在达到性能极限时,CPU占用率仍然较低。通常会设置CPU核心数几倍的线程
最后,具体创建多少线程,还是需要根据实际的业务情况进行不断地优化
说完了线程数量的设置,我们再引入两个概念:同步和异步。当一个进程在执行某个请求且该请求需要一段时间才能返回信息时:
线程同步
我们知道,线程是共享进程的资源,而且是无序运行的,当并发时,很有可能出现资源争用甚至死锁等问题,为了避免这些问题,就需要对线程进行同步,保证线程能够按照一定的顺序来访问共享资源。常用的线程同步机制有很多种实现方式,它们底层全都是基于锁来实现的。
Java提供了关键字synchronized实现线程同步机制,保证线程执行的
有序性。synchronized的知识点非常重要,我们后面会做一篇详细的分析。
线程通信
通信的目的是为了更好的协作,一般来说,线程通信主要有两种实现方式,一种是基于共享内存,另一种是基于消息传递。
1、共享内存
线程可以共享进程的资源,但是每个进程都是独立运行的,它们之间不能互相干扰,所以每个线程都有自己的程序栈。每次在处理时,先把进程内存的数据加载到栈,处理完成后如果需要,再把数据写回内存。那么,这就会存在一个并发问题,即同时操作一个共享数据时,如何保证线程可以知道这个共享数据的最新值
Java提供了关键字volatile,它有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就可以让线程之间通信。
2、消息传递
Object类提供了线程间通信的方法:wait()、notify()、notifyaAl()
wait() 使当前执行该代码的线程进入该对象锁的阻塞队列中进行等待,当被唤醒时,会接着往下执行
notify() 随机通知一个当前对象锁的阻塞队列中的线程
notifyAll() 唤醒当前对象锁阻塞队列中的所有线程
有一点要说明一下,如果当前的线程不是该对象锁的所有者,却调用该对象的notify()、notifyAll()、wait()方法时,会抛出java.lang.IllegalMonitorStateException异常
注:上面全部只是列出了一种具体的实现方式而已
线程死锁
简单来说,就是自己拥有别人要的东西,别人拥有自己要的东西,互不相让而造成一直在互相等待的状态就被称为死锁。这里的“东西”其实就是锁。
一般来说,造成死锁的主要原因是线程的调度顺序不对。死锁产生的四个必要条件:
线程已经拥有了一个资源,当等待获取新的资源时,保持原拥有的资源不释放
当资源被一个线程使用且没有被主动释放时,该资源不能被抢占
多个线程之间存在资源循环等待链
一般在处理死锁时,会使用破坏死锁产生的四个必要条件之一的方式,但是在编写程序时,也要注意逻辑顺序,合理地分配资源。一旦程序出现死锁,可以考虑强行剥夺资源。比如说,线程只有把需要的资源全部申请到位才能开始运行、线程在获取新的资源前先释放已有资源、给资源设计一个有序的分配方案等。
实践
一、抢票
说明:
1、抢票是生活中再熟悉不过的例子了,这里模拟3个人抢2张票的情况
2、结果应该是有1个人抢不到票,但是这个结果是都抢到票了,说明线程在操作共享资源时出现了线程同步的问题,可以使用关键字synchronized来解决
3、如果不使用关键字synchronized,而是修改sleep()方法的位置,会出现什么情况呢?
线程是并发乱序执行的,但是没有出现线程同步问题,这是怎么回事?说白了,就是程序逻辑简单,没有耗时操作而已。所以,第一行的sleep()方法去掉也是同样的结果,仅限于此。在实际的项目开发中,业务处理总是要耗费一段时间的,那时如果不通过线程同步机制控制,就可能出现一些奇怪的问题。
二、助力自增
说明:
1、测试结果每次运行几乎都不一样,程序的原意是打算使用两个线程分别对sum自增10000次,最后的结果应该是20000,可是实际的运行结果却总是小于20000
2、根本原因在于sum++操作不是一个原子操作,我们看一下编译文件
实际上这个运算被分为了三步:获取值、做加法、设置值。所以在并发过程中出现了问题。使用synchronized就能解决
3、每台机器的性能不一样,性能快的机器,可以把循环的次数弄大一些,就能看到效果了。
三、经典的生产者消费者问题
举个简单的例子,我们作为消费者去包子铺买包子,如果发现还有包子那就交钱走人,如果没有包子那就得等待包子蒸熟,而商家作为生产者,如果发现包子卖完了那就得包包子然后蒸熟,如果发现还有包子那就等待消费者来买。
包子类
卖家生产者
买家消费者
说明:
1、包子作为生产者和消费者的共享资源,如果不对包子进行锁定的话,根据定义将抛出异常
2、最终达到的效果就是生产者生产包子和消费者消费包子交替运行
3、如果有多个生产者和多个消费者时,只有生产者内部和消费者内部出现了竞争机制,即谁来生产和谁来消费,整体的交替运行没有变化
4、在判断等待时,此处使用了while循环处理,这是为什么呢?
这是JavaDoc上的说明,因为可能会产生虚假唤醒。虚假唤醒是一种现象,只会发生在多线程环境中。简单来说,当所有线程被唤醒后就去竞争锁,由于同一时刻只会有一个线程能拿到锁,其他的线程就会阻塞,当成功争抢到锁的线程释放锁后,其他的线程才能继续运行,但是此时对于拿到锁时的那个条件很可能已经不满足了,这个时候线程应该继续阻塞下去,而不应该继续执行,如果继续执行了,就是发生了虚假唤醒。
四、死锁
想象这么一个情景:两个人面前只有1双筷子,每个人一次只能拿一支筷子,只有拥有一双筷子的人才有资格吃饭
说明:
1、模拟程序死锁,只能通过结束程序处理
2、如果要使程序正常运行,可以让一个人先获取1双筷子吃饭,然后另一个人再吃,即把需要的资源全部申请到位再运行程序。
总结:
1、介绍了线程同步和通信的基本知识
2、介绍了线程在并发中可能存在的问题
3、用程序说明了今天学习的知识
希望大家一定要动手做一做,自己验证一下。需要代码的朋友,可以留下邮箱。
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: lzxmw777