线程简介
进程:每个进程都有独立的代码和数据空间,进程之间的切换会有较大的开销,一个进程包含1-n个线程。进程是资源分配的最小单位。
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。线程是cpu调度的最小单位。
线程就是进程中一个负责程序执行的控制单元(执行路径)。一个进程中可以有多个执行路径,称之为多线程。
一个进程中至少要有一个线程,开启多个线程是为了同时运行多部分代码。每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务。
多线程的好处:解决了多部分同时运行的问题。
多线程的弊端:线程太多会导致效率降低。
应用程序的执行都是CPU在做着快速的切换完成的,这个切换是随机的。
JVM启动时就启动了多个线程,至少有两个线程可以分析出来:
1,执行main函数的线程,该线程的任务代码都定义在main函数中
2,负责垃圾回收的线程,该线程的任务代码都在垃圾回收器中,在内部自动执行。
finalize():是Object类的方法。当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写finalize方法,以配置系统资源或者执行其他清除。
System类中有一个gc()方法,功能是运行垃圾回收器,它不是即时启动垃圾回收器的,只是告诉它应该启动。
例一:
class Demo extends Object
{
public void finalize()
{
System.out.println("demo ok");
}
}
class ThreadDemo
{
public static void main(String[] args)
{
new Demo();
new Demo();
System.gc();
new Demo();
System.out.println("hello world!");
}
}
运行后输出:
hello world!
demo ok
demo ok
出现这样的结果是因为此程序是由两个线程完成的。输出结果不是一定的,因为两个线程的执行顺序不一定。(线程随机切换)
主线程结束,虚拟机不一定结束,还有其他线程在执行。
实现线程的两种方式
在java中主要提供两种方式实现线程,分别继承java.lang.Thread类与实现java.lang.Runnable接口
继承Thread类
Thread类中常用的两个构造方法:
public Thread():创建一个新的线程对象
public Thread(String threadName):创建一个名称为threadName的线程对象。
完成线程的真正功能的代码放在类的run方法中,当一个类继承Thread类后,就可以覆盖run方法,将实现该线程功能的代码写入run()方法中。
步骤:
1,定义一个类继承Thread类
2,覆盖Thread类中的run方法
3,直接创建Thread子类对象创建线程
4,调用start方法开启线程并调用线程的任务run方法执行
可以通过Thread的getName获取线程的名称 Thread-编号(从0开始),线程一创建就带着编号,获取当前线程对象的方法为currentThread()
主线程的名字是main。
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程它的任务在哪?
Thread类用于描述线程,线程是需要任务的。所以Thread类也对任务的描述。这个任务就通过Thread类中的run方法来实现。也就是说,run方法就是封装自定义线程运行任务的函数。run方法中定义的就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只要继承Thread类,并复写run方法。
将运行的代码定义在run方法当中。
例二:
class Demo extends Thread
{
private String name;
Demo(String name)
{
super(name);//线程类的带参数的构造方法
}
public void run()
{
for(int x=0;x<10;x++)
System.out.println(name+"...x="+x+"...name="+Thread.currentThread().getName());
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d1=new Demo("wangwang");
Demo d2=new Demo("miaomiao");
d1.start();//开启线程,调用run方法
d2.start();
System.out.println("over");
}
}
输出为:
over
null...x=0...name=wangwang
null...x=0...name=miaomiao
null...x=1...name=wangwang
null...x=1...name=miaomiao
null...x=2...name=miaomiao
null...x=3...name=miaomiao
null...x=4...name=miaomiao
null...x=2...name=wangwang
null...x=5...name=miaomiao
null...x=3...name=wangwang
null...x=6...name=miaomiao
null...x=4...name=wangwang
null...x=7...name=miaomiao
null...x=5...name=wangwang
null...x=8...name=miaomiao
null...x=6...name=wangwang
null...x=9...name=miaomiao
null...x=7...name=wangwang
null...x=8...name=wangwang
null...x=9...name=wangwang
开始的name为空是因为开始的时候name没有初始化的值,默认为null。
直接调用run方法和调用start方法有什么区别?
run()方法:在本线程内调用该Runnable对象的run()方法,可以重复多次调用; 直接调用run()和其他方法的调用没任何不同,main方法会按顺序执行它。
start()方法:启动一个线程,调用该线程对象的run()方法,不能多次启动一个线程;如果start()方法调用一个已经启动的线程,系统将抛出IllegalThreadStateException异常。调用start方法,会告诉JVM去分配本机系统的资源,才能实现多线程。
实现Runnable接口
如果需要继承其他类(非继承类),而且还要使当前类实现多线程,就不能再继承Thread类,因为java语言中不支持多继承,这时候可以通过实现Runnable接口使其具有线程的功能。
实现Runnable接口的程序会创建一个Thread类,并将Runnable对象与Thread对象相关联。Thread类中有以下两个构造方法:
public Thread(Runnable target)
public Thread(Runnable target,String name)
步骤:
1,定义类实现Runnable接口;
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中;
3,通过Thread类方法创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递;
这是因为线程的任务都封装在Runnable接口子类对象的run方法中,
所以要在运行线程对象创建时就必须明确要运行的任务
4,调用线程对象的start方法开启线程。
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装
按照面向对象的思想将任务的封装成对象;
2,避免了java单继承的局限性。
所以,创建线程的第二种方式较为常用。
例三:
class Demo implements Runnable//准备扩展Demo类的功能,让其内容可以作为线程的任务执行
{ //通过接口的形式来实现
public void run()
{
show();
}
public void show()
{
for(int x=0;x<20;x++)
{
System.out.println(Thread.currentThread().getName()+"..."+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d=new Demo();
Thread t1=new Thread(d);
Thread t2=new Thread(d);
t1.start();
t2.start();
}
}
线程的生命周期
线程具有生命周期,其中包括7中状态:出生状态,运行状态,等待状态,休眠状态,阻塞状态和死亡状态。其中等待状态和休眠状态可以统称为冻结状态。
出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用start()方法之前线程都处于出生状态;当用户调用start方法后,线程处于就绪状态(可执行状态,具有执行资格),当线程得到系统资源后进入运行状态(具有执行权)。
一旦进入可执行状态,它就会在就绪和运行状态下转换(抢夺系统资源,即执行权),同时也可能进入等待、休眠、阻塞或者死亡状态。当处于运行状态的线程调用Thread类中的wait()方法时,该线程便进入等待状态,进入等待状态的线程必须调用Thread类中的notify()方法或者notifyAll()方法才能被唤醒;当线程调用Thread类中的sleep()方法的时候,就会进入休眠状态,休眠时间结束会自动醒来。如果一个线程在运行状态发出输入/输出请求,该线程将进入阻塞状态,在输入输出结束时线程会进入就绪状态。当run()方法执行完毕,线程进入死亡状态。
线程的声明周期状态图
CPU的执行资格:可以被CPU处理,在处理队列中排队。
CPU的执行权:正在被CPU处理。
运行:具备执行资格,具备执行权。
冻结:释放执行权的同时释放执行资格。
start()方法的调用并不是立即执行多线程代码,而是使得该线程变成可运行状态,什么时候运行是由操作系统决定的。
操作线程的方法
线程的休眠
sleep()方法需要一个参数用于指定该线程休眠的时间,该时间以毫秒为单位。它通常在run方法内的循环中被使用。
sleep方法的执行可能会抛出InterruptedException异常,所以将sleep()方法放在try-catch块中。
wait和sleep区别:
1,wait可以指定时间,也可以不指定
sleep必须指定时间
2,在同步中时,对CPU的执行权和锁的处理不同
wait:释放执行权,释放锁
sleep:释放执行权,不释放锁
线程的加入
当某个线程使用join()方法加入到另外一个线程时,另一个线程会等待该线程执行完毕后继续执行。
线程的礼让
Thread方法提供了一种礼让方法,使用yield方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程。
例四:join()方法和yield()方法:
class Demo implements Runnable
{
public void run()
{
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"..."+x);
Thread.yield();//释放执行权
}
}
}
class JoinDemo
{
public static void main(String[] args) throws Exception
{
Demo d=new Demo();
Thread t1=new Thread(d);
Thread t2=new Thread(d);
t1.start();
//t1.join();//t1线程要申请加入进来运行。此时主线程会释放执行权和执行资格,等t1执行完终止后才会接着运行。
t2.start();
t2.setPriority(Thread.MAX_PRIORITY);//线程优先级
t1.join();//主线程等待t1运行完再运行。放在这里,t1,t2会交替抢夺执行权
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"..."+x);
}
}
}
线程的中断
停止线程的方法:
1,stop方法,已过时;
2,run方法结束。
怎么控制线程的任务结束?
任务中都会有循环结构,只要控制住循环,就可以结束任务。
控制循环通常就用定义标记来完成:在run()方法中使用无线循环,然后使用一个布尔型标记控制循环的停止。或者直接定义其他标记控制循环。
但是如果线程处于了冻结状态,无法读取标记,如何结束?
可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格,
但是强制动作会发生了InterruptedException,记得要处理。
例五:
//未处于冻结状态
class StopThread implements Runnable
{
private boolean flag=true;
public synchronized void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"..."+e);
}
}
public void setFlag()
{
flag=false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st=new StopThread();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
t1.start();
t2.start();
int num=1;
for(;;)
{
if(++num==50)
{
st.setFlag();//没有冻结发生时,可以直接定义标记来结束任务
break;
}
System.out.println("main.."+num);
}
System.out.println("over");
}
}
例六:
//处于冻结状态
class StopThread implements Runnable
{
private boolean flag=true;
public synchronized void run()
{
while(flag)
{
try
{
wait();
}catch(InterruptedException e){
System.out.println(Thread.currentThread().getName()+"..."+e);
flag=false;
}
System.out.println(Thread.currentThread().getName()+"...");
}
}
public void setFlag()
{
flag=false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st=new StopThread();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
t1.start();
t2.start();
int num=1;
for(;;)
{
if(++num==50)
{
//st.setFlag();
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("main.."+num);
}
System.out.println("over");
}
}
守护进程
setDeamon(boolean on):将该线程标记为守护进程或用户线程,可以理解为后台线程,当前台线程结束时,后台线程会自动结束。
例七:
class StopThread implements Runnable
{
private boolean flag=true;
public synchronized void run()
{
while(flag)
{
try
{
wait();
}catch(InterruptedException e){ System.out.println(Thread.currentThread().getName()+"..."+e);
flag=false;
} System.out.println(Thread.currentThread().getName()+"...");
}
}
public void setFlag()
{
flag=false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st=new StopThread();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
t1.start();
t2.setDaemon(true);//t2设置为守护线程或用户线程,可以理解为后台线程,当前台线程结束时,后台线程会自动结束
t2.start();
int num=1;
for(;;)
{
if(++num==50)
{
t1.interrupt();
break;
}
System.out.println("main.."+num);
}
System.out.println("over");
}
}