玩转Java多线程

进程与线程

进程:是执行中的一段程序,一旦程序被载入到内存中并准备运行,它就是一个进程。进程是表示资源分配的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

举个例子,手机中的微信,支付宝,淘宝,酷狗。一旦你点开这个软件(其实就是一些代码),它就会加载到你的手机内存中运行,当加载到内存,就会开启一个进程。

线程:一个进程里边每个执行任务就是一个线程。线程是进程最小的调度单位。

举个例子,你打开酷狗音乐,要做什么操作?搜索音乐,当你执行搜索音乐这个任务,就会有一条线程去执行,并返回结果。

顺序,并发和并行

顺序:排着队,按序执行。

吃饭的时候,电话响了,我很淡定的吃完饭,再去接电话。这是顺序

并发:两件或多件事情同时发生

我现在非常饿,但是在吃饭的时候,电话响了。我起身去接,和好基友说了一会话,让他等一会,又去吃饭。吃了两口饭,再去听他说什么。我不断的在吃饭和接电话这两件事中切换。

并行,两件或多件事情同时执行

我现在非常饿,但是在吃饭的时候,电话响了。我起身去接,好基友失恋了,不能像刚才那样来回切换了,要是被他知道他说的话我没听,那还了得!但我现在很饿,于是我一边吃饭,一边听他的电话。这是并行。

生活如此,在程序中更是如此,遇到同时发生的事情简直就是家常便饭!在计算机中 CPU就对应上文中的“我”,第一条线程就对应“吃饭”,第二条线程对应“接电话”。可以同时处理多少个线程,是由cpu的核心数来决定的,单核可以处理一个线程,双核可以同时处理两个线程,两个核心同时处理自己的线程,互不干扰!

对天文学感兴趣的,可能时常听说用计算机模拟一个陨星撞击地球的画面。再通俗一点,就是我们经常玩的游戏,吃鸡,荣耀。游戏里边还原我们的现实世界。

那现在我们就用线程模拟一下火车站买票的场面。

时间:2222年2月2号,22点22分22秒

地点:22点火车站

目的地:零点火车站

售票窗口:2个

票的余量:30张

排队人数:100

天气:鹅毛大雪

体感温度:零下7摄氏度

我吸溜着鼻涕来到22点火车站,上面龙飞凤舞写着五个大字“22点火车站”。购票处的滚动条显示着“请注意,去往零点火车站的车票还有 30 张票,请大家依次排队购票!”,此时1号窗口排队19人,2号窗口排队15人。于是我轻车熟路的来到1号售票窗口。你问我为什么不去二号窗口?好吧,因为那前边有个哆嗦老太太,她把身上的零钱已经数了三遍了,应该快数清楚了。

余票:

package com._12306.SellTickets;

import java.util.concurrent.TimeUnit;

class Ticket {

    //总票数
    private int number = 30;

    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,去往零点火车站剩余:" + number+"张票");
        }
    }

    public int getNumber() {
        return number;
    }
}

 售票窗口:

package com._12306.SellTickets;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 "+ticket.getNumber()+" 张票,请大家依次排队购票!");

        //售票员A
        new Thread(() -> { for (int i = 1; i < 40; i++) ticket.sale(); }, "窗口1").start();

        //售票员B
        new Thread(() -> { for (int i = 1; i < 40; i++) ticket.sale(); }, "窗口2").start();

        if (ticket.getNumber() == 0){
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

 模拟结果:

好吧,可能你看程序结果看不太懂,我来给你解释一下当时事情的经过。

  1. 窗口1一直卖了24张票,窗口2一直在等老太太。此时余票6张
  2. 窗口1的颤巍老头看到还有6张票,慢腾腾的伸手拿钱。窗口2的老太太买到了票并离开,此时余票5张,老太太后面的人赶紧前去购买。
  3. 窗口1的颤巍老头拿出钱,抬头一看,余票0张。忍不住仰天长叹,“哆嗦老太要是再数一会就好了!”

上边买票的模拟,两个窗口就是两个线程。那么问题来了,线程的创建方式有几种呢?就是我想开启多个窗口,在程序中如何开启?

有4种方式

  • 继承Thread
  • 实现Runnable
  • 实现Callable
  • 通过线程池获取

上文用的是使用匿名内部类实现Runnable接口。这里着重讲一下使用Callable创建线程。

博主今年28,没有女朋友,但是博主认识一个小朋友,名叫FutureTask,听说她的小姨25,名叫Thread,目前单身。于是第二天我带他去吃肯德基,通过他我认识了他的父母(Runnable),然后通过他的父母认识了他的小姨。(上面的故事都是假的,只有单身是真的)

上文中的小男孩就是FutureTask,小男孩带我去他家见了他的父母Runnable,他的父母见了我,觉得我很不错,就邀请她妹妹Thread,明天一起去看电影。结果把这机会给了我,让我代她前去。于是我就认识了Thread,和她结下了不解之缘。

程序

package com._12306.SellTickets;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        //Thread:目标
        new Thread(

                //FutureTask:小男孩
                new FutureTask<Integer>(

                //Callable:我
                new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        for (int i = 1; i < 40; i++)
                            ticket.sale();

                        //我的手机号
                        return 123456789;
                    }
                })
        ,"窗口1").start();


        //Thread:目标
        new Thread(

                //FutureTask:小男孩
                new FutureTask<Integer>(

                        //Callable:我
                        new Callable<Integer>() {
                            @Override
                            public Integer call() throws Exception {
                                for (int i = 1; i < 40; i++)
                                    ticket.sale();

                                //我的手机号
                                return 123456789;
                            }
                        })
                ,"窗口2").start();


        if (ticket.getNumber() == 0) {
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

这里用的是call方法来代替run方法,而且还有返回值,比如你执行一条sql,开启一条线程并不想把返回结果在线程中处理,而是想让他变成公用的资源,就可以使用callable返回结果并放入main线程。

它们的启动方式都是start(),至于为什么用run()不能启动线程,Thread源码中有一点解释

public synchronized void start() {
        /**
         * 由系统线程调用此方法,将来添加到run方法的新功能都会添加到虚拟机中
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

       
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

重写了run() 方法,没有通过start() 方法调用,就相当于在main线程中调用了一个普通方法。

重写了run()方法,调用了start()方法,start()方法就会调用操作系统的本地方法,创建一个比肩main线程的新线程。

线程创建后的几种状态

在Thread类中有一个枚举类,里边标记了线程的6种状态

    public enum State {
        /**
         * 尚未启动的线程的线程状态。
         */
        NEW,

        /**
         * 可运行线程的线程状态。 处于可运行状态的线程正在 Java 虚拟机中执行,
         * 但它可能正在等待来自操作系统的其他资源,例如处理器。
         */
        RUNNABLE,

        /**
         * 线程阻塞等待监视器锁的线程状态。 
         * 处于阻塞状态的线程在调用 {@link Object#wait() Object.wait}
         * 后正在等待监视器锁进入同步块/方法或重新进入同步块/方法。
         */
        BLOCKED,

        /**
         * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:{@link Object#wait()
         * Object.wait} 没有超时 {@link #join() Thread.join} 没有超时    
         * {@link LockSupport# park() LockSupport.park} 处于等待状态的线程正在等待另一个
         * 线程执行特定操作。例如,在一个对象上调用了 Object.wait() 的线程正在等待另一个线
         * 程在该对象上调用 Object.notify() 或 Object.notifyAll() 。调用 Thread.join()
         * 的线程正在等待指定的线程终止。
         */
        WAITING,

        /**
         * 具有指定等待时间的等待线程的线程状态。由于调用具有指定正等待时间的以下方法之一,线程处
         * 于定时等待状态:{@link #sleep Thread.sleep} {@link Object#wait (long)
         * Object.wait} 超时{@link #join(long) Thread.join} 超时{@link 
         * LockSupport#parkNanos LockSupport.parkNanos}{@link LockSupport#parkUntil
         * LockSupport.parkUntil}
         */
        TIMED_WAITING,

        /**
         * 终止线程的线程状态。线程已完成执行。
         */
        TERMINATED;
    }

当我们new Thread(),线程处于新生状态,对应的状态为NEW

当我们调用了start(),线程处于就绪状态,对应的状态为RUNNABLE

当线程抢到了CPU的执行权,线程处于运行状态,对应的状态为RUNNABLE

当线程看到了synchronized关键字,线程处于阻塞状态,对应的状态为BLOCKED

当线程自己调用了wait(),join()方法,线程处于等待状态,对应的状态为WAITING

当线程自己调用了sleep()方法,线程处于定时等待状态,对应的状态为TIMED_WAITING

当线程的run方法执行完毕后,线程就是销毁状态,对应的状态为TERMINATED

与线程息息相关的是它的生命周期,每个状态,代表线程这个时间段的处境。

下图: 

线程中的常用方法

大家看到哆嗦老太太不知道有没有什么想法,想插个队?还是老太太良心发现,站到一边数钱,等数好后再过来排队。这就要看老太太具体的选择。那我们从购票者老太太的角度切入进去吧!

接下来将演示以下方法

1、public final void setName(String name) 改变线程名称,使之与参数 name 相同

2、public final void setPriority(int piority) 更改线程的优先级。

3、public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。

4、public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。

5、public void interrupt() 中断线程。

6、public final boolean isAlive() 测试线程是否处于活动状态。

7、public static void static yield() 暂停当前正在执行的线程对象,并执行其他线程。

8、public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

9、public static Thread currentThread() 返回对当前正在执行的线程对象的引用。

Hello,大家好,我是哆嗦老太太,上文因为我的缘故,我的这个窗口只售出了6张票。这不禁让我小脸一红,源于尊老爱幼的美德,我的哆嗦举动,竟然没有遭到任何人的打断。但这着实让我挺不好意思的!好吧,现在科技已经这么发达,时光倒流已不是难事,那让我回到2222年2月2号,22点22分22秒,22点火车站购票窗口重新做出选择吧!

情景再现,我此刻站在2号窗口购票,看着越来越少的票数,我心里默念,快了!快乐!我马上就要数完了。但是我身后的小伙子们却急了眼,只有六张票了,后面还排着五十多个人呢。于是,每隔一会,后面就有人询问,“老太太,你的钱数完了吗?票马上就要没了!我们还等着回家呢!”但我心里清楚,这是他们碍于火车站的规定,只要站在购票窗口不离开,身后的挡闸会挡住身后的人。

package com._12306.SellTickets;


import java.lang.reflect.Array;
import java.util.ArrayList;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        String[] array = {"哆嗦老太太", "路人甲", "路人乙", "路人丙", "路人丁", "路人戊", "路人己", "路人庚"};

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        for (int i = 0; i < array.length; i++) {
            new Thread(ticket::sale, array[i]).start();
        }

        if (ticket.getNumber() == 0){
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

路人己:老太太,我是凌晨2点来的,已经排到现在了,当时1号窗口只有七八个人,我是看这边人少才过来排的,你看就因为比你晚了几分钟,我的票都快买不到了!

老太太听后,瞬间老泪纵横,决定让出自己的位置,和路人己调换一下位置!

package com._12306.SellTickets;


import java.lang.reflect.Array;
import java.util.ArrayList;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        Thread thread7 = new Thread(ticket::sale, "路人己");

        Thread thread1 = new Thread(() -> {
            try {
                //老太太表示可以让你插队,等你卖完票了,我再去买
                thread7.join();
                ticket.sale();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"哆嗦老太太");
        Thread thread2 = new Thread(ticket::sale, "路人甲");
        Thread thread3 = new Thread(ticket::sale, "路人乙");
        Thread thread4 = new Thread(ticket::sale, "路人丙");
        Thread thread5 = new Thread(ticket::sale, "路人丁");
        Thread thread6 = new Thread(ticket::sale, "路人戊");
        Thread thread8 = new Thread(ticket::sale, "路人庚");

        thread7.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        thread6.start();
        thread8.start();

        if (ticket.getNumber() == 0) {
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

老太太一看,这不行啊,每次都等你路人己买完我再买,我岂不是买不到了。于是老太太想出了另外的方法,就是我现在售票员这里登记一下信息,再去一边慢慢数钱。等我数完钱回来,还有票我就买,没有票我就不买了。

package com._12306.SellTickets;


import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        Thread thread7 = new Thread(ticket::sale, "路人己");

        Thread thread1 = new Thread(() -> {
            try {
                //老太太表示可以让你插队,等你卖完票了,我再去买
                //thread7.join();

                //释放cpu的执行权,让cpu去执行其他线程,老太太去一边数钱,数完后再来执行下边的代码
                //该方法如果写在同步方法或同步代码块中是不会释放锁的
                Thread.yield();
                
                //老太太睡着了1秒
                //TimeUnit.SECONDS.sleep(1);

                ticket.sale();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"哆嗦老太太");
        Thread thread2 = new Thread(ticket::sale, "路人甲");
        Thread thread3 = new Thread(ticket::sale, "路人乙");
        Thread thread4 = new Thread(ticket::sale, "路人丙");
        Thread thread5 = new Thread(ticket::sale, "路人丁");
        Thread thread6 = new Thread(ticket::sale, "路人戊");
        Thread thread8 = new Thread(ticket::sale, "路人庚");

        thread7.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        thread6.start();
        thread8.start();

        if (ticket.getNumber() == 0) {
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

  

很幸运,老太太抢到了票

但是也有意外发生,老太太数着数着睡着了,醒来后,票已经卖完了(放开代码中sleep()注释)

这时路人甲看不下去了,看到老太太睡着了,选择把她叫醒。调用老太太中的interrupt()方法。

package com._12306.SellTickets;


import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        Thread thread7 = new Thread(ticket::sale, "路人己");

        Thread thread1 = new Thread(() -> {
            try {
                //老太太睡着了1秒
                TimeUnit.SECONDS.sleep(1);

            } catch (Exception e) {

                //得到当前运行的线程(老太太)
                Thread thread = Thread.currentThread();

                //老太太没有睡眠,在一旁数钱被打断,返回true
                //老太太睡着了,被打断,老太太很生气,返回false
                boolean interrupted = thread.isInterrupted();

                //阻塞打断,false,取反,true
                if (!interrupted) {
                    ticket.sale();
                }
            }
        }, "哆嗦老太太");
        Thread thread2 = new Thread(() -> {

            //选择叫醒老太太
            thread1.interrupt();
            ticket.sale();

        }, "路人甲");

        Thread thread3 = new Thread(ticket::sale, "路人乙");
        Thread thread4 = new Thread(ticket::sale, "路人丙");
        Thread thread5 = new Thread(ticket::sale, "路人丁");
        Thread thread6 = new Thread(ticket::sale, "路人戊");
        Thread thread8 = new Thread(ticket::sale, "路人庚");

        thread7.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        thread6.start();
        thread8.start();

        if (ticket.getNumber() == 0) {
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

 就算你生气,我也要叫醒你,不然你就买不到票了!路人甲叫醒老太太,老太太买到了倒数第二张票,路人甲买到了最后一张票!果然,心地善良的人运气都不会差。

随着路人甲买到最后一张票,去往零点火车站已无余票。但是后面还有很多人没有买到票呢?他们这时有两个选择,一张选择是留在这里继续排队,等明天的票。另一种是回家,明天再过来买。

package com._12306.SellTickets;

import java.util.concurrent.TimeUnit;

class Ticket {

    private int number = 6;

    public synchronized void sale() {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "抢到了第" + (number--) + "张票,去往零点火车站剩余:" + number + "张票");
            } else {

                if (number == 0) {
                    System.out.println("去往零点火车站剩余:" + number + "张票");
                }
                number--;

                if (number == -1) {

                    System.out.println("我" + Thread.currentThread().getName() + "选择留下来等票");
                    while (true) {

                        //这里必须用wait,释放锁,给别的线程机会
                        this.wait();
                    }
                } else {
                    System.out.println("我" + Thread.currentThread().getName() + "选择回家睡觉");
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public int getNumber() {
        return number;
    }
}

路人丁选择留下来等票,路人丙选择回家睡觉。但是火车站新规定,售票窗口不允许乘客留宿,等候车票要去门外等待。于是,路人丁想留下来等票也称为了一种奢望。火车站的规定,就按要求去做吧!

package com._12306.SellTickets;


import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

public class _22点火车站 {

    public static void main(String[] args) {

        //余票类,通过它可以得知还剩多少票
        Ticket ticket = new Ticket();

        System.out.println("请注意,去往零点火车站的车票还有 " + ticket.getNumber() + " 张票,请大家依次排队购票!");

        Thread thread1 = new Thread(ticket::sale,"哆嗦老太太");
        Thread thread2 = new Thread(ticket::sale,"路人甲");
        Thread thread3 = new Thread(ticket::sale, "路人乙");
        Thread thread4 = new Thread(ticket::sale, "路人丙");
        Thread thread5 = new Thread(ticket::sale, "路人丁");
        Thread thread6 = new Thread(ticket::sale, "路人戊");
        Thread thread7 = new Thread(ticket::sale, "路人己");
        Thread thread8 = new Thread(ticket::sale, "路人庚");

        thread3.start();
        thread4.start();
        
        //设置该线程为守护线程,当jvm关闭后,它也随之消亡,即便它有任务在身。
        //火车站要关门了,这个线程想等下去也不给机会。(经过多次测试,路人丁会留下来等票,所以我把它设置为守护线程)
        thread5.setDaemon(true);
        thread5.start();
        
        thread1.start();
        thread2.start();
        thread6.start();
        thread7.start();
        thread8.start();

        if (ticket.getNumber() == 0) {
            System.out.println("很抱歉,去往零点火车站已无余票,请明天赶早!");
        }
    }
}

 ​​​​​​​

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海上钢琴师_1900

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值