1. 多线程
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?a
要解决上述问题,咱们得使用多进程或者多线程来解决.
1.1 并发与并行
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
- 并发:指两个或多个事件在同一个时间段内发生。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.2 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
进程
线程
进程与线程的区别
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
注意:下面内容为了解知识点
1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
线程调度:
计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令。所谓多进程并发运行,从宏观上看,其实是各个进程轮流获得CPU的使用权,分别执行各自的任务。那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度。JVM采用的是抢占式调度,没有采用分时调度,因此可以能造成多线程执行结果的的随机性。
1.3 创建线程类
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
创建Thread子类的实例,即创建了线程对象
调用线程对象的start()方法来启动该线程
代码如下:
测试类:
public class Demo01 { public static void main(String[] args) { //创建自定义线程对象 MyThread mt = new MyThread("新的线程!"); //开启新线程 mt.start(); //在主方法中执行for循环 for (int i = 0; i < 200; i++) { System.out.println("main线程!"+i); } }}
自定义线程类:
public class MyThread extends Thread { //定义指定线程名称的构造方法 public MyThread(String name) { //调用父类的String参数的构造方法,指定线程的名称 super(name); } /** * 重写run方法,完成该线程执行的逻辑 */ @Override public void run() { for (int i = 0; i < 200; i++) { System.out.println(getName()+":正在执行!"+i); } }}
1.4 多线程原理
昨天的时候我们已经写过一版多线程的代码,很多同学对原理不是很清楚,那么我们今天先画个多线程执行时序图来体现一下多线程程序的执行流程。
代码如下:
自定义线程类:
public class MyThread extends Thread{ /* * 利用继承中的特点 * 将线程名称传递 进行设置 */ public MyThread(String name){ super(name); } /* * 重写run方法 * 定义线程要执行的代码 */ public void run(){ for (int i = 0; i < 20; i++) { //getName()方法 来自父亲 System.out.println(getName()+i); } }}
测试类:
public class Demo { public static void main(String[] args) { System.out.println("这里是main线程"); MyThread mt = new MyThread("小强"); mt.start();//开启了一个新的线程 for (int i = 0; i < 20; i++) { System.out.println("旺财:"+i); } }}
流程图:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
1.5 Thread类
在上一天内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:
构造方法:
- public Thread():分配一个新的线程对象。
- public Thread(String name):分配一个指定名字的新的线程对象。
- public Thread(Runnable target):分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
常用方法:
- public String getName():获取当前线程名称。
- public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
- public void run():此线程要执行的任务在此处定义代码。
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
- public static Thread currentThread():返回对当前正在执行的线程对象的引用。
翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。
1.6 实现接口方式
采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动线程。
代码如下:
public class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName()+" "+i); } }}
public class Demo { public static void main(String[] args) { //创建自定义类对象 线程任务对象 MyRunnable mr = new MyRunnable(); //创建线程对象 Thread t = new Thread(mr, "小强"); t.start(); for (int i = 0; i < 20; i++) { System.out.println("旺财 " + i); } }}
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
适合多个相同的程序代码的线程去共享同一个资源。
可以避免java中的单继承的局限性。
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
1.7 匿名内部类方式
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread { public static void main(String[] args) { //new Runnable(){//public void run(){//for (int i = 0; i < 20; i++) {//System.out.println("张宇:"+i);//}//} // }; //---这个整体 相当于new MyRunnable() Runnable r = new Runnable(){ public void run(){ for (int i = 0; i < 20; i++) { System.out.println("张宇:"+i); } } }; new Thread(r).start(); for (int i = 0; i < 20; i++) { System.out.println("费玉清:"+i); } }}
2. 线程安全
2.1 线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
模拟票:
public class Ticket implements Runnable { private int ticket = 100; /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while (true) { if (ticket > 0) {//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name + "正在卖:" + ticket--); } } }}
测试类:
public class Demo { public static void main(String[] args) { //创建线程任务对象 Ticket ticket = new Ticket(); //创建三个窗口对象 Thread t1 = new Thread(ticket, "窗口1"); Thread t2 = new Thread(ticket, "窗口2"); Thread t3 = new Thread(ticket, "窗口3"); //同时卖票 t1.start(); t2.start(); t3.start(); }}
结果中有一部分这样现象:
发现程序出现了两个问题:
相同的票数,比如5这张票被卖了两回。
不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
2.2 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
同步代码块。
同步方法。
锁机制。
2.3 同步代码块
- 同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){ 需要同步操作的代码}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
锁对象 可以是任意类型。
多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
public class Ticket implements Runnable{ private int ticket = 100; Object lock = new Object(); /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ synchronized (lock) { if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket--); } } } }}
当使用了同步代码块后,上述的线程的安全问题,解决了。
2.4 同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){ 可能会产生线程安全问题的代码}
同步锁是谁?
对于非static方法,同步锁就是this。 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
代码演示同步方法的锁对象:
略
2.5 死锁
2.5.1 概述
在实现多线程程序时, 不要出现同步代码块嵌套的行为, 否则可能会出现死锁的情况, 死锁指的是多个线程同时抢多把锁, 因为CPU执行线程的随机性, 从而导致线程"卡死"的情况. 即: 死锁至少需要两个线程, 两把锁, 一个线程先抢锁A, 后抢锁B, 而另一个线程先抢锁B, 后抢锁A, 则它们就可能发生死锁现象.
2.5.2 图解
2.5.3 代码演示
略
2.6 线程的生命周期
2.6.1 概述
线程的生命周期指的是某一个线程从开始创建直至销毁时, 所经历的全部阶段, 主要分为:
- 新建
- 就绪
- 运行(运行的时候可能会发生阻塞)
- 死亡
2.6.2 图解
初级版图解
![](https://i-blog.csdnimg.cn/blog_migrate/5785931aea32e1ffb466762f72999156.png)
进阶版图解
![](https://i-blog.csdnimg.cn/blog_migrate/5d858009be0b613ab996c7db7477b3f8.png)
2.7 线程安全的类
- StringBuffer
– 线程安全,可变的字符序列
– 从版本JDK 5开始,被StringBuilder 替代。 通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步
- Vector
– 从Java 2平台v1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Vector被同步。 如果不需要线程安全的实现,建议使用ArrayList代替Vector
- Hashtable
– 该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键或者值
– 从Java 2平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Hashtable被同步。 如果不需要线程安全的实现,建议使用HashMap代替Hashtable
3. 线程进阶
3.1 线程优先级
- 线程调度
– 两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
– Java使用的是抢占式调度模型
– 随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
- 优先级相关方法
方法名 | 说明 |
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级, 线程默认优先级是5;线程优先级的范围是:1-10 |
- 代码演示
public class ThreadPriority extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); } }}public class ThreadPriorityDemo { public static void main(String[] args) { ThreadPriority tp1 = new ThreadPriority(); ThreadPriority tp2 = new ThreadPriority(); ThreadPriority tp3 = new ThreadPriority(); tp1.setName("高铁"); tp2.setName("飞机"); tp3.setName("汽车"); //public final int getPriority():返回此线程的优先级 System.out.println(tp1.getPriority()); //5 System.out.println(tp2.getPriority()); //5 System.out.println(tp3.getPriority()); //5 //public final void setPriority(int newPriority):更改此线程的优先级// tp1.setPriority(10000); //IllegalArgumentException System.out.println(Thread.MAX_PRIORITY); //10 System.out.println(Thread.MIN_PRIORITY); //1 System.out.println(Thread.NORM_PRIORITY); //5 //设置正确的优先级 tp1.setPriority(5); tp2.setPriority(10); tp3.setPriority(1); tp1.start(); tp2.start(); tp3.start(); }}
3.2 线程控制
- 相关方法
方法名 | 说明 |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,<br />Java虚拟机将退出 |
- 代码演示
sleep演示:public class ThreadSleep extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}public class ThreadSleepDemo { public static void main(String[] args) { ThreadSleep ts1 = new ThreadSleep(); ThreadSleep ts2 = new ThreadSleep(); ThreadSleep ts3 = new ThreadSleep(); ts1.setName("曹操"); ts2.setName("刘备"); ts3.setName("孙权"); ts1.start(); ts2.start(); ts3.start(); }}Join演示:public class ThreadJoin extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); } }}public class ThreadJoinDemo { public static void main(String[] args) { ThreadJoin tj1 = new ThreadJoin(); ThreadJoin tj2 = new ThreadJoin(); ThreadJoin tj3 = new ThreadJoin(); tj1.setName("康熙"); tj2.setName("四阿哥"); tj3.setName("八阿哥"); tj1.start(); try { tj1.join(); } catch (InterruptedException e) { e.printStackTrace(); } tj2.start(); tj3.start(); }}Daemon演示:public class ThreadDaemon extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); } }}public class ThreadDaemonDemo { public static void main(String[] args) { ThreadDaemon td1 = new ThreadDaemon(); ThreadDaemon td2 = new ThreadDaemon(); td1.setName("关羽"); td2.setName("张飞"); //设置主线程为刘备 Thread.currentThread().setName("刘备"); //设置守护线程 td1.setDaemon(true); td2.setDaemon(true); td1.start(); td2.start(); for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } }}
3.3 Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
- ReentrantLock构造方法
方法名 | 说明 |
ReentrantLock() | 创建一个ReentrantLock的实例 |
- 加锁解锁方法
方法名 | 说明 | |
void lock() | 获得锁 | |
void unlock() | 释放锁 |
- 代码演示
public class SellTicket implements Runnable { private int tickets = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } finally { lock.unlock(); } } }}public class SellTicketDemo { public static void main(String[] args) { SellTicket st = new SellTicket(); Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); t1.start(); t2.start(); t3.start(); }}
4. 线程池
4.1 概述
有了多线程技术, 服务器端就可以同时为多个客户端提供服务, 但是线程运行结束后就会死亡, 频繁创建新的线程, 会导致系统开销过大, 因此Java在JDK5版本中引入了线程池概念. 将多个线程对象存储到容器中, 用的时候从容器中取出, 使用完毕后再放回,这样可以大大减少系统开销。
4.2 实现方式一
4.2.1 涉及到的API
JDK提供的线程池并不是直接创建对象,而是用工厂类创建线程池对象。
- 线程池工厂类:Executors
– public static ExecutorService newFixedThreadPool(int nThreads)
方法解释: 线程池工厂类创建固定线程数的方法
- 线程池接口:ExecutorService
– public Future<?> submit(Runnable task)
方法解释: 使用线程池对象方法submit提交线程执行任务,这是实现多线程的第三种方式
– public void shutdown()
方法解释: 关闭线程池
4.2.2 参考代码
//案例: 线程池入门public class Demo01 { public static void main(String[] args) { //1. 工厂类创建线程池对象,该池中存储10个线程对象。 ExecutorService ex = Executors.newFixedThreadPool(10); //2. 提交任务 /* 上例的线程池对象已经包含了10个线程,线程池提供了submit方法, 接收不同的线程执行目标类对象,接收到后,线程池会自动分配一个线程执行。 格式: ex.submit(线程执行目标类对象); */ //这里使用匿名内部类的方式简单演示: for (int i = 0; i < 20; i++) { ex.submit(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " Hello World!..." + i); } } }); } //3. 销毁线程池. ex.shutdown(); }}
4.3 实现方式二
JDK5以后, 实现多线程有了一种新的方式, 即: 通过Callable接口实现.
好处
- 线程执行后, 可以具有返回值.
- 可以抛出异常.
弊端
- 该方式只能结合线程池一起使用.
- 线程池工厂类:Executors
4.3.1 涉及到的API
– public static ExecutorService newFixedThreadPool(int nThreads)
方法解释: 线程池工厂类创建固定线程数的方法
– public static ExecutorService newCachedThreadPool()
方法解释: 创建一个可根据需要创建新线程的线程池
- 线程池接口:ExecutorService
– public Future<?> submit(Runnable task)
方法解释: 使用线程池对象方法submit提交线程执行任务,这是实现多线程的第三种方式
– public Future submit(Callable task)
方法解释: 使用线程池对象方法submit提交线程执行任务,这是实现多线程的第三种方式
– public void shutdown()
方法解释: 关闭线程池
4.3.2 参考代码
//Callable接口的实现类public class MyCallable implements Callable { @Override public Object call() throws Exception { System.out.println("实现Callable接口的方式 实现多线程"); return "我是返回值"; }}//测试类public class MyCallableTest { public static void main(String[] args) throws Exception { //1. 创建线程池 //ExecutorService service = Executors.newCachedThreadPool(); //创建一个可根据需要创建新线程的线程池 ExecutorService service = Executors.newFixedThreadPool(5); //线程池工厂类创建固定线程数的方法 //2. 提交任务 //方式一: 提交任务, 但是不接收返回值. /*service.submit(new MyCallable()); service.submit(new MyCallable());*/ //方式二: 提交任务, 接收返回值 /*Future fu = service.submit(new MyCallable()); //获取线程执行后的返回值. System.out.println(fu.get());*/ //方式三: 提交Runnable接口的子类对象. Future fu2 = service.submit(new Runnable() { @Override public void run() { System.out.println("runnable接口的方式实现 线程池..."); } }); System.out.println(fu2.get()); //null //3. 关闭线程池 service.shutdown(); }}
4.3.3 细节
Callable:
- Callable是另外一种形式的线程执行目标类,相当于Runnable接口.
- 其中的call方法相当于Runnable中的run方法。
- 不一样的是,call方法具有返回值,run方法没有返回值。
Future:
- 将方法返回值封装成了对象,结合线程提供了返回结果的更多信息和功能。
- Future获取具体返回值的方法:V get()
- 比如run方法的返回值为void,获取到的值即为null。而返回值的类型是随着submit方法的调用而传入的。
5. 生产者消费者
5.1 生产者和消费者模式概述
5.1.1 图解
5.1.2 概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
5.1.3 Object类的等待和唤醒方法
为了体现生产和消费过程中的等待和唤醒, Java就提供了几个方法供我们使用, 这几个方法在Object类中.
方法名 | 说明 |
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程(随机唤醒) |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
5.2 生产者和消费者案例
5.2.1 案例需求
生产者消费者案例中包含的类:
奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
①创建奶箱对象,这是共享数据区域
②创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
③创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
④创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
⑤启动线程
5.2.2 代码实现
public class Box { //定义一个成员变量,表示第x瓶奶 private int milk; //定义一个成员变量,表示奶箱的状态 private boolean state = false; //提供存储牛奶和获取牛奶的操作 public synchronized void put(int milk) { //如果有牛奶,等待消费 if(state) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果没有牛奶,就生产牛奶 this.milk = milk; System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱"); //生产完毕之后,修改奶箱状态 state = true; //唤醒其他等待的线程 notifyAll(); } public synchronized void get() { //如果没有牛奶,等待生产 if(!state) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果有牛奶,就消费牛奶 System.out.println("用户拿到第" + this.milk + "瓶奶"); //消费完毕之后,修改奶箱状态 state = false; //唤醒其他等待的线程 notifyAll(); }}public class Producer implements Runnable { private Box b; public Producer(Box b) { this.b = b; } @Override public void run() { for(int i=1; i<=5; i++) { b.put(i); } }}public class Customer implements Runnable { private Box b; public Customer(Box b) { this.b = b; } @Override public void run() { while (true) { b.get(); } }}public class BoxDemo { public static void main(String[] args) { //创建奶箱对象,这是共享数据区域 Box b = new Box(); //创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作 Producer p = new Producer(b); //创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作 Customer c = new Customer(b); //创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递 Thread t1 = new Thread(p); Thread t2 = new Thread(c); //启动线程 t1.start(); t2.start(); }}