创建线程并指定线程实例名 |
指定创建线程的目标对象,它实现了Runnable接中的run方法 |
其实run方法是Runnable接口中的抽象方法(Runnable接口中只有一个run方法),只不过我们在Thread类中重写了run()方法。
而采用继承Thread的方式时,我们有一步操作是重写了run()方法,那么由继承的知识可以知道,当调用了父类重写的方法时,实际执行的是子类重写后的方法,当启动多线程时(调用start方法),JVM会调用run()方法,那么调用的就是我们重写后的run()方法,而run()方法里面就是我们想让线程做的事情。
采用实现Runnable的底层原理分析
对于上述程序,我们点进去看一下底层的执行原理
我们发现构造器的参数是Runnable类型,而我们传递的参数实现了Runnable接口,此时发生了多态。
我们在查看Init方法,如下
我们只关注其中的重要一行代码:如下
它把我们传递的参数赋给了Thread类中的target,下面查看run()方法
此处run方法中调用了我们传递过来的实现Runnable类的对象的run方法,当我们调用Thread对象的start()方法时(start()方法作用,创建一个线程,执行run()方法),也就执行了我们在实现类重写的run()方法。
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式。都需要有一个run方法,但是这个run方法有不足:
1、没有返回值 2、不能抛出异常
基于以上两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:实现Callable接口好处:1、有返回值 2、抛出异常 3、支持泛型操作 缺点:创建线程比较麻烦。
我们知道启动一个线程必须调用Thread类中的start()方法,上述程序我们创建了一个实现Callable接口的实例对象,并把它丢入到Thread中的构造器中,但程序报错了,说明我们Thread中的构造器没有参数是Callable的:
但是我们注意到存在Runnable类型参数的构造器:
所以我们可以传入一个实现了Runnable接口的实现类对象,但是Callable接口不是Runnable接口的实现类,所以我们就像借助一个FutureTask类,FutureTask类的声明如下
【1】线程池使用到的是阻塞队列
线程的生命周期:出生----》死亡
新生(假设用时3s)----》就绪(2s)-----》运行(1)-----》死亡(3s)
使用线程池目的:将运行之前的时间节省,将运行之后的时间节省—》线程池:减少创建和消亡的时间。
执行线程5时,队列以及满了,就会创建新线程执行该任务。创建的新线程是“pool-1-thread-2",它执行完并不是立即销毁,如果3ms内有任务,他就会和核心线程
"pool-1-thread-1"分摊执行剩下的任务,如果3ms内没任务,该新线程消亡。试着在运行上述程序,查看“pool-1-thread-2"确实分摊任务了。
报错了,因为线程队列以及满了,此时的新任务要创建新的线程来执行,但是目前已经有两个线程了,我们初始也设置了最大线程数为2,所以此时就拒绝执行了。
在java5以前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来实现线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中 |
创建一个可重用的、具有固定线程数的线程池。 |
创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1. |
创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。 |
创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。 |
上面5个方法中的前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。
JDK1.5之后提供了内置线程池
最开始没有核心线程,来一个任务,新建一个线程来执行这个任务,当这个任务执行完以后,这个线程继续执行其他的任务,所以在结果中可以看到线程大量重复的
运行结果:只有3个线程在执行任务
1、通过继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。除非加static变成类变量。或者把继承Thread的子类对象作为参数传递给Thread对象(其实相当于实现了Runnable接口,具体看下文演示)。
2、采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类对象)的实例属性。
1、继承Thread类的方式来演示抢票操作,不能实现数据的共享
运行结果如下:如果不加static抢票就会发生错误(每个窗口买票都会从100号开始卖)。tickets是实例属性,因为程序每次创建线程对象时都需要创建一个Window对象,所以“窗口1“线程、”窗口2“线程不能共享属性,解决就是加static,让其成为类属性。
把继承Thread的子类对象作为参数传递给Thread对象也可以实现数据共享(其实相当于实现了Runnable接口,具体看下文演示)。
因为MyThreadr继承了Thread类,Thread类又实现了Runnable接口,相当于MyThreader间接的实现了Runnable接口,所以可以把MyThread的对象传递给声明为Runnable类型的参数,此时发生了多态。虽然我们new了好几个线程对象,但其实操作的都是同一个MyThread的子类对象,当然可以共享tickets属性。
创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下
**注意:**优先级高的线程获得更多的执行机会,并不是一定先执行。
启动线程,并执行对象的run()方法 |
线程在被调度时执行的操作 |
返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类 |
释放当前cpu的执行权,让其它的线程去获取,但该方法只会给优先级相同,或优先级更高的线程执行机会。它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程进入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。。 |
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。 |
让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。 |
已过时。当执行方法时,强制结束当前线程。 |
重点是这句话:yield()不会将线程转入阻塞转态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
出现问题(继承和实现都会出现下面问题):
1、出现了两张10号票或者3张10号票。
上面的代码出现问题:出现了重票,错票----》线程安全引起的问题
原因:多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来了,开始争抢。
解决方式:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。–Java 中通过加**“锁”(同步机制、同步监视器)**
下面我们对通过继承Thread实现的抢票代码执行同样的操作。
说明并没有解决线程的安全问题,要想保证安全,锁必须只有一个,而上述程序我们锁用this的话指的是t1、t2、t3(this含义是哪个对象调用了该方法this就是哪个对象)。我们在synchronized中做个改变如下:用类的字节码,因为一个类只有一份。
运行如上代码发现没有出现问题。
总结1:认识同步监视器(锁子)—synchronized(同步监视器){ …}
必须是引用数据类型,不能是基本数据类型
也可以创建一个专门的同 步监视器,没有任何业务含义
一般使用共享资源做同步监视器即可
在同步代码块中不能改变同步监视器对象的引用
尽量不要String和包装类Integer做同步监视器
建议使用final修饰同步监视器
总结2:同步代码块的执行过程
1) 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中 ,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了Cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取cpu,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生Cpu的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁close)
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住了一个代码块的同时,也锁住了所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。
1、同步方法解决实现Runnable接口买票的方式
使用同样的方式对继承Thread的方式进行修改
并没有解决问题,因为synchronized锁住的是this,而对于继承方式来说,this代表的是不同的窗口对象。解决方法是在同步方法加static让其在内存中只有一份,那么它的同步监视器就是当前类名.class
JDK1.5后新增新一代的线程同步方式:Lock锁
与采用synchronized相比,lock可提供多种锁方案,更灵活
synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
原因分析:本程序中使用的是相同的监视器。比如cpu先执行如下这个线程,我们称这个线程为线程1
线程1拿到str1这把锁,进入到同步代码块中,此时线程睡了10毫秒,那么此时cpu可能切换到下面的线程,我们成为线程2
线程2的同步代码块需要str2这把锁,而此时这把锁还没被占用,所以此时线程2继续执行。当执行到如下的代码处时:
它需要str1这把锁,而这把锁str1正在使用,所以该线程就在这阻塞等着str1这把锁释放。比如此时cpu又切换到线程1,执行到如下代码之处
它需要str2这把锁,而这把锁正被线程2使用,所以他等着线程2释放str2这把锁,就这样线程1等着线程2释放str2锁,线程2等着线程1释放str1这把锁,两个线程就阻塞在这了。
解决死锁方法:减少同步资源的定义,避免嵌套同步
1、线程安全,效率低;线程不安全,效率高
2、可能造成死锁现象。
线程通信例子1:使用两个线程打印1-100。线程1,线程2 交替打印
线程通信例子2:生产者消费者问题
实现要求:生成者和消费者要交替输出
1、生产者和消费者没有交替输出(我们的要求是要交替输出)
----》原因没有加同步
方式一:使用同步代码块
方式二:使用同步方法只需在Product中写就行
分别在生产者类和消费者类把run方法中的代码剪切到一个同步方法中可不行,因为此时的同步监视器是this(即当前类的对象),这样的话生产者和消费者的同步方法中用的就不是同一个监视器了,没有解决线程安全。
线程安全问题解决,但是先执行生产者,在执行消费者还没实现,而实现该功能就要用到线程之间的通信了。
分解3:实现线程之间通信
就如我们在饭店吃饭一样,当我们付完款之后,服务员会给我们一个号,如果饭做好了服务员叫号,我们就去端饭,如果没叫到我们的号我们就等着;类比,如果生成者生产好了产品把灯泡置为红色,生产者停下来,通知消费者进行消费,否则的话,生产者就生产;生产者如果看到是绿色说明还没做好饭,那么消费者就等着,让生产者进行生产,否则的话消费者消费掉商品,并通知生产者进行生产。
只需要更改product中的方法即可
一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。 |
一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个 |
一旦执行此方法,就会唤醒所有被wait的线程。 |
由以上知识知道生产者和消费者轮流持有锁。谁持有了这个锁,另一个线程就进入到等待池中。比如说生产者已经生产出商品了,那么我就不需要在生产了,就进入到等待池中进行等待;如果是消费者的话,现在想进行消费,但发现没有商品的话,我还在同一个等待池中进行等待。所以我们发现,生产者消费者等待时都在同一个等待池中进行等待。比如当前等待池中有一个生产者P1、消费者c1,那么这个等待池要么p1用,要么c1用,此时没啥关系。如果现在等待池中有多个生产者、消费者。比如现在存在4个线程分别是生产者:p1、p2和消费者c1和c2。目前p1持有这锁了,那么p2
c1 c2都在同一个线程池中等待,然后p1释放锁了,要通知等待池中的线程持有这个锁,但是p1通知的时候线程池里的生产者p1和消费者c1和c2都有资格拿到这个锁。如我本应该唤醒的是一个消费者,确有可能唤醒的是生产者c1和c2,那么这个情况就不友好了。我们上面的刚好是一个生产者一个消费者,所以他们没有问题,当有多个生产者、消费者就有问题了。
解决:就是生产者p1、p2一个等待池,c1、c2一个等待池,比如p1持有这个锁进行生产,生产完之后,它应该通知c1、c2这个等待池。所以我们应该解决把生产者和消费者分别放置子两个不同的池子中,lock锁可以解决该问题。结构如图。
lock锁解决上述问题代码如下:
product类代码修改如下:
它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition
一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列, 而Lock(同步器)拥有一个同步队列(锁池)和多个等待队列。
造成当前线程在接到信号或被中断之前一直处于等待状态。
与此 Condition 相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一 以前,当前线程将一直处于休眠状态:
· 其他某个线程调用此 Condition 的 signal() 方法,并且碰巧将当前线程选为被唤醒的线程;或者
· 其他某个线程中断当前线程,且支持中断线程的挂起;或者
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证它保持此锁。
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。