简介
引用百度百科中关于多线程的定义:多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
通俗易懂的说就是让机器同时做几件事(单核心的设备),比如你打开了网易云音乐,边听音乐边翻着评论,看到有意思的评论给他点了个赞或者评论了几句再或者转发了一下并在微信里跟朋友讨论起这首歌。由于计算机运算速度很快,在我们看来就好像在同时做几件事一样。虽然多线程可以提升CPU的性能,但是多线程也不是十全十美的,它也有它的缺点。引用网上看到的一句话概括一下:
多线程在CPU密集型的作业下的确不能提高性能甚至更浪费时间,但是在IO密集型的作业下则可以提升性能(或者更准确点说叫平均响应时间)。
一个程序执行的时候,会产生一个进程。而一个进程由一个个的线程组成。虽然你在编程的时候没有使用到多线程技术,但是在程序运行时后台也会有多个线程,例如main主线程、gc线程等等。或者你使用了某个框架、容器它们可能就使用了多线程技术。多线程看起来离我们很远但却又无处不在。
核心概念
- 线程就是独立的执行路径。
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程。
main()
称之为主线程,为系统的入口,用于执行整个程序。- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不可干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
- 线程会带来额外的开销,如CPU调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
创建方式
小结
Thread类与Runnable接口的关系:代理角色与抽象角色。参考: 静态代理模式
继承Thread类:
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用(因为Java单继承的局限性)
实现Runnable接口:
- 实现Runnable接口具有多线程能力
- 启动线程:传入目标对象创建Thread对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
线程状态
线程状态有些书籍叫线程生命周期。
线程方法
停止线程
- 不推荐使用JDK提供的stop()、destroy()方法(已废弃)
- 推荐线程自己停下来(有停下来的条件)
- 建议使用一个标志位进行终止变量。例当
flag=false
时,终止线程运行。
停止线程代码演示:传送门。
线程休眠
Thread.sleep( 时间 )
指定当前线程阻塞的毫秒数- sleep存在异常
InterruptedException
- sleep时间到达后,线程进入就绪状态
- sleep可以模拟网络延时,倒计时等等
- 每一个对象都有一个锁,sleep不会释放锁
线程休眠代码演示:传送门。
线程礼让
Thread.Yield()
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功
下边的例子中,24行代码的Thread.Yield()
为线程礼让,让12、13行的两个线程在执行了run方法一半时暂停线程作为就绪状态,等待CPU的再次调度。(线程礼让是转为就绪状态,CPU再次调度时有可能还是调度到礼让的线程,所以说礼让不一定成功)
线程合并
Thread对象.join()
- join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以想象成插队(强制执行)
下边的例子中,25行代码的thread.join()
为线程合并,即在主线程(main)运行到25行时转为阻塞状态,强制执行18行的线程。(上边的线程礼让是转为就绪状态,CPU再次调度时有可能还是调用到先前礼让的线程,所以说礼让不一定成功,而线程合并可以想象成插队,强制执行。)
正是因为线程合并会导致其他线程阻塞,所以要谨慎使用。
观测线程状态
线程状态有:NEW、RUNNABLE、TIMED_WAITING、TERMINATED、BLOCKED、WAITING
。代码演示:传送门。
线程优先级
java提供一个线程调度器来监控程序中启动后(start)进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行(优先级高的先调度)。使用getPriority()
方法来获取优先级,使用setPriority(int x)
方法来改变优先级。
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用,得看CPU的调度。
- 线程的优先级用数字表示,范围从1~10
Thread.MIN_PRIORITY = 1;
Thread.NORM_PRIORITY = 5;
Thread.MAX_PRIORITY = 10;
注意:先设置优先级,再启动。
代码演示:传送门。
守护线程
- 线程分为用户线程和守护线程。
- 虚拟机必须确保用户线程执行完毕。(例如:main线程,咱们编写的线程)
- 虚拟机不用等待守护线程执行完毕。(例如:gc()线程、后台记录操作日志、监控内存等等)
- 设置某个线程为守护线程,只需要
setDaemon(true)
即可。
线程同步
线程同步就是一种等待机制,多个需要同时访问同一个对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。解决线程同步问题,可以使用:
- synchronized关键字
- Lock锁
代码演示:传送门。
线程通信
仅有synchronized是不够的,无法进行线程之间的通信。
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
线程通信的目的是为了更好的协作,线程无论是交替式执行,还是接力式执行,都需要进行通信告知。Java线程的通信方式可以分为:1、volatile。2、等待/通知机制。3、join方式。4、threadLocal。
等待/通知机制,使用到wait()、notify/notifyAll() 方法
,实现线程通信的方法有:
- 管程法
- 信号灯法
代码演示:传送门。
线程池
线程池,一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。代码演示:传送门
问题
使用多线程可能遇到的问题:
- 并发问题。由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了解决这个问题,需要实现线程同步。实现线程同步见上方。
- 死锁问题。多个线程各自占有一些共享资源,并且互相对待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。代码演示:传送门。