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了。

 

 

 

 

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

posted @ 2020-11-09 18:17  峡谷挨打记  阅读(124)  评论(0编辑  收藏  举报