线程详解及面试题(一)

本文深入解析了程序、进程和线程之间的关系,包括它们的定义、动态变化过程、线程核心概念,以及创建线程的不同方式,如继承Thread类、实现Runnable接口和Callable接口。重点讨论了线程的状态转换、调度、并发控制和线程间的协作,适合理解多线程编程的初学者。
摘要由CSDN通过智能技术生成

程序、进程、线程的关系

  • 程序是指令和数据的有序集合,其本身没有任何的运行意义,是一个静态的概念。
  • 进程是执行程序一次执行的过程,是一个动态的概念,是系统分配资源的单位。
  • 通常在一个进程中可以包含多个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位。

线程核心概念

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程(垃圾回收线程)
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

创建线程的三种方式

第一种创建方式(继承Thread类)

package com.wjr.Demo1;

/**
 * @Auther:TrueLies
 * @Date:2021/4/25-21:18
 * @Description:com.wjr.Demo1
 * @version:1.0
 */
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class Demo extends Thread{
    @Override
    public void run(){
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("java代码--"+i);
        }
    }

    public static void main(String[] args) {
        //main线程,主线程

        //创建一个线程对象
        Demo demo = new Demo();

        //调用start()方法开启线程
        demo.start();//调用start方法,同时执行

        for (int i = 0; i < 20; i++) {
            System.out.println("实习实习实习"+i);
        }
    }
}
  • 注意:线程开启不一定立即执行,由CPU调度执行

第二种创建方式(实现Runnable接口)

package com.wjr.Demo1;

/**
 * @Auther:TrueLies
 * @Date:2021/4/26-18:34
 * @Description:com.wjr.Demo1
 * @version:1.0
 */
//创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable{
    @Override
    public void run(){
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("java代码--"+i);
        }
    }

    public static void main(String[] args) {
        //创建一个runnable接口的实现类对象
        TestThread2 testThread2 = new TestThread2();

        //创建线程对象,通过线程对象来开启我们的线程,代理
//        Thread thread = new Thread(testThread2);
//        thread.start();
        new Thread(testThread2).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("实习实习实习"+i);
        }
    }
}
两种方法的区别
  • 第一种方法继承了Thread类,第二种方法实现了Runnable接口
  • 第一种方法直接调用start方法,第二种方法从Thread类构造器中的实现类对象调用start方法
  • 本质一样,因为第一种方法中Thread类也是继承了Runnable接口
  • 建议使用实现Runnable接口的方法,避免了单继承的局限性,方便一个对象被多个线程使用

第三种创建方式(实现Callable接口)

下载网络图片:

package com.wjr.Demo2;

import com.wjr.Demo1.TestThread;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/**
 * @Auther:TrueLies
 * @Date:2021/4/26-21:55
 * @Description:com.wjr.Demo2
 * @version:1.0
 */
public class TestCallable implements Callable<Boolean> {
    private String url;//网络图片地址
    private String name;//保存的文件名

    public TestCallable(String url, String name) {
        this.name = name;
        this.url = url;
    }

    //下载图片线程的执行体
    @Override
    public Boolean call() {
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
        return true;
    }

    //主线程
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://i2.hdslb.com/bfs/archive/41575b1afec5305fa329d1cdf853373955d4e62a.jpg@380w_240h_100Q_1c.webp","狂神说java");
        TestCallable t2 = new TestCallable("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/13/kuangstudy5584e809-f8cd-42c5-b8e2-cc2e69a2f9a8.png","狂神");
        TestCallable t3 = new TestCallable("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/13/kuangstudy4abb3661-55d7-4e27-94bb-f666134b92ba.jpg","狂神SSM整合");

        //创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);

        //获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        //关闭服务
        ser.shutdownNow();
    }
}

//下载器
class WebDownLoader {
    //下载方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }
    }
}
区别
  • 实现Callable接口:

    • 可以定义返回值

    • 可以抛出异常

    • 重写call方法

    • 打印方式不同:

      • 创建执行事务

      • ExecutorService ser = Executors.newFixedThreadPool(3);//3代表线程数
        
      • 提交执行

      • Future<Boolean> r1 = ser.submit(t1);//boolean类型
        
      • 获取结果

      • boolean rs1 = r1.get();
        
      • 关闭服务

      • ser.shutdownNow();
        
  • 继承Thread类、实现Runnable接口:

    • 重写run()方法
    • 直接使用start方法开启线程

线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

新建状态(NEW)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,
并初始化其成员变量的值

就绪状态(RUNNABLE)

当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序
计数器,等待调度运行。

运行状态(RUNNING)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

等待阻塞(o.wait->等待对列):

运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

同步阻塞(lock->锁池)

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

其他阻塞(sleep/join)

运行(running)的线程执行 Thread.sleep(long ms)或t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。

线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。正常结束

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

线程休眠

  • sleep(时间):指定当前线程阻塞的毫秒数
  • sleep存在异常需要抛出:InterruptedException
  • sleep时间达到后,线程进入就绪状态
  • sleep可以模拟网络延时、倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁
package com.wjr.state;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Auther:TrueLies
 * @Date:2021/4/27-18:06
 * @Description:com.wjr.state
 * @version:1.0
 */
//模拟倒计时
public class TestSleep {
    public static void main(String[] args) {
        //打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
        while (true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());//更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //模拟倒计时
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<0){
                break;
            }
        }
    }
}

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 让线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功,看CPU的调度
package com.wjr.state;

/**
 * @Auther:TrueLies
 * @Date:2021/4/27-18:15
 * @Description:com.wjr.state
 * @version:1.0
 */
public class TestYield{
    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }

}
class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

Join合并线程

  • 在main走到200之前,还会有线程并发得存在
  • 在main执行到200之后,join方法会正式进入,等到join的线程走完之后,才会继续走main线程
package com.wjr.state;

/**
 * @Auther:TrueLies
 * @Date:2021/4/27-18:23
 * @Description:com.wjr.state
 * @version:1.0
 */
public class TestJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("线程vip来了" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动我们的线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        //主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                thread.join();//插队
            }
            System.out.println("main" + i);
        }
    }
}

sleep()和wait() 有什么区别?

  • 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
  • sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
  • 在调用 sleep()方法的过程中, 线程不会释放对象锁。
  • 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程 。

Thread类中的yield方法有什么作用?

Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值