程序员

Java 进阶:多线程1

2020-12-04  本文已影响0人  架构小菜

目录
一、概述
二、Thread 类
三、创建线程:继承 Thread 类
四、创建线程:实现 Runnable 接口
五、线程优先级
六、线程的生命周期
七、同步代码块


一、概述

1. 进程和线程

在没有出现线程之前,进程既是操作系统进行资源分配的基本单位,又是调度的基本单位

操作系统发展:单道批处理操作——>多道批处理操作系统——>分时操作系统(多进程的)——>线程
批处理:程序在执行过程中,不会响应用户的请求
单道批处理操作:一次只能运行一个程序,如果要在计算机运行多个程序,这多个程序,只能一个一个的顺序执行,如果这个正在运行的程序,在运行过程中,执行了一些非常耗时的IO操作(传输数据的过程是没有使用到cpu),这样一来,cpu就闲下来了,但是cpu是计算机中,最为昂贵的
多道批处理操作系统:同时运行多个程序,显著的提高了cpu的利用率,但是我们一旦一个,程序运行起来,都是是需要使用计算机资源的
分时操作系统:每一个进程,有一个固定的时间片,在运行一个固定的时间片后,紧接着轮到下一个进程运行(切换)

2. 线程调度

1. CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
2. 多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

3. 线程控制

//让主线程,休眠3秒
Thread.sleep(3000);
//sleep新的写法
TimeUnit.SECONDS.sleep(3);
System.out.println("睡醒了");
SecondThread sec = new SecondThread("zs");
SecondThread thr = new SecondThread("lsii");
SecondThread fou = new SecondThread("wangwu");

//启动这三个线程对象
sec.start();
//sec线程上调用join方法 ,让当前线程(main)等待sec线程执行完毕
sec.join();
thr.start();
fou.start();

只是让当前线程放弃cpu执行权,但是不能阻止它放弃后继续抢夺cpu执行权

SecondThread sec = new SecondThread("zs");
SecondThread thr = new SecondThread("lsii");
//将sec和thr线程设置为守护线程
sec.setDaemon(true);
thr.setDaemon(true);
//启动
sec.start();
thr.start();
//之后发现sec和thr是守护线程,就会中断

1. 如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 等阻塞方法处于阻塞状态,它还将收到一个 InterruptedException
2. 中断一个不处于活动状态的线程不需要任何作用。
中断一个不处于阻塞状态的线程,没有其他任何效果

public class Test {
    public static void main(String[] args) {
        FiveThread fiveThread = new FiveThread();
        fiveThread.start();
        //在主线程中,终止fiveThread,休眠状态
        fiveThread.interrupt();
    }
}

// 睡5秒之后,再让该线程输出一句话
class FiveThread extends Thread{
    @Override
    public void run() {
        try {
            //假如申请了很多的系统资源
            TimeUnit.SECONDS.sleep(5);
            System.out.println("FiveThread 睡醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
            //异常的意义:即使我的线程被异常终止,我也可以保证资源的正常释放
        }
    }
}

//会抛出 java.lang.InterruptedException: sleep interrupted

4. JAVA程序的运行原理

public class ThreadDemo {
    public static void main(String[] args) {
        //利用垃圾回收器来证明
        while(true) {
            //这里虽然一直在堆空间中,创建数组对象,
            // 但是始终没有耗尽堆空间,就是因为垃圾回收器,
            // 在另外一个线程中,帮我们回收垃圾,所以才不会耗尽heap内存
            // 从而证明,jvm是线程的
            int[] ints = new int[1024];
            ints = null;
        }
    }
}

二、Thread 类

1. 概述

2. 构造方法

3. 常用方法

4. 创建新执行线程的两种方法

虽然实现线程有两种方式,其实从客观来讲,线程本身只代表独立的执行路径, 执行的具体内容其实是Task本身,和执行路径的实现本身没有联系;只是我们开发者,想将一个task放在某条独立的执行路径(Thread 类对象,也就是一个线程中)来运行


三、创建线程:继承 Thread 类

线程对象调用 run方法 不开启线程。仅是对象调用方法
线程对象调用 start 开启线程,并让 jvm 调用 run 方法在开启的线程中执行

//测试类
public class Test {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread mt = new MyThread("新的线程!");

        //错误启动线程,这只是普通的方法调用
        //firstThread.run();

        //开启新线程
        mt.start();

        //再次启动一个线程,会抛异常IllegalThreadStateException 
        //因为一个线程对象只能启动一次,
        // 如果同一个线程对象,启动多次,就会抛出异常
        //firstThread.start();
        //只能创建一个新的对象
        new MyThread("第二个线程!").start();
        //获取主线程的名字
        System.out.println(Thread.currentThread().getName()+":主线程!");
    }
}

//自定义线程
class MyThread extends Thread {
    //定义指定线程名称的构造方法
    public MyThread(String name) {
        //调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }

    //重写run方法,完成该线程执行的逻辑
    @Override
    public void run() {
        //获取线程的名字getName()
        System.out.println(getName() + ":正在执行!");
    }
}

四、创建线程:实现 Runnable 接口

1. Runnable 接口的构造方法

2. 创建线程的步骤

Thread 类的构造函数:

  1. Thread(): 分配新的 Thread 对象
  2. Thread(String name):分配新的 Thread 对象,将指定的 name 作为其线程名称
//测试类
public class Test {
    public static void main(String[] args) {
        //创建实现 Runnable 接口的子类对象
        MyRunnable myrunnable = new MyRunnable();
        //创建Thread实例,在Thread的构造方法中传递Runnable实例
        //Runnable就代表在 Thread 上运行的任务
        Thread thread = new Thread(myrunnable);
        //开启线程
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:正在执行!"+i);
        }
    }
}

//自定义线程执行任务类
class MyRunnable implements Runnable{
    //定义线程要执行的run方法逻辑
    //run方法,不能抛出编译时异常
    //run方法,没有参数没有返回值
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("我的线程:正在执行!"+i);
        }
    }
}

3. 实现 Runnable 接口的原理

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务

4. 两种方式的比较

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象;线程分为两部分,一部分线程对象,一部分线程任务。


五、线程优先级

1. 概述

2. 线程优先级的范围

MAX_PRIORITY 10 //最大优先级
MIN_PRIORITY 1 //最小优先级
NORM_PRIORITY 5 //默认优先级

//使用
thread1.setPriority(Thread.MAX_PRIORITY); //设置
thread2.setPriority(Thread.MIN_PRIORITY);
System.out.println(thread1.getPriority());//获取

3. 注意!

也就是说 jvm 在实际调度线程的时候,它使用的优先级可能不仅仅包含我们给每个线程所设置的静态优先级,可能还考虑了其他的很多因素(各个线程的运行状态),所以我们所设置的优先级对于 jvm 只起一个参考作用


六、线程的生命周期

1. 新建状态

2. 就绪状态

3. 运行状态

4. 阻塞状态

5. 死亡状态

线程的生命周期

七、同步代码块

1. 问题引入——线程安全问题

1. 多线程运行环境
2. 多线程访问线程共享数据(存在共享数据)
3. 访问共享数据的操作不是原子操作
注:原子操作:不可分割的操作 ,相当于一次性完成的操作

2. 同步代码块

线程异步:线程之间,互不干扰,各自独立运行(我走我的,你走你的)

synchronized(对象){
    //critical section
        对共享资源的访问的代码
}

1. 同步代码块中所使用的对象,称之为锁对象 ——> 锁的角色
锁对象中,有一个标志位:可以表示两种状态,加锁解锁
2. 持有锁的是线程(一个线程给一个锁对象加锁,我们就说这个线程持有了锁对象)
3. 同步代码块何时给锁对象加锁?进入同步代码块的同时,就给锁对象加锁
4. 线程何时释放持有的锁? 当执行完同步代码块的时候,就会释放同步代码块的锁对象
5. 判断和修改锁对象标志位的操作,这是由 jvm 保证一定是原子操作
6.** 锁对象,究竟是什么对象? java语言中任意一个对象**,都可以充当锁对象的角色(因为任意一个对象中都有一个表示加锁,解锁状态的标志位)

虽然锁对象可以是任意对象,但是针对同一个(同样的多个共享变量) 的所有操作,都必须保证在同步代码块中,使用同一个锁对象,才能避免线程安全问题。

3. 同步方法

静态方法是否可以定义为同步方法?
静态的同步方法, 在方法声明上加上static synchronized
它的所使用的锁对象是:当前类的字节码文件对象类名.class

上一篇 下一篇

猜你喜欢

热点阅读