文章目录
在学习完java的基本语法、对象、继承、多态、反射、集合等基础知识后
假如我要实现如下功能:
程序不停地在屏幕上输出一句问候的语句(比如“你好”),同时,当我通过键盘输入固定输入的时候,程序停止向屏幕输出问候的语句(比如说输入gun)。
用我之前所学习的技术似乎无法做到,这时候就需要了解多线程相关知识。
如何理解线程?
从代码的执行路径的角度来理解线程。
首先,Java程序中所有的代码都运行在一条执行路径当中,而一条执行路径其实就是一个线程。换言之,我们所有的代码都是运行在某个线程当中的。
- 同一条执行路径的代码,他们只能顺序执行(main方法所在的那条执行路径当中)。
- 不同执行路径中的代码他们互不影响相互独立,可以”同时执行“。
了解多线程之前,首先了解多进程。
(一)多进程
那么什么是进程呢?一个进程就代表一个运行中的程序(这并非是进程的概念,只是形象化的理解)
前提:计算机中,cpu就是专门用来做运算,一切运算都是由cpu,而且cpu是计算机当中最宝贵的资源,事实上我们是把cpu的用来计算的时间当作是一种资源。
1.为什么会有多进程?
只有一个cpu,单核情况下。
-
单道批处理:
-
单道:在整个操作系统当中,同一时间内内存中只有一个程序运行,程序的运行只能是上一个程序运行完才开始运行下一个程序。
-
批处理:程序运行过程中,不会有任何响应,一次执行完毕。
-
单道批处理操作系统,它并不能很好的利用cpu的计算时间。
假设,在单道批处理系统中运行了一个程序,在他的程序中需要执行IO(例如和打印机传输数据),在IO的数据传输过程中,有很大一部分时间是不会用到cpu的计算功能的,此时cpu闲置。
-
-
多道批处理操作系统
-
多道:在操作系统中,内存中同时可以有多个应用程序在运行,这样一来一旦某个程序不需要使用cpu的计算功能,操作系统就会把cpu的计算时间分配给内存中其他的应用程序在运算,这样一来大大提高了cpu的利用效率。
应用程序的执行:在单核情况下,一个cpu在同一时间点只能为一个应用程序服务。当程序运行,占用cpu执行时间,这个应用才算真正的执行。在多道批处理系统中,多个应用程序交替执行,看起来好像在“同时”运行。
核心原因:进程的交替执行,交替过程是需要付出额外代价的——进程的上下文切换。
上下文切换
程序的运行数据在运行时要放到cpu的寄存器里,供cpu的运算器来运算,对于单进程来说,多个程序交替运行的过程中,为了保证程序之间的运行数据不相互干扰。
- 在程序切走的时候,该进程失去了cpu的使用权,在被其他进程占用cpu之前,要利用cpu功能,将当前这一刻计算机等相关状态保存起来。
- 当该进程重新获取cpu执行权,开始执行之前利用cpu的功能以及刚刚保存起来的上下文,把计算机恢复到上次切走时的程序执行状态。
上下文切换是一个比较耗时的工作,而且需要使用到cpu(cpu虽然不闲了,但是被加大了无关于程序本身运行的工作量,这就是额外的代价)。
-
-
现代操作系统
引入了另外一个东西:线程。
-
线程,又被称为轻量级进程,一个进程可以有多个线程。同一个进程的多个线程中,线程的上下文切换的时候付出的额外代价小得多——进一步提高cpu的利用率。
以线程为单位占用cpu,同一个进程的多个线程,共享一个进程的数据。
-
- 并发:并发执行就是交替执行,一段时间内多个程序“同时”运行。(其实是交替执行)
- 并行:并行执行多cpu,同一个时间点,多个程序同时运行。(真正的同时运行,要求硬件需要多个cpu,多核操作系统)。
通常生活中所说的同时,指的是并行。
结论:java语言当中利用多线程,实现的“并发”concurrent的“同时”效果。
2.两个问题
1.Java命令运行一个Java程序的过程?
java 主类类名(命令行打入Java 类名,运行这个java程序)
- 其实java命令,它启动了一个jvm进程(运行了一个程序)。
- 该jvm进程,在执行的时候,首先会创建一个线程,main线程。
- 在main线程中,运行主类中的main方法代码。
2.JVM是单线程还是多线程?
jvm是多线程的,其实至少还应该有另外一个垃圾回收器线程在执行。
例如:当堆上的某个对象没有引用变量指向它的时候,该对象就变成垃圾,被同时运行的垃圾回收器线程回收,保障堆上的内存不会被用完。
(二)多线程01
1.Thread实现方式一
- 继承Thread,定义Thread的子类
- 重写子类的run方法
- 创建该子类的对象
- 启动线程(对象)start()
注意事项
-
一个Thread类(Thread子类)对象代表一个线程。
-
为什么我们重写Thread类中的run方法?
只有Thread run()方法中的代码,才会执行在子线程中。为了保证子线程中运行的是我们想要在子线程中运行的代码,必须重写run()方法。
-
但是,如果想要让代码,在子线程中运行,并非一定,代码要写在run方法方法体中。
对于定义在该Thread子类中,其他方法方法体中的代码,也可以运行在子线程中。换句话说,一个方法,被哪个线程中的代码调用,被调用的方法,就运行在调用它的线程中。
-
启动线程,必须使用start()方法来启动,这样才能使得Thread中的run方法运行在子线程中。
如果通过调用run方法,来执行Thread的run方法代码,这仅仅只是普通的方法调用。 -
同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次。如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象。
2.线程调度
线程调度是指系统为线程分配处理器使用权的过程。
调度方式
假设在单CPU的情况下线程的两种主要调度模型:
-
协同式线程调度(Cooperative Thread-Scheduling)
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制。当他把自己的工作完成之后,会主动通知系统切换到另外一个线程上去。
好处:实现简单。
缺点:线程执行时间不可控,不稳定
-
抢占式调度(Preemptive Thread-Scheduling)
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
好处:现成的执行时间是可控的,根据线程的优先级来控制线程的切换以及执行时间。线程的优先级是动态可变的(随等待时常增长而增加,随执行时长而降低)。
Java使用哪种调度模型
抢占式调度。
Java中的优先级之所以会不太可靠:
- 不同操作系统的优先级机制不同,比如Solaris中有2147483648种优先级,但windows就只有7种优先级
- 可能会被系统改变,比如windows中存在一个称为“优先级推进器(Priority Boosting,当然他可以被关闭掉)的功能,他的大致作用,就是当他发现一个线程执行的特别勤奋的时候,可能会越过线程的优先级,去为他分配执行时间”。
3.Thread类API
Thread类成员方法
-
获取和设置线程名称
-
public final String getName()
-
public final void setName(String name)
-
public static Thread currentThread()//返回当前正在执行的线程对象的引用。该方法在哪个线程中被调用,那个线程就是该方法的当前线程。
思考,如何获取main线程的名称呢?
只要能拿到main方法所在线程的线程对象即可。在主方法中调用用currentThread()方法。
-
-
多线程的优先级
- public final int getPriority()//获取线程优先级(默认值为5)
- public final void setPriority(int priority)//修改线程优先级
注意事项:
-
多线程的优先级的取值范围1 <= priority <=10
线程的默认优先级为5
-
然而给线程对象设置优先级并没有什么用。
事实上我们在Java语言中设置的线程优先级,他仅仅只能被看成是一种“建议”(给操作系统的建议)。实际上操作系统本身有它自己的一套优先级(静态优先级+动态优先级)。
java官方:线程优先级并非完全没有用,我们Thread的优先级它具有统计意义,总的来说高优先级的线程占用cpu的执行时间多一点,低优先级的线程占用cpu的时间少一点。
-
结论:千万不要试图通过设置线程的优先级控制线程的执行的先后顺序
多线程带来了并发,但是同时多线程也给我们的程序带来了不确定性。
-
线程控制API
-
public static native void sleep (long millis)//在指定的毫秒数内让正在执行的线程休眠(暂时暂停)。指定的休眠时间以毫秒为单位。
被native修饰的方法称之为本地方法,本地方法都不是由java语言实现的。
-
public final void join()//等待该线程终止。
线程对象.join(),”该线程“:在哪个线程对象上调用join方法,该线程指的就是哪个线程对象所表示的线程。
谁等待?当前线程等待,join线程在哪个线程中被调用,哪个线程就等待。
等待谁?该线程,在哪个线程对象上调用join方法,等待的就是哪个对象的线程。
主线程等joinThread线程执行完毕再继续执行。
-
-
public static void yield()//(礼让方法)
-
暂停当前正在执行的线程对象(自己放弃cpu的使用权,yield方法可以实现)
-
执行其他线程。(yield方法不一定实现这个功能)
线程放弃cpu的使用权之后就会进入等待状态,此时cpu会在众多等待线程中再选择一个,不排除选到刚才自己放弃使用权的线程。
不确定性导致实际开发中也不会使用yield方法来进行线程调度。(Demo中可能会看到。)
-
-
public final void setDaemon(boolean on)//将该线程标记为守护线程(参数为true时)或用户线程(参数为false时)(我们创建出来的普通线程)。
运行特征:
-
当正在运行的线程都是守护线程,jvm直接退出(结束执行)。
使用场景:垃圾回收器实际上就运行在守护线程中。
-
该方法必须在线程启动前调用。
-
-
public void interrupt()//中断线程(打断线程的阻塞状态)
如果该线程在调用wait方法或join方法或sleep方法过程中受阻,线程将收到一个InterruptedException。
interrupt方法可以打断线程的休眠或者等待,跳转到catch分支,这意味着这个子线程中try块中的功能被异常终止了。
实际在应用层很少去使用,但是不是不用(比较底层的代码例如线程池会用到interrupt方法)。
4.线程的生命周期
-
新建:线程处于刚刚创建的状态。start方法。
-
就绪:有执行资格,等待cpu调度获得执行权。万事俱备只欠cpu,除了cpu资源,之外的程序执行条件都已经满足了。
-
运行:取得执行权,正在cpu上执行。优先级降低到某种程度后,操作系统会剥夺线程的cpu资源,调度其他优先级较高的就绪态线程,此时若上一个线程还没运行完,他又会回到就绪态。
-
阻塞:无执行资格,无执行权。处于执行态的线程如果调用了sleep(),join(),或者执行IO,就会处于等在状态,此时不该让他继续占用cpu,处于阻塞状态。
除了缺少cpu资源之外,还需要满足其他未满足的线程执行条件。当执行条件重新满足之后,就会再次转回就绪态,等待下一次被调度。
-
死亡:线程正常终止(run()方法执行完毕)或异常终止,线程对象成为垃圾,等待垃圾回收器回收。
5.线程的实现方式二
-
定义实现Runnable接口的子类。
-
实现Runnable接口的run方法。
-
创建该子类对象
-
在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象。
Thread并不是一个抽象类,我们是可以直接new Thread类的对象的。
-
启动Thread对象(启动线程),Thread.start();
注意事项:
- 我们的Runnable接口子类的run方法中的代码,会运行在子线程当中。
- 所以在线程的第二种实现方式当中,我们自己来定义子类,实现Runnable接口的run方法,将要在子线程中执行的代码在run方法中。
- 但是runnable子类对象并不代表一个线程,他只代表线程中执行的任务。
逻辑上说,第二种实现方法逻辑更加清晰。
- 线程就是一条执行路径,至于在线程这条执行路径上,究竟执行什么样的具体代码应该是和线程本身没有关系的。
- 也就是说线程和在线程(执行路径)上执行的任务应该是没有直接关系的。
- 线程的第二种实现方式,把线程(Thread对象代表线程)和在线程上执行的任务(Runnable接口子类的run方法)
Runnable接口子类对象的run方法运行在Thread接口中的原因图解。
实现方式一VS实现方式二
-
方式一比方式二少了一步。
-
方式一的实现方式存在一种单重继承的局限性。
复用已有代码并且让其运行在子线程中,做不到。
-
方式二将线程和任务解耦。
-
方式二便于多线程的数据的共享。
练习
多线程仿真如下场景:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
分析:
- 3个窗口售票,互不影响,同时进行。
- 3个窗口共同出售这100张电影票
//Ver1.0,实现方式一
class SalesWindow extends Thread {
int tickets = 100;
@Override
//售票
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
System.out.println(getName()+"售出了第"+(this.tickets--)+"张票");
}
}
}
三个窗口各买各的,每个窗口卖一百张,总共卖了三百张,功能实现失败。若要实现数据共享可以在tickets前加static修饰,使所有对象共享它。
//Ver2.0,实现方式二
class SalesTask implements Runnable {
int tickets = 100;
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
三个线程都使用一个功能,实现了数据共享。
在不考虑其他因素的前提下,通过多线程程序模拟上述的售场景,基本没啥问题。
但我们的程序其实还可以做的更真一些,因为实际情况下,售票数据通过网络传输,总是存在一些延迟的情况。
所以在真正售出一张票后,需要一段时间,才可以真正去修改剩余票数。
因此,我们可以在程序中,增加对于售票延迟的模拟,即每次卖票延迟100ms,后再去修改剩余票数
class SalesTaskVer2 implements Runnable {
private int tickets = 100;
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
当增加售票延迟之久,出现了一些明显的错误。
- 相同的票,被卖出了多次。(多卖问题)
- 卖出了不存在的票。(超卖问题)
其实不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。
6.数据安全
在多线程运行环境下,在多个线程访问线程之间共享的数据时,访问到了错误的共享数据的值。不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。
产生原因
-
多线程运行环境。
需求决定,无法打破
-
数据共享。
需求决定,无法打破
-
共享数据的非原子操作。
原子操作:就是一组不可分割的操作。这组操作要么一次全部完成,要么一步都不做。当某个线程在进行原子操作的过程中产生了线程的切换,其他线程访问了这个时刻的中间状态,这可能会导致多线程共享数据错乱。
可以打破
解决多线程数据安全问题一一>如何将多线程中对共享变量一组的操作变成原子操作。
解决方案
打破以上三个产生原因之一即可。
两种思路构造原子操作(一个线程对共享数据的访问一次完成)
- 在一次原子操作过程当中阻止线程切换。思路没问题,但我们做不到,这是涉及到线程调度,在抢占式线程调度当中,线程调度是系统决定的。
- 线程切换无法阻止,给共享变量加锁,保证:
- 只有加锁的线程,能访问到共享变量。
- 在加锁线程没有完成对共享变量的一组操作之前,不会释放锁。
- 只要不释放锁,其他线程即使被调度执行也无法访问共享变量。
7.同步代码块synchronized
synchronized(锁对象){
//一组要作为原子操作的代码(需要访问共享数据)
}
-
synchronized代码块中的锁对象可以是java语言中的任意一个对象(仅限于synchronized代码块语句中)。
- 因为java中所有的对象内部都有一个标志位,表示加锁和解锁的状态。
- 所以其实锁对象就充当着锁的角色,所谓的加锁和解锁其实就是设置锁对象的标志位,来表示加锁解锁的状态。
-
我们的代码都是在某一条执行路径(某一个线程)中运行,当某个线程执行到synchronized代码块时,会尝试在当前线程中对锁对象加锁。
- 此时如果锁对象处于未加锁状态,jvm就会设置锁对象标志为(加锁),并且在锁对象中记录是哪个线程加的锁,然后让枷锁成功的当前线程执行同步代码块中的代码。
- 此时如果锁对象已经被枷锁而且加锁线程不是当前线程,系统会让当前线程处于阻塞状态(等待),直到加锁线程执行完了对共享变量的一组操作并释放锁。
-
加锁线程何时释放锁?
当枷锁线程执行完了同步代码块中的代码(对共享变量的一组操作),在推出同步代码块之前jvm会自动清理锁对象的标志位,将锁对象变成未上锁状态(释放锁)。
class SalesTaskVer3 implements Runnable {
private int tickets = 100;
Object lockObj = new Object();//锁对象
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockObj) {
//double check
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
}
}
千万要注意:
- 虽然synchronized代码块中的锁对象可以是java语言中的任意对象。
- 但是在多线程运行环境下,想要让访问同一个共享变量的多个synchronized代码块中的代码是原子操作,那么对同一个共享变量的访问必须使用同一个锁对象!
同步异步
解决多线程数据安全问题是通过加锁,构造一个线程对共享变量的原子操作。但其实加锁其实就是在完成线程同步。
- 同步:我走你不走,你走我不走。所有的加锁失败的线程步调变得一致了,都需要等待锁对象被释放。
- 异步:你走你的我走我的,多线程天生异步,不同的线程相互独立不受影响,各自按照各自的执行步调,执行各自的代码。
所以,最终,其实我们是用线程同步来解决线程数据安全问题的。
线程同步优缺点:
- 优点:解决了多线程数据安全问题
- 缺点:相比于异步,因为等待锁资源而引发的阻塞,降低了程序运行的效率。
同步方法
整个方法就是一个同步代码块,其效果等价于同步代码块。同步方法的锁对象是this.当前对象(锁对象是隐式给出的)
class SalesTasknew implements Runnable {
private int tickets = 100;
Object lockObj = new Object();
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.salesingLock();
}
}
private synchronized void salesingLock() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
静态方法也可以是同步方法。静态方法通过类名.静态方法()调用。静态方法依赖于类而存在,而jvm中一个类对应一个class对象,所以一个静态方法锁对象就是表示静态方法所属类的Class对象(隐式给出的)。
(三)多线程02
1.Lock锁(线程锁)
synchronized关键字虽然可以理解加锁,解锁的原理,但是却看不到具体的过程。实现同步代码块,除了使用synchronized之外,其实JDK1.5之后,提供了另外的方式Lock锁机制。
利用线程同步,解决多线程的数据安全问题,有两种方式:
- synchronized+java中任意对象。
- Lock锁对象。
Lock锁对象VSsynchronized锁对象
-
synchronized锁对象只提供了用来模拟锁状态的标志位(加锁和释放锁),但是加锁和释放锁都是由jvm隐式完成的,和锁对象本身无关,所以synchronized锁对象不是一把完整的锁。
-
一个Lock对象,就代表一把锁,而且还是一把完整的锁,Lock对象,它如果要实现加锁和释放锁,不需要synchronized关键字配合,它自己就可以完成。
Lock:loch()加锁;unlock释放锁。
-
两种锁对象,实现方式完全不同
class SalesTasknew implements Runnable { private int tickets = 100; //常用子类:ReentrantLock Lock lock = new ReentrantLock(); @Override public void run() { //当票没卖完,就一直卖票 while (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //this.salesingLock(); lock.lock(); if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票"); } lock.unlock(); } } }
联系:都可以实现线程同步
-
synchronized(锁对象){需要同步的代码}
-
lock.lock()
需要同步的代码
lock.unlock()
-
它们分别什么情况下使用
推荐使用synchronized+java对象完成线程同步。
- 两种方式实现的效果是相同的,而synchronized+java对象更简单易用。
- jdk1.5/1.6等早期版本中,Lock锁效率高一些,而现在从效率角度来讲,使用synchronized和使用Lock对象,效率几乎完全相同。
Lock锁比较标准的用法
Lock l=......;
l.lock();
try{
//
}finally{
//不管正常还是异常都可以释放这把锁
l.unlock();
}
死锁问题
同步另一个弊端:如果出现了嵌套锁,可能产生死锁。
某个线程要同时处有两把锁lockA和lockB,换个说法,这个线程在成功持有lockA锁的情况下,再持有lockB锁。
synchronized(lockA){
//当某线程的代码执行到这里
synchronized(lockA){
//执行到这里,在成功持有lockA锁的情况下,再持有lockB锁,此时当前线程就同时持有两把锁
}
}
案例:
public class Exnew02 {
public final static Object lockA = new Object();
public final static Object lockB = new Object();
}
class ABThread extends Thread {//线程一
@Override
public void run() {
synchronized (Exnew02.lockA) {
//访问共享变量,计算,得到一些中间结果
System.out.println("ABThread,A锁");//输出了
synchronized (Exnew02.lockB) {
//访问打印机,打印中间结果
System.out.println("ABThread,B锁");
}
}
}
}
class BAThread extends Thread {//线程二
@Override
public void run() {
//尝试访问打印机
synchronized (Exnew02.lockB) {
//获得打印机访问权
System.out.println("BAThread,B锁");//输出了
synchronized (Exnew02.lockA) {
//再获取共享变量访问权,进行计算
System.out.println("BAThread,A锁");
}
}
}
}
死锁问题
死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象。
解决方式:
-
调整线程获取多把锁的顺序,将多个线程中获取锁的顺序换成一样的。
-
要么一个线程同时持有所需要的多把锁,要么一把锁都不加。实质就是把加多把锁作为一个原子操作。
再定义一把新锁,利用这把锁(synchronized+该对象),实现将加多把锁的操作变成一个原子操作。
生产者消费者模型
多个生产者和多个消费者各自都是以异步的方式运行
但是在某些情况下,生产者和消费者之间必须保持协作:
- 当缓冲区空的时候,不允许消费者到缓冲区中取数据。
- 当缓冲区满的时候,不允许生产者向缓冲区中放入数据。
- 同时缓冲区中的一个单元,只能放入一个产品。
- 同时还要注意,因为生产者和消费者都是异步的,但是它们都共享缓冲区!!
线程间通信
Java中主要通过Object中的方法来实现:
-
public final void wait()//阻止自己,在其他线程调用此对象的notify方法或notifyAll方法前导致当前线程等待。
-
wait方法的阻塞条件:
在当前线程中,如果在对象上调用wait的方法,就会导致当前线程阻塞。
-
wait方法的的唤醒条件:
首先有一种说法,当某线程因为在某对象上调用了wait方法,处于阻塞状态,我们就说该线程在该对象上阻塞。
如果要唤醒在某对象上阻塞的线程,就必须在其他线程中,在同一(线程阻塞的)对象上调用notify或notifyAll方法。
-
wait方法的使用条件:
当前线程必须拥有此对象监视器。当前线程持有该锁对象(把该对象当成锁对象,并让当前线程加锁)。
当某线程成功对某把锁加锁,我们就说某线程持有这把锁。
-
wait方法执行特征(锁对象的角度):
该线程释放(release)对此监视器的所有权并等待。释放锁对象的持有。
理解:wait方法只能被一个线程的锁对象调用。当调用wait方法后,当前线程先释放锁,进入阻塞状态等待下次被调用。当有其他线程中的notify或者notifyAll被调用,通知这个被阻塞的线程,这个线程就会进入就绪态等待cpu的调度,被调度后先再次加锁,然后接着上次执行被切走的地方继续执行。
-
-
notify()//通知别人
唤醒在此对象监视器(锁)上等待的线程;如果有多个线程都在此对象上等待,则选择一个线程,选择是随机的并在对实现做出决定时发生。
-
notifyAll()//通知别人
唤醒在此对象监视器上阻塞的所有线程。
/第二版蒸笼,智能蒸笼,完成功能
//线程同步:利用同步方法,都在同一个container上调用同步方法
//线程通信(协作)wait,notifyAll
class Container {
private Food food;
//消费者用
public synchronized void eatFood() {
if (food == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "吃了" + food);
food = null;
this.notifyAll();
}
}
//生产者用
public synchronized void setFood(Food newfood) {
if (food == null) {
//newfood是PDTask类里的新做的食物,把它放到蒸笼里
food = newfood;
System.out.println(Thread.currentThread().getName() + "做了" + food);
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Food {
private double price;
private String name;
public Food(double price, String name) {
this.price = price;
this.name = name;
}
@Override
public String toString() {
return "Food{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
}
class ConsumerTask implements Runnable {
private Container container;
public ConsumerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
container.eatFood();
}
}
}
class PDTask implements Runnable {
private Container container;
public PDTask(Container container) {
this.container = container;
}
private Food[] foodMenue = {new Food(30, "蟹黄包"), new Food(3, "豆沙包"),
new Food(8, "小笼包"), new Food(6, "牛肉包")};
@Override
public void run() {
while (true) {
container.setFood(foodMenue[new Random().nextInt(foodMenue.length)]);
}
}
}
当有多个生产者消费者的时候,一定要用notifyAll通知别人。
面试题
Thread.sleep() VS Object.wait()
Thread.sleep() | Object.wait() | |
---|---|---|
所属不同 | sleep是定义在Thread类中的静态方法 | wait方法是定义在Object类中的非静态方法 |
唤醒条件不同 | 休眠时间到 | 在其他线程中的同一个所对象上调用了notify或notifyAll方法 |
使用条件不同 | 没有任何前提条件 | 必须在当前线程的锁对象上调用wait() |
休眠时对锁的持有状态不同(核心区别) | 线程因为sleep而处于阻塞状态时,阻塞的时候不会放弃对锁的持有 | wait方法在阻塞的时候放弃对锁的持有 |
![]() |
(四)线程池
概念
线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了侵犯创建线程对象的操作,无需反复创建线程而消耗过多的资源。
线程池容器——集合:ArraryList,HashSet,LinkedList(推荐),HashMap
底层原理
- 当程序第一次启动的时候,创建多个线程到集合中。
- 当我们需要使用线程的时候,就可以从集合中拿线程出来使用。
- 当我们使用完毕线程,需要把线程归还给线程池。
JDK1.5之后,JDK中内置了线程池可以直接使用。
线程池优点:
- 降低资源消耗,减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务。
- 提高了响应速度,当任务到达时,任务可以不需要等到线程创建就能立刻执行。
- 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作现成的数目,防止因为消耗过多的内存而把服务器累趴下。(每个线程大约需要1MB内存,线程开得越多,消耗的内存就越大,最后死机)
线程池:JDK1.5之后提供的。
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池。
Executors类中的静态方法
static ExercutorService newFixedThreadPool(int nThreads)//创建一个可以复用固定数量线程的线程池。
- 参数:Int nThreads:创建线程池中包含的线程数量。
- 返回值:ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务。
submit(Runnable task)提交一个Runnable任务用于执行。
关闭和销毁线程池的方法:void shutdown()
线程池的使用步骤:
- 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池。
- 创建一个类,实现Runnable接口,设置线程任务。
- 调用ExecutorService中的submit方法,传递线程任务(实现类),开启线程,执行run方法。
- 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)。