java 线程与进程基础

基础

  • 进程

    • 定义:

      进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。资源管理器中的一个应用就是一个进程。引入进程是为了使多个程序可以并发的执行,以提高系统的资源利用率和吞吐量。

    • 状态 图解
      • 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行。
      • 运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
      • 阻塞状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
    • 多进程
      • 单进程计算机在只能做一件事情,然而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。比如:Windows,Mac和Linux等。
      • 对于单核计算机来讲,游戏进程和音乐进程并不是同时进行的,因为CPU在某个时间点上只能做一件事情,计算机在游戏进程和音乐进程间做着频繁的切换,且切换速度很快。
      • 多进程的作用不是提高cpu的执行速度,而是提高cpu的使用率
    • 进程通信

      进程通信是指进程之间的信息交换。PV操作是低级通信方式,髙级通信方式是指以较高的效率传输大量数据的通信方式。高级通信方法主要有以下三个类。

      • 共享存储
        • 在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换。在对共享空间进行写/读操作时,需要使用同步互斥工具(如 P操作、V操作),对共享空间的写/读进行控制。共享存储又分为两种:低级方式的共享是基于数据结构的共享;高级方式则是基于存储区的共享。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成。
        • 需要注意的是,用户进程空间一般都是独立的,要想让两个用户进程共享空间必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的。
      • 消息传递
        在消息传递系统中,进程间的数据交换是以格式化的消息(Message)为单位的。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接收消息两个原语进行数据交换。
        • 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
        • 间接通信方式:发送进程把消息发送到某个中间实体中,接收进程从中间实体中取得消息。这种中间实体一般称为信箱,这种通信方式又称为信箱通信方式。该通信方式广泛应用于计算机网络中,相应的通信系统称为电子邮件系统。
      • 管道通信
        管道通信是消息传递的一种特殊方式。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程),则从管道中接收(读)数据。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在。
    • 进程同步

      多进程虽然提高了系统资源利用率和吞吐量,但是由于进程的异步性可能造成系统的混乱。进程同步的任务就是对多个相关进程在执行顺序上进行协调,使并发执行的多个进程之间可以有效的共享资源和相互合作,保证程序执行的可再现性。

      • 同步机制原则:
        • 空闲让进:当没有进程处于临界区的时候,应该许可其他进程进入临界区的申请
        • 忙则等待:当前如果有进程处于临界区,如果有其他进程申请进入,则必须等待,保证对临界区的互斥访问
        • 有限等待:对要求访问临界资源的进程,需要在有限时间内进入临界区,防止死等
        • 让权等待:当进程无法进入临界区的时候,需要释放处理机,边陷入忙等
      • 经典的进程同步问题:生产者-消费者问题;哲学家进餐问题;读者-写者问题
      • 同步的解决方案:管程,信号量。参考:线程通信
    • 进程死锁

      死锁是指多个进程在运行过程中,因为争夺资源而造成的一种僵局,如果没有外力推进,处于僵局中的进程就无法继续执行。

      • 原因:
        • 竞争资源:请求同一有限资源的进程数多于可用资源数
        • 进程推进顺序非法:进程执行中,请求和释放资源顺序不合理
      • 产生的必要条件:
        • 互斥条件:进程对所分配的资源进行排他性的使用
        • 请求和保持条件:进程被阻塞的时候并不释放锁申请到的资源
        • 不可剥夺条件:进程对于已经申请到的资源在使用完成之前不可以被剥夺
        • 环路等待条件:发生死锁的时候存在的一个 进程 - 资源 环形等待链
      • 处理:
        • 预防死锁:破坏产生死锁的4个必要条件中的一个或者多个。
        • 避免死锁:在资源的动态分配中,防止系统进入不安全状态
        • 检测死锁:允许系统运行过程中产生死锁,在死锁发生之后,采用一定的算法进行检测,并确定与死锁相关的资源和进程,采取相关方法清除检测到的死锁。实现难度大
        • 解除死锁:与死锁检测配合,将系统从死锁中解脱出来(撤销进程或者剥夺资源)。对检测到的和死锁相关的进程以及资源,通过撤销或者挂起的方式,释放一些资源并将其分配给处于阻塞状态的进程,使其转变为就绪态。实现难度大
    • 并行和并发
      • 并行指在某一个时间点内同时运行多个程序
        🌰:比如高速公路上有8个窗口 , 所以同一时刻,高速公路可以放行的车辆是8辆,所以就说并行的数量是8
      • 并发指在单位时间内运行多个程序
        🌰:高速公路上的8个窗口,一个窗口处理一辆车的需要10秒钟,那么一分钟 这个收费站的并发量6*8=48,也就是说,这个收费站并发量是48量/分钟

  • 线程

    • 定义

      在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。
      线程是程序使用CPU的基本单位,引入线程是为了减少程序在并发执行过程中的开销,使OS的并发效率更高。
      注意:线程是依赖于进程存在的。

    • 多线程
      • 程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大。也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率。但是即使是多线程程序,也无法确定他们之中哪个能抢占到CPU的资源,所以多线程具有随机性。
      • 多线程的作用不是提高执行速度,而是提高应用程序的使用率。
      • 🌰:JVM至少启动了垃圾回收线程和主线程,所以JVM的启动是多线程的
    • 线程执行流程

      图解
      注意:如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过跑出InterrruptedException来唤醒它。如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,java代码并没有办法直接接触到操作系统。


    • 线程创建
      • 继承Thread类创建线程类

        步骤:
        1: 定义一个类,让该类去继承Thread类
        2: 重写run方法
        3: 创建该类的对象
        4: 启动线程
        
        public class ThreadDemo {
        	public static void main(String[] args) {
        		// 创建对象
        		MyThread t1 = new MyThread() ;
        		MyThread t2 = new MyThread() ;
        		// 启动线程: 需要使用start方法启动线程,start方法会调用run方法。如果直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,就相当于调用一个普通方法。
        		//		t1.run() ;
        		t1.start() ;		// 一个线程只能被启动一次
        		t2.start() ;
        		
        	}
        }
        
        public class MyThread extends Thread {
        	@Override
        	public void run() {
        		for(int x = 0 ; x < 1000 ; x++) {
        			System.out.println(x);
        		}
        	}
        }
        
      • 通过Runnable接口创建线程类

        步骤:
        1: 定义一个类,让该类去实现Runnable接口
        2: 重写run方法
        3: 创建定义的类的对象
        4: 创建Thread的对象吧第三步创建的对象作为参数传递进来
        5: 启动线程
        
        public static void main(String[] args) {
        	// 创建定义的类的对象
        	MyThread mt = new MyThread() ;
        	// 创建Thread的对象吧第三步创建的对象作为参数传递进来
        	Thread t1 = new Thread(mt , "张三") ;
        	Thread t2 = new Thread(mt , "李四") ;
        	// 启动线程
        	t1.start() ;
        	t2.start() ;
        }
        
        public class MyThread implements Runnable {
        	@Override
        	public void run() {
        		for(int x = 0 ; x < 1000 ; x++) {
        			System.out.println(Thread.currentThread().getName() + "---" + x);
        		}
        		
        	}
        }
        
      • 通过Callable和Future创建线程
        Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。 Future 对象表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果;

        步骤:
        1: 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
        2: 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
        3: 使用FutureTask对象作为Thread对象的target创建并启动新线程。
        4: 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,调用get()方法会阻塞线程。
        public class CallableThread implements Callable<Integer> {
            @Override
            public Integer call() throws Exception {
                int i = 0;
                for (; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
                return i;
            }
        }
        
        
        public class ThreadDemo {
            public static void main(String[] args) {
                CallableThread ctt = new CallableThread();
                FutureTask<Integer> ft = new FutureTask<>(ctt);
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
                    if (i == 20) {
                        new Thread(ft, "有返回值的线程").start();
                    }
                }
                try {
                    System.out.println("子线程的返回值:" + ft.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
        
      • 区别
        • 采用实现Runnable、Callable接口的方式创建多线程时
          • 优势:
            • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
            • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
          • 劣势
            • 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
        • 使用继承Thread类的方式创建多线程时
          • 优势: 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
          • 劣势:不能再继承其他类

    • 线程调度

      应用程序在执行的时候都需要依赖于线程去抢占CPU的时间片 , 谁抢占到了CPU的时间片,那么CPU就会执行谁。怎么确定抢占规则呢?

      • 分时调度模型
        所有线程轮流使用CPU的使用权,平均分配每个线程占用 CPU 的时间片
      • 抢占式调度模型
        优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。Java使用的是抢占式调度模型。

    • 线程控制
      • 休眠线程 : sleep()

        • 作用
          • 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序经度和准确性的影响。
          • 让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常。
        • 优先级
          两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。
        • 唤醒阻塞线程
          如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;
        • Thread.sleep(0)
          由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
      • 加入线程 : join()

        • 作用
          等待该线程执行完毕了以后,其他线程才能再次执行,🌰:

          //下述代码表示先启动t1,直到t1的任务结束,才轮到t2启动。
          t1.start();
          t1.join(); 
          t2.start();
          
        • 注意:在线程启动之后,才能调用该方法。

      • 礼让线程 : yield()

        • 作用 :暂停当前正在执行的线程对象,并执行其他线程。
        • 原理:暂停当前的线程,让CPU去执行其他的线程,这个暂定的时间是相当短暂的;当我某一个线程暂停完毕以后,其他的线程还没有抢占到cpu的执行权 ;那么这个是时候当前的线程会和其他的线程再次抢占cpu的执行权;
        • 注意:yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
      • 守护线程 : setDaemon()

        • 作用:将该线程标记为守护线程或用户线程。
        • 注意:该方法必须在启动线程前调用。如果线程全部是守护线程,那么jvm就停止。
      • 中断线程 : interrupt()
        当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞。

      • 开启线程 : start()
        多次start()会抛出java.lang.IIIegalThreadStateException。


    • 终止线程

      线程对象属于一次性消耗品,一般线程执行完run方法之后,线程就正常结束了,线程结束之后就报废了,不能再次start,只能新建一个线程对象。但有时run方法是永远不会结束的,例如在程序中使用线程进行Socket监听请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。我们可以使用以下方式退出线程。

      • 使用退出标志(即使用一个变量来控制循环),使线程正常退出,也就是当run方法完成后终止线程。🌰:

        public class ThreadSafe extends Thread {
            public volatile boolean exit = false;
                public void run() {
                while (!exit){
                    //do something
                }
            }
        }
        
        /**
         * stop thread running
         */
        public void stop() {
            if (exit ) {
                exit = false;
            }
        }
        
      • 若线程处于阻塞状态,当调用线程的interrupt()方法时,系统会跑出InterruptedException药行,代码中通过捕获异常,然后break跳出循环状态,使线程正常结束。

      • 若线程未进入阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环,当使用interrupt()方法时,中断标志就会置为true,和使用退出标志来控制循环是一样的道理。🌰:

        public class ThreadSafe extends Thread {
            public void run() {
                while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
                    try{
                        Thread.sleep(5*1000)//阻塞过程捕获中断异常来退出
                    }catch(InterruptedException e){
                        e.printStackTrace();
                        break;//捕获到异常之后,执行break跳出循环。
                    }
                }
            }
        }
        
      • 使用stop()强行终止线程,stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。


    • 常见问题
      • wait和sleep方法的区别
        wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值