一. 进程和线程
1. 什么是进程
进程是处于运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的独立单位。
2. 进程的三个特征
(1)独立性。进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有进过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
(2)动态性。进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,在进程中增加了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
(3)并发性。多个进程可以在单个处理器上并发的执行,多个进程之间不会互相影响。
操作系统多进程支持的理解:程序指令通过cpu执行,在某个时间点只有一个程序的指令得到执行,但是cpu执行指令的速度非常快,所以多个程序指令在cpu上轮流切换执行的速度也很快,这样在宏观上感觉是多个程序在并发的执行。可以这样理解程序 的并发,宏观上并发执行,微观上顺序执行。
3. 什么是线程
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须拥有一个父进程。线程可以拥有自己的的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程的所拥有的全部资源。
二.创建线程的3种方式
1. 继承Thread类创建线程
步骤如下:
a.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。
b.创建Thread子类实例,即创建了线程对象。
c.调用线程对象的start()方法来创建并启动线程。
public class FirstThread extends Thread{
private int i;
//重写run()方法,run()方法的方法体就是线程的执行体
public void run(){
for(;i<100;i++){
// 当线程类继承Thread类时,直接使用this即可获取当前线程
//Thread对象的getName()方法返回当前线程的名字
//因此可以直接调用getName()方法返回当前线程的名字
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
//调用Thread类的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}
2. 实现Runnable接口创建线程类
a.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体就是线程的线程执行体。
b.创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
public class SecondThread implements Runnable{
private int i;
//run()方法同样是线程的执行体
@Override
public void run(){
for(;i<100;i++){
//当线程实现Runnable接口时
//如果想获取当前线程,只能通过Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondThread st=new SecondThread();
//通过new Thread(target,name)方法创建新线程
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
3. 使用Callable和Future创建线程
a.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程的执行体,且该call()方法有返回值,再创建Callable实现类的实例。
b.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
c.使用FutureTask对象作为Thread对象的target创建并启动新线程。
d.使用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class ThridThread {
public static void main(String[] args) {
//创建Callable对象
ThridThread rt=new ThridThread();
//先使用Lambda表达式创建Callable<Integer>对象
//使用FutureTask来包装Callable对象
FutureTask<Integer> task=new FutureTask<>((Callable<Integer>)()->{
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
//call()方法的返回值
return i;
});
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"循环变量i的值: "+i);
if(i==20){
//实质是以Callable对象来创建并启动线程
new Thread(task,"有返回值的线程:").start();;
}
}
try{
//获取线程的返回值
System.out.println("子线程的返回值:"+task.get());
}
catch (Exception e) {
e.printStackTrace();
}
}
}
三.线程的生命周期
每个线程都要经历新建、就绪、运行、阻塞、死亡5种状态。
新建状态:就是通过new关键字创建线程对象时的状态。
就绪状态:即通过线程对象的start()方法启动线程时对应的状态,此时线程并不一定马上能进入运行状态,线程的运行由操作系统的调度程序进行线程的调度。
运行状态:是指线程获得cpu的执行权,线程正在执行需要执行的代码。
-
阻塞状态:当发生以下情况时线程进入阻塞状态。
- 线程调用sleep()方法主动放弃所占有的处理器资源。
- 线程调用了一个阻塞式IO方法,在方法返回之前线程阻塞。
- 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有。
- 线程正在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。
-
死亡状态:线程会以以下三种方式结束,结束后的线程处于死亡状态。
- run()方法和call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用线程的stop()方法来结束线程。
线程的状态转换图如下:
注意点:
- 启动一个线程是使用线程对象的start()方法,而不是直接调用run()方法。如果直接调用run()方法,则和普通的对象调用实例方法一样,没有启动一个线程来执行该方法。启动一个线程只能对处于新建状态的线程启动,调用处于新建状态的线程对象使用start()方法来启动一个线程。如果对处于新建状态的线程对象调用了run()方法或其他方法,则此线程对象就不再处于新建状态了,以后调用该线程对象的start()方法将不会启动一个线程。
- 对于处于死亡状态的线程不能再次调用该线程的start()方法。程序只能对处于新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的,会引起IllegalThreadStateException异常。
四.控制线程
1. join线程
join()方法时Java的Thread类提供的让一个线程等待另一个线程完成的方法。当在某个程序的执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止。
public class JoinThread extends Thread {
//提供一个有参构造器,用于设置该线程的名字
public JoinThread(String name){
super(name);
}
//重写run方法,定义线程的执行体
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<100;i++){
if(i==20){
JoinThread jt=new JoinThread("被join的线程");
jt.start();
//main线程调用了jt线程的join()方法,main线程必须等待jt线程执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
2. 后台线程
后台线程(Daemon Thread)是在后台运行的,它的任务是为其他的线程提供服务,也被称为守护线程或精灵线程。
后台线程的特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将一个指定的线程设置为后台线程。
public class DaemonThread extends Thread {
public void run(){
for(int i=0;i<1000;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
DaemonThread dt=new DaemonThread();
//将此线程设置为后台线程
dt.setDaemon(true);
dt.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
//程序执行到此处,前台线程main线程结束
//后台线程也应该随之结束
}
}
3. 线程睡眠:sleep
Thread类的sleep()方法用来暂停线程的执行,调用sleep()的线程将会进入阻塞状态。Thread类的sleep()方法是Thread类的静态方法。
public class SleepTest {
public static void main(String[] args) throws Exception {
for(int i=0;i<10;i++){
System.out.println("当前时间:"+new Date());
//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
4. 线程让步:yield
yield()方法也是Thread类提供的静态方法,让线程暂停执行,与sleep()方法不同的是,yeild()方法不会将线程阻塞,当某个线程调用了yield()方法时,该线程会暂停执行进入就绪状态,只有优先级与当前线程相同或者优先级比当前线程高的处于就绪状态的线程才会获得执行的机会。
public class YieldTest extends Thread {
public YieldTest(String name){
super(name);
}
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
//当i=20时,使用yield()方法让当前线程让步
if(i==20){
Thread.yield();
}
}
}
public static void main(String[] args) {
//启动两个并发线程
YieldTest yt1=new YieldTest("高级");
//将yt1线程的设置为最高优先级
//yt1.setPriority(MAX_PRIORITY);
yt1.start();
YieldTest yt2=new YieldTest("低级");
//将yt2线程的设置为最低优先级
//yt2.setPriority(MIN_PRIORITY);
yt2.start();
}
执行上面的程序将会看到在i=20的时候yt1线程执行yield()方法,因为yt2线程与yt1线程处于同一优先级别,所以yt2线程将会获得执行权,然后在yt2执行到i=20的时候,yt2调用线程让步方法yeild(),同样的原因线程yt1将会获得执行权。
5. 改变线程的优先级
Java中每个线程都有一定的优先级,优先级高的线程获得执行的机会多,而优先级低的线程获得执行的机会少。对于创建的线程,Java默认的优先级同创建它的父线程的优先级相同。如果想改变线程的优先级,则可以使用Thread类提供的setPriority(int newPriority)方法设置线程的优先级,而getPriority()方法返回线程的优先级。Java中的优先级的参数范围是1-10的整数。