zoukankan      html  css  js  c++  java
  • JavaBasics-15-多线程

    课程目标

    1.什么是线程

    2.线程的组成

    3.线程的状态

    4.线程安全

    5.线程池

    6.线程安全的集合

    什么是线程

    什么是进程?

    当我们在电脑上安装一个程序(比如QQ),我们不运行它,它只是占用了一定的硬盘资源。当我们点击exe文件执行它的时候,它就成了一个进程。对于多个进程,计算机是通过PID(ProcessID)来区分的。

    对于原本的单核CPU,看似是一次性执行多个程序,其实是错觉,单核CPU一次只能执行一个进程,只是通过切换让你觉得执行了多个。对于现在的多核CPU才是真正的实现同一时间点执行多个进程。

    什么是线程?

     

    对于单核CPU而言,其实这里的同时执行,也是宏观并行,微观串行。

    一般来说一个进程都是有多个线程的。

     进程和线程的区别

    1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位;
    2. 一个程序运行后至少有一个进程;
    3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的;
    4. 进程间不能共享数据段地址,但同进程的线程之间可以。

    线程是进程中一条执行路径,在进程中实际是由谁来负责代码的执行的?就是线程。而线程的运行又离不开CPU,单核CPU同一时间点只能执行一条线程。

    线程的组成

    任何一个线程都具有基本的组成部分:

    • CPU时间片:操作系统(OS)会为每个线程分配执行时间。
    • 运行数据:
      • 对空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
      • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
    • 线程的逻辑代码。

     线程的特点

    1. 线程抢占式执行
      • 效率高
      • 可防止单一线程长时间独占CPU
    2. 在单核CUP中,宏观上同时执行,围观上顺序执行。

    解释:有线程抢占式执行就有非抢占式执行,比如老板(CPU)有100个任务,有5个线程(5个工作狂)。如果老板来分配每个线程几个任务就是非抢占式的;如果老板说这100个任务,你们5个谁抢到谁干,就是抢占式的。抢占式效率高是为什么?谁抢到谁就立刻干,这样不会说有休息的时间,不如刺客A线程在执行,突然他不用CPU了,要去做其它事情,那么抢占式执行方案就可以让其它线程赶紧上,不给CPU休息的时间。为什么说可防止单一线程长时间独占CPU,那是自然,其实上面也说过了,不过再补充一下,抢占式执行,是CPU给每个线程分配了时间片,如果A线程该时间片执行完了,就要释放CPU资源,此时这5个线程再次同时抢CPU资源,谁抢到是谁的。

    解析:对于单核CPU,宏观上同时执行,围观上顺序执行。这里就不用说了,宏观上看是大家都在执行,实际上是这几个线程(被分配了时间片),在抢着执行。同一时间点只能有一个线程执行。不过现在都是多核的了。

    创建线程

    创建线程三种方式

    1. 【继承Thread类,重写run方法】
    2. 【实现Runnable接口】
    3. 实现Callable接口(这个是JDK1.5之后新增的方法,许多地方只写了前面两种)

    创建线程方式一

     线程创建步骤:1.创建线程类,继承Thread类,并重写run方法(写该线程运行的代码);2.创建对象,并调用start方法执行子线程(不要调用run方法)

    运行结果:

    获取线程名称

    1. 在Thread的子类中调用this.getId()或this.getName()
    2. 使用Thread.currentThread().getId()和Thread.currentThread().getName()。(推荐)

    修改线程名称

    1. 调用线程对象的setName()方法
    2. 使用线程子类的构造方法赋值

     用第一种方式来获得线程ID和线程name

    运行结果:

     这种方式具有局限性,什么局限性??

    getId()和getName()是从Thread类中继承过来的方法,因此这种方式必须用继承Thread的方式实现多线程才能使用,但是我们实现多线程的方式不止这一种,用其它方式是西安多线程时就不能用这种方法。

    因此,用第二种方式实现,用Thread类中的静态方法:即Thread.currentThread(),该方法获取的是当前线程,即正在执行该代码的线程。(推荐)

    我上面的代码有写错地方,怪不得看着结果不对,你看到了吗??(要用start,我写成了run)运行结果:

     修改线程名称

    注意:我们可以修改线程名称,但无法修改线程ID,线程ID是在线程启动时自动分配的。

    方式一:调用线程对象的setName()方法(只能在线程启动前,即调用start方法前进行修改)

     运行结果:

    方法来修改线程名字,我能不能创建时就修改名字呢??当然,可以在创建子类时用构造方法赋值。

    方式二:使用线程子类的构造方法赋值

     

    运行结果:

     对于这两种方法,怎么说呢,更倾向于第二种。至于为什么吗?说不清。

    实战-卖票案例

    使用继承Thread类实现4个窗口各卖100张票?

     

     

     创建线程方式二

    1:创建实现Runnable接口,并覆盖run方法的类;2.创建实现类的对象;3.创建线程对象,传入参数为实现类对象;4.线程对象调用start方法启动线程。

    怎么感觉这个更复杂一点,多了一步。但是也自由其妙处,比如线程名字就更好写了。在创建线程对象时候,除了传入实现类对象,也可以传入线程名字。

     运行结果:

    知识补充:使用匿名内部类

    假如这个实现Runnable接口的类只使用一次,那么创建出来就比较多余。此外,我们想上面创建线程对象时,第一个参数其实就是一个实现了Runnable接口的子类,这也为匿名内部类的使用创造的条件。

    这里问一个问题,还在哪里用过匿名内部类??答:比如创建TreeSet、TreeMap对象时传入的Comparator接口的子类。

    package com.yuncong.java_thread;
    public class RunnableDemo02 {
        public static void main(String[] args) {
            Runnable runnable = new Runnable() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println(Thread.currentThread().getName()+"======="+i);
                    }
                }
            };
            Thread thread = new Thread(runnable, "我的第一个线程");
            thread.start();
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"======="+i);
            }
        }
    }

    运行结果:

     实战-案例一

    实现4个窗口共卖100张票?

     

     运行结果:

    这里Ticket是公共资源,多个线程都来操作他,操作的方法在run里面重写了。

     这里发现票有重卖的现象,暂时先不做处理。等我们讲同步的时候再来解决。

    实战-案例二

    你和你女朋友公用一张银行卡,你向卡中存钱,你女朋友从中取钱,使用程序模拟该过程?

     这里的银行卡你可以理解成共享资源,就像上面的票,刚刚时4个人在处理共享资源(买票)。这时候时两个人(两个线程)在处理共享资源。只是你是存钱的,你女朋友是取钱的。这个每个线程执行的功能是不一样的,和刚刚稍微有一点区别。

     那现在就有一个问题,刚刚是只有一个功能,我们就写在了Ticket类中的run方法里面了,但是呢?现在有两个功能,如果我们新建BankCard这个公共资源类,还能用run来重写吗?不能,因为run是一个方法,那该怎么办??

    那么我们就不让BankCard类实现runnable这个接口,把存钱和取钱的方法放在两个不同类里面,如AddMoney和SubMoney。

     

     运行结果:

     下面对这个代码进行改造一下,用匿名内部类(刚开始不熟练,可以用上面的方式写,思路更清晰一点,等你熟练了,用匿名内部类,代码会更加简洁)

    当然此时的BankCard类和测试类还是要有的,只是两个实现了Runnable接口的线程类(含有线程执行的方法),可以用匿名内部类代替。不过你可能有疑问,如果用匿名内部类代替了,上面写的时候这两个类中的公共资源(原本是用私有属性声明并用构造方法获得)该怎么办?匿名内部类可不能传递参数啊!!

    问题真好,但是呢?其实你压根都不用传,因为。。。。请看代码。

     此外,这里的启动也简化了。

    package com.yuncong.java_thread;
    //简化版
    public class TestBankCard02 {
        public static void main(String[] args) {
            BankCard card = new BankCard();
            //存钱,根本不用传对象,在一个类中传什么??这个类已经声明了,可以直接用了。
            Runnable add = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        card.setMoney(card.getMoney()+1000);
                        //每存一笔,我们看看存了多少了
                        System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+card.getMoney());
                    }
                }
            };
            //取钱,根本不用传对象,在一个类中传什么??这个类已经声明了,可以直接用了。
            Runnable sub = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        if (card.getMoney() >= 100) {
                            System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+(card.getMoney()-1000));
                            card.setMoney(card.getMoney()-1000);
                        }else {
                            System.out.println("余额不足,请赶快存钱");
                            i--;//为什么要i--??i--表示该次不成功,要退回原有状态,相当于这次取钱都没有发生。
                            //如果不写,可以吗?老师说不可以,我觉得可以,可以的前提是解决了线程安全问题
                            //我们这里还没有解决,因此,要i--,如果不写,最终卡中的钱不是0,也就是说可能没有取完。
                        }
                    }
                }
            };
            //创建线程对象,并启动
            new Thread(add,"明明").start();
            new Thread(sub,"丽丽").start();
        }
    }

    运行结果:

     线程的状态(基本)——基本状态意思就是说,我们后面还会遇到一些状态。

    我这里稍微解释一下,初始状态就是线程还没有调用start方法之前,但已经被创建(继承Thread的话,就是MyThread thread =  new MyThread("这里可能需要线程名字,看你有没有添加构造方法"),如果是实现Runnable接口的话,就是new Thread(实现runnable的类对象))的状态。其它几个无需解释。

    线程休眠

    常用方法:

    休眠:

    • public static void sleep(long millis);是Thread类中的静态方法,因此可以直接用类名调用。
    • 当前线程主动休眠millis毫秒。

    放弃:

    •  public static void yield();(静态方法,类名调用即可)
    • 当前线程主动放弃时间片,回到就绪状态,竞争下一个时间片。

    加入:

    • public final void join();(非静态方法,要用对象来调用,这也很好理解)
    • 允许其他线程加入到当前线程中。(让其它线程进入该线程并执行,而当前线程暂停,那当前线程何使执行呢?等加入线程执行结束。)

    优先级:

    • 线程对象.setPriority();
    • 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。

    守护线程:

    • 线程对象.setDaemon(true);设置为守护线程
    • 线程有两类:用户线程(前台线程)、守护线程(后台线程)
    • 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
    • 垃圾回收器线程属于守护线程。

    休眠:

    这里有几个注意点:

    1. 你想要哪个线程休眠,就要把Thread.sleep(1000)写在哪个方法里面,比如你要向main线程休眠,就写在主方法,你向要继承了Thread类的MyThread01类的线程休眠,就放在该类的run方法里面。
    2. 该方法有异常,异常处理有两种方式,可以抛出,也可以捕获。这里只能捕获,为什么?因此无论是继承的Thread类还是实现的Runnable接口都没有抛出异常,因此你不能抛出。

     

     运行结果:

     运行过程中,你会发现一个很奇怪的问题,控制台每次输出两个结果,即输出2条,停一下,再输出两条。。。后面接着学你就可以解释这种现象了。

    放弃:

    这个方法调用后,该线程会放弃时间片,和其它线程一起再次共同抢夺CPU资源。

     运行结果:

     调用Yield方法后,如果有多个线程,则更可能会出现交叉执行的情况。但是也不是说一定出现交叉相乘。这个例子也就相当于抛硬币,概率大约是1/2。

    加入:

     运行结果:

     未加入前,两个线程交替执行,加入后,加入线程先执行结束,然后才是该线程(main)执行。

    我们上面已经讲到了三个常用线程方法,分别是Sleep()、Yield()和join(),其中前两个是静态方法,后两个是非静态方法(必须对象调用)。那么我们现在思考一个很深奥的问题,该如何使用这些方法。

    有人可能会说,我会呀,我知道这几个方法的意思,可是又如何,知道意思是第一步,用在哪里是第二步,何时用是第三步。你知道用在哪里吗?

    首先我们想前两个是静态方法,所以说任何地方都可以调用,第二个是非静态方法,要对象才能调用。我们要用这些方法肯定是在处理线程问题的时候,即其实你调用这些方法的位置是很有限的,只有两类地方,第一类是线程类中重写run方法体内(即继承Thread或重写Runnable接口的重写run方法体内),因为这里才是线程操作的地方,同时处理这个run方法的可能是一个线程,也可能是多个线程;第二类就是放在主线程中,这里是主线程以及定义其它线程运行的地方。

    那我们看这两个地方有什么区别??对于第一类:你可以调用静态方法,比如上面的Sleep()、Yield(),但不能调用join(),为什么,因为你仅能在这里获得一个对象,就是this对象,你让this一个对象加入谁??对于第二类:这里面所有线程对象你都能得到,因此这三种方法都可以调用。但我想更多的是调用第三个方法,为什么??因为前两个没有方法体呀,这里只有主线程的方法体。

    说了这么多,我也没想太清楚,就是想到哪里说到哪里。后面多做项目,理解应该会更加深刻。

    优先级:

     运行结果:

     守护线程:

    守护线程就是用来守护前台线程(用户线程的),用户线程结束,则守护线程自动结束。

    当线程刚开始创建的时候默认是用户线程。

     运行结果:

     我们发现守护线程本来是要打印到50的,但是它没有执行完,当主线程结束后,守护线程就立刻结束了(即使它没有工作完)。

    线程的状态(等待)

     其中初始状态、就绪状态、运行状态和终止状态上面已经讲述过了,这里再加入一个等待状态,其中线程调用sleep()方法则成了限期状态,等休眠结束变为就绪状态;如果线程调用join()方法,则进入无时间等待状态(上面的无期限不对),等加入的线程结束就变回就绪状态。

    线程安全问题:

     这里解释下上图,现在又一个共享资源,即一个长度为5的数组,里面的数值全为空。现在有两个线程来执行这个数组,A线程是向数组中插入“Hello”字符串。B线程是向数组中插入“World”字符串。假设此时A线程抢到了CPU资源,执行其时间片,A线程开始看该插入到哪个位置了,它一看,要插入0位置,欣喜的想要插入的时候,时间片结束了。才是B线程在新一轮的CPU争夺大战中抢到了使用权,开开心心的取执行自己的时间片内容,去插入“World”,它就看要查到哪里?它从头开始判断,0号位置是null,咦,没有元素,于是就要把元素放到这个位置,不知道是它时间片长还是在新一轮大战中成功了,反正它是插入了。插入后它的时间片结束,A线程抢到了资源,一顿操作猛如虎,直接讲"Hello"插入到0位置,你可能会问,它为什么不去看看要插入到哪里呢?A线程说,看什么看,老子已经判断过了,就是这个位置,没错了,插,不会错。因此就出现了这里面只有"Hello"字符串的现象。这就是线程安全问题。还挺严重的。
    原子操作:这个词看着很陌生,但是起的名字很能说明问题,原子是不可分割的化学元素,在这里的意思就是“寻找插入位置和插入操作”是不可分割的整体,是要一体性执行的。

    临界资源:就是共享资源,只有保证一次仅允许一个线程使用,才可保证其正确性。

    下面来演示一下这个问题。

    package com.yuncong.java_thread;
    import java.util.Arrays;
    public class ThreadSafeDemo01 {
        private static int index = 0;
        public static void main(String[] args) {
            //创建数组
            String[] s = new String[5];
            //创建两个操作
            Runnable runnableA = new Runnable() {
                @Override
                public void run() {
                    s[index]="hello";
                    index++;
                }
            };
            Runnable runnableB = new Runnable() {
                @Override
                public void run() {
                    s[index]="world";
                    index++;
                }
            };
            //创建两个线程对象,并执行
            Thread a = new Thread(runnableA,"A");
            Thread b = new Thread(runnableB,"B");
            a.start();
            b.start();
            //嗲用join方法,让主线程进入阻塞状态,a,b线程执行完再执行主线程
            try {
                a.join();//加入线程
                b.join();//加入线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Arrays.toString(s));
        }
    }

    运行结果:可能是正确的,也可能是不正确的(恕我直言,我没有演示出来,但这种情况确实是存在的)

    疑问:其实这里写代码的时候遇到一个问题,就是index必须声明为static类型放在类的属性位置。不能放在方法体内(包括main方法体)。这一点,我其实不是很明白。

    思考:在程序应用中,如何保证线程的安全性??这就需要java的同步机制。

    同步方式一:

    同步代码块

    语法:

    //同步代码块
    synchronized ("临界资源对象") {//对临界资源对象加锁
         //代码(原子操作)
    }

    注:

    • 每个对象都有一个互斥锁标记,用来分配给线程的。
    • 只有拥有对象互斥锁标记多线程,才能进入该对象加锁的同步代码块。
    • 线程退出同步代码块时,会释放相应的互斥锁标记。

    下面进行演示:

     运行结果:

     这样电话,两个字符串一定都可以放进去,至于谁是前,谁是后不一定。

    现在我们用同步代码块,解决曾经的买票重复问题(四个窗口共同卖100张票),

     

     运行结果:

     其实你思考一个问题:这里可以用this吗?可以,如果用了this,就代表该类的对象,即Ticket对象。这里可以直接用new Object()吗?不可以,这样的话,用的不是一个公共的锁。

    对于上面讲过的存钱取钱问题,我们再写一次,看看如何实现加锁功能。

     

    ------------恢复内容开始------------

    课程目标

    1.什么是线程

    2.线程的组成

    3.线程的状态

    4.线程安全

    5.线程池

    6.线程安全的集合

    什么是线程

    什么是进程?

    当我们在电脑上安装一个程序(比如QQ),我们不运行它,它只是占用了一定的硬盘资源。当我们点击exe文件执行它的时候,它就成了一个进程。对于多个进程,计算机是通过PID(ProcessID)来区分的。

    对于原本的单核CPU,看似是一次性执行多个程序,其实是错觉,单核CPU一次只能执行一个进程,只是通过切换让你觉得执行了多个。对于现在的多核CPU才是真正的实现同一时间点执行多个进程。

    什么是线程?

     

    对于单核CPU而言,其实这里的同时执行,也是宏观并行,微观串行。

    一般来说一个进程都是有多个线程的。

     进程和线程的区别

    1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位;
    2. 一个程序运行后至少有一个进程;
    3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的;
    4. 进程间不能共享数据段地址,但同进程的线程之间可以。

    线程是进程中一条执行路径,在进程中实际是由谁来负责代码的执行的?就是线程。而线程的运行又离不开CPU,单核CPU同一时间点只能执行一条线程。

    线程的组成

    任何一个线程都具有基本的组成部分:

    • CPU时间片:操作系统(OS)会为每个线程分配执行时间。
    • 运行数据:
      • 对空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
      • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
    • 线程的逻辑代码。

     线程的特点

    1. 线程抢占式执行
      • 效率高
      • 可防止单一线程长时间独占CPU
    2. 在单核CUP中,宏观上同时执行,围观上顺序执行。

    解释:有线程抢占式执行就有非抢占式执行,比如老板(CPU)有100个任务,有5个线程(5个工作狂)。如果老板来分配每个线程几个任务就是非抢占式的;如果老板说这100个任务,你们5个谁抢到谁干,就是抢占式的。抢占式效率高是为什么?谁抢到谁就立刻干,这样不会说有休息的时间,不如刺客A线程在执行,突然他不用CPU了,要去做其它事情,那么抢占式执行方案就可以让其它线程赶紧上,不给CPU休息的时间。为什么说可防止单一线程长时间独占CPU,那是自然,其实上面也说过了,不过再补充一下,抢占式执行,是CPU给每个线程分配了时间片,如果A线程该时间片执行完了,就要释放CPU资源,此时这5个线程再次同时抢CPU资源,谁抢到是谁的。

    解析:对于单核CPU,宏观上同时执行,围观上顺序执行。这里就不用说了,宏观上看是大家都在执行,实际上是这几个线程(被分配了时间片),在抢着执行。同一时间点只能有一个线程执行。不过现在都是多核的了。

    创建线程

    创建线程三种方式

    1. 【继承Thread类,重写run方法】
    2. 【实现Runnable接口】
    3. 实现Callable接口(这个是JDK1.5之后新增的方法,许多地方只写了前面两种)

    创建线程方式一

     线程创建步骤:1.创建线程类,继承Thread类,并重写run方法(写该线程运行的代码);2.创建对象,并调用start方法执行子线程(不要调用run方法)

    运行结果:

    获取线程名称

    1. 在Thread的子类中调用this.getId()或this.getName()
    2. 使用Thread.currentThread().getId()和Thread.currentThread().getName()。(推荐)

    修改线程名称

    1. 调用线程对象的setName()方法
    2. 使用线程子类的构造方法赋值

     用第一种方式来获得线程ID和线程name

    运行结果:

     这种方式具有局限性,什么局限性??

    getId()和getName()是从Thread类中继承过来的方法,因此这种方式必须用继承Thread的方式实现多线程才能使用,但是我们实现多线程的方式不止这一种,用其它方式是西安多线程时就不能用这种方法。

    因此,用第二种方式实现,用Thread类中的静态方法:即Thread.currentThread(),该方法获取的是当前线程,即正在执行该代码的线程。(推荐)

    我上面的代码有写错地方,怪不得看着结果不对,你看到了吗??(要用start,我写成了run)运行结果:

     修改线程名称

    注意:我们可以修改线程名称,但无法修改线程ID,线程ID是在线程启动时自动分配的。

    方式一:调用线程对象的setName()方法(只能在线程启动前,即调用start方法前进行修改)

     运行结果:

    方法来修改线程名字,我能不能创建时就修改名字呢??当然,可以在创建子类时用构造方法赋值。

    方式二:使用线程子类的构造方法赋值

     

    运行结果:

     对于这两种方法,怎么说呢,更倾向于第二种。至于为什么吗?说不清。

    实战-卖票案例

    使用继承Thread类实现4个窗口各卖100张票?

     

     

     创建线程方式二

    1:创建实现Runnable接口,并覆盖run方法的类;2.创建实现类的对象;3.创建线程对象,传入参数为实现类对象;4.线程对象调用start方法启动线程。

    怎么感觉这个更复杂一点,多了一步。但是也自由其妙处,比如线程名字就更好写了。在创建线程对象时候,除了传入实现类对象,也可以传入线程名字。

     运行结果:

    知识补充:使用匿名内部类

    假如这个实现Runnable接口的类只使用一次,那么创建出来就比较多余。此外,我们想上面创建线程对象时,第一个参数其实就是一个实现了Runnable接口的子类,这也为匿名内部类的使用创造的条件。

    这里问一个问题,还在哪里用过匿名内部类??答:比如创建TreeSet、TreeMap对象时传入的Comparator接口的子类。

    package com.yuncong.java_thread;
    public class RunnableDemo02 {
        public static void main(String[] args) {
            Runnable runnable = new Runnable() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println(Thread.currentThread().getName()+"======="+i);
                    }
                }
            };
            Thread thread = new Thread(runnable, "我的第一个线程");
            thread.start();
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"======="+i);
            }
        }
    }

    运行结果:

     实战-案例一

    实现4个窗口共卖100张票?

     

     运行结果:

    这里Ticket是公共资源,多个线程都来操作他,操作的方法在run里面重写了。

     这里发现票有重卖的现象,暂时先不做处理。等我们讲同步的时候再来解决。

    实战-案例二

    你和你女朋友公用一张银行卡,你向卡中存钱,你女朋友从中取钱,使用程序模拟该过程?

     这里的银行卡你可以理解成共享资源,就像上面的票,刚刚时4个人在处理共享资源(买票)。这时候时两个人(两个线程)在处理共享资源。只是你是存钱的,你女朋友是取钱的。这个每个线程执行的功能是不一样的,和刚刚稍微有一点区别。

     那现在就有一个问题,刚刚是只有一个功能,我们就写在了Ticket类中的run方法里面了,但是呢?现在有两个功能,如果我们新建BankCard这个公共资源类,还能用run来重写吗?不能,因为run是一个方法,那该怎么办??

    那么我们就不让BankCard类实现runnable这个接口,把存钱和取钱的方法放在两个不同类里面,如AddMoney和SubMoney。

     

     运行结果:

     下面对这个代码进行改造一下,用匿名内部类(刚开始不熟练,可以用上面的方式写,思路更清晰一点,等你熟练了,用匿名内部类,代码会更加简洁)

    当然此时的BankCard类和测试类还是要有的,只是两个实现了Runnable接口的线程类(含有线程执行的方法),可以用匿名内部类代替。不过你可能有疑问,如果用匿名内部类代替了,上面写的时候这两个类中的公共资源(原本是用私有属性声明并用构造方法获得)该怎么办?匿名内部类可不能传递参数啊!!

    问题真好,但是呢?其实你压根都不用传,因为。。。。请看代码。

     此外,这里的启动也简化了。

    package com.yuncong.java_thread;
    //简化版
    public class TestBankCard02 {
        public static void main(String[] args) {
            BankCard card = new BankCard();
            //存钱,根本不用传对象,在一个类中传什么??这个类已经声明了,可以直接用了。
            Runnable add = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        card.setMoney(card.getMoney()+1000);
                        //每存一笔,我们看看存了多少了
                        System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+card.getMoney());
                    }
                }
            };
            //取钱,根本不用传对象,在一个类中传什么??这个类已经声明了,可以直接用了。
            Runnable sub = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        if (card.getMoney() >= 100) {
                            System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+(card.getMoney()-1000));
                            card.setMoney(card.getMoney()-1000);
                        }else {
                            System.out.println("余额不足,请赶快存钱");
                            i--;//为什么要i--??i--表示该次不成功,要退回原有状态,相当于这次取钱都没有发生。
                            //如果不写,可以吗?老师说不可以,我觉得可以,可以的前提是解决了线程安全问题
                            //我们这里还没有解决,因此,要i--,如果不写,最终卡中的钱不是0,也就是说可能没有取完。
                        }
                    }
                }
            };
            //创建线程对象,并启动
            new Thread(add,"明明").start();
            new Thread(sub,"丽丽").start();
        }
    }

    运行结果:

     线程的状态(基本)——基本状态意思就是说,我们后面还会遇到一些状态。

    我这里稍微解释一下,初始状态就是线程还没有调用start方法之前,但已经被创建(继承Thread的话,就是MyThread thread =  new MyThread("这里可能需要线程名字,看你有没有添加构造方法"),如果是实现Runnable接口的话,就是new Thread(实现runnable的类对象))的状态。其它几个无需解释。

    线程休眠

    常用方法:

    休眠:

    • public static void sleep(long millis);是Thread类中的静态方法,因此可以直接用类名调用。
    • 当前线程主动休眠millis毫秒。

    放弃:

    •  public static void yield();(静态方法,类名调用即可)
    • 当前线程主动放弃时间片,回到就绪状态,竞争下一个时间片。

    加入:

    • public final void join();(非静态方法,要用对象来调用,这也很好理解)
    • 允许其他线程加入到当前线程中。(让其它线程进入该线程并执行,而当前线程暂停,那当前线程何使执行呢?等加入线程执行结束。)

    优先级:

    • 线程对象.setPriority();
    • 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。

    守护线程:

    • 线程对象.setDaemon(true);设置为守护线程
    • 线程有两类:用户线程(前台线程)、守护线程(后台线程)
    • 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
    • 垃圾回收器线程属于守护线程。

    休眠:

    这里有几个注意点:

    1. 你想要哪个线程休眠,就要把Thread.sleep(1000)写在哪个方法里面,比如你要向main线程休眠,就写在主方法,你向要继承了Thread类的MyThread01类的线程休眠,就放在该类的run方法里面。
    2. 该方法有异常,异常处理有两种方式,可以抛出,也可以捕获。这里只能捕获,为什么?因此无论是继承的Thread类还是实现的Runnable接口都没有抛出异常,因此你不能抛出。

     

     运行结果:

     运行过程中,你会发现一个很奇怪的问题,控制台每次输出两个结果,即输出2条,停一下,再输出两条。。。后面接着学你就可以解释这种现象了。

    放弃:

    这个方法调用后,该线程会放弃时间片,和其它线程一起再次共同抢夺CPU资源。

     运行结果:

     调用Yield方法后,如果有多个线程,则更可能会出现交叉执行的情况。但是也不是说一定出现交叉相乘。这个例子也就相当于抛硬币,概率大约是1/2。

    加入:

     运行结果:

     未加入前,两个线程交替执行,加入后,加入线程先执行结束,然后才是该线程(main)执行。

    我们上面已经讲到了三个常用线程方法,分别是Sleep()、Yield()和join(),其中前两个是静态方法,后两个是非静态方法(必须对象调用)。那么我们现在思考一个很深奥的问题,该如何使用这些方法。

    有人可能会说,我会呀,我知道这几个方法的意思,可是又如何,知道意思是第一步,用在哪里是第二步,何时用是第三步。你知道用在哪里吗?

    首先我们想前两个是静态方法,所以说任何地方都可以调用,第二个是非静态方法,要对象才能调用。我们要用这些方法肯定是在处理线程问题的时候,即其实你调用这些方法的位置是很有限的,只有两类地方,第一类是线程类中重写run方法体内(即继承Thread或重写Runnable接口的重写run方法体内),因为这里才是线程操作的地方,同时处理这个run方法的可能是一个线程,也可能是多个线程;第二类就是放在主线程中,这里是主线程以及定义其它线程运行的地方。

    那我们看这两个地方有什么区别??对于第一类:你可以调用静态方法,比如上面的Sleep()、Yield(),但不能调用join(),为什么,因为你仅能在这里获得一个对象,就是this对象,你让this一个对象加入谁??对于第二类:这里面所有线程对象你都能得到,因此这三种方法都可以调用。但我想更多的是调用第三个方法,为什么??因为前两个没有方法体呀,这里只有主线程的方法体。

    说了这么多,我也没想太清楚,就是想到哪里说到哪里。后面多做项目,理解应该会更加深刻。

    优先级:

     运行结果:

     守护线程:

    守护线程就是用来守护前台线程(用户线程的),用户线程结束,则守护线程自动结束。

    当线程刚开始创建的时候默认是用户线程。

     运行结果:

     我们发现守护线程本来是要打印到50的,但是它没有执行完,当主线程结束后,守护线程就立刻结束了(即使它没有工作完)。

    线程的状态(等待)

     其中初始状态、就绪状态、运行状态和终止状态上面已经讲述过了,这里再加入一个等待状态,其中线程调用sleep()方法则成了限期状态,等休眠结束变为就绪状态;如果线程调用join()方法,则进入无时间等待状态(上面的无期限不对),等加入的线程结束就变回就绪状态。

    线程安全问题:

     这里解释下上图,现在又一个共享资源,即一个长度为5的数组,里面的数值全为空。现在有两个线程来执行这个数组,A线程是向数组中插入“Hello”字符串。B线程是向数组中插入“World”字符串。假设此时A线程抢到了CPU资源,执行其时间片,A线程开始看该插入到哪个位置了,它一看,要插入0位置,欣喜的想要插入的时候,时间片结束了。才是B线程在新一轮的CPU争夺大战中抢到了使用权,开开心心的取执行自己的时间片内容,去插入“World”,它就看要查到哪里?它从头开始判断,0号位置是null,咦,没有元素,于是就要把元素放到这个位置,不知道是它时间片长还是在新一轮大战中成功了,反正它是插入了。插入后它的时间片结束,A线程抢到了资源,一顿操作猛如虎,直接讲"Hello"插入到0位置,你可能会问,它为什么不去看看要插入到哪里呢?A线程说,看什么看,老子已经判断过了,就是这个位置,没错了,插,不会错。因此就出现了这里面只有"Hello"字符串的现象。这就是线程安全问题。还挺严重的。
    原子操作:这个词看着很陌生,但是起的名字很能说明问题,原子是不可分割的化学元素,在这里的意思就是“寻找插入位置和插入操作”是不可分割的整体,是要一体性执行的。

    临界资源:就是共享资源,只有保证一次仅允许一个线程使用,才可保证其正确性。

    下面来演示一下这个问题。

    package com.yuncong.java_thread;
    import java.util.Arrays;
    public class ThreadSafeDemo01 {
        private static int index = 0;
        public static void main(String[] args) {
            //创建数组
            String[] s = new String[5];
            //创建两个操作
            Runnable runnableA = new Runnable() {
                @Override
                public void run() {
                    s[index]="hello";
                    index++;
                }
            };
            Runnable runnableB = new Runnable() {
                @Override
                public void run() {
                    s[index]="world";
                    index++;
                }
            };
            //创建两个线程对象,并执行
            Thread a = new Thread(runnableA,"A");
            Thread b = new Thread(runnableB,"B");
            a.start();
            b.start();
            //嗲用join方法,让主线程进入阻塞状态,a,b线程执行完再执行主线程
            try {
                a.join();//加入线程
                b.join();//加入线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Arrays.toString(s));
        }
    }

    运行结果:可能是正确的,也可能是不正确的(恕我直言,我没有演示出来,但这种情况确实是存在的)

    疑问:其实这里写代码的时候遇到一个问题,就是index必须声明为static类型放在类的属性位置。不能放在方法体内(包括main方法体)。这一点,我其实不是很明白。

    思考:在程序应用中,如何保证线程的安全性??这就需要java的同步机制。

    同步方式一:

    同步代码块

    语法:

    //同步代码块
    synchronized ("临界资源对象") {//对临界资源对象加锁
         //代码(原子操作)
    }

    注:

    • 每个对象都有一个互斥锁标记,用来分配给线程的。
    • 只有拥有对象互斥锁标记多线程,才能进入该对象加锁的同步代码块。
    • 线程退出同步代码块时,会释放相应的互斥锁标记。

    下面进行演示:

     运行结果:

     这样电话,两个字符串一定都可以放进去,至于谁是前,谁是后不一定。

    现在我们用同步代码块,解决曾经的买票重复问题(四个窗口共同卖100张票),

     

     运行结果:

     其实你思考一个问题:这里可以用this吗?可以,如果用了this,就代表该类的对象,即Ticket对象。这里可以直接用new Object()吗?不可以,这样的话,用的不是一个公共的锁。

    对于上面讲过的存钱取钱问题,我们再写一次,看看如何实现加锁功能。

    BankCard代码

    package com.yuncong.java_thread;
    public class BankCard02 {
        private double money;
        public double getMoney() {
            return money;
        }
        public void setMoney(double money) {
            this.money = money;
        }
    }

    测试类代码,为了代码更加清晰,这里使用了匿名内部类:

    package com.yuncong.java_thread;
    public class TestForBankCard02 {
        public static void main(String[] args) {
            //1.创建银行卡
            BankCard02 card = new BankCard02();
            //2.创建两个操作
            Runnable add = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        card.setMoney(card.getMoney()+1000);
                        System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+card.getMoney());
                    }
                }
            };
            Runnable sub = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        if (card.getMoney() >= 1000) {
                            card.setMoney(card.getMoney()-1000);
                            System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+card.getMoney());
                        }else {
                            System.out.println("账户余额不足,赶快打钱!");
                            i--;
                        }
                    }
                }
            };
            //3.创建两个线程对象
            Thread xiaoli = new Thread(add, "小李");
            Thread xiaoyue = new Thread(sub, "小月");
            xiaoli.start();
            xiaoyue.start();
        }
    }

    运行结果:

     发现这里是有问题的,小李存了1000院,余额确实0。这是为什么呢?比如小李存了1000,但还没来得及打印,小月这个线程就执行了取钱,也还没有来得及打印;刺客小李开始打印,结果确实0。

    现在用同步代码块执行。

     运行结果:

     线程的状态(阻塞)

     

     现在对上面的三幅图片说明一下:对于第一幅图是其中状态,但是就操作系统而言,很难区分就绪状态和运行状态,因此就把这两个统称为Runnable状态。这是就有了六种状态。通过源码的观察得知这六种状态是JDK1.5引入的,是一个枚举类。

     同步方式二:

    同步方法:

    synchronized 返回值类型 方法名称(形参列表0) { //对当前对象(this)加锁
      //代码(原子操作)
    }

     注:

    只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。

    线程退出同步方法时,会释放相应的互斥锁标记。

    package com.yuncong.java_thread;
    public class Ticket02 implements Runnable {
        private int ticket = 100;//100张票
        //创建锁
        private Object obj = new Object();
        @Override
        public void run() {
            while (true) {
                if (!sale()) {
                    break;
                }
            }
        }
        //卖票(同步方法)
        public synchronized boolean sale() { //这个锁是this,如果这里是静态方法,那么锁就是这个类Ticket02.class
            if (ticket <= 0) {
                return false;
            }
            System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
            ticket--;
            return true;
        }
    }

    同步规则

    注意:

    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
    • 如调用不包含同步代码块的方法,或者普通方法时,则不需要锁标记,可直接调用。

    已知JDK中线程安全的类

    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均位synchronized修饰的同步方法,当然同步后性能会有一些影响。

    经典问题

    死锁:

    • 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
    • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

    我们应该避免死锁问题,因为这样会造成阻塞。下面演示以下死锁现象。

    package com.yuncong.java_thread;
    //创建两个锁对象
    public class MyLock {
        //两个锁(相当于两根筷子)
        public static Object a = new Object();
        public static Object b = new Object();
    }
    package com.yuncong.java_thread;
    public class Boy extends Thread{
        @Override
        public void run() {
            synchronized (MyLock.a) {
                System.out.println("男孩拿到了a");
                synchronized (MyLock.b) {
                    System.out.println("男孩拿到了b");
                    System.out.println("男孩可以吃东西了...");
                }
            }
        }
    }
    package com.yuncong.java_thread;
    public class Girl extends Thread{
        @Override
        public void run() {
            synchronized (MyLock.b) {
                System.out.println("女孩拿到了b");
                synchronized (MyLock.a) {
                    System.out.println("女孩拿到了a");
                    System.out.println("女孩可以吃东西了...");
                }
            }
        }
    }
    package com.yuncong.java_thread;
    public class TestDeadLock {
        public static void main(String[] args) {
            Boy boy = new Boy();
            Girl girl = new Girl();
            boy.start();
            /*try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            girl.start();
        }
    }

    运行结果:

    如果加上主线程休眠后的运行结果:

    对于没有假如主线程休眠时,两个线程都运行很快,各抢到一根筷子,互相等着另一根筷子空闲,如果另一根空闲了才可能抢到,否则就一直阻塞下去。

    线程通信

    在讲线程通讯之前,先回顾以下前面的小案例,存钱和取钱,在取钱时很可能会出现余额不足的现象,那怎么办呢?如果我们约定取钱时必须要先存入一笔,这样就没问题了。这就需要线程之间的通讯。

    等待:

    • public final void wait()
    • public final void wait(long timeout)
    • 必须在对obj加锁的同步代码块中,才能使用wait方法。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在obj的等待队列中。释放锁,进入等待队列。等待唤醒。

    通知:

    • public final void notify()
    • public final void notifyAll()

    还是以前的存钱和取钱问题,我们使用线程间的通讯实现先存后取。但是本质上我们是无法控制CPU的,也就是说无法控制谁先执行,即先存还是先取。这里在每个存和取前面加上一个门槛,只要达标才能存或取,以此来实现存一笔取一笔。这里的程序和上面的不太一样。把存钱和取钱的功能当作银行卡BankCard类中,AddMoney和SubMoney里面的run方法只是对其进行调用。

    package com.yuncong.java_thread;
    /*正常情况下,我们时无法控制谁先抢到cpu,即哪个线程先执行,这样就会出现取钱抢到资源,
    但里面一直位空的现象。但是呢?我们可以设置这个线程抢到锁后,是否执行其中的代码。
    这里添加一个标记,如果是false,则是没钱,取钱线程拿到资源后不能取,要释放锁,并到队列中等待
    存钱后唤醒,如果是true,则可以取,但取之后把锁变为false,代表没钱了。
    * */
    public class BankCard03 {
        //余额
        private double money;
        //标记
        private boolean flag=false;// true 表示有钱可以取钱 false没钱 可以存取,默认是false,因为要先存
        //存钱
        /*
        * 如果有钱,就放在队列中,等着,什么也不执行,等到没有钱了,再去存;如果直接是没钱,就存。
        * */
        public synchronized void save(double m) {
            if (flag) { //存钱,有钱,则进入队列,等待唤醒
                try {
                    this.wait(); //这里是锁.wait();因此这个方法或者是代码块要加同步
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money + m;
            System.out.println(Thread.currentThread().getName()+"存了" + m + " 余额是" + money);
            // 修改标记
            flag = true;
            // 唤醒取钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
        }
        //取钱
        public synchronized void take(double m) { //这里锁就是this
            if (!flag) { //这也是一道坎,下面不要用else
                try {
                    this.wait(); //锁.wait(),这里为什么要总是写这个,是向告诉你调用wait方法要用锁,因此必须在同步方法或者同步代码块中
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money - m;
            System.out.println(Thread.currentThread().getName()+"取了" + m + " 余额是" + money);
            // 修改标记
            flag = false;
            // 唤醒存钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
        }
    }
    /*
    * 上面关卡的作用,虽然我们不能保证哪个线程先执行,但是通过管卡设置,一定会保证先执行
    * //注意:这里不要写else,因为上面的if相当于是门槛
            money = money + m;
            System.out.println(Thread.currentThread().getName()+"存了" + m + " 余额是" + money);
            // 修改标记
            flag = true;
            // 唤醒取钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
    
            再执行
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money - m;
            System.out.println(Thread.currentThread().getName()+"取了" + m + " 余额是" + money);
            // 修改标记
            flag = false;
            // 唤醒存钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
            再执行存
            再执行取
            。。。。。
    * */
    package com.yuncong.java_thread;
    public class AddMoney03 implements Runnable {
        private BankCard03 card;
        public AddMoney03(BankCard03 card) {
            this.card = card;
        }
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                card.save(1000);
            }
        }
    }
    package com.yuncong.java_thread;
    public class SubMoney03 implements Runnable {
        private BankCard03 card;
        public SubMoney03(BankCard03 card) {
            this.card = card;
        }
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                card.take(1000);
            }
        }
    }
    package com.yuncong.java_thread;
    public class TestBankCard03 {
        public static void main(String[] args) {
            //1. 创建银行卡
            BankCard03 card = new BankCard03();
            //2. 创建操作
            AddMoney03 add = new AddMoney03(card);
            SubMoney03 sub = new SubMoney03(card);
            //3. 创建线程对象
            Thread chenchen = new Thread(add, "晨晨");
            Thread bingbing = new Thread(sub, "冰冰");
            //4. 启动
            chenchen.start();
            bingbing.start();
        }
    }

    运行结果:

     多存多取问题分析:

    注意:上面两人中,我们虽然不能控制哪个线程先执行,但是控制了先存再取,此时如果是多人呢??

    package com.yuncong.java_thread;
    public class TestBankCard03 {
        public static void main(String[] args) {
            //1. 创建银行卡
            BankCard03 card = new BankCard03();
            //2. 创建操作
            AddMoney03 add = new AddMoney03(card);
            SubMoney03 sub = new SubMoney03(card);
            //3. 创建线程对象
            Thread chenchen = new Thread(add, "晨晨");
            Thread bingbing = new Thread(sub, "冰冰");
    
            Thread mingming = new Thread(add, "明明");
            Thread lili = new Thread(sub, "莉莉");
            //4. 启动
            chenchen.start();
            bingbing.start();
            mingming.start();
            lili.start();
        }
    }

    运行结果:,此时出现余额大于1000和小于0的问题,这是问什么呢??

     

     对于左侧的现象,会出现大于1000和小于0的问题,怎么解决??将if改为while即可,但是还是存在死锁问题,比如右侧这种,怎么办,将notify()改为notifyAll()。这样即可解决。运行结果不再展示,代码如下:

    package com.yuncong.java_thread;
    /*正常情况下,我们时无法控制谁先抢到cpu,即哪个线程先执行,这样就会出现取钱抢到资源,
    但里面一直位空的现象。但是呢?我们可以设置这个线程抢到锁后,是否执行其中的代码。
    这里添加一个标记,如果是false,则是没钱,取钱线程拿到资源后不能取,要释放锁,并到队列中等待
    存钱后唤醒,如果是true,则可以取,但取之后把锁变为false,代表没钱了。
    * */
    public class BankCard03 {
        //余额
        private double money;
        //标记
        private boolean flag=false;// true 表示有钱可以取钱 false没钱 可以存取,默认是false,因为要先存
        //存钱
        /*
        * 如果有钱,就放在队列中,等着,什么也不执行,等到没有钱了,再去存;如果直接是没钱,就存。
        * */
        public synchronized void save(double m) {
            while (flag) { //存钱,有钱,则进入队列,等待唤醒
                try {
                    this.wait(); //这里是锁.wait();因此这个方法或者是代码块要加同步
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money + m;
            System.out.println(Thread.currentThread().getName()+"存了" + m + " 余额是" + money);
            // 修改标记
            flag = true;
            // 唤醒取钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notifyAll(); //锁.notify
        }
        //取钱
        public synchronized void take(double m) { //这里锁就是this
            while (!flag) { //这也是一道坎,下面不要用else
                try {
                    this.wait(); //锁.wait(),这里为什么要总是写这个,是向告诉你调用wait方法要用锁,因此必须在同步方法或者同步代码块中
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money - m;
            System.out.println(Thread.currentThread().getName()+"取了" + m + " 余额是" + money);
            // 修改标记
            flag = false;
            // 唤醒存钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notifyAll(); //锁.notify
        }
    }
    /*
    * 上面关卡的作用,虽然我们不能保证哪个线程先执行,但是通过管卡设置,一定会保证先执行
    * //注意:这里不要写else,因为上面的if相当于是门槛
            money = money + m;
            System.out.println(Thread.currentThread().getName()+"存了" + m + " 余额是" + money);
            // 修改标记
            flag = true;
            // 唤醒取钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
    
            再执行
            //注意:这里不要写else,因为上面的if相当于是门槛
            money = money - m;
            System.out.println(Thread.currentThread().getName()+"取了" + m + " 余额是" + money);
            // 修改标记
            flag = false;
            // 唤醒存钱线程(另外一个线程可能在队列中等待,也可能没有,但是我们不管在不在,都唤醒一下,万一在了)
            this.notify(); //锁.notify
            再执行存
            再执行取
            。。。。。
    * */

    经典问题

    生产者、消费者

    若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区(一般用数组,也可以用集合去实现),生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

    下面是get和set方法,有参和无参构造,toString方法。后面是容器,生产者、消费者和测试方法。

    package com.yuncong.java_thread;
    //容器,存和取肯定要同步,因此方法上要加锁
    public class BreadCon {
        //存放面保的数组,容器大小是6
        private Bread[] cons = new Bread[6];
        //存放面保的位置,即下标
        private int index = 0;
        //存放面包
        public synchronized void input(Bread b) { //锁this
            //虽然不能控制哪个线程先抢到cpu,但是可以控制代码的执行顺序
            //先判断容器有没有满
            if (index >= 6) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            cons[index] = b;
            System.out.println(Thread.currentThread().getName()+"生产了"+b.getId()+"");
            index++;
            //唤醒
            this.notify();
        }
        //取出面包
        public synchronized void output() { //锁this
            if (index <= 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            index--;//为什么要先--呢??因此在添加面包时index++了,而取的话这里时空的
            Bread b = cons[index];
            System.out.println(Thread.currentThread().getName()+"消费了"+b.getId()+"生产者:"+b.getProductName());
            cons[index] = null;
            //唤醒生产者
            this.notify();
        }
    }
    package com.yuncong.java_thread;
    public class Product implements Runnable {
        private BreadCon con;//生产的话,要生产到哪里
    
        public Product(BreadCon con) {
            this.con = con;
        }
    
        @Override
        public void run() {
            //生产30个
            for (int i = 0; i < 30; i++) {
                con.input(new Bread(i,Thread.currentThread().getName()));
            }
        }
    }
    package com.yuncong.java_thread;
    public class Consume implements Runnable {
        private BreadCon con;//消费的话,要到哪里消费
        public Consume(BreadCon con) {
            this.con = con;
        }
        @Override
        public void run() {
            //也消费30个
            for (int i = 0; i < 30; i++) {
                con.output();
            }
        }
    }
    package com.yuncong.java_thread;
    public class TestForProCon {
        public static void main(String[] args) {
            //容器
            BreadCon con = new BreadCon();
            //生产和消费
            Product product = new Product(con);
            Consume consume = new Consume(con);
            //创建线程对象
            Thread chenchen = new Thread(product, "晨晨");
            Thread bingbing = new Thread(consume, "消费");
            //启动线程
            chenchen.start();
            bingbing.start();
        }
    }

    运行结果:

    如果再来两个人,则和上面的修改是一样的。

    总结:

    线程的创建

    • 方式1:继承Thread类
    • 方式2:实现Runnable接口(一个任务Task),传入给Thread对象并执行。

    线程安全:

    • 同步代码块:为方法中的局部大妈(原子操作)加锁(注意,锁必须是同一个对象)。
    • 同步方法:为方法中的所有代码(原子操作)加锁。

     线程间的通信:

    • wait() / wait(long timeout): 等待
    • notify() / notifyAll(): 通知

    高级多线程

    线程池概念

    问题:

    • 线程是宝贵的内存资源、单个线程约占1MB空间(不算运行用的内存),过多分配易造成内存溢出(1MB虽然少,但是如果线程多,就可能移除)。
    • 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降(如果有100个很小的任务,如果我们创建100个线程去操作,其实用于执行任务的时间远小于虚拟机的开销,即创建和收回的时间,如果可以只创建几个线程,每个线程执行多个任务,可能会有更好的效果)。

    线程池(其实就是提前创建好了一定量的线程,用的时候拿):

    • 线程容器,可设定线程分配的数量上限。
    • 将预先创建的线程对象存入池中,并重用线程池中的线程对象。
    • 避免频繁的创建和销毁。

     线程池原理

    将4个任务提交给线程池去管理,任务1由线程1执行,任务2由线程2执行,任务3由线程3执行,任务4由于没有线程空闲,等待。等任务1执行完,空出线程,则该贤臣根治性任务4.最后所有任务执行完毕。

    创建线程池

    常用的线程池接口和类(所在包java.util.concurrent):

    • Executor:线程池的顶级接口,里面只有一个方法。
    • ExecutorService:线程池接口,可通过submit(Runnable task)提交任务,实现了Executor接口。
    • Executors工厂类:通过此类可以获得一个线程池。其实就是工具类,就像Arrays,Collections一样。因为上面两个都是接口,我们肯定要用具体的实现来创建线程池,就是用这个类。
    • 通过newFixedThreadPool(int nThreads) 获取固定数量的线程池。参数:指定线程池中线程的数量。
    • 通过newCachedThreadPool() 获得动态数量的线程池,如不够则创建新的,没有上限。
    package com.yuncong.java_thread;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    /*
    * 演示线程池的创建
    * Executor:线程池的根接口,execute()
    * ExecutorService:包含管理线程池的一些方法,submit shutdown等
    *   ThreadPoolExecutor
    *       ScheduledThreadPoolExecutor
    *       但我们很少用这两个实现方法,因为创建线程池时参数非常多,很复杂
    * Executors:创建线程池的工具类
    *       (1)创建固定线程个数线程池
    *       (2)创建缓存线程池,由任务的多少决定
    *       (3)创建单线程池
    *       (4)创建调度线程池 调度意思是周期、定时执行
    * */
    public class ThreadPoolDemo01 {
        public static void main(String[] args) {
            // 1.创建固定线程个数的线程池
            ExecutorService es = Executors.newFixedThreadPool(4);
            //2.提交任务
            Runnable runnable = new Runnable() {
                private int ticket = 100;
                @Override
                public void run() {
                    while (true) {
                        if (ticket <= 0) {
                            break;
                        }
                        System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
                        ticket--;
                    }
                }
            };
            //3.提交任务,4个线程共卖100张票
            for (int i = 0; i < 4; i++) {
                es.submit(runnable);
            }
            //4.关闭线程池
            es.shutdown();//等待所有任务执行完毕,关闭线程池
            //es.shutdownNow();//不等待,直接关系
            //单两个方法后面都不能再用submit,因为不再接收新的任务
        }
    }

    上面的老师为什么没有考虑同步问题。 

    下面用newCachedThreadPool()方法,这个不需要指定线程个数,会根据任务决定个数。

     

     它创建线程个数不确定,这里一般是几个任务几个线程。单和任务大小等都有关。

    其它创建线程池方法

     Callable接口

    前面讲了联众创建线程的方法,现在再来讲第三种。

    public interface Callable<V> {

       public V call() throws Exception;

    }

    • JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。
    • Callable具有泛型返回值、可以声明异常。这是和Runnable接口不同的两点。
    package com.yuncong.java_thread;
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    /*
    * 演示Callable接口的使用
    * Callable和Runnable接口的区别
    * (1)Callable接口中call方法有返回值,Runnable接口中run方法没有返回值
    * (2)Callable接口中call方法有声明异常,Runnable接口中run方法没有异常
    * */
    public class CallableDemo01 {
        public static void main(String[] args) throws Exception {
            //功能需求,shiyongCallable实现1-100和
            //1.创建Callable对象
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName()+"开始计算");
                    int sum = 0;
                    for (int i = 0; i < 100; i++) {
                        sum += i;
                    }
                    return sum;
                }
            };
            //Callable接口也是交给Thread去执行,但是不能像Runnable那样直接交给,因为Thread构造方法中没有直接接收Callable的
            // 2.把Callable对象转成可执行任务
            FutureTask<Integer> task = new FutureTask<>(callable);
            /*
            * FutureTask代表将要执行的任务
            * 其实通过看源码,发现FutureTask类实现了RunnableFuture接口,该接口继承了Runnable接口
            * */
            //3.创建线程
            Thread thread = new Thread(task);
            //4.启动线程
            thread.start();
            //5.获取结果(该方法只有等待call方法执行完毕后,才会运行或者说返回)
            Integer sum = task.get();
            System.out.println("结果是:" + sum);
        }
    }

    对于上面这个操作,非常麻烦,因为要创建一个匿名内部类,再转化为可执行的任务,再传给线程,最后再获取结果。

    这个接口其实和线程池配合的非常好,下面我们开始学习。 

    package com.yuncong.java_thread;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    /*
    * 使用线程池配合Callable计算1-100的和
    * */
    public class CallableDemo02 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //1.创建线程池
            ExecutorService es = Executors.newFixedThreadPool(1);
            //2.提交任务Future:表示将要执行完任务的结果
            Future<Integer> future = es.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName() + "开始计算");
                    int sum = 0;
                    for (int i = 0; i < 100; i++) {
                        sum += i;
                    }
                    return sum;
                }
            });
            //3.获取任务结果,等待任务执行完毕
            System.out.println(future.get());
            //4.关闭线程池
            es.shutdown();
        }
    }

    上面怎么获取任务的返回值呢?其实这个submit有一个返回值是Future,里面就有运行后的结果,在前面的程序中我们用submit,我们没有用返回值。这里可以通过Future得到任务运行后的结果。

    以后这种线程池和Callable结合是我们经常使用的。

    Future接口

    • Future:表示将要完成任务的结果。(可以获得任务执行后的结果,上面已经演示了,这里再演示下)
    • 需求:使用两个线程,并发计算1~50、51~100的和,再进行汇总统计。一个大任务分成两个小任务,最后再做汇总。
    package com.yuncong.java_thread;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    /*
    * 使用两个线程,并发计算1~50、51~100的和,再进行汇总统计。
    * */
    public class CallableDemo03 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //1.创建线程池
            ExecutorService es = Executors.newFixedThreadPool(2);
            //2.提交任务
            Future<Integer> future1 = es.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int i = 1; i <= 50; i++) {
                        sum += i;
                    }
                    System.out.println("1-50计算完毕");
                    return sum;
                }
            });
            Future<Integer> future2 = es.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int i = 51; i <= 100; i++) {
                        sum += i;
                    }
                    System.out.println("51-100计算完毕");
                    return sum;
                }
            });
            //3.获取结果,结果汇总
            System.out.println(future1.get()+ future2.get());
            //4.关闭线程池
            es.shutdown();
        }
    }
    • 表示ExecutorService.submit()所返回的状态结果,其实就是call()的返回值。
    • 方法:V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)
    • 思考:什么是异步?什么是同步?

    同步:大家记住一点,只要有等待,就是同步。

    同步是没有等待,两个线程还是一起争夺时间片。

    Lock接口(一句话,比synchronized更强大)

     为什么出现Lock接口??在之前我们多线程访问共享资源时,需要加同步(synchronized),但是这种同步效率不高,因此又引入了另一种同步的API,就是Lock接口。

    • JDK5加入,与synchronized比较,显示定义,结果更灵活。
    • 提供更多实用性方法,功能更强大、性能更优越。
    • 常用方法:
      • void lock() //获取锁,如果锁被占用,则等待。以前就是用synchronized标记,现在是显示定义。
      • boolean tryLock() //尝试获取锁(成功返回true。失败返回false,不阻塞)很少用。
      • void unlock() //释放锁

    我们要用Lock接口的话,可以用匿名内部类,但是太麻烦,所以必须用其实现类。

    重入锁(是Lock接口实现类之一)

    • ReentrantLock:Lock接口的实现类,与synchronized一样具有互斥锁功能。

    package com.yuncong.java_thread;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class MyList {
        //创建锁
        private Lock lock = new ReentrantLock();
        private String[] str = {"A","B","","",""};
        private int count = 2;
    
        public void add(String value) {
            lock.lock();
            try {
                str[count] = value;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                System.out.println(Thread.currentThread().getName()+"添加了"+value);
            }finally {
                lock.unlock();
            }
        }
        public String[] getStr() {
            return str;
        }
    }
    package com.yuncong.java_thread;
    import java.util.Arrays;
    public class TestMyList {
        public static void main(String[] args) throws InterruptedException {
            MyList list = new MyList();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    list.add("hello");
                }
            };
            Runnable runnable2 = new Runnable() {
                @Override
                public void run() {
                    list.add("world");
                }
            };
            Thread t1 = new Thread(runnable);
            Thread t2 = new Thread(runnable2);
            t1.start();
            t2.start();
            //像啊哟打印结果,必须加join,因为代表上面两个线程执行完了
            t1.join();
            t2.join();
            System.out.println(Arrays.toString(list.getStr()));
        }
    }

    运行结果:

    下面再把之前那个卖票问题写一下 

    package com.yuncong.java_thread;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class Ticket03 implements Runnable {
        private int ticket = 100;
        private Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    if (ticket <= 0) {
                        break;
                    }
                    System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
                    ticket--;
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    package com.yuncong.java_thread;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    public class TestTicket03 {
        public static void main(String[] args) {
            Ticket03 ticket = new Ticket03();
            ExecutorService es = Executors.newFixedThreadPool(4);
            for (int i = 0; i < 4; i++) {
                es.submit(ticket);
            }
            es.shutdown();
        }
    }

    读写锁

    ReentrantReadWriteLock:

    • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
    • 支持多次分配读锁,使多个读操作可以并发执行。

    互斥规则:

    • 写-写:互斥,阻塞。
    • 读-写:互斥,读阻塞写、写阻塞读。
    • 读-读:不互斥、不阻塞。
    • 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。

    该看P50了。

    ------------恢复内容结束------------

  • 相关阅读:
    1143 Lowest Common Ancestor (30)
    PAT 1135 Is It A Red-Black Tree
    PAT 1119 Pre- and Post-order Traversals
    1102 Invert a Binary Tree(25 分)
    PAT总结
    c++ getline的用法
    PAT 1049 Counting Ones (30)
    PAT 1022 Digital Library (30)
    java jar包
    NIO的理解
  • 原文地址:https://www.cnblogs.com/G-JT/p/13892945.html
Copyright © 2011-2022 走看看