最近一直在研究JAVA多线程的问题,但是由于刚入职工作忙,一直没时间做一个总结,今天终于抽出空来总结一下这段时间的成果,废话不多说,直接开始
一、背景
在过去单 CPU 时代,单个CPU在一段运行时间内只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个 CPU,并由操作系统来完成多个任务间的来回切换,以使得每个任务都有机会获得一定的时间片(极短的时间片)运行。
表面上看起来,因为不同任务之间的来回切换有一定的性能损耗,似乎顺序执行程序比多线程并行执行程序更高效。但在实际的编程中,由于阻塞的存在(通常是I/O),使得一切变得不同了,一旦发生阻塞,顺序执行的程序就会停顿下来,整个CPU处于无事可做的状态,极度浪费资源。并发因此出现
二、多进程和多线程
我们常常听说过这两个词——进程、线程,那么到底什么是进程什么又是线程呢?
进程就是一个程序在计算机上的一次执行过程。一个程序,当得到CPU的时候,相关的资源必须也已经就位,比如打印机,显示屏幕,GPS,用户输入等,必须立刻能够执行,它才有资格得到CPU的一次使用机会,当它得到CPU的时候,必须加载它上一次获得CPU执行后所保存的环境(如函数、变量等),然后执行,分配给他的CPU执行时间用完了后,保存本次执行的环境(上下文),切换出去,等待着下一次使用CPU,因此:
> 一次CPU执行进程的时间= CPU加载上下文+CPU执行+CPU保存上下文
进程的颗粒度太大,每次都要有上下文的调入,加载,执行,保存,调出,它是可以再分割的。如果把打开QQ比作进程,那么传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。如果把打开迅雷比作进程,那么每打开一个下载任务就是执行一个线程。因此:
> 多个线程组成了进程
当然,线程也是有上下文切换的,并且我们在研究多线程的过程中,将多次听到 “线程上下文切换” 一词
三、并发和并行
实现并发最直接的方式是使用操作系统级别的进程,进程和进程之间很少互相干涉,即使他们使用的是同一台机器的内存,但他们并不共享内存(进程A结束会保存自己的上下文,进程B使用的也是自己的上下文)
JAVA使用多线程的方式实现并发,这就意味着线程之间是共享内存和IO的,也就是说,如果你在线程A中将变量var修改了值,它会影响到B线程的执行;线程A要读取文件F的同时,线程B在对文件F进行写入(这在进程实现的并发中不可能出现),线程B势必影响线程A的读取,这无疑加大了编程的难度,但它更加灵活、强大
并行则是指多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时,并不是通过上下文切换轮流使用cpu而模拟出的同时
四、线程安全与同步
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。(Vector,HashTable等都是线程安全型集合)
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据导致所得到的数据是脏数据。(ArrayList,LinkedList,HashMap等都是线程不安全型集合)
线程同步是为了防止多个线程同时访问同一个数据对象时,对数据造成破坏,通过人为的控制和调度,保证共享资源的多线程访问的线程安全。通俗的说就是让线程们”排队”使用数据,不要蜂拥而上
五、线程的优先级
在多线程开发中,不同的线程对于程序的重要性不同,我们根据其重要性对线程进行基于权重的划分,这就是线程的优先级。Java的每一个线程都有自己的优先级,其范围是1~10,默认为5。高优先级的线程会优先于低优先级的线程执行,但这并不能保证线程的执行顺序,也就是说,高优先级相较于低优先级更容易获得cpu资源,但也有可能出现低优先级比高优先级线程先执行的情况,因此优先级机制并不能保证高优先级线程一定比低优先级先执行。
可以通过调用线程的setPriority()方法来设置线程优先级,设置线程优先级应当尽量放在线程开始前
六、守护线程
Java的线程分为两种:
- Daemon Thread(守护线程):Daemon守护线程的作用是为其他线程提供便利服务,只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。比如JVM的GC垃圾回收线程就是一条典型的守护线程
- User Thread(用户线程):User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
可以通过 setDaemon()方法设置守护线程
不要尝试使用守护线程来完成资源回收的工作,因为所有的User Thread结束时,守护线程不一定会执行到回收资源的代码处
七、死锁
线程的安全性和活跃性之间存在某种平衡,我们通过加锁保证线程安全,但加锁过多或加锁不当会导致线程活跃性故障,如:死锁
关于死锁有一个很经典的哲学家进餐问题:
五位哲学家去吃午餐,坐在一张圆桌旁,桌上有五根筷子,每两人之间摆一根,他们只能拿自己左右手边的筷子,每位哲学家要么思考要么进餐,当某位哲学家进餐时,他必须拿到两双筷子,每一位哲学家在未拿到筷子前,不会放弃手中的筷子,当哲学家进餐完毕后,他必须将手中的筷子放回原处
假设出现这么一种情况,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反),我们就称其为死锁
死锁:两个或两个以上的进程在执行过程中,互相持有对方所需要的资源,而又相互等对方释放锁,而造成的一种阻塞的现象
八、活锁、饥饿、无锁
活锁:和死锁相反,活锁是指两个或两个以上的进程在执行过程中都拿到了对方所需的资源,但又互相谦让,然后再次获得对方所需资源,再次谦让……线程始终无法正常执行的现象
死锁会导致发生死锁的线程处于阻塞状态,互相等待对方释放锁,而活锁虽然不会阻塞相关线程,却一直处于不断尝试加锁的状态,实际上是白白浪费CPU资源
饥饿:因为某种原因,一条线程始终无法无法获取CPU执行,这种原因可能是优先级高的线程一直抢占优先级低的线程资源导致的,也可能是某个线程始终占用其他线程资源不放导致的
无锁:即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试(自旋)。如CAS就是一种无锁技术