多线程基础

115 阅读10分钟

多线程

1、进程和线程之间的关系:

一个进程有多个线程,线程是进程划分最小的运行单位。线程和进程最大的不同在于基本上各进程独立的,而各线程不一定,因为同一进程中的线程极有可能相互影响。线程执行开销小,但不利于资源管理和保护;而进程相反。

image-20220816091058247.png

堆:是进程中最大的一块内存,主要存放新创建的对象,几乎所有对象都在这里分配内存。

方法区:主要用于存放已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2、并发与并行的区别

并发:两个及两个以上的作业,在同一时间段执行。

并行:两个及两个以上的作业,在同一时刻执行。

最关键的点是:是否同时执行。

3、使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

4、线程的生命周期和状态

java线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态:

  • NEW:初始状态,线程被创建出来但是没被调用start()。
  • RUNNABLE:运行状态,线程被调用了start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

image-20220816164310323.png

5、线程的几种状态转换

5.1线程sleep时的状态

static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException{ 
    Thread t1 = new Thread(()->{
                try {
                    while (running){
                        System.out.println("t1 running is false ,t1将sleep");
                        Thread.sleep(5000L);
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            });
            System.out.println("new t1 的状态"+t1.getState());
            t1.start();
            Thread.sleep(2000L);
            running = false;
            Thread.sleep(2000L);
            System.out.println("t1.sleeps的状态"+t1.getState());
   }

运行结果:

new t1 的状态NEW
t1 running is false ,t1将sleep
t1.sleeps的状态TIMED_WAITING

运行结果分析:当new Thread时,线程t1 状态为NEW。线程启动start(),执行run()方法,打印t1 running is false ,t1将sleep, 此时线程t1休眠5s。主线程睡眠2s,变量running设置为fasle。此时线程t1还在睡眠中。再将主线程睡眠2s,线程t1仍然在睡眠中。此时t1状态为:TIMED_WAITING。

此时将线程t1睡眠时间修改一下:

Thread.sleep(5000L) -> Thread.sleep(4000L);

再运行结果如下:

new t1 的状态NEW
t1 running is false ,t1将sleep
t1.sleeps的状态TERMINATED

线程t1终止,所以说看代码不一定看到sleep()就是超时等待【TIMED_WAITING】状态,要看线程是否终止了。

5.2线程join时的状态

public static void main(String[] args) throws InterruptedException{        
		//线程join时的状态
        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(10000L);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(()->{
            try {
                System.out.println("t2中执行t1.join(5000L)");
                t1.join(5000L);//t2等待t1 5s
                System.out.println("t2中执行t1.join()");
                t1.join();//t2等待t1执行完
                System.out.println("t2执行完");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000L);
        System.out.println("t2 status is: "+t2.getState());
        Thread.sleep(5000L);
        System.out.println("t2 status is: "+t2.getState());
}

运行结果:

t2中执行t1.join(5000L)
t2 status is: TIMED_WAITING
t2中执行t1.join()
t2 status is: WAITING
t2执行完

运行结果分析:

线程t1休眠10s,此时进入线程2,打印 t2中执行t1.join(5000L)。接下来t1会抢占5s,进入主线程,主线程休眠1s,此时t2还在等待t1,所以t2的状态为【TIMED_WAITING】。此时主线程又休眠5s,t2里面开始执行t1.join(),此时t2的状态为【WAITING】。

5.3线程synchronized时的状态

public static void main(String[] args) throws InterruptedException{    
	Thread t1 = new Thread(()->{
            synchronized (ApjobFeeConfirmStatusUpdateExec.class){
                System.out.println("t1 抢到锁");
            }
        });
        synchronized (ApjobFeeConfirmStatusUpdateExec.class){
            t1.start();
            Thread.sleep(1000L);
            System.out.println("t1 抢不到锁状态: "+t1.getState());
        }
}

运行结果:

t1 抢不到锁状态: BLOCKED
t1 抢到锁

运行结果分析:主线程启动,先抢到锁。此时t1.start()启动了t1线程,这时候主线程休眠1s,锁还没释放。此时t1的状态为【BLOCKED】。

5.4线程wait时的状态

 public static void main(String[] args) throws InterruptedException {
        Object object  = new Object();
        Thread t1 = new Thread(()->{
            synchronized (object){
                try {
                    System.out.println("t1将wait(1000L)");
                    object.wait(1000L);
                    System.out.println("t1将wait");
                    object.wait();
                    System.out.println("t1将执行完");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        Thread.sleep(600L);
        synchronized (object){
            System.out.println("t1的状态: "+t1.getState());
            object.notify();
            Thread.sleep(1000L);
            System.out.println("t1的状态: "+t1.getState());
        }
        Thread.sleep(3000L);
        System.out.println("t1的状态: "+t1.getState());
        Thread.sleep(1000L);
        synchronized (object){
            object.notify();
        }
        System.out.println("t1的状态: "+t1.getState());
        Thread.sleep(1000L);
        System.out.println("t1的状态: "+t1.getState());
    }

运行结果:

t1将wait(1000L)
t1的状态: TIMED_WAITING
t1的状态: BLOCKED
t1将wait
t1的状态: WAITING
t1将执行完
t1的状态: RUNNABLE
t1的状态: TERMINATED

运行结果分析:

主线程启动,执行t1.start(),进入t1,执行t1将wait(1000L),此时t1让出锁。在t1超时等待的同时,主线程休眠0.6s,当0.6s后这里主线程抢到锁,t1的状态为【TIMED_WAITING】。这里主线程执行object.notify(),但是锁还没释放,t1还没获取到锁,所以t1的状态为【BLOCKED】。之后主线程释放锁,t1获得锁,执行object.wait(),此时t1的状态为【WAITING】,t1执行完成。然后回到主线程,并获得锁,执行object.notify()。此时t1线程被唤醒并处于运行状态【RUNNABLE】。t1执行完成状态为【TERMINATED】。

6、什么是上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如线程的程序计数器,栈信息等。当出现以下情况的时候,线程会从占用CPU状态中退出。

  • 主动让出CPU,比如调用了sleep(),wait()等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或进程饿死。
  • 调用了阻塞类型的系统中断,比如IO请求,线程阻塞。
  • 被终止或结束运行。

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU线程的上下文。这就是所谓的上下文切换。

7、什么是线程死锁?如何避免死锁?

7.1认识线程死锁

线程死锁描述的是这一种情况:多个线程同时阻塞,它们中的一个或者全部都在等待某个资源释放。由于线程被无限期阻塞,因此程序不可能正常终止。

image-20220817155616580.png

    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (resource1){
                System.out.println(Thread.currentThread()+"get resource1");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+"waiting get resource2");
            synchronized (resource2){
                System.out.println(Thread.currentThread()+"get resource2");
            }
            }
        },"线程1").start();

        new Thread(()->{
            synchronized (resource2){
                System.out.println(Thread.currentThread()+"get resource2");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+"waiting get resource1");
                synchronized (resource1){
                    System.out.println(Thread.currentThread()+"get resource1");
                }
            }
        },"线程2").start();
    }

运行结果:

Thread[线程1,5,main]get resource1
Thread[线程1,5,main]waiting get resource2
Thread[线程2,5,main]get resource2
Thread[线程2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

上面的例子符合产生死锁的四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁如何预防和避免线程死锁?

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

        new Thread(()->{
            synchronized (resource1){
                System.out.println(Thread.currentThread()+"get resource1");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+"waiting get resource2");
                synchronized (resource2){
                    System.out.println(Thread.currentThread()+"get resource2");
                }
            }
        },"线程2").start();

输出:

Thread[线程1,5,main]get resource1
Thread[线程1,5,main]waiting get resource2
Thread[线程1,5,main]get resource2
Thread[线程2,5,main]get resource1
Thread[线程2,5,main]waiting get resource2
Thread[线程2,5,main]get resource2

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

8、 sleep() 方法和 wait() 方法对比

共同点:两者都可以暂停线程的执行。

区别:

  • sleep()方法没有释放锁,而wait()方法释放了锁。
  • wait()被常用于线程间交互/通信,sleep()被常用于暂停执行。
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)线程超时后自动苏醒。
  • sleep()是Thread类的静态本地方法,wait()方法则是Object类的本地方法。为什么这样设计呢?

8.1为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

类似的问题:为什么 sleep() 方法定义在 Thread 中?

因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

8.2可以直接调用 Thread 类的 run 方法吗?

这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。