多线程
一、基本概念
并发:指两个或多个事件在一同一个时间段内交替发生。
并行:指两个或多个事件在一同一时刻同时发生(同时发生)。
一般在单处理器上多线程是交替并发执行的。但在多处理器
上并发执行的程序可以分配到多个处理器上,实现多任务并行执行
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
n*线程==>进程 n*进程==>程序
线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,
让CPU的使用率更高。
二、实现步骤
Java使用java. lang. Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
1、java实现多线程的三种方法
① 继承Thread类来创建并启动多线程
1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
2. 创建Thread子类的实例,即创建了线程对象
3. 调用线程对象的start()方法来启动该线程代码如下:
② 实现Runnable接口 重写run方法
java.lang.Runnable
步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3.调用线程对象的start()方法来启动线程
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。
Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnabletarget)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
③ 匿名内部类方式实现线程的创建
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法
④ Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。
但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:实现Runnable接口比继承Thread类所具有的优势:
1.适合多个相同的程序代码的线程去共享同一个资源。
2.可以避免java中的单继承的局限性。
3.增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
4.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程
三、代码实现
1、java实现多线程的三种方法
①、继承Thread类
public class MyThread extends Thread {
public MyThread ( String name) {
super ( name) ;
}
@Override
public void run ( ) {
for ( int i = 0 ; i < 10 ; i++ ) {
System. out. println ( getName ( ) + ":正在执行!" + i) ;
}
}
}
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
1.2 、Thread类的常用方法
public Thread ( ) : 分配一个新的线程对象。
public Thread ( Stringname) : 分配一个指定名字的新的线程对象。
public Thread ( Runnabletarget) : 分配一个带有指定目标新的线程对象。
public Thread ( Runnabletarget, Stringname) : 分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public String getName ( ) : 获取当前线程名称。
public void start ( ) : 导致此线程开始执行; Java虚拟机调用此线程的run方法。!主程序开启线程时使用!
public void run ( ) : 此线程要执行的任务在此处定义代码。
public static void sleep ( longmillis) : 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。 public static Thread currentThread ( ) : 返回对当前正在执行的线程对象的引用。
翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。
②、实现Runnable接口 重写run方法。
public class runnableimp implements Runnable {
@Override
public void run ( ) {
for ( int i = 0 ; i < 20 ; i++ ) {
System. out. println ( Thread. currentThread ( ) . getName ( ) + i) ;
}
}
}
public class runnnable1 {
public static void main ( String[ ] args) {
runnableimp ra = new runnableimp ( ) ;
Thread th = new Thread ( ra) ;
th. start ( ) ;
for ( int i = 0 ; i < 20 ; i++ ) {
System. out. println ( Thread. currentThread ( ) . getName ( ) + i) ;
}
}
}
③、匿名内部类实现多线程
new Thread ( ) {
@Override
public void run ( ) {
for ( int i = 0 ; i < 100 ; i++ ) {
System. out. println ( Thread. currentThread ( ) . getName ( ) + i) ;
}
}
} . start ( ) ;
new Thread ( new Runnable ( ) {
@Override
public void run ( ) {
for ( int i = 0 ; i < 100 ; i++ ) {
System. out. println ( Thread. currentThread ( ) . getName ( ) + i) ;
}
}
} ) . start ( ) ;
四、线程安全
1、线程同步的四种方式
①基础概念
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
一个线程对共享数据进行更改时,其他线程不可以对改线程进行改动!
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
②同步代码块:synchronized
synchronized ( 任意对象) {
代码块内容同一时刻只可以有一个线程进行访问
}
③同步方法 方法有synchronized修饰
public synchronized void method ( ) {
方法内容同一时刻只可以有一个线程进行访问
}
调用该方法时同一时刻也就只有一个线程可以调用
④lock锁
Lock l = new ReentrantLock ( ) ;
public void run ( ) {
while ( true ) {
l. lock ( ) ;
try {
if ( ticket > 0 ) {
System. out. println ( Thread. currentThread ( ) . getName ( ) + " w第" + ticket-- ) ;
}
}
finally {
l. unlock ( ) ;
}
}
}
⑤等待唤醒机制
等待唤醒机制什么是等待唤醒机制这是多个线程间的一种协作机制。
谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码过后再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。等待唤醒中的方法等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
3. notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态调用wait和notify方法需要注意的细节1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
⑥等待唤醒实列
package demo04;
public class Baozi {
private String pi;
private String xian;
private Boolean flag= false ;
public Baozi ( ) {
this . pi = pi;
}
public String getPi ( ) {
return pi;
}
public void setPi ( String pi) {
this . pi = pi;
}
public String getXian ( ) {
return xian;
}
public void setXian ( String xian) {
this . xian = xian;
}
public Boolean getFlag ( ) {
return flag;
}
public void setFlag ( Boolean flag) {
this . flag = flag;
}
@Override
public String toString ( ) {
return "Baozi{" +
"pi='" + pi + '\'' +
", xian='" + xian + '\'' +
", flag=" + flag +
'}' ;
}
}
package demo04;
public class dian extends Thread {
private Baozi bz;
public dian ( ) {
}
public dian ( Baozi bz) {
this . bz= bz;
}
@Override
public void run ( ) {
while ( true ) {
synchronized ( bz) {
if ( bz. getFlag ( ) == true ) {
try {
bz. wait ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
System. out. println ( "生产包子;等待2s" ) ;
try {
sleep ( 2000 ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
bz. setFlag ( true ) ;
bz. setPi ( "厚皮" ) ;
bz. setXian ( "猪肉" ) ;
System. out. println ( bz + "包子生产好了 可以开吃了" ) ;
bz. notify ( ) ;
}
}
}
}
package demo04;
public class chihuo extends Thread {
private Baozi bz;
public chihuo ( ) {
}
public chihuo ( Baozi bz) {
this . bz = bz;
}
@Override
public void run ( ) {
while ( true ) {
synchronized ( bz) {
if ( bz. getFlag ( ) == false ) {
try {
bz. wait ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
}
}
bz. setFlag ( false ) ;
System. out. println ( "吃货吃了" + bz+ "的包子!" ) ;
bz. notify ( ) ;
}
}
}
}
package demo04;
public class main {
public static void main ( String[ ] args) {
Baozi bz = new Baozi ( ) ;
dian d = new dian ( bz) ;
chihuo c = new chihuo ( bz) ;
d. start ( ) ;
c. start ( ) ;
}
}
总体逻辑:
包子店判断没有包子就生产包子 唤醒顾客释放资源 包子店和顾客竞争资源 如果包子店再次获得资源 判断他有包子 进入无限等待 资源返回 顾客得到资源 吃完包子 唤醒包子店 释放资源 然后包子店和顾客再次竞争资源 如果顾客竞争到了资源 判断没有包子 顾客进入无限等待 返回资源 包子店得到资源 判断没有包子:(循环!)
2、线程状态
线程状态导致状态发生条件
1、NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
2、Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。3、Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
4、Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
5、TimedWaiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入TimedWaiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait。
6、Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡