多线程
(进程和线程的区别)
根本区别: 进程是操作系统资源分配的基本单位,线程是CPU调度和执行的基本单位
环境区别: 在操作系统中有多个进程同时运行,在同一个进程中又有多个线程同时执行.在通过CPU的调度
下,在每个时间片中只有一个线程执行
开销区别: 因为每个进程都有独立的代码和数据空间,所以进程之间的切换开销比较大.
而线程可以看做一个轻量级的进程,在同一类的线程中,他们的代码和数据空间是共享的,所以线
程之间的切换开销比较小.
内存分配区别: 系统在运行的时候会给每个进程分配不同的内存空间,而对于线程来说,除了CPU外,系统不
会给线程分配内存,线程之间只能共享资源.
包含关系: 线程是进程的一部分.
没有线程的进程可以看做是单线程的
如果一个进程有多个线程,则执行的过程不是一条线,而是多条线程共同完成的.
1.程序和进程
1.程序和进程的概念
程序: 就是硬盘上存储的、静止的代码。
**进程:**程序的一次执行产生进程。进程是动态向前的。
2.多任务操作系统的工作原理
假定 A,B,C 三个程序开始运行,A、B、C 产生进程 Pa、Pb、Pc。Pa、Pb、Pc 排队轮流使用 CPU。
假设 Pa 先抢占到 CPU,Pa 执行,执行过程中 Pa 需要等待数据输入(I/O 操作、请求网络资源),
此时 CPU 空转,为了提高 CPU 利用率,Pa 被切换出去,Pa 保存当前执行状态,Pa 挂起。Pb 抢占 CPU,Pb 开始执行,Pb 如果没有数据输入,Pb 也可能被切换出去(CPU 时间片到了),Pb 挂起;Pc 抢占到 CPU,开始执行,如果 Pc 有数据输入,Pc 保存当前状态并挂起。此时 3 个进程都挂起,CPU 空闲。CPU 挑选一个进程运行,根据 CPU 执行原则,选中 Pb,Pb 继续执行。
CPU 通过时间片实现多任务,这样的操作系统称为多任务操作系统,但同一时刻还是只有一个进程执行。
3.并发和并行
**并发:**在一段时间内多个进程轮流使用同一个CPU,多个进程形成并发。
**并行:**在同一时刻多个进程使用各自的CPU
2.线程
1.线程工作原理
线程的出现时为了解决实时性问题.
线程是进程的细分.
进程被划分为多个可以独立运行的线程,多个线程配合完成一个进程的任务.
总结:
[1] 线程再次提高了CPU的利用率;
[2] 线程共享进程资源,线程包含在进程中;
[3] 线程细分后称为CPU调度的基本单位.进程称为操作系统资源分配的基本单位.
3.实现线程的方式
1.继承Thread类
// 继承Thread类必须重写run方法
public class AThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5;i++){
System.out.println("AThread:" + i);
}
}
}
// 测试类
public class Test01 {
public static void main(String[] args) {
// 创建一个线程A
AThread ta = new AThread();
// 必须通过start开启线程,不能通过run,通过run就不是一个线程了.
ta.start();
// 在主线程中执行一段代码
for (int i = 0; i < 5;i++){
System.out.println("main AThread:" + i);
}
// 可能存在一个gc线程
}
}
// 因为多个线程轮流使用CPU,先占用先使用,所以执行结果不确定
2.实现Runnable接口
/**
* MyRun不是线程,但具备在线程中执行的能力
*/
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5;i++){
System.out.println("main AThread:" + i);
}
}
}
// 测试类:
public class Test01 {
public static void main(String[] args) {
// 1> 创建MyRun对象
MyRun myRun = new MyRun();
// 2> 创建线程,把myRun 放入线程中执行
Thread t = new Thread(myRun);
t.start();
// main thread
for (int i = 0; i < 5;i++){
System.out.println("myRun:" + i);
}
}
}
3.两种实现方式的区别
自定义线程的方式有2种,分别是继承Thread方式和实现Runnable接口方式
他们的区别:
[1]继承Thread类后,不能继承其他类,而实现Runnable接口还可以继承其他的类;
[2]实现Runnable接口更方便共享资源.同一份资源,多个线程并发访问.
如果多个线程要访问共享资源,优先考虑Runnable方式;如果线程不访问共享资源,可以考虑用继承Thread.
多线程访问共享资源的同时,存在一个严重的问题,导致访问共享资源数据错误问题。
4.多线程的执行轨迹
窗口A卖出一张票,还剩3张
窗口C卖出一张票,还剩2张
窗口B卖出一张票,还剩3张
窗口C卖出一张票,还剩0张
窗口A卖出一张票,还剩1张
// 假设窗口B抢占到CPU,执行
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;///
System.out.println(...getName() + "买出一张票,还剩" + count + "张");
}
}
// i = 0;i < 5;count > 0条件成立,count-- => count=4;
cpu时间片到,B挂起
// A抢占到CPU
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");///
}
}
// i =0;i < 5;count > 0条件成立,count-- => count =3,输出
窗口 A 买出一张票,还剩 3 张
cpu时间片到,A挂起
// 窗口 B 抢占到 CPU,B 从挂起位置开始执行
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(//...getName() + "买出一张票,还剩" + count + "张");
}
}
// 准备语句:窗口B卖出一张票,还剩3张,还没来得及输出;
cpu时间片到,B挂起.
// C抢占到CPU
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");///
}
}
// i =0;i < 5;count > 0条件成立,count-- => count =2,输出
窗口 C 买出一张票,还剩 3 张
cpu时间片到,C挂起
// 窗口 B 抢占到 CPU,B 从挂起位置开始执行
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");///
}
}
// 执行准备好的输出语句"窗口B卖出一张票,还剩3张",继续执行,执行到if(count > 0)挂起
// A 抢占到 CPU,A 从挂起的位置开始执行,
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");
}
}
//执行到准备语句 “窗口 A 卖出一张票,还剩 1 张”,还没输出,
CPU 时间片到,A 挂起。
// C 抢占 CPU,从挂起位置开始执行
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");
}
}
输出 “窗口 C 买出一张票,还剩 0 张”
// A 抢占到 CPU,A 从挂起的位置开始执行,
for (int i = 0; i < 5; i++) {
if (count > 0) {
count--;
System.out.println(...getName() + "买出一张票,还剩" + count + "张");
}
}
执行输出语句 “窗口 A 卖出一张票,还剩 1 张”...
总结:
-
线程通过抢占CPU的方式工作,随时肯CPU时间片到从而被挂起。
-
由于随时被挂起或者被切除CPU,导致访问共享资源数据错乱。
5.线程的生命周期
新生状态:用new关键字建立一个线程后,该对象就会处于新生状态
该线程有资源,通过调用start()方法进入就绪状态
就绪状态:有资源,无资格:也就是该状态下的线程具备了运行的条件,但是还没分配到CPU.通过CPU 调度进入运行状态.
运行状态:有资源,有资源:该状态下的线程具备了运行的条件,也分配到了CPU,执行自己的run方法 中的代码,直到完成任务而死亡或者是因为等待某资源而造成阻塞.如果在给定的时间片 内没有执行结束,就会被系统给换下来回到就绪状态.
阻塞状态:无资源,让资格:该状态下的线程不具备运行的条件,则让出CPU的资格.
造成阻塞状态的原因有:
[1] 等待I/O资源
[2] 等待网络资源
[3] 线程A join导致其他线程阻塞
[4] 线程A sleep导致线程A阻塞
当阻塞原因消除时该线程就会进入就绪状态.
死亡状态:[1]当一个正常运行的线程完成了它的全部工作;
[2]线程被强制性的终止,如通过stop()方法终止一个线程
[3]线程抛出未捕获的异常.
6.线程的常用方法
1.线程优先级
public class PriorityThread extends Thread{
public PriorityThread() {
}
public PriorityThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(super.getName());
}
}
public class Test01Priority {
// 优先级越高,被CPU调动的可能越高,但不一定是优先级越高就一定执行
// 系统默认的三种优先级
System.out.println(Thread.MAX_PRIORITY);// 最高
System.out.println(Thread.MIN_PRIORITY);// 最低
System.out.println(Thread.NORM_PRIORITY);// 默认
public static void main(String[] args) {
PriorityThread ta = new PriorityThread("Ta");
ta.setPriority(Thread.MAX_PRIORITY);
PriorityThread tb = new PriorityThread("Tb");
ta.setPriority(Thread.MIN_PRIORITY);
ta.start();
tb.start();
}
}
2.线程的强制执行 join()
/*
t1.join() t1 强制执行,导致其他线程(mainThread)阻塞。当 t1 执行完成后,其他线程阻塞原因消除,进入就绪态。
*/
public class JoinThread extends Thread{
public JoinThread() {
}
public JoinThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10;i++){
System.out.println(super.getName() + i);
}
}
}
public class Test02Join {
public static void main(String[] args) {
JoinThread ta = new JoinThread("线程A");
ta.start();
// main thread
for (int i = 0; i < 10;i++){
if (2 == i){
// ta强制执行
try {
ta.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main thread" + i);
}
}
}
3.线程休眠 sleep()
/*
线程调用 sleep(毫秒)方法,当前线程进入阻塞态,阻塞时间到后线程进入就绪态
*/
public class SleepThread extends Thread{
public SleepThread() {
}
public SleepThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("开始执行多线程");
try {
// 等待10s
System.out.println("线程即将开始休眠...10s");
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程在外界被中断...");
}
System.out.println("线程正常结束");
}
}
public class Test03Sleep {
public static void main(String[] args) {
SleepThread ta = new SleepThread();
ta.start();
System.out.println("主线程开始休眠");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 试图打断ta线程
ta.interrupt();
}
}
总结:
总结:
[1] 线程休眠导致当前线程进入阻塞态,休眠时间结束后,线程进入就绪态,抢占 CPU,抢到后继续运行。
[2] 线程休眠过程中可以被中断 ,所有存在一个检查时异常InterruptedException 异常,外界程序中断该线程时,休眠时间提前结束,进入就绪状态,等待 CPU 调度执行。
4.线程礼让 yield()
public class YieldThread extends Thread{
public YieldThread() {
}
public YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10;i++){
System.out.println(super.getName() + i);
}
}
}
5.线程结束 interrupt(), stop()(不建议)
/*
stop 表示强制停止一个线程。停止一个线程的风险较大,现在不建议使用,通过 interrupt 发中断信号 中断线程,线程会在何时的时间点结束
interrupt 中止正在运行的线程,该线程不会立即结束,而是继续执行。在适当的时机选择结合异常处理 机制,异常处理机制可以保证线程继续执行。通过异常处理让一个线程正常结束。interrupt 方法可以 引导线程正常结束
*/
public class Test03Sleep {
public static void main(String[] args) {
SleepThread ta = new SleepThread();
ta.start();
System.out.println("主线程开始休眠");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 试图打断ta线程
ta.interrupt();
}
}
7.线程同步
**原子性操作:**如果希望一系列操作,要么都执行,要么都不执行。 原子性操作可以认为是业务上不可分割的单元。
1.同步代码块
把原子性操作放到同一个代码块中,就是同步代码块,使用关键字synchronized
语法:
synchronized(mutex){
// 原子性操作
}
// mutex称为同步锁,也叫互斥锁
public class MyRun implements Runnable{
private int count = 100;
@Override
public void run() {
// 模拟每个窗口有10个人买票
for (int i = 0;i < 200;i++){
synchronized (this){
if (count > 0){
count--;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"已经卖出一张,还剩"+count+"张");
}
}
}
}
}
总结:
[1] 如果要实现原子性操作,必须对共享资源加锁(毕加索)
[2] 如果线程运行时申请不是该线程加锁的资源,就会导致该线程阻塞,进入阻塞状态
同步监视器
synchronized(obj){}中的 obj 称为同步监视器
同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器,且同步监视器不能是基本数据类型。
2.同步方法
当原子性操作的代码很长且可以被重复调用时,可以考虑使用同步方法
语法:
[修饰符] synchronized 返回值类型 方法名称(){
// 原子性操作
}
// 同步方法中无需指定同步监视器,因为同步方法的监视器是 this,也就是该对象本身
public class MyRun implements Runnable{
private int count = 100;
@Override
public void run() {
// 模拟每个窗口有10个人买票
for (int i = 0;i < 200;i++){
this.saleTicket();
}
}
public synchronized void saleTicket(){
if (count > 0){
count--;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"已经卖出一张,还剩"+count+"张");
}
}
}
un implements Runnable{
private int count = 100;
@Override
public void run() {
// 模拟每个窗口有10个人买票
for (int i = 0;i < 200;i++){
this.saleTicket();
}
}
public synchronized void saleTicket(){
if (count > 0){
count--;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"已经卖出一张,还剩"+count+"张");
}
}
}