最全的 Java 线程知识点及案例

作者: Android架构师丨小熊 | 来源:发表于2019-08-16 22:30 被阅读9次

    进程、线程介绍

    多线程编程是我们图形化操作系统的基本要求,比如之前的DOS操作系统,它以命令行的形式来获取用户行为,这种方式比较单一,程序在同一时间内也不会去做其他工作。再比如现在的Windows操作系统、Linux系统也罢,只要是提供丰富的图形化界面的操作系统,程序就不会局限于单一的工作。

    而多线程编程正式为了解决这个问题,如在同一个进程内,比如QQ,我可以一边聊天,一边去下载群里的文件,同时也可以一边上传文件。这就用到了多线程的技术,让程序不局限于单一的工作,利用多余的CPU资源去同时工作,提升用户的体验,这也是图形化系统提升用户体验的最佳实践。

    而进程却和线程有所不同,比如我可以一边写博客(浏览器)、一边听歌(网易云)、一边聊天(QQ、微信)。这里用到了多个不同的程序 ,每个程序都互相独立的工作,在没有进程通信时,大多情况下都不会影响对方工作。我们可以打开任务管理器,可以看到操作系统下的大量进程在同时工作,这就是多进程的概念。

    程序、进程、线程概念

    程序(program),是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
    进程(process),是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
    线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。 若一个程序可同一时间执行多个线程,就是支持多线程的

    何时应用多线程?

    程序需要同时执行两个或多个任务。
    程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
    程序需要一些后台运行的程序时。

    实现方式

    一、继承Thread类

    步骤:

    • 定义子类继承Thread类
    • 子类中重写Thread类中的run方法
    • 创建Thread子类对象,即创建了线程对象
    • 调用线程对象start方法:启动线程,调用run方法
      代码:
    public class TestThread {
        public static void main(String[] args) {
     
            MyThread1 th1 = new MyThread1();
            th1.start();
     
            /**
             * 主线程
             */
            for (int i = 10; i >= 0; i--) {
                System.out.println("Main-" + i);
            }
        }
    }
     
    class MyThread1 extends Thread {
        public MyThread1() {
        }
     
        @Override
        public void run() {
            for (int i = 10; i >= 0; i--) {
                System.out.println("MyThread1-" + i);
            }
        }
    }
    

    二、实现Runnable接口

    步骤:

    • 定义子类,实现Runnable接口
    • 子类中重写Runnable接口中的run方法
    • 通过Thread类含参构造器创建线程对象
    • 将Runnable接口的子类对象作为实际参数传递给 Thread 类的构造方法中
    • 调用Thread类的start方法:开启线程,调用 Runnable子类接口的run方法
      代码:
    public class TestThread {
        public static void main(String[] args) {
     
            MyThread2 th2 = new MyThread2();
            Thread thread = new Thread(th2);
            thread.start();
            /**
             * 主线程
             */
            for (int i = 10; i >= 0; i--) {
                System.out.println("Main-" + i);
            }
        }
    }
     
    class MyThread2 implements Runnable {
        public void run() {
            for (int i = 10; i >= 0; i--) {
                System.out.println("MyThread2-" + i);
            }
        }
    }
    

    实现 Runnable 接口的优点:

    Java 是单继承的,用实现接口的方式可以避免单继承的局限问题
    只需 new 一个实现 Runnable 接口的实例,保证了可以共享同一份资源

    线程重要内容

    一、常用方法

    • void start(); 启动线程,并执行对象的run()方法
    • run(); 线程在被调度时执行的操作
    • String getName(); 返回线程的名称
    • void setName(String name); 设置该线程名称
    • static currentThread(); 返回当前线程
      修改如上代码:
    public class TestThread {
        public static void main(String[] args) {
            /**
             * 继承 Thread 的方式
             */
            MyThread1 th1 = new MyThread1();
            th1.setName("==th1==");
            th1.start();
     
            /**
             * 实现 Runnable 接口的方式
             */
            MyThread2 th2 = new MyThread2();
            Thread thread = new Thread(th2);
            thread.setName("==th2==");
            thread.start();
            /**
             * 主线程
             */
            for (int i = 10; i >= 0; i--) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
     
    class MyThread1 extends Thread {
        public MyThread1() {
        }
     
        @Override
        public void run() {
            for (int i = 10; i >= 0; i--) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
     
    class MyThread2 implements Runnable {
        public void run() {
            for (int i = 10; i >= 0; i--) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    

    二、线程调度

    Java对线程的调度方法:

    对于同优先级线程,组成一个队列,以先进先出的方式抢占CPU资源
    对于高优先级的线程,赋予优先的抢占式资源(但是也不是绝对的能够抢到)
    1、设置线程优先级

    线程的优先级分为三个等级,分别为 MAX_PRIORITY(10); MIN _PRIORITY (1); NORM_PRIORITY (5);通过:

    getPriority() :返回线程优先值,默认为5
    setPriority(int newPriority) :改变线程的优先级,线程创建时继承父线程的优先级
    2、yieid()、join()、sleep()

    yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。
    join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行 。
    sleep(long millis)(毫秒) : 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
    isAlive():判断线程是否还活着
    例子:实现有三个人同时去银行取款机取钱,这三个同时操作。设银行共有10000元,第一人取1500,第二人取2000,第三人取3000,结算银行剩余多少钱?

    这个问题比较简单,但是存在一个bug,线程抢夺cpu资源的问题。如果第一个人在取的时候,恰巧cpu资源权被第二个人抢了,那就造成问题。

    public class TestThread {
        public static void main(String[] args) {
            Bank bank = new Bank();
     
            bank.getMoney(1500);
            Thread person1 = new Thread(bank);
            person1.setName("==person1==");
            person1.start();
            try {
                person1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            bank.getMoney(2000);
            Thread person2 = new Thread(bank);
            person2.setName("==person2==");
            person2.start();
            try {
                person2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            bank.getMoney(3000);
            Thread person3 = new Thread(bank);
            person3.setName("==person3==");
            person3.start();
            try {
                person3.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
     
    class Bank implements Runnable {
     
        int totalMoney = 10000;
        int money;// 要取出的钱
     
        public void getMoney(int money) {
            this.money = money;
        }
     
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name + "取出:" + money);
            int remainMoney = totalMoney - money;
            totalMoney = remainMoney;
            System.out.println("银行剩余:" + remainMoney);
        }
    }
    

    正常执行结果等于3500:

    如果注释上面代码中线程的 join() 方法,意味着三个人对cpu的获取权一样大,比如第一个人取到一半,执行权被第二个人给抢了,这就会导致金钱出现异常。

    • 注释全部 join() 后执行结果:


    三、线程生命周期

    四、线程同步(synchronized)

    线程同步指的是同一个线程来操作同一份资源,那不同步就有线程安全问题了。在线程并发时,如果不同的多个线程同时操作同一封资源的话,那将会造成数据紊乱。

    举个实际中的例子,我在乘坐高铁时想上厕所,这时厕所显示绿色,发现厕所没人用,我就进去了,却不小心门没有关紧。这时又来了一位想上厕所的人,由于门没关好,厕所上面的灯是绿色的,所以这位后面来的人就开门进来了,这就导致厕所紊乱了。

    用这个例子反证线程的执行过程,简直一模一样。这个厕所,就如线程处理的同一份资源。多个人就对应多个线程,在同时处理一份资源时,问题就来了。

    互斥锁(synchronized),这是一个关键字。作用在同一份资源上时,就是相当于厕所上面的指示器的作用,给这个资源加上一把锁,你其他线程不许进来,等我处理结束后再说。

    看一个例子:

    一家电影院有三个售票窗口,这部电影共有30个座位(30张票)。如果三个窗口同时卖票,则该如何操作?

    public class TestThread {
        public static void main(String[] args) {
            Cinema cinema = new Cinema();
     
            Thread window1 = new Thread(cinema);
            window1.setName("==窗口1==");
            window1.start();
     
            Thread window2 = new Thread(cinema);
            window2.setName("==窗口2==");
            window2.start();
     
            Thread window3 = new Thread(cinema);
            window3.setName("==窗口3==");
            window3.start();
        }
    }
     
    class Cinema implements Runnable {
     
        int ticket = 30;
     
        public void run() {
            String name = Thread.currentThread().getName();
            while (true) {
                if (ticket > 0) {
                    try {
                        Thread.currentThread().sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "售出" + ticket-- + "号座位");
                }
            }
        }
    }
    

    从打印结果可以看出,不仅出现了重复座位,而且还出现了 0 号座位,存在极大的bug。出现bug的原因:多个线程参与同一个数据的操作,如上代码中,多个线程同时卖30张票,却没有给操作同一个资源加锁,就会出现这种bug。


    1、同步代码块

    修改 run(),添加 synchronized(Object obj) 关键字。这里一般传入 this ,this 即 Cinema 类的对象。

    class Cinema implements Runnable {
     
        int ticket = 30;
     
        public void run() {
            while (true) {
                synchronized (this) {
                    if (ticket > 0) {
                        try {
                            Thread.currentThread().sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
                    }else {
                        break;
                    }
                }
            }
        }
    }
    

    2、同步方法

    class Cinema implements Runnable {
     
        int ticket = 30;
        boolean flag = true;
     
        public void run() {
            while (flag) {
                sell();
            }
        }
     
        public synchronized void sell() {
            if (ticket > 0) {
                try {
                    Thread.currentThread().sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
                flag = true;
            } else {
                flag = false;
            }
        }
    }
    

    同步代码块、同步方法都可以决解线程安全问题。其实,线程安全的单利模式也是可以的,只要保证操作资源的线程同一时间内是唯一的就可以了。

    结果正常:

    注意:线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,此时并不会释放锁。

    五、死锁
    说到调用 sleep() 方法不会释放锁,那么如果多个线程同时操作对方的资源,谁都不愿意释放的话,那程序就会停止,就会造成死锁的情况了。死锁就是不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

    举个实际例子:有一天,老王和老张在工地里吃饭,恰巧剩下最后一双筷子。老王拿了一根,老张拿了一根(就当这俩有毛病吧)。于是老张在等待老王放弃筷子,那么老王也想让老张先放弃。这时,他们俩就抢了起来啊,然后咔嚓一声,其中一根断了(意味着程序bug),那么这俩货就这样僵持住了,谁也吃不了。

    死锁代码:

    public class TestDeadlock implements Runnable {
     
        Zhang zhang = new Zhang();
        Wang wang = new Wang();
     
        public void init() {
            zhang.waitting(wang);
        }
     
        public static void main(String[] args) {
            System.out.println("老张、老王各有一根筷子");
            TestDeadlock dl = new TestDeadlock();
            new Thread(dl).start();
            dl.init();
        }
     
        @Override
        public void run() {
            wang.waitting(zhang);
        }
    }
     
    class Zhang {
     
        public synchronized void waitting(Wang wang) {
            try {
                Thread.currentThread().sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            wang.eat();
        }
     
        public synchronized void eat() {
            System.out.println("老张想吃饭,等待老王给筷子");
        }
    }
     
    class Wang {
        public synchronized void waitting(Zhang zhang) {
            zhang.eat();
        }
     
        public synchronized void eat() {
            System.out.println("老王想吃饭,等待老张给筷子");
        }
    }
    

    死锁的原因就是不同的线程分别占了对方也需要的资源,这时谁也不肯退让,导致程序停止。我们不可能去专门编写死锁,但出现死锁时就要我们去解救。解决方法:专门的算法、原则。或者尽量减少同步资源的定义。

    六、线程通信

    线程的通信,通过wait() 与 notify() 和 notifyAll()三个方法实现。所谓通信,就是某一个线程被wait()之后,其他线程通过notify()和notifyAll()将其唤醒。wait()不同于sleep(),这一点很重要。sleep()方法可以通过自定义的一段时间后自动唤醒,而wait()只能被notify的时候才可以苏醒,否则线程将进入停滞状态。

    wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,当前线程排队等候再次对资源的访问
    notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
    notifyAll ():唤醒正在排队等待资源的所有线程结束等待
    例子:打印整数,实现单双号交替

    public class TestThread3 {
     
        public static void main(String[] args) {
     
            MyThread myThread = new MyThread();
     
            Thread th1 = new Thread(myThread);
            th1.setName("==单数==");
            th1.start();
     
            Thread th2 = new Thread(myThread);
            th2.setName("==双数==");
            th2.start();
        }
     
    }
     
    class MyThread implements Runnable {
        int count = 21;
     
        public void run() {
            while (true) {
                synchronized (this) {
                    notify();
                    if (count > 0) {
                        System.out.println(Thread.currentThread().getName() + count--);
                    } else {
                        break;
                    }
                    
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    到此为止,线程的几个内容已经基本讲完了。掌握这些线程的知识点,可以开发出更加高效的软件,多线程编程也是能写出更高效软件的一种手段。

    最后

    最后我准备了一些面试的知识汇总,数据结构,计算机网络等等都有。自己整理和分类的,还请尊重知识产出。
    分享给大家的资料包括高级架构技术进阶脑图、Android开发面试专题资料,还有高级进阶架构资料包括但不限于【高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术】希望能帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也是可以分享给身边好友一起学习的!
    资料免费领取方式:加群:797404811

    相关文章

      网友评论

        本文标题:最全的 Java 线程知识点及案例

        本文链接:https://www.haomeiwen.com/subject/vgscsctx.html