JavaBasics-15-多线程
课程目标
1.什么是线程
2.线程的组成
3.线程的状态
4.线程安全
5.线程池
6.线程安全的集合
什么是线程
什么是进程?
当我们在电脑上安装一个程序(比如QQ),我们不运行它,它只是占用了一定的硬盘资源。当我们点击exe文件执行它的时候,它就成了一个进程。对于多个进程,计算机是通过PID(ProcessID)来区分的。
对于原本的单核CPU,看似是一次性执行多个程序,其实是错觉,单核CPU一次只能执行一个进程,只是通过切换让你觉得执行了多个。对于现在的多核CPU才是真正的实现同一时间点执行多个进程。
什么是线程?
对于单核CPU而言,其实这里的同时执行,也是宏观并行,微观串行。
一般来说一个进程都是有多个线程的。
进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位;
- 一个程序运行后至少有一个进程;
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的;
- 进程间不能共享数据段地址,但同进程的线程之间可以。
线程是进程中一条执行路径,在进程中实际是由谁来负责代码的执行的?就是线程。而线程的运行又离不开CPU,单核CPU同一时间点只能执行一条线程。
线程的组成
任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间。
- 运行数据:
- 对空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
- 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
- 线程的逻辑代码。
线程的特点
- 线程抢占式执行
- 效率高
- 可防止单一线程长时间独占CPU
- 在单核CUP中,宏观上同时执行,围观上顺序执行。
解释:有线程抢占式执行就有非抢占式执行,比如老板(CPU)有100个任务,有5个线程(5个工作狂)。如果老板来分配每个线程几个任务就是非抢占式的;如果老板说这100个任务,你们5个谁抢到谁干,就是抢占式的。抢占式效率高是为什么?谁抢到谁就立刻干,这样不会说有休息的时间,不如刺客A线程在执行,突然他不用CPU了,要去做其它事情,那么抢占式执行方案就可以让其它线程赶紧上,不给CPU休息的时间。为什么说可防止单一线程长时间独占CPU,那是自然,其实上面也说过了,不过再补充一下,抢占式执行,是CPU给每个线程分配了时间片,如果A线程该时间片执行完了,就要释放CPU资源,此时这5个线程再次同时抢CPU资源,谁抢到是谁的。
解析:对于单核CPU,宏观上同时执行,围观上顺序执行。这里就不用说了,宏观上看是大家都在执行,实际上是这几个线程(被分配了时间片),在抢着执行。同一时间点只能有一个线程执行。不过现在都是多核的了。
创建线程
创建线程三种方式
- 【继承Thread类,重写run方法】
- 【实现Runnable接口】
- 实现Callable接口(这个是JDK1.5之后新增的方法,许多地方只写了前面两种)
创建线程方式一
线程创建步骤:1.创建线程类,继承Thread类,并重写run方法(写该线程运行的代码);2.创建对象,并调用start方法执行子线程(不要调用run方法)
运行结果:
获取线程名称
- 在Thread的子类中调用this.getId()或this.getName()
- 使用Thread.currentThread().getId()和Thread.currentThread().getName()。(推荐)
修改线程名称
- 调用线程对象的setName()方法
- 使用线程子类的构造方法赋值
用第一种方式来获得线程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);设置为守护线程
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
- 垃圾回收器线程属于守护线程。
休眠:
这里有几个注意点:
- 你想要哪个线程休眠,就要把Thread.sleep(1000)写在哪个方法里面,比如你要向main线程休眠,就写在主方法,你向要继承了Thread类的MyThread01类的线程休眠,就放在该类的run方法里面。
- 该方法有异常,异常处理有两种方式,可以抛出,也可以捕获。这里只能捕获,为什么?因此无论是继承的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而言,其实这里的同时执行,也是宏观并行,微观串行。
一般来说一个进程都是有多个线程的。
进程和线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位;
- 一个程序运行后至少有一个进程;
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的;
- 进程间不能共享数据段地址,但同进程的线程之间可以。
线程是进程中一条执行路径,在进程中实际是由谁来负责代码的执行的?就是线程。而线程的运行又离不开CPU,单核CPU同一时间点只能执行一条线程。
线程的组成
任何一个线程都具有基本的组成部分:
- CPU时间片:操作系统(OS)会为每个线程分配执行时间。
- 运行数据:
- 对空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
- 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
- 线程的逻辑代码。
线程的特点
- 线程抢占式执行
- 效率高
- 可防止单一线程长时间独占CPU
- 在单核CUP中,宏观上同时执行,围观上顺序执行。
解释:有线程抢占式执行就有非抢占式执行,比如老板(CPU)有100个任务,有5个线程(5个工作狂)。如果老板来分配每个线程几个任务就是非抢占式的;如果老板说这100个任务,你们5个谁抢到谁干,就是抢占式的。抢占式效率高是为什么?谁抢到谁就立刻干,这样不会说有休息的时间,不如刺客A线程在执行,突然他不用CPU了,要去做其它事情,那么抢占式执行方案就可以让其它线程赶紧上,不给CPU休息的时间。为什么说可防止单一线程长时间独占CPU,那是自然,其实上面也说过了,不过再补充一下,抢占式执行,是CPU给每个线程分配了时间片,如果A线程该时间片执行完了,就要释放CPU资源,此时这5个线程再次同时抢CPU资源,谁抢到是谁的。
解析:对于单核CPU,宏观上同时执行,围观上顺序执行。这里就不用说了,宏观上看是大家都在执行,实际上是这几个线程(被分配了时间片),在抢着执行。同一时间点只能有一个线程执行。不过现在都是多核的了。
创建线程
创建线程三种方式
- 【继承Thread类,重写run方法】
- 【实现Runnable接口】
- 实现Callable接口(这个是JDK1.5之后新增的方法,许多地方只写了前面两种)
创建线程方式一
线程创建步骤:1.创建线程类,继承Thread类,并重写run方法(写该线程运行的代码);2.创建对象,并调用start方法执行子线程(不要调用run方法)
运行结果:
获取线程名称
- 在Thread的子类中调用this.getId()或this.getName()
- 使用Thread.currentThread().getId()和Thread.currentThread().getName()。(推荐)
修改线程名称
- 调用线程对象的setName()方法
- 使用线程子类的构造方法赋值
用第一种方式来获得线程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);设置为守护线程
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
- 垃圾回收器线程属于守护线程。
休眠:
这里有几个注意点:
- 你想要哪个线程休眠,就要把Thread.sleep(1000)写在哪个方法里面,比如你要向main线程休眠,就写在主方法,你向要继承了Thread类的MyThread01类的线程休眠,就放在该类的run方法里面。
- 该方法有异常,异常处理有两种方式,可以抛出,也可以捕获。这里只能捕获,为什么?因此无论是继承的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了。
------------恢复内容结束------------