线程的生命周期
1. 线程与进程
(1)进程
一个在运行中的程序。进程是操作系统分配cpu资源的最小单位,一个进程就是一个独立的执行环境。进程有着完整的,私有的基本的运行时资源,尤其是每个进程都有自己的内存空间。一个进程包含1–n个线程。大多数的java虚拟机的实现都是作为一个单独的进程的。多进程是指操作系统能同时运行多个任务(程序)。
(2) 线程
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
多线程是指在同一程序中有多个顺序流在执行。
(3)线程和进程的区别
线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护,⽽进程正相反。
2. 线程的创建
2. 1 创建方式一
步骤:
(1)建一个java类
(2)使这个类继承java.lang.Thread类
(3)重写Thread类中的run()方法
例如:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(500);//让线程睡500毫秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} System.out.println(Thread.currentThread().getName()+"----------"+i);
}
}
public static void main(String[] args) {
//打印出当前线程的名称
System.out.println(Thread.currentThread().getName());
MyThread t=new MyThread();//创建一个线程的对象
t.start();//让线程进入可运行状态。
MyThread t1=new MyThread();//创建一个线程的对象
t1.start();//让线程进入可运行状态。
}
}
◣注意:
(1)main方法运行在主线程里面的,主线程的名称叫做main;
(2)获取当前线程名称的方法:Thread.currentThread().getName();
(3)开启线程的时候直接创建子类的对象,然后调用start()方法即可;
(4)sleep(毫秒数)该方法是让线程睡眠一定的时间。
2. 2 创建方式二
步骤:
(1)建一个java类
(2)实现java.lang.Runnable接口,并实现接口中的run()方法
(3)使用Thread类进行包装
例如:
public class MyThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(500);//让线程睡500毫秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} System.out.println(Thread.currentThread().getName()+"----------"+i);
}
}
public static void main(String[] args) {
MyThread1 t=new MyThread1();
//t.run();//不是开启一个线程,而是简单的方法调用。
//第一个参数是一个Runnable对象,第二个参数为定义的线程的名字。
Thread t1=new Thread(t,"myThread");
t1.start();
}
◣注意:
(1)线程启动的时候要通过该线程类的对象创建一个Thread对象,然后调用start()方法,比如new Thread(Runnable对象).start();
(2)run()方法不是进行线程启动,而是简单的方法调用;
(3)线程调用start()方法之后并不是立马进入运行状态,而是进入就绪状态;
(4)一般推荐方式二,因为java是单继承,一旦继承一个类就没有办法再继承别的类,但是实现接口是可以多实现。
3. 多线程示例
使用多线程进行多个文件同时复制的例子:
public class CopyThread extends Thread{
public String name;
public String newName;
@Override
public void run() {
try {
copy(name,newName);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void copy(String name,String newName) throws IOException{
System.out.println(name+"开始复制!");
//创建输入流
InputStream is=new FileInputStream(name);
//创建一个输出流
OutputStream os=new FileOutputStream(newName);
//循环进行读写
int i=0;
while((i=is.read())!=-1){
os.write(i);
}
os.close();
is.close();
System.out.println(name+"结束复制!");
}
public static void main(String[] args) {
//创建线程对象
long start=System.currentTimeMillis();
for (int i = 0; i <5; i++) {
CopyThread t=new CopyThread();
t.name="f:/a"+i+".txt";
t.newName="e:/a"+i+".txt";
t.start();
}
long end=System.currentTimeMillis();
System.out.println(end-start);
}
}
◣注意:
(1)使用多线程操作可以大大增加程序执行的效率。
(2)每个java程序都默认会有一个主线程,主线程的名字叫做main,我们平时使用main()方法就运行在主线程中。
(3)每一个创建的线程都会有名称,如果没有手动设置线程的名称,则系统会默认为线程分配对应的线程名。
4. 线程的生命周期(重要)
4.1 新生状态(new)
刚创建出来的一个线程对象就处于新生态。
4.2 就绪状态/可运行状态(Runnable)
当对象创建以后,调用他的start()方法后,线程进入到可运行状态。该线程处于可运行的线程池中,等待cpu分配资源进行调度。
4.3 运行状态(running)
当就绪状态的线程,被CPU调度到,分配资源以后则进入运行状态。
4.4 阻塞状态(blocked)
线程阻塞是因为线程由于某种原因而放弃CPU的使用权,暂停运行状态,直到线程再次进入可运行状态,才有机会转到运行状态。导致线程阻塞的情况分三种:
4.4.1 等待阻塞
当一个线程的对象调用了wait()方法以后,JVM就会将该线程放入到等待池中,线程进入阻塞状态;当对象调用了notify()或者notifyAll(),则解除线程的等待。(以下三个方法都属于Object对象)
wait():导致当前的线程进入一个等待状态,在等待的过程中是不会让出cpu资源的。
notify():随机唤醒当前对象上等待的某一个线程。
notifyAll():唤醒当前对象上面等待的全部线程。
4.4.2 加锁阻塞
运行的线程在获取对象的同步锁的时候,如果该同步锁被别的线程持有,则JVM会将该线程放入到锁池中。
线程同步:通过synchronized修饰符可以为方法进行加锁。
使用方式一:加在方法上面
例如:public synchronized static void saveTest(){}
使用方式二:加在代码块上
例如:synchronized (对象) { } ,对象就是线程共同操作的对象。
作用:可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
建议:一般不要在方法上面加锁,而是使用代码块加锁,尽量将操作对象的代码锁起来,和对象无关的代码不要放在锁中,提高程序运行效率。
◣注意:
synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
例如
两个线程共同操作一张银行卡取钱的操作,由于他们访问的资源是共享的,所以如果要保证最后卡中的数据的正确性,需要在取钱的方法上面加锁。
银行卡类:
public class Bank {
public int money=1000;
public String name;
/**
* 取钱的操作
* @param mo
*/
public /*synchronized*/ void getMoney(int mo){
//同步块:将会发生冲突的代码放入到同步块中(推荐使用)。
synchronized (this) {
if(mo>money){
System.out.println("余额不足!!!");
}else{
money-=mo;
System.out.println(Thread.currentThread().getName()+"取走了"+mo+"元,卡余额为:"+money);
}
}
System.out.println("asdasdasda");
//其他的操作
}
}
取钱的线程类:
public class GetMoney extends Thread{
private Bank bank;
//private String name;
public GetMoney(Bank bank,String name) {
this.bank=bank;
//bank.name=name;
}
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//bank.name=name;
bank.getMoney(800);//调用取钱的方法
}
}
测试类:
public class Test1 {
public static void main(String[] args) {
Bank bank=new Bank();
GetMoney t1=new GetMoney(bank,"张三");
GetMoney t2=new GetMoney(bank,"李四");
t1.start();
t2.start();
}
}
结果:
Thread-0取走了800元,卡余额为:200
余额不足!!!
◣注意:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁,而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
生产消费者模式:
① 必须有两个线程
② 必须要有一个共同的操作对象。
③ 在操作对象的方法上面必须要添加锁。然后使用wait和notify方法进行线程的控制。
4.4.3 其他阻塞
运行的线程调用了sleep()/join()方法的时候,或者是遇到I/O请求时,JVM会让该线程进入阻塞状态,当sleep()睡眠时间到,join()方法等待的线程结束或者I/O请求处理完毕的时候,线程继续回到可运行状态。
**sleep():**代表让当前的线程睡眠指定的时间,时间单位:ms,当调用该方法线程进入阻塞状态,但是会让出CPU资源让其他线程去运行,当睡眠时间到了则进入就绪状态。一个线程睡眠了以后下次运行的时间一定是等于或者大于睡眠的时间。
join(): 让当前线程进入一个阻塞状态,然后使得调用他的线程先去运行,运行完成以后当前线程再去运行。
yield(): 让步,只能让线程进入就绪状态,所以让步不一定能成功
面试问题
问题1:Java线程中sleep和yield?
(1)线程执行sleep方法后进入阻塞状态,但是会让出CPU资源让其他线程去运行,当睡眠时间到了则进入就绪状态;而线程执行yield方法后只能进入就绪状态。
(2)sleep方法给其他线程运行机会时不考虑线程的优先级,因此可以使低优先级的线程得到执行的机会,因为在一个运行系统中,如果较高优先级的线程没有调用sleep方法,也没有受到I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,方可有机会运行。
(3)而yield方法只会给相同优先级或更高优先级的线程以运行的机会,执行yield方法时会检测当前是否有相同优先级的线程处于可运行状态,如有,则把CPU的占有权交给这个线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。
(4) sleep方法声明抛出InterruptedException;而yield方法没有声明任何异常。
问题2:sleep和wait?
sleep和wait都能让线程进入到阻塞状态。它们的区别如下:
(1)当一个线程执行sleep方法后进入到其他阻塞状态,但是会让出CPU资源让其他线程去运行,当睡眠时间到了则进入就绪状态。当一个线程的对象调用了wait方法以后,JVM就会将该线程放入到等待池中,线程进入等待阻塞的状态,在等待的过程中是不会让出CPU资源的,当对象调用了notify或者notifyAll的方法,则解除线程的等待。
(2)wait、notify和notifyAll方法都是Object类方法,而sleep方法是Thread类方法。
(3)sleep方法不会释放锁,但是wait会释放锁,而且会加入到等待队列中。
(4)sleep方法不依赖于同步器synchronized,因此它可以在任何地方使用;但是wait需要依赖同步器,wait、notify和notifyAll方法只能在同步控制方法或者同步控制块里面使用。
4.5 死亡状态
当线程的run()方法结束,包括run()方法正常执行完成或者异常结束以后,线程进入死亡状态,处于死亡状态的线程不能再进入可运行状态。
5 线程池
问题1:使用线程池的好处?
(1)降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
(2)提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
(3)提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。