Android开发经验谈Android开发Android技术知识

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

2019-08-16  本文已影响9人  Android架构师丨小熊

进程、线程介绍

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

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

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

程序、进程、线程概念

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

何时应用多线程?

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

实现方式

一、继承Thread类

步骤:

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接口

步骤:

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 接口的实例,保证了可以共享同一份资源

线程重要内容

一、常用方法

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的获取权一样大,比如第一个人取到一半,执行权被第二个人给抢了,这就会导致金钱出现异常。

三、线程生命周期

四、线程同步(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

上一篇 下一篇

猜你喜欢

热点阅读