进程:正在进行中的程序(直译)。
线程:就是进程中的一个负责程序执行的控制单元(也叫执行路径),进程只是负责在内存中分配空间,并不负责程序的执行,由程序中的线程执行。
一个程序中可以有多个执行路径,称之为多线程(并发执行)
一个进程中至少要有一个线程
开启多个线程是为了同时运行多部分代码
每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
多线程的好处:解决了多部分程序同时运行的问题
弊端:线程太多,cpu切换次数多,单个程序或线程执行效率降低
其实应用程序的执行都是cpu在做着快速的切换完成的,这个切换是由时间片的划分完成的。
JVM启动时就启动了多个线程,至少有两个线程可以分析的出来:
1、 执行主函数main()的线程,该线程的任务代码都定义在main函数中
2、 负责垃圾回收的线程,该线程的任务代码都在垃圾回收器里定义的
Java.lang包下有一个Thread类(线程类),java虚拟机允许应用程序并发地执行多个线程。
创建新的执行线程有两种方法:
1、 将类声明为Thread类的子类,该子类应重写Thread类的run方法。
为何要重写run方法?—-答:创建线程的目的是为了开启一条执行路径,去运行指定的代码,从而实现和其他代码的并发执行。
而运行的指定代码就是这个执行路径的任务(即任务是指这个执行路径要运行的代码)
Jvm创建的主线程的任务(主线程要执行的代码)都定义在了主函数中。
而自定义的线程它的任务(即自定义线程中要执行的代码)在哪儿呢?
Thread类用于描述线程,线程是需要任务(执行代码)的,所以Thread类也是对任务的描述。
这个任务(线程中的执行代码)就是通过Thread类中的run方法来体现的,也就是说,run方法就是封装自定义线程运行任务(执行代码)的函数。run方法中定义的就是线程要运行的执行代码。
开启线程是为了运行指定的代码,所以只有继承Thread类,并覆写run方法。将要运行的代码定义在run方法中即可。
创建线程方式一:
1、 定义一个类,继承Thread类
2、 重写Thread类的run方法。
3、 直接创建Thread类的子类对象,即创建一个线程。(如果在main函数中创建了自定义线程后,直接调用run方法,和常规调用(创建对象,调用对象的方法)没有任何区别);要想体现线程特性,则需进行第四步操作
4、 使用自定义的Thread的子类对象,调用start()方法启动该线程(即执行该线程),start方法会告诉java虚拟机调用该线程的run方法,然后执行。
示例:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+": x="+x);
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo(“春风”);
Demo d2=new Demo(“得意”);
/*如果是使用
d1.run();
d2.run();
则和普通方法调用一样,先调用d1的run方法,等到其执行完后,在执行d2.run()这一条调用语句
*/
/*要体现线程的并发执行特性,需调用线程对象的start方法。
cpu根据一定的调度算法在主线程、d1线程、d2线程之间进行切换(忽略了系统开启的其他线程)*/
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
System.out.println“这是一个线程测试”);
}
}
线程的名称:
每个线程对象在创建时就定义了一个名字,在Thread类中定义了一个getName()方法,可以获取线程的名称—格式为:Thread-编号(从0开始);主线程的名称默认为main
即使使用线程对象名.run()的方式调用也可使用getName()获取该线程对象的名称
示例:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+": x="+x+”…name=”+getName());
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo(“春风”);
Demo d2=new Demo(“得意”);
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
/* d1.run();
d2.run();
可发现调用start和run的结果一样,这是因为getName()获得的是线程对象的名称,而线程对象一创建就已经分配了一个名称(Thread-编号,编号从0开始)
*/
System.out.println“这是一个线程测试”);
}
}
如何获取运行时线程的名称呢?: 在Thread类中定义了一个currentThread()方法,返回对当前正在执行的线程对象的引用。
static Thread currentThread();
示例:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+”: x=”+x+”…name=”+Thread.currentThread().getName());
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo(“春风”);
Demo d2=new Demo(“得意”);
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
/* d1.run();
d2.run();
因为d1、d2未开启,则只是普通调用,则程序中正在运行的线程只有主线程 main,Demo类中的Thread.currentThread().getName()返回的就是主线程的名字main
*/
System.out.println(“这是一个线程测试”+”……name=” + Thread.currentThread().getName());
}
}
但是Thread-编号的名称并不好区分,可以自定义线程的名称,在Thread类中提供了一个name属性,且由一个带一个String参数的构造器(初始化name);故可在自定义类Demo中使用super调用父类Thread的构造器
class Demo extends Thread
{
private String name;
Demo(String name)
{
super(name);
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+": x="+x+”…name=”+Thread.currentThread().getName());
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo(“春风”);//d1线程对象的名字就是:春风
d1.start();//启动线程d1,并调用其run方法
System.out.println(“这是一个线程测试”+”……name=” + Thread.currentThread().getName());
}
}
只要在main函数中开启了新线程,就会在内存栈中为该新线程开辟一块独立于main线程的内存空间,则即使main线程运行结束出栈,只要其他线程未执行完,则JVM不会停止,仍会继续执行其他线程。
如果在新线程开启后,main线程出现异常,只是main线程结束,不会影响其他线程的执行;而如果在开启新线程前,main线程发生异常,则还没来的及开启线程,main线程就已经结束,其他线程无法开启,得不到执行。main线程和其他线程都开启执行后,线程程序异常,只会结束自己的线程,对其他线程无影响。
示例①:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+": x="+x+”…name=”+Thread.currentThread().getName());
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo(“春风”);
Demo d2=new Demo(“得意”);
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
System.out.println(4/0);//抛出算术异常
System.out.println(“这是一个线程测试”);
}
}
示例②:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
for(int x=0;x<9999;x++)
{
System.out.println(name+”: x=”+x+”…name=”+Thread.currentThread().getName());
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
System.out.println(4/0);//抛出算术异常
Demo d1=new Demo(“春风”);
Demo d2=new Demo(“得意”);
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
System.out.println(“这是一个线程测试”);
}
}
示例③:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name=name;
}
public void run()
{
System.out.println(“将要发生异常”);
int [ ] arr=new int [2];
System.out.println(arr[2]);//抛出越界异常
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo("春风");
Demo d2=new Demo("得意");
d1.start();//启动线程d1,并调用其run方法
d2.start();//启动线程d1,并调用其run方法
System.out.println(4/0);//抛出算术异常
System.out.println(“这是一个线程测试”);
}
}
创建线程方式二:
在java中如果一个类已经继承于其他类,则不能再继承Thread类,但是又需要多线程性,需要扩展子类的功能,让其中的内容可以作为线程的任务(执行代码)执行,通过接口实现的形式完成。
java.lang包中的Runnable接口就是为了实现这一功能的,Runnable接口中只有一个抽象的run()方法,实现类重写该run()方法,将要在线程中执行的代码封装在该方法体中。
在Thread类中有一个构造器,该构造器接受一个runnable类型的形参:Thread(Runnable target),该构造器将形参target初始化给Thread类对象的Runnable类型的target属性。
当开启start线程后,系统会判断Runnable类型的target属性是否为空,若不为空,则执行target所引用的对象的run方法,而不是去执行Thread类对象本身的run方法。
步骤:
1、定义类,实现Runnable接口
2、覆写Runnable接口中的run()方法,将线程的任务代码(线程中要执行的代码)封装到run()方法中
3、通过Thread类的构造器创建线程对象,并将Runnable接口的子类对象作为Thread类的构造器的参数进行传递。
为什么? 答:因为线程的任务都封装在Runnable接口子类对象的run方法中,而在线程对象创建时就要明确运行的任务(执行代码)。
4、调用线程对象的start方法开启线程。
示例:
class Demo implements Runnable
{
public void run()
{
show();
}
Public void show()
{
for(int x=0;x<20;x++)
{
System.out.println(Thread.currentThread().getName()+”……”+x);
}
}
}
public class ThreadDemo
{
public static void main(String [ ] args)
{
Demo d1=new Demo();
Thread t1=new Thread(d1); //Thread类对象t1的target属性初始化为d1
t1.start();
System.out.println(“这是一个线程测试”);
}
}
创建线程方式二的细节:
Demo d1=new Demo();
Thread t1=new Thread(d1);
d1中重写了Runnable接口中的run()方法,而Thread类中也定义了run()方法,则t1.start();将运行哪个对象的run()方法呢?
答:Thread类的类定义中类似有如下代码:
class Thread
{
private Runnable target;
public Thread(Runnable target)
{
this.target=target;
}
public void run()
{
if(target!=null)
{
target.run();
}
}
piblic void start()
{
run();
}
}
创建线程方式二的好处:
1、 将线程的任务(执行代码)从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象
2、 避免了java单继承的局限性
创建线程的第二种方式较为常用
线程的状态
sleep()方法需要指定睡眠时间,时间是毫秒
还可以细分出一个特殊的状态:就绪状态。即,具备了执行资格,但是还没有获得资源。
Wait和sleep的区别:
1、 Thread类的静态方法:sleep()
Object的方法:wait()和notify()等
2、wait可以指定时间也可以不指定时间,sleep必须指定时间
3、在同步中,对cpu的执行权和锁的处理不同,wait:释放执行权,释放锁,进入锁的线程池,处于冻结状态,其他使用同一锁的线程,获得cpu的执行权便可运行;sleep释放执行权,不释放锁,进入冻结状态,但是其他使用同一个锁的线程无法执行,睡眠时间结束后恢复运行状态。
通常,使用wait是线程释放执行权。