lang包之:多线程(概念、生命周期)
一、线程与进程的关系和概念
线程也称作轻量级进程,是进程的执行单元。是独立的、并发的执行流。
进程 Process:程序进入内存运行时,是处于运行过程中。
并发:同一时刻只能有一条指令执行,多个进程之间进行快速轮换;
并行:同一时刻多条指令在多个CPU上同时执行。
线程是进程的组成部分,一个进程至少拥有一个或多个线程,一个线程必须有一个父进程。
二、线程创建和启动
1.继承Thread类创建线程
1.1思路
(1)定义Thread类的子类,并重写run()方法;
run()方法中的代码就代表了线程需要完成的任务;run()方法也叫线程执行体。
(2)创建Thread子类的实例,即创建了线程对象;
(3)调用线程对象的start()方法来启动该线程(并执行线程中的run()方法)。
1.2示例
public class TestThread extends Thread{
private int a=10;
public void run(){
for( ; a<100; a++){
System.out.println(this.getName+a);
//当该类继承Thread类时,直接使用this即可获得当前线程名
}
}
}
public static void main(String[] args){
new TestThread().start();
new TestThread().start();
// 或者:
// TestThread tt1=new TestThread();
// TestThread tt2=new TestThread();
// tt1.start();
// tt2.start();
}
1.3注意事项
(1)关于线程中的变量
java程序运行时默认的主线程是main()方法中的代码。
两个线程中的实例变量a是无法共享的,也就是不会相互干涉,出现重叠。
(2)关于run()方法:
直接调用run()方法,则是main方法中主线程的run()方法,不会产生多线程;通过start()方法调用run()方法,则是开启多线程,并执行run()方法。永远不要直接调用run()方法。
2.实现Runnable接口创建线程类
2.1思路
(1)定义Runnable接口的实现类,并重写该接口的run()方法;
(2)创建实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程的start()方法来启动线程,并执行线程中的run()方法。target:目标,对象
2.2示例
public class TestRunnable implements Runnable{
private int a=10;
public void run(){
for( ; a<100; a++){
System.out.println(this.getName+a);
//当该类继承Thread类时,直接使用this即可获得当前线程名
}
}
}
public static void main(String[] args){
TestRunnable tr=new TestRunnable();
// 错误 new TestRunnable()是点不出start()方法的
new Thread(tr).start(); // 将Runnable实例化的对象做为线程对象。
new Thread(tr).start();
// 或者给线程指定个名字
// new Thread(tr,"新线程名").start();
}
2.3注意事项
(1)关于线程名的获取
Thread.currentThread():返回正在执行的线程对象。
getName(): 返回线程对象的名字。在继承Thread类中,获得线程名可以直接使用this.getName(),但如果创建线程没指定名,都指向为一个:Thread-0但在实现Runnable接口中,获得线程名则只能使用Thread.currentThread().getName();
推荐使用第二种方式。
三、线程的生命周期
1. 新建和就绪
新建:当new了一个线程之后。
Java虚拟机为其分配内存,初始化成员变量的值。
就绪:当线程对象调用了start()方法之后。
Java虚拟机为其创建方法调用栈和程序计数器,表示该线程可以运行了。
2. 运行和阻塞
运行:处于就绪状态的线程获得了CPU执行权,开始执行run方法中的代码。
阻塞:当线程在运行中,没有获得CPU的执行权。
当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。
被阻塞的线程会在合适的时候重新进入就绪状态,等待获取CPU的执行权。
3. 死亡
死亡:线程结束。线程会以三种方式结束:
(1)run()执行完,线程正常结束;
(2)线程抛出一个未捕获的Exception或Error;
(3)直接调用该线程的stop()方法来结束,但该方法容易导致死锁,不推荐使用。
注意事项:
(1) 一但子线程启动后,他与主线程拥有相同的地位。
当主线程结束时,其他线不受任何影响。
(2) 不要试图对一个已死亡的线程调用start()方法重启,死亡就是死亡,该线程不可再次作为线程执行。
报IllegalThreadStateException异常。
isAlive()方法:
当线程处于就绪、运行、阻塞三种状态时,返回true。
当线程处于新建、死亡两种状态时,返回false。
四、控制线程
1. join线程
让一个线程等待另一个线程完成。(你们等着让我先执行完)
join()方法通常由使用的线程的程序调用,以将大问题划
join(); 等待被join的线程执行完成。
join(long millis); 等待被join的线程的时间最长为多少毫秒
2. 后台线程
也叫守护线程或精灵线程,它在后台运行,它的任务是为其他的线程提供服务。
JVM虚拟机的垃圾回收线程就是个典型的后台线程。
后台线程的特征:如果所有的前台线程死亡,后台线程会自动死亡。
setDaemon(true); 将指定的线程设置成后台线程。
isDaemon(); 判断指定的线程是否为后台线程。
举例:
在main()中创建个线程,并设置为后台线程,运行程序:
当main()主线程(前台线程)结束,设置的后台线程也就结束了。
注意:
(1)setDaemon()方法放在start()方法前,也就是先设置线程为后台线程,再启动线程。
(2)isDaemon()方法放在setDaemon()方法之后;
(3)主线程默认是前台线程。
3. 线程睡眠:sleep
让正在执行的线程暂停一段时间,并进入阻塞状态。
sleep(long millis); //让线程睡眠,直到多少毫秒后醒来
当线程调用sleep()方法进入阻塞状态,在其睡眠的时间内,该程序不会获得执行机会,即使没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
暂停主线程执行:Thread.sleep(1000);
4. 线程让步:yield
让正在执行的线程暂停下,进入就绪状态。相当于放弃一次机会,大家从新再抢一次。实际上,当某个线程调用了yield()方法暂停后,只有优先级与当前线程相同,或者更高的处于就绪状态的线程才会获得执行的机会。
sleep()与yield()的区别:
(1)sleep()方法暂停当前线程后,会给其他线程执行机会,不理会其他线程的优先级;
但yield()方法只会给优先级相同或更高的线程执行机会。
(2)sleep()方法会将线程转入阻塞状态,直到转入就绪状态;
而yield()方法是强制当前线程进入就绪状态,因此完全有可能某个线程暂停后再次被获得执行。
(3)sleep()方法要抛异常;yield()方法则不需要。
(4)sleep()方法移植性好,通常不建议使用yield()方法控制并发线程的执行。
五、改变线程的优先级
1.概念
每个线程都具有优先级,优先级高的线程获得较多的执行机会。每个线程的优先级都与创建它的父线程的优先级相同;
默认情况下,main()线程具有普通优先级,由main线程创建的子线程也都是普通优先级。
2. 设置线程优先级
(1)每个线程都有一个优先级;
(2)高优先级线程的执行优先于低优先级线程;
(3)每个线程都可以或不可以标记为一个守护程序。
(4)当某个线程中运行的代码创建一个新 Thread 对象时,该新线程的初始优先级被设定为创建线程的优先级,并且当且仅当创建线程是守护线程时,新线程才是守护程序。
3.方法
setPriority(int newPriority); 设置线程的优先级
// int newPriority的值1~10,也可用三个常量:
MAX_PRIORITY,其值:10
MIN_PRIORITY, 其值:1
NORM_PRIORITY,其值:5
getPriority(); 得到线程的优先级
4. 注意事项
(1)设置线程优先级的代码,要放在.start()前面。
(2)优先级高不一定代表该线程就先抢到执行权,理论是是可以,但实际情况不一样。
六、线程同步
1. 线程安全问题
2. 同步代码块
run()方法不具有同步安全性。解决:同步监视器。同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问
synchronized(obj){
//同步的代码块
}
obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
3. 同步方法
用synchronized修饰一个方法
注意:synchronized可修饰代码块、方法,但不能修饰构造方法和成员变量等。同步方法obj可以用this代替。
4. 死锁
死锁是两个线程相互等待对方释放同步监视器时就会发生死锁。
死锁不会出现任何异常和提示,只是所有线程处于阻塞状态,无法继续。
七、多线程
1. 为什么需要多线程?
现代大型应用程序都需要高效地完成大量任务,其中使用多线程就是一个提高效率的重要途径。
2. 多线程存在的意义
多线程可以让我们的程序部分可以产生同时运行的效果,各玩各的。提高效率是其次,主要是能让多段代码同时执行。
3.多线程的目的
为了最大限度的利用CPU资源。
4.线程创建和操作
4.1. 为什么要重写run方法?
目的:将自定义的代码存储在run方法中,让线程运行(也就是将同时要运行的代码写在run()方法中)。
4.2. 通过对象.run()调用,不用start()方法调用也可以吗?
可以,但run()就变成主线程中的方法,与线程没关系。
因为线程没有开启,你只执行了调用。
4.3. run()方法中仅仅是封装多线程运行的代码,而start()则是开启多线程的钥匙。
4.4. start()方法是开启多线程,并执行run()方法。
4.5. 多线程的一个特性:随机性(谁抢到谁执行,至于执行多长,CPU说了算)
4.6多线程的安全性。
卧倒:具有执行资格,执行权被其它线程抢走了。
经过了判断,在执行输出代码时卧倒了,CPU再过来执行时,数可能已变了,票有可能输出0号票,-1,-2等等错票。
//模拟让它停一下:Thread类下面的sleep(毫秒值);
分析问题出在哪?
问题原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句时执行了一部分,还没有执行完,另一个线程参与进来执行。导致了共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程执行完。在执行过程中,其他线程不可以参与执行。
4.7旗标(同步):synchronized
(1) 同步代码块:
synchronized(对象){
//需要被同步的代码
}
//表示该段代码上锁
//如果是代码块后面要放上锁旗标,如果修饰方法,那么它的锁旗标是隐含的this。
(2)锁旗标也可以修饰方法----同步方法。
5线程的继承方式和实现方式有什么区别吗?(面试)
继承Thread类:线程代码存放在Thread子类run方法中;
实现Runnable:线程代码存在接口的子类的run方法中。
八、单例模式
1.破解单例模式
通过多线程调用静态公开方法
2.完善单例模式
将单例模式中的静态公开方法 同步。
3.示例
public Myf{
//设置静态成员变量
private static Myf m;
//公开方法
public static Myf getM(){
if(m==null){
m=new Myf();
}
return m;
}
//私有化公开方法
private Myf(){}
}
破解单例模式:采用多线程;
new Thread(){
public void run(){
Myf m=Myf.getM();
System.out.println(m.hashCode());
}
}.start();
//再开个线程
new Thread(){
public void run(){
Myf m=Myf.getM();
System.out.println(m.hashCode());
}
}.start();
如何防止存解?为单例模式中的公开方法设置锁。
public synchronized static Myf getM(){
……
}