java学习---多线程

Posted 易小顺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java学习---多线程相关的知识,希望对你有一定的参考价值。

多线程

1、 简介

学习多线程之前,我们先要了解一些相关的名词含义,线程、进程、多线程…。

 1.1 名词解释

  • 进程

     进程 Process 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是 操作系统 结构的基础,也可以认为是一个正在运行的 程序实例 。 一个进程中 至少 有一个线程存活。

  • 线程

     线程 thread操作系统 能够进行运算调度的 最小单位 。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,是一条单独的执行路径,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

  • 多线程

     多线程 multithreading,是指从软件或者硬件上实现多个线程并发执行的技术,也就是一个进行中多个线程并发执行,每个线程之间可以相互切换。

  • 异步、同步

     异步:多个线程线程同时执行 , 效率高但是数据不安全。

     同步:多个线程间排队执行 , 效率低但是安全。

  • 并发、并行

     并发:指两个或多个事件在同一个时间段内发生。

     并行:指两个或多个事件在同一时刻发生(同时发生)。

  • 守护线程、用户线程

     守护线程可以看成是用户线程的 保姆,只要有一个用户线程还在运行,所有的守护线程都会运行;只要当前进程之中没有用户线程运行了,所有 守护线程自动死亡退出 ,进程结束,退出程序。

  • 公平锁、非公平锁

     先来后到的进行取锁为公平锁,所有线程一起抢锁为非公平锁。可通过创建锁时传入 fair = true 表示为公平锁,默认是非公平锁。

 1.2 线程调度

  • 分时调度

     所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度

  • 多线程执行机制

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

 1.3 线程六个状态

  • new

     线程已经创建成功但是还未启动。 从新建一个线程对象到程序 start() 这个线程之间的状态,都是新建状态;

  • runnable

     线程正在执行中。 就绪状态下的线程在获取 CPU 资源后就可以执行 run(),此时的线程便处于运行状态。

  • blocked

     线程排队等待资源中。 线程对象调用 start() 方法后,就处于就绪状态,等到JVM里的线程调度器的调度;

  • waiting

     线程休眠。在一个线程执行了 sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。

  • TIMED_WAITING

     等待时间休眠中,时间结束后会自动进入等待状态。

  • TERMINATED

     线程退出执行的状态。 run() 方法完成后或发生其他终止条件时就会切换到终止状态。

 1.4 多线程的意义

  • 优点
优点
更好的利用cpu资源如果只有一个线程执行,则第二个任务必须等到第一个任务结束后才能进行。如果使用多线程则在主线程执行任务的同时可以切换执行其他任务,而不需要等待其他任务完成。
数据共享各个进程之间的数据是独立的,但是线程之间除了独立的资源区外,还有资源可以共享。
开发简单Java内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程的开发。
  • 缺点

    一是上下文之间的切换时会带来 多余 的开销,在某些情况下反而会降低程序的性能,没有单线程执行的效率高。

      时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。

    二是线程安全问题,虽然线程之间可以资源共享方便了设计,但是也会面临数据的安全问题。

       当多个线程需要对 公有变量 进行写操作时,如果没有进行过同步处理,后一个线程可能会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改,而导致数据紊乱。

2、 线程创建方法

 2.1 继承 Thread 类

  继承 Thread 类创建线程需要重写父类的 run() 方法,通过创建对象的方式来调用 start() 来开启线程的执行。

  • 传送门:[Thread](‪E:\\file\\object file\\全栈项目\\summary\\Object.md) 类详解

  • 代码测试

    // 继承 Thread 重写 run() 方法
    public static class Test extends Thread{
        
            @Override
            public void run() {
                try {
                    // 线程休眠 0.1 秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 打印当前线程的名
                System.out.println(Thread.currentThread().getName() + "线程正在执行");
            }
        }
    
    // 创建线程并执行
    public static void main(String[] args) {
            Test test = new Test();
            test.start();
            System.out.println(Thread.currentThread().getName() + "线程正在执行");
        }
    

    输出结果

    main线程正在执行
    Thread-0线程正在执行
    

 2.2 实现 Runable

  实现 Runable 接口创建线程需要实现接口的 run() 方法,通过创建对象的方式作为参数传给一个Thread 的对象调用 start() 来开启线程。

  • 代码测试

    // 实现 Runnable 接口并实现其 run() 方法
    public static class Test implements Runnable{
        
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程正在执行");
            }
        }
    
    public static void main(String[] args) {
        	// 创建线程任务
            Test test = new Test();
            // 创建线程传入任务
            new Thread(test).start();
            System.out.println(Thread.currentThread().getName() + "线程正在执行");
        }
    

    输出结果

    main线程正在执行
    Thread-0线程正在执行
    

 2.3 实现 Callable 接口

  实现 Callable 接口需要实现 call() 方法,该方法可以返回数据。创建对象作为构造参数传给 FutureTask 对象后,将此 FutureTask 的对象传给一个 Thread 对象并通过 start() 开启线程。返回数据可通过 FutureTask 对象的 get() 方法获得,但是调用此方法主线程会暂停知道数据返回。

  • 代码测试

    public static class Test implements Callable<Integer>{
    		// 实现接口方法
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "线程正在执行");
                int result = 1;
                for (int i = 1; i < 10; i++){
                    result *= i;
                }
                // 返回计算的结果
                return result;
            }
        }
    
    public static void main(String[] args) {
            Test test = new Test();
            FutureTask<Integer> task = new FutureTask<>(test);
            new Thread(task).start();
            try {
                // get() 方法获取子线程的返回值
                System.out.println("子线程返回的数据结果 " + task.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "线程正在执行");
        }
    

    输出结果

    Thread-0线程正在执行
    子线程返回的数据结果 362880
    main线程正在执行
    

3、 线程使用

 3.1 线程的中断

  线程的结束应该由 本身 进行决定,而不是从外部直接取消线程,这样可能会导致线程内部的一些资源没有得到释放,而一直占用系统的资源。

  3.1.1 stop()

  从线程的外部强制停止该线程的执行,方法不安全,已经过时。

  • 代码测试

    // 进行迭代输出次数
    public static class Test implements Runnable{
            @Override
            public void run() {
                int count = 0;
                while(count < 20) {
                    try {
                        // 每隔一秒进行一次
                        Thread.sleep(1000);
                        count++;
                        System.out.println("正在进行第" + count + "次累加");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    public static void main(String[] args) {
            Test test = new Test();
            Thread thread = new Thread(test);
            thread.start();
            try {
                // 主线程休眠两秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("销毁子线程");
        	// 主线程结束后从外部强制结束子线程
            thread.stop();
        }
    

    输出结果

    // 子线程被强制结束
    正在进行第1次累加
    销毁子线程
    

  3.1.2 interrupt()

  中断此线程。给线程添加中断标记,告诉线程该死了,实际线程是否死亡由开发者进行决定,此方法是从线程内部销毁线程,也就是可以将所有资源进行关闭回收后在进行线程的返回销毁。

  • 如果在线程内部不进行销毁线程的处理,调用此方法指挥抛出一个 InterruptedException 的异常,而不会回影响线程的继续执行。

  • 代码测试

    public static void main(String[] args) {
            Test test = new Test();
            Thread thread = new Thread(test);
            thread.start();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("销毁子线程");
            // 子线程进行异常捕捉,但是没有做出处理
            thread.interrupt();
        }
        public static class Test implements Runnable{
            @Override
            public void run() {
                int count = 0;
                while(count < 20) {
                    try {
                        Thread.sleep(1000);
                        count++;
                        System.out.println("正在进行第" + count + "次累加");
                    } catch (InterruptedException e) {
                        // 捕捉中断异常后没有进行线程的销毁工作
                        e.printStackTrace();
                    }
                }
            }
        }
    

    输出结果

    正在进行第1次累加
    正在进行第2次累加
    销毁子线程
    // 输出异常后子线程继续执行
    sleep interrupted
    正在进行第3次累加
    正在进行第4次累加
    ......
    
  • 在捕捉到异常后进行系统资源的释放,然后销毁线程(一个线程的死亡,也就是 run() 函数执行返回了)。

    public static class Test implements Runnable{
            @Override
            public void run() {
                int count = 0;
                while(count < 20) {
                    try {
                        Thread.sleep(1000);
                        count++;
                        System.out.println("正在进行第" + count + "次累加");
                    } catch (InterruptedException e) {
                    	// 捕捉到中断标记后进行资源释放并进行线程的退出
                        System.out.println("正在销毁占用资源");
                        System.out.println("子线程死亡退出");
                        return;
                    }
                }
            }
        }
    

    输出结果

    // 子线程成功退出
    正在进行第1次累加
    正在进行第2次累加
    销毁子线程
    正在销毁占用资源
    子线程死亡退出
    

 3.2 线程安全问题

  3.2.1 问题引出

  两个线程在使用 读写 一个公用资源数据时(如果都只是读取数据,将不会出现安全问题),可能会出现一个线程需要的数据被另一个线程修改,然后线程拿到的数据错误,但是没有检查出来。

  • 事例

    public static void main(String[] args) {
            Test test = new Test();
            // 三个线程同时执行一个任务处理
            new Thread(test).start();
            new Thread(test).start();
            new Thread(test).start();
        }
        public static class Test implements Runnable{
            private Integer count = 5;
            @Override
            public void run() {
                // 只有数据大于 0 才会进行执行
                while (this.count >= 0) {
                    System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    this.count--;
                }
            }
        }
    

    结果输出

    // 可以看到两个线程同时拿到一个数据进行处理,并且还有一个数据处理小于 0
    Thread-1拿到了数据1
    Thread-2拿到了数据1
    Thread-1拿到了数据-1
    

  3.2.2 同步代码块

  将需要 数据的代码块 synchronized 进行 加锁,同一个时间段内只有一个线程能访问该数据块,能解决多个线程争抢的问题,但是效率会变慢。synchronized 需要传入一个锁对象,任何对象都可以,但是多个线程之间必须是共用的一把锁,不然各自使用自己的锁没有效果。

  • 代码测试

    public static void main(String[] args) {
            Test test = new Test();
            new Thread(test).start();
            new Thread(test).start();
            new Thread(test).start();
        }
        public static class Test implements Runnable{
            private Integer count = 5;
            @Override
            public void run() {
                while (true) {
                	// 写数据的区域进行加锁,同一时间段内只有一个线程能够访问
                    synchronized (this){
                        if (this.count <= 0)
                            break;
                        System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        this.count--;
                    }
                }
            }
        }
    

    输出结果

    // 线程之间队进行取数据,但是会降低执行效率
    Thread-0拿到了数据5
    Thread-2拿到了数据4
    Thread-2拿到了数据3
    Thread-1拿到了数据2
    Thread-1拿到了数据1
    

  3.2.3 同步方法

  用 synchronized 进行对方法进行修饰,和同步代码块效果一致。

  • 代码测试

    public static void main(String[] args) {
            Test test = new Test();
            new Thread(test).start();
            new Thread(test).start();
            new Thread(test).start();
        }
        public static class Test implements Runnable{
            private Integer count = 5;
            @Override
            public void run() {
                while (deal() != 0) {
                }
            }
            // 用 synchronized 修饰方法,即为同步代码块
            private synchronized int deal() {
                if (this.count <= 0)
                    return 0;
                System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this

    以上是关于java学习---多线程的主要内容,如果未能解决你的问题,请参考以下文章

    学习java第19天个人总结

    Java多线程学习笔记

    第十周java学习总结

    Java学习多线程2

    Java多线程——Lock&Condition

    java学习——多线程