java基础中多线程的解读

一、多线程的基本概念

1.什么是进程

  • 一个进程中对应一个应用程序,例如:在windows操作系统启动Word就表示启动了一个进程。在java的开发环境下启动JVM,就表示启动了一个进程,现代的计算机都是支持多进程的,在同一个操作系统中,可以同时启动多个进程。

2.多进程有什么作用?

  • 单进程计算机只能做一件事情
  • 多进程的作用不是提高执行速度,而是提高CPU的使用率
  • 进程和进程之间的内存是独立的

3.什么是线程

  • 线程是一个进程中的执行场景,在一个应用程序中可以同时执行多个功能,每一个功能就对应一个线程。一个进程可以启动多个线程

4.多线程有什么作用?

  • 多线程不是为了提高执行速度,而是提高应用程序的使用率
  • 线程和线程共享“堆内存和方法区内存”,栈内存是独立的,一个线程一个栈

5.java程序的运行原理

  • java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,表示启动一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法,所以main方法进行在主线程中。在此之前的所有程序都是单线程的。

6.简单辨别线程的案例

package Thread;

/**
 * 问题:分析以下程序有几个线程?
 */
public class test01 {
    public static void main(String[] args) {
        m1();
    }

    private static void m1() {
        m2();
    }

    private static void m2() {
        m3();
    }

    private static void m3() {
        System.out.println("m3...");
    }
}

  • 从以上可以看出,以上程序只有一个线程(单线程),就是主线程
  • main方法调用m1方法,再调用m2,再调用m3,一个线程一个栈,一个线程就是主线程,mian、m1、m2、m3这四个方法在同一个栈空间中(类似于数据结构中栈的栈顶、栈底),没有启动其他任何线程

二、线程的创建和启动

  • 实际上,java程序在运行中至少有两个线程主线程垃圾回收机制(gc)
    • 主线程:JVM启动时会创建一个主线程,用来执行main()中的代码
    • gc:低级别的线程,用来回收垃圾对象
    • 如果需要实现多个线程,可以自定义线程
  • 两种方式:
    • 继承Thread类
    • 实现Runnable接口

1.继承Thread类

  • 代码案例
package Thread;

public class test02 {
    public static void main(String[] args) {
        //创建自己的线程类对象
        Thread t = new Processor9();
        //启动线程。这行代码执行瞬间结束。告诉JVM再分配一个新的栈给t线程
        //run不需要程序员手动调用,系统线程启动之后自动调用run方法
        t.start();
//        t.run();  //这是普通方法调用,这样做程序只有一个线程主线程,run方法结束之后
        //下面程序才能继续执行
        for (int i=0;i<10;i++){
            System.out.println("main-->"+i);
        }
        //有了多线程之后,main方法结束只是主线程中没有方法栈帧了
        //但是其他线程或其他栈中还有栈帧
        //main方法结束,程序可能还在运行
    }
}
//定义一个线程
class Processor9 extends Thread{
    //重写run方法
    public void run(){
        for (int i=0;i<10;i++){
            System.out.println("run-->"+i);
        }
    }
}

  • 步骤:
    • 定义一个类,继承Thread,Thread是线程的父类,提供了一些操作线程的方法
    • 重写父类中run()方法
    • 创建线程类的实例,创建线程对象
    • 启动线程,线程对象调用start()方法,不能直接调用run()方法
  • 为了更好的方便理解,下面图示
    继承Thread类
  • 首先是java虚拟机(JVM),启动主线程,然后调用main()方法
  • 主线程启动,分配主线程的栈,第一次调用main()方法,会压栈
  • 堆内存里面存放创建线程的对象,内存地址指向线程对象
  • t调用start方法,在java虚拟机里面另分配一块栈空间(t线程栈
  • 再调用run方法,会在t线程栈压入run方法栈帧
  • run方法还会调用其他的方法,会继续压栈,也会在run方法中再次启动一个线程,会再次分配一个栈空间

2.实现Runnable接口

  • 代码案例
package Thread;

public class test03 {
    public static void main(String[] args) {
        //创建线程
        Processor10 p = new Processor10();
        Thread t = new Thread(p);
        //启动
        t.start();
    }
}
//这种方式是推荐的。因为一个类实现接口之外保留了类的继承
class Processor10 implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("run--->"+i);
        }
    }
}
  • 步骤:
    • 定义一个自定义类,实现Runnable接口
    • 实现 run()方法
    • 创建该类的实例,创建线程对象
    • 创建Thread类,将自己的线程类对象传入
    • 启动线程

3.对比

  • 继承Thread:java是单一继承,无法继承多个类
  • 实现Runnable:避免了单一继承的问题,保留了类的继承。适合多个线程去处理同一个资源(共享一个资源)
  • 一般使用实现Runnable接口的方式

三、线程的生命周期

1.CPU时间片

  • 对於单核系统,某个时间点只能操作一件事情
  • CPU为各个程序分配时间,称为时间片,该进程运行的时间(时间很短)
    • 从表面看每个程序同时运行的,实际上在同一时间点只能执行一个程序
    • 只是CPU在很短的时间内,在不同的程序之间切换,轮流执行每个程序,执行的速度很快,感觉上在同时执行

2.为了更便于理解,下面是线程的生命周期图示

线程的生命周期

  • 步骤:
  • new出来的线程(t1、t2、t3…),调用start()方法,进入就绪状态
  • 就绪状态有权利获取CPU时间片,拿到时间片之后,到运行状态,run()方法执行,CPU时间片用完,再回到就绪状态,等CPU时间片,拿到CPU时间片再运行,反复如此,直到该线程的run()方法执行结束,整个线程就会销毁
  • 线程也有可能遇到阻塞事件,进入阻塞状态,阻塞解除,进入到就绪状态
  • 所以线程的生命周期有5个状态:新建就绪运行阻塞销毁

3.方法

方法 含义
start() 启动线程,进入就绪状态,有权利获取CPU时间片
sleep() 该方法是一个静态方法,休眠线程,当线程执行该方法时,不抢夺CPU时间片,阻塞当前线程,腾出CPU,让给其他线程,从运行到阻塞状态。如果阻塞解除,进入到就绪状态,继续争夺CPU时间片。从运行到阻塞
join() 是一个成员方法,当前线程可以调用另一个线程的join方法,调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行。从运行到阻塞
yield() 该方法是一个静态方法,它与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会 。从运行到阻塞
interrupt() 中断该线程的休眠状态,使它不再休眠,进入到就绪状态。如果一个线程的休眠状态被中断,会报异常错误信息,所以要写异常错误处理机制。从休眠到就绪

四、线程的安全性问题

1.线程安全问题

  • 多个线程同时访问共享数据可能出现问题,称为线程的安全问题性
  • 当多个线程同时访问数据时,由于CPU的切换,导致一个线程只执行了一部分代码,没有执行完成,此时另一个线程又参与进来,导致共享数据发生异常例如:银行取款仓库总共5000元,t1取1000元,t2取款的时候一定要等t1取完款,银行仓库更新为4000元的时候,t2才可以进行取款操作。如果银行仓库的钱还没有更新,有可能还是5000元,t2取款的时候,数据就会出现异常)

2.同步线程和异步线程

  • 同步线程:在多个线程同时执行时,一个线程要等待上一个线程执行完成后,才开始执行(类似上厕所排队)
  • 异步线程:在多个线程同时执行时,不用等待上面的线程是否结束,多个线程一起执行,谁也不等谁

3.什么时候要同步?为什么引入线程同步?什么条件下要使用线程同步?

  • 为了数据的安全,尽管应用程序的使用率降低,但是为了保证数据是安全的,必须加入线程同步机制
  • 什么条件下要使用线程同步?
    • 必须是多线程环境
    • 多线程环境共享同一个数据
    • 共享的数据涉及到修改操作(提醒:查询操作不需要使用线程同步)

4.解决线程安全问题

  • 线程的同步机制(就是将异步线程变为同步线程)
  • synchronized + 锁
    • 同步方法(使用synchronized 关键字修饰成员方法,线程拿走的也是this的对象锁)
      public synchronized void withdraw(参数){......}
    • 同步代码块(在成员方法里面使用synchronized 修饰代码块)synchronized(对象锁) { 代码块 }
  • 上述添加线程同步两种方法的对比
    • 使用同步方法方式,整个成员方法都需要同步
    • 使用同步代码块方式,会比较经济,因为该成员方法有可能会有别的代码块,不一定是同步代码块
  • :称为对象锁,每个对象都只带一个锁(标识),不同对象有不同的锁
  • 线程安全的还有vectorHashtableStringBuffer

5.代码案例(线程同步+锁)

package Thread;


/**
 * 以下程序演示取款例子,以下程序使用线程同步机制保证数据的安全
 */
public class ThreadTest02 {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        //创建一个公共的账户
        Account1 act = new Account1("actno-01",5000.0);
        //创建线程对同一个账户取款
        Processor1 p = new Processor1(act);

        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);

        t1.start();
        t2.start();

        long endTime = System.currentTimeMillis();
        System.out.println("运行时间:"+(endTime-startTime));
    }
}

class Processor1 implements Runnable{
    //账户
    Account1 act;
    //Constructor
    Processor1(Account1 act){
        this.act = act;
    }

    @Override
    public void run() {
        act.withdraw(1000.0);
        System.out.println("取款1000.0成功,余额:"+act.getBalance());
    }
}

class Account1{
    private String actno;
    private double balance;

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public Account1(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public Account1() {
    }

    //对外提供一个取款的方法
    public void withdraw(double money){ //对当前账户进行取款操作
        synchronized (this){
            double after = balance - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新余额
            this.setBalance(after);
        }

    }
}

  • 原理执行过程:
    • 把需要同步的代码,放到同步语句块
    • t1线程和t2线程。t1线程执行到同步代码块时,遇到了synchronized 关键字,就会去找this的对象锁,如果找到this对象锁,则进入同步语句块中执行程序,此时该对象不再拥有锁。当同步语句块中的代码执行结束之后,t1线程释放this的对象锁
    • 在t1线程执行同步语句块的过程中,如果t2线程也过来执行以下代码,也遇到synchronized 关键字,所以也去找this的对象锁,但是该对象锁被t1线程持有,t2线程会进入对象的锁池中等待,直到锁被归还,此时需要锁的线程去竞争

五、线程的通信

1.锁池和等待池(根据上面线程的生命周期那张图对应着看)

  • 首先我们要明确每个对象都有锁池和等待池
  • 锁池
    • 当线程无法获取锁,此时进入锁池
    • 如果对象的锁被释放锁池中的多个线程竞争锁
  • 等待池
    • 当线程获取锁后,可以调用wait()放弃锁,进入等待池
    • 当其他线程调用notifynotifyAll方法,等待池中的线程将被唤醒,进入锁池
    • 锁池中继续竞争锁

2.方法

方法 含义
wait() 放弃对象锁
notify() 随机唤醒一个等待池中的线程
notifyAll() 唤醒等待池中的所有线程
  • 重点提醒
    • 这三个在Object类中定义的
    • 这三个方法只能synchronized 中使用只有获取了锁的线程才能使用
    • 等待和唤醒必须使用的是同一个对象

3.代码案例

package Thread;

public class ThreadTest11 {
    public static void main(String[] args) {
        Object o = new Object();
        Thread t1 = new MyT1(o);
        Thread t2 = new MyT2(o);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();

    }
}

class MyT1 extends Thread{
    private Object o;
    public MyT1(Object o) {
        this.o = o;
    }

    public void run(){
        System.out.println("t1的线程");
        synchronized (o){
            try {
                o.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("222");
    }
}

class MyT2 extends Thread{
    private Object o;
    public MyT2(Object o){
        this.o = o;
    }
    public void run(){
        System.out.println("t2的线程");
        synchronized (o){
            o.notifyAll();
        }
    }
}

  • 输出有两个结果:
//第一种结果
t1的线程
t2的线程
222
-----------------------
//第二种结果
t2的线程
t1的线程
  • 第一种结果的原理:
    • t1线程遇到synchronized 关键字,执行代码块中的wait()方法释放当前锁,让出CPU,进入等待池
    • t2线程遇到synchronized 关键子,执行代码块中的notifyAll()方法,等待池中的线程将被唤醒,进入锁池
    • t1在锁池中继续竞争锁,执行上一次没有完成的代码,会再输出222
  • 第二种结果的原理:
    • t2线程遇到synchronized 关键字,执行代码块中的notifyAll()方法,但是等待池中没有线程可以唤醒,所以输出222
    • t1线程线程遇到synchronized 关键字,执行代码块中的wait()方法释放当前锁,让出CPU,进入等待池中。由于没有其它线程唤醒t1线程,t1线程一直在等待池中,程序一直不结束,一直在运行

六、面试题(写一个死锁的程序)

1.小故事

  • 程序猿应该都知道哲学家进餐问题的故事
  • 简单来说就是有5位哲学家围在一个圆桌上吃饭,但是每位哲学家只有一只筷子,要想吃饭,必须要两只筷子才行。所以其中一位哲学家要依靠左边的或者右边的哲学家一起夹菜才能吃到,但是大家都不愿意,于是大家都吃不到菜

2.死锁原理

  • 在线程中,比如t1线程已经得到其中一个锁,但是我还需要另外一个t2线程的锁才能运行。然而t2线程得到了其中的锁,但是它也同时需要t1线程的锁才能执行,于是大家都不能执行相应的代码块,一直在争夺对方的锁,一直处在死锁当中,程序一直在运行当中,停不下来

3.代码案例

package Thread;

/**
 * 死锁
 */
public class ThreadTest07 {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new T1(o1,o2));
        Thread t2 = new Thread(new T2(o1,o2));

        t1.start();
        t2.start();
    }
}

class T1 implements Runnable{
   Object o1;
   Object o2;

   T1(Object o1,Object o2){
       this.o1 = o1;
       this.o2 = o2;
   }
    @Override
    public void run() {
       synchronized (o1){
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           synchronized (o2){

           }
       }
    }
}

class T2 implements Runnable{
    Object o1;
    Object o2;

    T2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    @Override
    public void run() {
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

如果对你有帮助,不如点个赞,也算是支持一下0.0
若有不正之处,请多多谅解并欢迎批评指正,不甚感激

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章