并发和并行
并发:多个事件在同一时间段内发生
并行:多个事件在同一时刻发生
这个Python那里有讲过,具体不再复述
进程
是指内存中的应用程序,我们WINDOWS常见任务管理器查看进程,原本的可执行程序是在硬盘中的,我们点击或者命令行运行程序,程序必须进入到内存中
线程
线程是进程的一个执行单元,一个进程至少一个线程(可以有多个),多线程可以理解为如360可以同时杀毒,清理垃圾,系统漏洞扫描
像AMD,Inter Corei7 4核8线程,4个cpu核,可以同时实现8个任务同时执行,我们点击360管家,就会将其进程刷入到内存中,当我们依次点击我们的任务,就会开启一条线程告诉cpu去执行我们的任务
其中一个cpu核有2个线程通路,每个线程通路会对多线程任务轮流切换,好像同时执行一样
线程调度
分时调度:所有任务分时使用线程
抢占调度:某任务占据线程(优先级高低),如果优先级相同会随机一个,Java使用的是抢占式调度
1windows任务管理器可以右键查看设置进程(线程)级别
单线程程序
程序从main方法开始从上到下依次执行
2我们定义Person类,定义构造getter,setter,然后定义run方法,循环20次,打印次数和名字
3当然程序是自顶向下的,老大跑完老二才能跑
从头到尾,main方法使内容进入栈,只有从上到下,就是一个线程,也叫主线程
单线程有缺陷,就是主线程会因异常而终止运行,
4如上,我们在老大老二之间添加了个人为异常,这时就会导致main程序终止,异常提示可以看到是main线程即主线程
Thread类
java.lang包下,不需要导包
5文档告诉我们创建新线程有2种方法,其中一种是创建继承Thread的子类,然后重新run方法,通过start启动
6如上,我们自定义线程类,重写run方法,当然父类super的run还是要保留,然后
7然后我们main里自身有主线程循环,同时子线程也开启,可以看到抢占式的效果,谁抢到资源执行谁,当jvm导入main方法时,jvm为main向os请求开启通向cpu的线程路径,当子线程start又会开启一条通往cpu路径,2者属于抢占cpu资源,而cpu则是不受我们控制,哪个执行是其自身控制
8我们需要通过内存图说明run和start的区别,我们调用main,把main压栈执行,创建线程实例,在堆内存开辟空间,这时调用其run方法,run进栈,但run是默认通过main执行的,所以其是单线程
9而我们使用start方法,是会开辟新的栈空间,在里面运行run方法,而对于cpu执行哪个栈空间就有了选择方法,所以多线程有好处是每个线程运行在不同的栈空间,一个挂了,可以运行其他的
Thread类常用方法
获取线程名称
获得当前的线程名称,有2种,一种是通过实例getName获得,另一种是其静态工具方法Thread.currentThread()获得,可以查看文档
10 11上面是通过getName获得
12 13如上,我们使用Thread.currentThread()也可以获取线程名称,这里其实是是默认用了toString方法,因为返回的是线程的引用
设置线程名称
void setName(String s)给线程设置名称
14设置后,获取线程名称就可以看到我们的设置
15 16还有一种设置是通过继承Thread类的带String字符串的构造方法,自带传入线程名称
sleep(long millis)休眠方法(静态)
使当前线程休眠特定毫秒,时间到后执行
17我们知道for循环间隔是很短的,我们使用sleep达到1s一数数,现在我们设置1000毫秒就是1s,这里java9要求抛出打断异常,而视频里没有,由于IDE提示只有照做
创建线程的另一种方法
之前讲了通过Thread的一种创建线程方法,使用复写run,这次使用Runnable接口
18 19Runnable接口,必须实现类定义run的无参方法,然后实现类对象传给Thread,创建线程对象,线程运行还是通过start方法
20 21如上
使用Runnable的实现类好处
实现类继承接口还可以继承其他,而Thread重新run就麻烦,这是黑马老师BB的,然而我觉得一样,都得写run,就是Runnable只要用Thread就好,但是里面传的是实现类对象,而后者是创建自己的线程类
匿名内部类实现创建线程
之前讲解过匿名内部类,只需要实例一次,而且此实例作为参数传递,可使用,这里我们就可以Thread类传入匿名内部类实例
22代码如上
线程安全
多线程访问共享数据是会有问题,如卖票,三个窗口都卖,开始都认为有满的票,但是第一个窗口被一下买了100张,后几个窗口应该就该提示买不到,否则出现了负数的票的问题
我们先对共享数据设置非负,如下,循环控制票数减少,开启3个线程运行如下
23 24查看运行结果。好像没什么问题,只是有的线程卖的快慢的问题
25可是我们加上休眠时间,问题就出现了,ticket正常是不会重复的,但是运行过程中就有重复的值
现在就需要解释下线程安全问题,当变成89时,另一个线程也是用ticket值,然而语句没运行完,没实现--,即2个89.其实全部打印数据还有很多问题
线程同步
为了防止出现上面的问题,需要进行线程同步,一共有3种同步方法,1同步代码块,2同步方法,3使用线程锁
同步代码块
格式 synchronized(锁对象){可能出现访问共享数据的代码}
锁对象可以保证把多线程锁住,同一时间只有一个线程访问共享数据,而且多个线程的锁对象必须都是一个,锁对象可以是任意对象(比如Object)
26如上,我们可以看到使用同步代码块,没有重复的票,而且也是1--100的值
其原理是使用同步锁对象,运行到同步代码块时,会自动检测代码块是否有锁对象,有则获取运行,没有得到锁的线程就会变为阻塞状态直到有锁可以获取,之前占用同步代码块的线程执行完同步代码块,就会把锁归还,几个线程再次进行竞争抢锁,在同步代码块内会降低一点效率,但是安全
同步方法
我们可以给访问共享变量的代码抽象成一个函数,然后使用synchronized修饰他,
格式 修饰符 synchronized 返回类型 函数名(参数列表){代码块}
27如上,虽然我们是想象中的同步代码,其实也是实现了同步,但是出现了0和-1这种情况,这是因为跳出循环是ticket不大于0,而一个线程到0后,其他线程还在循环,会继续执行--操作
28正确的方法是这种有while循环时,在同步函数里也定义条件,如上使用if
其实同步方法也是有锁对象,我们可以通过打印验证
29我们将之前的匿名类改成实现类,里面打印this,然后我们在实例时也打印实例
30可以看到几个地址值都相同,当然这没什么说的,本来就是几个实例引用传给构造,只不过视频说拿来做锁对象,这里记住就行了
31所以我们也可以把原来的同步函数写成普通函数加使用this做锁的同步代码块
静态同步方法
我们知道静态方法只能访问静态变量,而且方法一旦定义静态我们就不能使用实例了,他归于本类,如果我们static修饰了同步方法,那锁对象就不是this实例了,其是class本类文件对象
32Lock锁接口
Jdk1.5以后出现的,在java.util.concurrent.locks下,需要导包
我们之前使用同步代码块无法知道什么时候具体获得锁和释放锁
而其方法有获取锁lock()和释放锁unlock()可以看的比较明显
因为是接口我们需要使用实现类实现,文档可以看到实现类ReentrantLock互斥锁
我们可直接使用这个类实例,在可能出现共享变量访问之前使用获得锁,之后使用释放锁
33这里还是要判断ticket是否大于0再去--,因为while限制不住
34文档演示,要保证一定释放锁,否则造成死锁,要在finally使用unlock()
35线程状态概述
36我们查看文档,Thread.State属性返回线程状态
376种状态如上,我们新建的线程就是新建状态,当start后,就争抢cpu资源,抢到进入运行,没有的进入阻塞,当线程结束或stop终止或异常停止,就进入死亡状态,当线程sleep或者wait一定时间就进入到休眠状态,时间到了就查看cpu,有资源可用就运行,要么就进入阻塞,而等待状态,是使用Object对象的wait方法,想恢复到运行或者阻塞要使用Object.notify来唤醒,这里又把休眠和等待成为冻结状态
等待唤醒案例
等待分为计时等待,锁等待(也算是阻塞),还有wait无限等待,这里研究无限等待
我们研究一个案例,顾客买包子,然后老板做包子,5s后包子做好
38代码如上,这里需要使用ojbect的wait和notify,我们要给顾客和老板各开一个线程,而且他们以obj对象做1锁同步,顾客线程开始是靠obj.wait阻塞等待,然后老板线程的时间到,给obj对象唤醒,开始我想只开一个顾客线程,把老板放在主线程,且没有使用同步代码块,发现IDE提示模拟异常,还是老实的按视频操作吧
计时唤醒
sleep和wait可以加毫秒值,不同的是,wait可以在中间被notify,如果一直没有Notify,则时间到进入阻塞或者运行状态,notify如果多个线程wait则随机唤醒一个,想同时唤醒可以使用notifyAll
线程间通信--等待唤醒机制
多任务处理同一个资源,但是任务却不相同
39 40 41之前的吃包子可以理解为通过包子的有无,分别进行操作,如上
42如上,也是我们之前说了,必须要使用同一个锁对象来调用,而且必须使用同步代码块或者同步函数
我们继续改之前的案例,这里实现包子铺一个线程,顾客3个线程都吃包子,一个人吃3个,1s做一个,
43如上,我们定义包子线程,使用num同步,其是通过构造方法传递过来,为了2个对象都能使用同一个对象,将这个对象设为一个元素的num数组,
44如上,我们定义顾客线程,给顾客起好名字,同样传入num数组地址,然后包子数量大于3的时候就去抢包子,此部分被同步代码包裹,
45main方法,创建数组,初始化值为0,然后将其传给几个线程,运行如上
线程池
46我们使用线程实例创建,但是我们数量多时,而且每个线程很短时间就结束,频繁创建线程降低效率,因为线程创建销毁都要消耗时间,今天就来了解下线程池
47线程池就是存放线程对象的容器集合(队列),使用LinkedList或者ArrayList,删除返回被移除元素,然后这个对象去run,结束后,将其添加到集合末尾,先入先出,jdk1.5以后有内置线程池,我们直接使用即可
48如上,我们某个任务由5个子任务组成,我们线程池容量为3,前三个任务进来,被3个线程接走,而4,5任务只有等待其他3个线程运行完返回线程池,才能被接收执行
在java.util.cocurrent下。有Executors类,其一个方法newFixedThreadPool可以返回可复用线程池
49工厂类返回的是ExecutorService线程池接口对象,
50我们比较关注得到是shutdown方法和submit方法,分别用于关闭线程池,不接受新任务,和执行线程的start
线程池使用步骤:1使用Executors.newFixedThreadPool()生产一个指定容量的线程池
2创建runnable实现类,重新run方法
3调用1步返回的线程池的submit,传递实现类,执行
4调用shutdown关闭线程池,不再接收新的任务(不建议这步?。。视频里这么说)
51如上,我们使用线程池接口实现3个线程得到线程池,然后使用其执行5个任务,这时我们打印线程名字,可以看到,1线程执行了3次任务,2,3线程执行了1次
网友评论