进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
同一进程的线程共享本进程的地址空间和资源,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
而进程之间的地址空间和资源是相互独立的,程序之间的切换会有较大的开销
1、继承Thread类
1、继承Thread类并重写run方法
2、匿名内部类的方式
优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。
2、采用实现Runnable. Callable接口的方式创建多线程。
1、实现Runnable接口并重写run方法
2、Thread thread=new Thread(new MyRunnab1e());
thread.start();
线程类只是实现了Runnable接口或allabl接口,还可以继承其他类。
但是编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
Callable规定(重写)的方法是call(), Call方法可以返回结果或抛出检查异常
Runnable规定 (重写)的方法是run(),run方法不会返回结果或抛出检查异常,如果任务不需要返回结果或抛出异常推荐使用Runnable接口,这样代码看起来会更加简洁
callable线程之间本质上是没有关系的,但是Callable接口有返回值,比如说主线程创建了一个Callable,主线程需要等这个执行完返回值后,主线程才能执行,相当于有关联关系,就是Callable线程先执行完,返回给接收方的线程,接收方的线程才能执行
线程池中多线程并行任务会用到Callable
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
1)新建状态(New) :当线程对象对创建后,即进入了新建状态,如: Thread t= new MyThread);
2)就绪状态( Runnable) :当调用线程对象的start()方法(t.start)😉 ,线程即进入就绪状态。处于
就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此
线程立即就会执行;
3)运行状态( Running) :当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进
入到运行状态。注:就绪状态是进入到运行状态的唯一入口, 也就是说,线程要想进入运行状态执行,
首先必须处于就绪状态中;
4)阻塞状态(Blocked) :处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执
行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻
塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法, 使本线程进入到等待阻塞状态;
2.同步阻塞-线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状.
态;
3.其他阻塞-通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep(状
态超时. join0等待线程终止或者超时.或者I/0处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead) :线程执行完了或者因异常退出了run(方法,该线程结束生命周期。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXIHbMXm-1670228195248)(…/img/image-20220905172750312.png)]
1.等待阻塞( Object.wait -> 等待队列)
RUNNING状态的线程执行object. waitO)方法后,JVM会将线程放入等待序列(waitting queue) ;
2.同步阻塞(lock>锁池)
RUNNING状态的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM将该线程放入
锁池(lock pool)中;
3.其他阻塞(sleep/join)
RUNNING状态的线程执行Thread.sleep(long ms)或Thread. join(O)方法,或发出1/O请求时,
JVM会将该线程置为阻塞状态。当sleep() 状态超时,joinO) 等待线程终止或超时.或者I/0处理完
毕时,线程重新转入可运行状态( RUNNABLE ) ;
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
两个或多个线程为争夺同一资源而造成的互相等待的局面,没有外力推动无法解除
1.预防死锁–针对死锁的必要条件进行预防
破坏占有且等待条件,一次性申请进程所需要的所有资源。改进:允许进程只获得运行初期的资源,在运行过程中逐步释放使用完毕的资源,申请需要的资源。
破坏不可抢占条件:当一个进程申请需要的资源没有被满足时,释放掉所占有的所有资源。
破坏循环等待条件:给每个资源编号,当一个进程占有某个资源时,只能申请比这个编号大的资源
2.避免死锁–在分配资源之前判断是否会出现死锁
若一个进程的请求会导致死锁,则不启动。
若一个进程的增加资源会导致死锁,则不启动。
实现:银行家算法
需要记录可利用的资源向量,每个进程最大需求资源,已分配的资源,仍需要的资源。
当一个进程申请资源时,假设从可利用资源中分配给它申请的资源,看剩余的资源是否能满足某个进程执行完毕,若不能,则是不安全的,拒绝分配,若能,则假设可执行完毕的进程所占用的资源返还到可利用资源中,将其标记为可完成进程,继续判断其它进程,资源分配顺序则为安全序列。
1.正常结束
run()或者call方法执行完成后,线程正常结束;
2.异常结束
线程抛出一个未捕获的Exception 或Error, 导致线程异常结束;
3.调用stop(
直接调用线程的stop() 方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导致死锁;
start()表示进去就绪态,不表示立刻执行,
start()执行后会把线程的地址存在就绪队列中,同时start()方法会在栈中开辟一个新的空间,只要空间开辟出来,start()方法就结束了,新的线程启动成功,start()执行完毕后会在栈中创建一个新的线程栈
CPU执行一个线程时,如果另一个线程start了,这个线程就进入到了就绪态,等待上一个线程执行完,由于操作系统采用的时非公平的策略,CPU会从就绪队列随机选择一个线程执行(人为不可控),所以子线程的执行的顺序不确定,由于子线程是依赖主线程启动的,所以一般主线程比子线程先启动,但主线程不一定先执行完,
因为在分时操作系统中,线程在执行的过程中,时间片的时间到了,就会产生线程切换,就是在执行主线程的时候,有可能会切换到其中一个子线程执行,子线程执行的时候可能会切换到另一个子线程执行,从而导致主线程的执行完毕的顺序可能排在子线程后面
多线程一定比单线程快吗?
线程数远远大于核数,一个核需要快速切换执行多个线程,但是执行期间需要记录每个线程的执行状态,方便下次执行时好续上,这属于CPU的额外工作,会消耗CPU,消耗高达1ms的时间,也需要额外开辟空间,进行记录,这叫做线程的上下文切换,这种既消耗内存又浪费时间 切换的越快,消耗的总时间就越多
但是如果我们一个核对线程一个一个的执行,就是单线程执行,不需要上下文切换,浪费的时间就比多线程来回切换浪费的时间少,这样单线程比多线程快 ,就是当没有CPU浪费的时候,单线程要比多线程快,因为多线程一般需要浪费好多工作
●两者都可以暂停线程的执行。
●wait方法: 是Object的方法, 必须与synchronized关键字一 起使用,线程进入阻塞状态,当notify
或者notifall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠
时,会释放互斥锁。
调用对象wait之前-定是对这个对象上锁了。
1、使用wait()、notify( )和notifyAll( )时需要先对调用对象加synchronized锁。
2、调用wait( )方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
3、notify( )或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait( )返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到阻塞队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被
移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
●sleep方法:是Thread类的静态方法,当前线程将睡眠n亳秒,线程进入阻塞状态。当睡眠时间到
了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
●sleep方法会让出CPU但没有释放锁,而wait方法释放了锁。
●sleep通常被用于暂停执行Wait通常被用于线程间交互/通信
●sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏
醒。
●new一个Thread, 线程进入了新建状态;调用start()会执行线程的相应准备工作,然后自动执行run()方法的内容,(调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。)这是真正的多线程工作。.
●直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
优先级高,线程被选中的概率就高,优先级低,线程被选中的概率就低
yield():申请让出时间片,但是这个方法不会说马上就让出线程,所以一般优先级使用这个方法没什么用,所以一般用sleep(1)
sleep(1) 沉睡一毫秒 肯定可以让出时间片, 睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。
Sleep(0):
在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争,所谓的cpu调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配cpu时间。
Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.Sleep(0) 是你的线程暂时放弃cpu,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作.
在线程中,调用sleep(0)可以释放cpu时间,让线程马上重新回到就绪队列而非等待队列,sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系统切换其他线程来执行,提升效率。
.join()方法的作用
作用:让主线程等待(WAITING状态),一直等到其他线程不再活动为止。
底层原理是wait和notify
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些
发生的事件。在Java中垃圾回收线程就是特殊的守护线程。
进程和线程的区别
进程是系统进行资源分配和调度的一个独立单位,有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个 进程中的不同执行路径。线程是进程的基本执行单元,线程是操作系统的最小执行单元
进程有的通信方式线程都有,线程的通信方式进程不一定有
每个进程都有自己的用户空间,而操作系统内核空间是每个进程共享的。因此进程之间想要进行通信,就需要通过内核来实现
1、管道:是内核里面的一串缓存
管道是最简单,效率最差
管道传输的数据是单向的,若相互进行通信的话,需要进行创建两个管道才行的。而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
缺点:
2、消息队列:
管道的通信方式效率是低下的,不适合进程间频繁的交换数据。这个问题,消息队列的通信方式就可以解决。A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
而且,数据会被分为一个一个的数据单元,称为消息体,消息发送方和接收方约定好消息体的数据类型,不像管道是无格式的字节流类型,这样的好处是可以边发送边接收,而不需要等待完整的数据。
但是也有缺点,每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的,这是其中一个不足之处。
另一个缺点是消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
3、共享内存:
共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。
现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。
4、信号量机制:
用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
5、信号:
对于异常的情况下的工作模式,需要用信号的方式来进行通知进程。
信号是进程间通信机制中的唯一的异步通信机制,任何时候给某一进程发送信息,一旦信号产生,就会有这几种的方式:
1、执行默认的操作
2、扑捉信号
3、忽略信号
6、socket:
前面提到的管道,消息队列,共享内存,信号量和信号都是在同一台主机上进行进程间通信,如果想要跨网络和不同主机上的进程进行通信,则需要用到socket。
实际上,Socket不仅可以跨网络和不同主机进行进程间通信,还可以在同一主机进行进程间通信。
Socket是操作系统提供给程序员操作网络的接口,根据底层不同的实现方式,通信方式也不同。
1、等待通知机制,使用 Object 类的 wait()/notify()
两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯。
等待通知有着一个经典范式:
线程 A 作为消费者:
获取对象的锁。
进入 while(判断条件),并调用 wait() 方法。
当条件满足跳出循环执行具体处理逻辑。
线程 B 作为生产者:
获取对象锁。
更改与线程 A 共用的判断条件。
调用 notify() 方法。
3、volatile 共享内存
多个线程操作同一变量,就是只要能产生关系就叫产生了通信,volatile修饰的变量,保障了写后读,一个线程修改后,其他线程可见
同步:排队执行 异步:同时处理
4、管道通信
5、消息传递 就是方法调用
CPU利用率达到50%以上,会开启多线程,根据CPU的浪费比例,选择开启几个线程
对应着分时操作系统的知识
每个线程执行都有一个时间片
当一个线程的时间片用完后会去执行下一个线程
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现 这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
目前的linux的时间片大概是20ms一次, windows的不确定 几毫秒-几十毫秒
消息传递 就是方法调用**
CPU利用率达到50%以上,会开启多线程,根据CPU的浪费比例,选择开启几个线程
对应着分时操作系统的知识
每个线程执行都有一个时间片
当一个线程的时间片用完后会去执行下一个线程
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现 这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
目前的linux的时间片大概是20ms一次, windows的不确定 几毫秒-几十毫秒
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个 任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。但是线程切换会消耗CPU和存储,所以线程切换是有性能损耗的