【Java高级篇】Java多线程篇(一)

1. 进程

几乎所有操作系统都支持进程的概念,所有运行中的任务通常对应一条进程(Process)。当一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位

1.1进程包含如下三个特征:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

1.2 区别

  1. 并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
  2. 多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每条线程也是互相独立的。
  3. 线程是进程的组成部分,一个进程可以拥有多个线程一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,我们必须确保线程不会妨碍同一进程里的其他线程。
  4. 一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
  5. 线程共享的环境包括:进程代码段进程的公有数据等。利用这些共享的数据等,线程很容易实现相互之间的通信。

多线程的优点:

  • 进程间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
  • Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编。

2. 线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每条线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用run方法来封装这样一段程序流。

2.1 继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就是代表了线程需要完成的任务。因此,我们经常把run方法称为线程执行体
  2. 创建Thread子类的实例,即使创建了线程对象。
  3. 用线程对象的start方法来启动该线程。

2.2 实现Runnable接口创建线程类

实现Runnable接口来创建并启动多条线程的步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 使用Runnable接口创建线程可以共享同一个线程类实例的资源。
//实现Runnable 可以共享一份数据   存再并发问题
public class Racer implements Runnable{
    private String winner;//胜利者

    public void run() {
        for(int steps=1;steps<=100;steps++){
            if (Thread.currentThread().getName().equals("rabbit")&&steps%10==0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"--->"+steps);
            boolean flag= gameOver(steps);
            if (flag){
                break;
            }
        }
    }
    public boolean gameOver(int steps){
        if (winner!=null){//存在胜利者
            return true;
        }else{
            if (steps==100){
                winner=Thread.currentThread().getName();
                System.out.println("winner==>"+winner);
                return true;
            }
        }

        return false;
    }
    public static void main(String[] args) {
        //一份资源
        Racer racer = new Racer();
        //多个代理
        new Thread(racer,"rabbit").start();
        new Thread(racer,"tortoise").start();
    }


}

2.3 实现Callable接口创建线程类

实现Callable接口来创建并启动多条线程的步骤如下:

  1. 定义Callable接口的实现类,并重写call方法,该call方法的方法体同样是该线程的线程执行体
  2. 区别:可以抛异常,和返回的对象类型

2.4 两种方式所创建线程的对比

采用实现Runnable接口方式的多线程:

  • 线程类只是实现了Runnable接口,还可以继承其他类。
  • 在这种方式下,可以多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 劣势是:编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

采用继承Thread类方式的多线程:

  • 劣势是:因为线程类已经继承了Thread类,所以不能再继承其他父类。
  • 优势是:编写简单,如果需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。

实际上几乎所有的多线程应用都可采用第一种方式,也就是实现Runnable接口的方式。

start()方法使用的 静态代理

/*
* 静态代理
* 实现同一个接口
* 1. 真是角色
* 2.代理角色:必须有真实角色的目标对象
*
* */
public class staticPoxy {
    public static void main(String[] args) {
        new WeddingCompany(new You()).HappuMarry();
    }



}
//接口
interface  marry{
   void HappuMarry();
}
//真实角色
class  You implements  marry{

    public void HappuMarry() {
        System.out.println("我要结婚了,非常舒服");
    }
}
//代理角色:有真实角色的目标对象
class  WeddingCompany implements  marry{
   private You you;
   public   WeddingCompany(You you){
    this.you=you;
   }

   public void HappuMarry() {
     realdy();
     this.you.HappuMarry();
     after();
    }

    private void realdy() {
        System.out.println("布置现场---");
    }


    private void after() {
        System.out.println("闹新房---");
    }


}

3. 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(new)、就绪(Runnable start())、运行(Running,cpu调度)、阻塞(Blocked)和死亡(Dead(定义标志,外部结束))五种状态。尤其是当线程启动以后,它不能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

3.1 新建和就绪状态

新建 :当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程执行体中的线程执行体。
就绪 :当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,它只是表示该线程可以运行了。至于该线程何时开始运行取决于jvm里线程调度器的调度。

不要对已经处于启动状态的线程再次调用start()方法,否则将引发IllegalThreadStateException异常。

3.2 运行和阻塞状态

运行:如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,在任何时刻只有一条线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行(注意是并行:parallel)执行;但当线程数大于处理器数时,依然会有多条线程在同一个CPU上轮换的现象。
阻塞:当发生如下情况下,线程将会进入阻塞状态:

  • 线程调用sleep方法主动放弃所占用的处理器资源。(抱着资源睡觉)
  • 线程调用了一个阻塞式io方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程调用wait()方法进入阻塞状态
  • 线程调用join()方法合并线程,当前线程进入阻塞状态

当运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()可以让当前处于运行状态的线程转入就绪状态.

3.3 线程死亡

线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用线程的stop()方法来结束该线程.该方法容易导致死锁,通常不推荐使用

注: 当主线程结束时候,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。

总结:

1、 进程: 是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路劲,或者叫一个控制单元
2、 线程: 就是进程中的一个独立的控制单元。线程在控制着进程的执行。
3、一个进程中至少有一个线程。
4、JVM启动的时候会有一个进程java.exe.该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
5、扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
6、如何在自定义的代码中,自定义一个线程呢?

  • 通过对api的查找,java已经提供了对线程这类事物的描述。就Thread类。
  • 创建线程的第一种方式:继承Thread类。
  • 定义类继承Thread。复写Thread类中的run方法。目的:将自定义代码存储在run方法。让线程运行。
  • 调用线程的start方法,该方法两个作用:启动线程,调用run方法。

7、发现运行结果每一次都不同。因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
8、 为什么要覆盖run方法呢?

  • Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。

9、 创建线程的第二种方式:实现Runable接口

  • 定义类实现Runnable接口
  • 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
  • 通过Thread类建立线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
  • 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

10、实现方式和继承方式有什么区别呢?

  • 实现方式好处:避免了单继承的局限性。实现方式好处:避免了单继承的局限性。

4. 线程安全

1、问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

2、解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。

synchronized(对象){
    需要被同步的代码 
}

对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

3、同步的前提:必须要有两个或者两个以上的线程。必须是多个线程使用同一个锁。必须保证同步中只能有一个线程在运行。
4、 好处:解决了多线程的安全问题。
5、 弊端:多个线程需要判断锁,较为消耗资源
6、同步函数用的是哪一个锁呢?

  • 函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。

7、如果同步函数被静态修饰后,使用的锁是什么呢?

  • 通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class 该对象的类型是Class

8、 如何找问题:

  • 明确哪些代码是多线程运行代码。
  • 明确共享数据。
  • 明确多线程运行代码中哪些语句是操作共享数据的。

5. 死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采用措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

6. 线程通信

在同步代码中可以使用锁对象的wait()方法让当前线程等待使用锁对象的notify()方法可以将正在等待的线程唤醒如果多个线程都在等待,notify()唤醒随机1个notifyAll()方法可以唤醒所有在等待的线程

7. Thread中的一些方法

7.1 如何停止线程?

只有一种,run方法结束。开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束
特殊情况: 当线程处于了冻结状态。就不会读取到标记。那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。Thread类提供该方法 interrupt();
join: 当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。join可以用来临时加入线程执行
yield: 暂停当前正在执行的线程对象,并执行其他线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
sleep: 线程调用sleep方法主动放弃所占用的处理器资源。(抱着资源睡觉)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李熠漾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值