一.多线程的引入
如果实际应用场景多次进行进程的申请毫无疑问那将会是很大的开销,特别是在Java中每次创建进程都要创建一个JVM虚拟机,因此为减少此开销轻量级进程也就是线程应运而生。
多个线程通过共享一份给进程分配的资源从而优化了进程开辟销毁的开销,一般一个进程内一定有一个线程而资源分配就是在第一个线程创建时,而这样第二个第三个线程的创建就不再需要重复申请空间了,这些线程可能在同一份资源中各取所需也可能还需要额外申请资源也有一定的资源是公共的,但是并不是所有的线程都可以共享一份资源,这些可以共享一份资源的线程称为一个线程组。
在线程之前进程是资源调度和执行的单位,在线程之后进程是资源只管资源分配,线程是进行执行的基本单位。
同时多线程的引入还可以提高任务执行完成的效率
我们以完成100零件加工为例
如果是进程每次多一个人就需要多一个房间
而多线程就是相当于在一个房间中同时搞多个人,去完成同一个任务
但是线程越多越好吗
当一个房间中人非常多的时候,就会有很多人无法接触到零件
同时线程之间可能发生冲突出现安全问题,比如掀桌操作,大家都别干导致程序崩溃
有的线程可能与驱动有关从而导致系统崩溃
线程的状态:
1.NEW 线程实例已经创建但还未调用start方法还未在内核上进行线程执行
2.TIMED_WAITING sleep等有时间的阻塞
3.RUNNBLE 就绪状态正在cpu上运行
4.WAITING wait,sleep等产生死等状态
5.BLOCKED 由于锁竞争产生的线程阻塞
6.TERMINATED 线程执行完毕在内核中已经被销毁
进程与线程的区别:
一个进程一定有一个线程,多线程编程时进程是资源分配的最小单位,线程是任务执行的基本单位
进程之间相互独立执行,进程中的一个线程出现问提可能影响到其它线程相互干扰出现线程安全问题甚至出现掀桌问题终止整个程序的运行。
每个线程是独立的执行流,都各自拥有(记账信息,状态,上下文等属性)
进程中的线程共用一份资源(内存,文件符表)
二. 线程的创建
Thread不需要导入包即可使用是因为其在java.lang包中自动导入
1.继承Thread类,重写run方法
2.实现runnable类,重写run方法
3.继承Thread类,匿名内部类重写run方法
4.实现runnable类,匿名内部类重写run方法
5.lambda表达式
6.线程池
7.callable方法
后两个暂且不论
run方法与start方法的区别:
通过直接run方法还是在一个线程中并未创建多线程
而调用start方法则开启新线程同时在线程中自动执行run方法
三.线程睡眠
此处线程睡眠时间是睡眠的最短时间但是并不能保证睡眠结束后线程立即继续执行而是进入到可执行状态,线程睡眠是为了给其它线程足够的时间进行执行.同时sleep是静态方法属于Thread不需要对象调用.
四.中断线程
中断线程中三个方法都是属于Thread类的,不通过对象调用
1.interrupt()给线程发送终端中断消息,修改中断标记,但是线程是否中断由线程自己决定
2.interrupted()线程中断测验如果接受到中断标记为true,否则为false,并将中断标记重新置为false
3.isinterrupted()同为线程中断的测验,与interrupt的区别就是不会将中断标记重置,且为实例方法
验证interrupted与isinterrupted的区别
还可以手动更改标记终止上述线程
匿名内部类变量捕获问题:
变量捕获就是匿名内部类内部不可使用外部局部变量,这是因为局部变量在主线程中而匿名内部类是另一个方法在另一个线程中两个变量的生命周期不同,为保证两变量生命周期相同java中解决方法就是直接在内部类中重新创建一个对象拷贝外部的局部变量,为防止外部的值不停变化,java中直接认为final即不被更改的值才可在内部类中使用.
五.join阻塞等待
在哪个线程中使用就是让这个线程等待其它线程结束
如在主线程中使用就是让主线程等待其它线程结束
六.线程安全问题
线程安全问题的产生
1.根本原因是由于各线程之间随机调度抢占式执行即:
一个线程被调度下cpu给其它线程让位和一个线程上cpu上执行的先后顺序不同.
2.直接原因是cpu指令不具备原子性,指令执行过程中线程被随机调度到cpu执行
3.指令重排序 也是java内部的一种优化操作
下以买菜为例:
如果按清单购买需要多跑几趟,这是java内部的优化就相当于是将清单中 买菜的顺序进行改变,从而一趟就可以完成购买
4.内存可见性 JavaJvm内部有自动优化机制当检测到一个线程从内存中长时间读取同一个值不发生改变时,java内部机制会自动进行优化不再从内存中读取数据,因为从内存中读取数据开销较大,此时其它线程再对此数据进行更改时无法影响到此线程
5.代码的结构:多个线程同时修改一个变量,或一个读一个写
(但多个线程读取不修改,一个线程修改一个变量都是没问题的)
例如,当count进行多次相加时,由于相加操作cpu指令分三步(详情参见,写文章-CSDN创作中心cpu指令运行一节)导致多个线程同时修改一个数据时出现线程安全问题.
为解决此问题我们就需要引入加锁,确保++操作的原子性
但是获取到锁的线程也仍然会被调度下cpu加锁只是确保了此时不会被其它的线程插入
就像是你去图书馆占了个座位,即使你中途离开别人也不能抢占你的座位
七.加锁及死锁问题
推荐加锁方式synchronized
synchornized不需要人工维护加锁解锁操作,
synchornized(锁对象){
}
出代码块自动解锁避免忘记解锁引起问题
任何对象都可以作为锁对象
加锁后线程锁竞争可以解决线程安全问题但也会引出死锁的新问题
锁竞争就是对于同一个锁对象线程只有拿到锁才能进行执行(加锁的核心规则)
锁的特性:
1.互斥性 一个线程获取到锁后其它线程想要获取只能等待
2.不可抢占 其它线程需等占有锁的线程主动释放锁后才能抢占锁然后继续执行
3.循环等待
4.请求保持 一个线程再获取到A锁的同时尝试获取B锁
死锁问题:
死锁问题的产生也是由于上述锁的特性
死锁的三种情况:
1.一个线程一把锁
不可重入锁在此情况下就会死锁,在一把锁内再加上一把锁,synchornized为可重入锁
2.两个线程两把锁
各自持有一把锁同时尝试获取对方的锁
A,B炒菜,A拿到醋,B拿到酱油,A对B说你把酱油给我我用完都给你,B对A说你把醋给我我用都完给你,然后谁也不让谁最后都卡在这不干活,最后都得饿死.
3.n个线程m把锁
哲学家问题
首先我们可以回顾一下前面的锁的四种特性
大多数情况下是不会发生线程阻塞问题的:
因为哲学家吃饭吃完后就会放下筷子思考人生,其它人也就有机会进行获取筷子吃饭
但极端情况下大家同时拿起左手边的筷子但当尝试拿起右手边的筷子发现无法获取因为右手边筷子已经被拿走了这时大家都吃不到饭都就卡死在这了
但是我们可以根据锁的特性来解决此问题:
不可抢占与互斥性都是锁的基本特性无法更改
请求保持是锁的代码结构决定的需要以具体情况而定
最好破坏的就是循环等待这个特性只需要指定规则并对筷子进行编号:规定只能先拿小号的再拿大号的筷子,这样就能保证一定会有一个哲学家可以吃到饭,从而避免死锁问题