线程(线程基本概念、java实现多线程、使用多线程、线程的生命周期、线程同步、线程死锁)

(一)线程基本概念

一、 程序, 进程, 线程的概念

程序: 使用某种语言编写一组指令(代码)的集合,静态的

进程: 运行的程序,表示程序一次完整的执行, 当程序运行完成, 进程也就结束了

个人电脑: CPU 单个, 双核, CPU的时间分片, 抢占式

每个独立执行的程序称为进程

每个进程都有自己独立的内存空间, 进制之间的通信很困难

在操作系统中进程是进行系统资源分配、调度和管理的最小单位,进程在执行过程中拥有独立的内存单元。比如:Windows采用进程作为最小隔离单位,每个进程都有自己的数据段、代码段,并且与别的进程没有任何关系。因此进程间进行信息交互比较麻烦

线程: 一个进程中,可以同时有多条执行链路, 这些执行链路称为线程, 线程是CPU的调度与分配最小单位, 同一个进程多个线程共享这个进程的内存资源: JVM内存模型

二、进程与线程区别:

进程包含线程, 一个进程包含多个线程, 一个进程最小必须包含一个线程(主线程,main线程), 运行main()方法的时候, 创建了一个main线程

一个进程死亡了, 这个进程中所有的线程死亡

线程销毁,进程未必会关闭

并行: 多CPU执行各种不同任务,

并发: 一个CPU执行不同的任务(多个线程)

多线程速度快吗? 看CPU的核数,个数

三、多线程

多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,形成多条执行线

(二)java实现多线程

在jdk.1.5之前: 创建线程的方式: 两种:

  • 继承Thread类

  • 实现Runnable接口

在JDK1.5之后: 多加了两种:

  • 实现 Callable接口

  • 线程池

第一种方式: 继承Thread

  1. 编写一个类继承Thread,该类是线程类

  2. 重写run(), 编写该线程需要完成的任务

  3. 创建线程类对象

  4. 调用start()方法,启动线程

注意事项:

  1. 线程启动,一定是调用start() , 不是调用run(), 如果直接调用run() ,只是方法的调用,没有创建线程

  2. 一个线程一旦启动,就不能重复启动

模拟使用多线程同时下载多个图片(从网络):

使用第三方的依赖: commons-io.jar

java项目添加jar包

  1. 在项目下创建一个lib的文件夹

  2. 拷贝需要jar

  3. 设置lib目录为库

/**
 * 实现线程的第一种方式:
 * 继承Thread
 */
public class MyThread1 extends Thread{
​
    //private static Object o = new Object();
    public MyThread1(String name) {
        super(name);
    }
​
    //重写run()方法
    @Override
    public void run() {
        //线程完成的功能
        for (int i = 1; i <=10 ; i++) {
            //得到当前正在运行的线程
           //Thread.currentThread()
            //getName() 得到线程的名字
            System.out.println(Thread.currentThread().getName()+":输出"+i);
        }
    }
}

第二种方式: 实现Runnable接口

启动线程: 都必须借助Thread的start()

Runnable实现类: 就是一个线程的任务类

继承Thread与Runnable接口的区别:

  1. 继承Thread类, 这个线程类就不能再继承其他类, 实现Runnable接口, 可以再继承其他类

  2. 实现Runnable接口, 可以让多个线程共享这个Runnable的实现类对象

  3. 继承Thread类,启动简单, 实现Runnable接口, 必须依赖Thread类的start()方法

推荐 实现Runnable接口

/**
 * 线程的第二种实现方式: 实现Runnable接口
 */
public class MyRunnable implements  Runnable {
    @Override
    public void run(){
        //线程需要完成的任务
        for (int i = 1; i <=10 ; i++) {
     System.out.println(Thread.currentThread().getName()+":输出"+i);
        }
    }
}

第三种方式: 实现Callable接口

  1. 第一步编写一个类实现Callable接口,重写call()方法

  2. 启动线程

  3. 创建Callable接口实现类对象

  4. 创建一个FutureTask对象, 传递Callable接口实现类对象, FutureTask异步得到Callable执行结果, 提供get() FutureTask 实现Future接口( get()) 实现Runnable接口

  5. 创建一个Thread对象, 把FutureTask对象传递给Thread, 调用start()启动线程
     

/**
 * 线程的第三种实现方式:
 *   实现Callable接口
 */
public class MyCallable implements Callable {
    @Override
    public String call() throws Exception {
        //线程睡眠 单位: 毫秒
        Thread.sleep(10000);                   System.out.println(Thread.currentThread().getName()
        +"执行完成");
        return "callable";
    }
}

(三)使用多线程

一、启动线程

1.实现线程的第一种方式:*继承Thread

抢占式执行,谁抢到就是谁的,main函数最先执行(多数情况下)

//启动线程
        //创建线程对象
        MyThread1 t1 =new MyThread1("t1");
        MyThread1 t2 =new MyThread1("t2");
        MyThread1 t3 =new MyThread1("t3");
        //调用start()启动线程, 线程与其他线程抢占cpu资源, 谁抢到,执行谁的run()中的代码
        t1.start();
        t1.start();
        t2.start();
        t3.start();
​
        System.out.println("main线程执行完成...");

2.线程的第二种实现方式: 实现Runnable接口

public static void main(String[] args) {        
//创建任务对象        
MyRunnable task = new MyRunnable();        
//创建线程: Thread        
Thread t1 = new Thread(task, "t1");       
Thread t2 = new Thread(task, "t2");        
//启动线程        
t1.start();        
t2.start();    
}

3.线程的第三种实现方式:实现Callable接口

get(); //阻塞的 main最后执行

MyCallable callable = new MyCallable();
        //再次封装 FutureTask
        FutureTask task = new FutureTask<>(callable);

        Thread t1 = new Thread(task, "t1");
        //启动线程
        t1.start();

        //获取结果
       // String rs = task.get(); //阻塞的,  是main线程在执行

        //设置一个超时时间, 一旦到达超时时间, 停止执行,抛一个异常
        //String rs = task.get(1, TimeUnit.SECONDS);

        //取消执行
        task.cancel(true);
        //System.out.println(rs);
        System.out.println("main线程执行完成!!");

二、下载多个图片

public static void main(String[] args) {
        String url1="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbkimg.cdn.bcebos.com%2Fpic%2Fb2de9c82d158ccbf8ee447ca19d8bc3eb03541e2&refer=http%3A%2F%2Fbkimg.cdn.bcebos.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1663384414&t=a35d40b858d92ee6817f17ba8bc0039e";
        String url2="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201312%2F26%2F20131226151644_KyK5Q.jpeg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1663384415&t=18e926f5199c2e6f5bdf0b37a6ea17de";
        MyThread2  t1 = new MyThread2(url1,"1.jpg","t1");
        MyThread2  t2 = new MyThread2(url2,"2.jpg","t2");
        t1.start();
        t2.start();
    }
public class MyThread2 extends  Thread {
    private String url;  //网址
    private String filename; //保存文件名

    public MyThread2(String url, String filename,String name) {
        super(name);
        this.url = url;
        this.filename = filename;
    }

    @Override
    public void run() {
        //下载  从网络下载
        downloadFile();
        System.out.println(Thread.currentThread().getName()+"线程下载完成");
    }

    public void downloadFile(){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(filename));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(四)线程的生命周期

新生状态:调用start()方法之前都处于出生状态

就绪状态:调用start()方法后处于就绪状态(又称为可执行状态)

运行状态:得到系统资源后处于运行状态

阻塞状态:如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入输出结束时线程进入了就绪状态。

线程阻塞:

  1. 同步阻塞

  2. sleep() Thread的方法 时间到自动醒,释放cpu资源,不释放锁, join() 阻塞

  3. yield() 礼让, 释放cpu资源, 与其他线程抢CPU资源

  4. wait() 等待: Object类中的方法, 一定等待唤醒(notify() notifyAll()), 释放cpu资源,释放锁资源, 线程通信

  5. suspend()和resume(),由jdk1.0提供,jdk1.2之后就被放弃了,它也是让线程暂停,但是它不释放资源,导致死锁出现

t1.yield(); //礼让
Thread.sleep(1000);
thread4.join();
//等方法会让线程进入阻塞状态

死亡状态:当线程run()方法执行完毕时线程进入死亡状态。

(五)线程同步

  1. 同步方法

在方法上添加一个synchronized修饰符, 往对象上加锁

非静态同步方法:

锁加在 this(对象)

静态同步方法:

锁加在类.class(对象)

锁的释放: 当把同步方法执行完之后,马上释放锁

同步方法: 锁住的代码范围整个方法, 锁的控制粒度太宽

  1. 同步代码块

    public 返回值类型 方法名(){
      //...
      synchronized(锁对象){
          //锁住的代码
      }
      //...
    }

    特点:1.锁对象任意的, this. 类.class,...      2. 锁住的只是方法的一部分

模拟: 多个窗口出售某趟车的车票

多个窗口: 多个线程

卖票: 任务 Runnable

共享资源: 票数

问题:

  1. 同一张票卖了多次

  2. 超卖

面试题:

线程A 访问C类同一个对象的一个非静态的同步方法,

在同一个时间点: 线程B 能访问这个C类同一个对象的那些方法(不需要等待)

非同步方法,静态同步方法

同步缺点:

  1. 效率低,排队

  2. 容易造成死锁

(六)线程死锁

多个线程,相互之间需要对方的锁, 但是又不释放自己的锁,造成程序卡住, 这些线程都在等待,等待对方的锁,

死锁形成的原因: 互斥锁,排他锁

1> 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2> 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3> 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占用。

4> 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路

形成死锁的这4个条件缺一不可, 避免死锁: 打破4个条件 的一个就可以

项目一定避免出现死锁:

解决死锁:

  1. 使用完某个锁,马上释放

  2. 多个线程获取锁的顺序是一致, A线程获取锁: a-->b-->c

B线获取锁: a-->b-->v

模拟死锁:

A线程 先获取objA锁, 再获取objB锁, 在获取objB锁时,不释放objA锁

B线程 先获取objB锁, 再获取objA锁, 在获取objA锁时,不释放objB锁

 

你可能感兴趣的:(java基础1,jvm,java,开发语言)