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

     线程阻塞。处于运行状态的线程可能会因为某些原因失去 CPU 的执行权,暂时进入阻塞状态,此时 CPU 不会分配给线程执行时间,直到线程重新进入就绪状态;

  • waiting

     线程等待。线程调用无时间参数限制的方法后,如 wait()、join() 等方法,就会进入等待状态,线程不能立即争抢 CPU 使用权,只有等其他线程进行唤醒后才可以。

  • TIMED_WAITING

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

  • TERMINATED

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

 1.4 多线程的意义

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

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

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

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

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

2、 线程创建方法

 2.1 继承 Thread 类

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

  • 传送门[Thread类详解

  • 代码测试

    // 继承 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.count--;
                return 1;
            }
        }
    

    输出结果

    Thread-0拿到了数据5
    Thread-1拿到了数据4
    Thread-1拿到了数据3
    Thread-1拿到了数据2
    Thread-2拿到了数据1
    
  3.2.4 显示加锁

  通过创建 Lock 对象调用 lock() 方法来显示加锁, unlock() 显示解锁。

  • 代码测试

    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;
            private Lock lock = new ReentrantLock();
            @Override
            public void run() {
                while (true) {
                    // 线程显示加锁
                    this.lock.lock();
                    if (this.count <= 0){
                        this.lock.unlock();
                        break;
                    }
                    System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count);
                    this.count--;
                    // 线程显示解锁
                    this.lock.unlock();
                    try {
                        // 让其他线程更容易的拿到锁
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    

    输出结果

    // 输出结果
    Thread-1拿到了数据5
    Thread-2拿到了数据4
    Thread-0拿到了数据3
    Thread-2拿到了数据2
    Thread-1拿到了数据1
    

 3.3 线程死锁问题

  3.3.1 问题引出
  • 生活中的 死锁 事件

    张三和李四一个在北京,一个深圳,有一天他们两怄气,张三说如果李四不从深圳出发去北京找他,他就永远不出门;李四说如果张三不从北京出发去深圳找他,他也永远不出门。
    
  • 最终的结果就是张三和李四都一辈子呆在家里面,不出门。

    一个流程需要两把锁才能解开,但是同时两个不同的线程各自拿到了一把锁,都在等着对方先撒开锁,这就导致了死锁,线程卡死。

  3.3.2 代码测试
public static void main(String[] args) {
        ZhangSan zhangSan = new ZhangSan();
        LiSi liSi = new LiSi();
        new Test(zhangSan, liSi).start();
        liSi.say(zhangSan);
    }

    static class Test extends Thread {
        private ZhangSan ZhangSan;
        private LiSi liSi;
        Test(ZhangSan ZhangSan, LiSi liSi) {
            this.ZhangSan = ZhangSan;
            this.liSi = liSi;
        }

        @Override
        public void run() {
            ZhangSan.say(liSi);
        }
    }
    static class ZhangSan {
        public synchronized void say(LiSi liSi) {
            System.out.println("你先从深圳过来找我");
            liSi.fun();
        }
        public synchronized void fun() {
            System.out.println("你不先从深圳过来找我我就永远不出门");
        }
    }

    static class LiSi {
        public synchronized void say(ZhangSan zhangSan) {
            System.out.println("你先从北京过来找我");
            zhangSan.fun();
        }
        public synchronized void fun() {
            System.out.println("你不先从北京过来找我我就永远不出门");
        }
    }

输出结果

// 线程卡死
你先从北京过来找我
你先从深圳过来找我

 3.4 线程通信问题

  • 往往我们利用多线程都不只是为了执行一个单一的任务,而是需要进行通信,共同完成一个任务。

    模拟现实生活中的厨师和服务员的关系,厨师每炒一个菜,服务员就进行一次端菜,没有菜时,服务员不能进行端菜行为。
    
  • 代码测试

    public static void main(String[] args) {
            Food food = new Food();
            Cook cook = new Cook(food);
            Waiter waiter = new Waiter(food);
            new Thread(cook).start();
            new Thread(waiter).start();
        
        }
        static class Cook extends Thread {
            private Food food;
            public Cook(Food food) {
                this.food = food;
            }
    
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if(i % 2 == 0){
                        food.set("老干妈拌饭", "辛辣");
                    }else {
                        food.set("黑米粥", "无味");
                    }
                }
            }
        }
    
        static class Waiter extends Thread {
            private Food food;
            public Waiter(Food food) {
                this.food = food;
            }
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    food.get();
                }
            }
        }
        static class Food {
            private String name;
            private String taste;
            private boolean flag = true;
            public synchronized void set(String name, String taste) {
                if (flag) {
                    this.name = name;
                    this.taste = taste;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    flag = false;
                    this.notifyAll();
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
    
            }
    
            public synchronized void get() {
                if (!flag) {
                    System.out.println("服务生端走 " + this.name + " " + this.taste);
                    flag = true;
                    this.notifyAll();
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
    
            }
        }
    

    输出结果

    服务生端走 老干妈拌饭 辛辣
    服务生端走 黑米粥 无味
    服务生端走 老干妈拌饭 辛辣
    服务生端走 黑米粥 无味
    服务生端走 老干妈拌饭 辛辣
    服务生端走 黑米粥 无味
    服务生端走 老干妈拌饭 辛辣
    服务生端走 黑米粥 无味
    服务生端走 老干妈拌饭 辛辣
    服务生端走 黑米粥 无味
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值