Java基础知识总结——线程及线程相关操作

一、基本概念

1、进程与线程

进程: 是系统进行资源分配和调度的基本单位。
线程: 也被称为轻量级进程,是进程执行的最小单位。
联系: 线程是进程的组成部分,一个进程可以有多个线程,但是至少要包含一个主线程。
区别:

  • 地址空间:每一个进程都拥有自己独立私有的地址空间;线程没有地址空间,线程包含在进程的地址空间中。
  • 资源:每一个进程都拥有自己独立私有的资源;线程拥有自己的堆栈、程序计数器和局部变量,不拥有系统资源,同一进程的所有线程共享该进程的资源。
  • 健壮性:一个进程的崩溃不会影响其它进程,但是一个线程崩溃会导致其所在的进程崩溃,从而导致该进程中的所有线程崩溃。
  • 开销方面:进程占用内存多,进行进程间切换时开销大;而线程占用内存少,进行线程间切换时开销小。

2、并发与并行

并行:在同一时刻,有多条指令在多个处理器上同时执行。
并发:在同一时刻,只有一条指令在一个处理器上被执行,但多个进程指令被快速轮换执行,是的在宏观上具有多个进程同时执行的效果。

二、线程的生命周期

线程的生命周期中有新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead) 5 种状态。线程状态的转换图如下所示:
在这里插入图片描述
当程序使用 new 关键字新建一个线程之后,线程会处与新建状态。

新建→就绪:

  • 线程对象调用 start() 方法后会进入就绪状态。

说明:①就绪状态并不是线程开始运行,只是进入了可以运行的状态,具体运行时间取决于虚拟机的调度。②启动线程使用 start() 方法而不是 run() 方法。直接调用 run() 方法时,系统会将线程当成普通对象立即执行其 run() 方法。

就绪→运行:

  • 就绪状态的线程得到 CPU 调度时会进入运行状态。

运行→就绪:

  • 线程失去处理器资源时会转回就绪状态。
  • 调用 yield() 方法让线程主动放弃占用的处理器资源时会转入就绪状态。

运行→阻塞:

  • 调用 sleep() 方法主动放弃占用的处理器资源。
  • 调用了一个阻塞式 IO 方法,在该方法返回前,该线程被阻塞。
  • 线程想获得同步锁的时候发现已被占用。
  • 线程正在等待某个通知(notify)。
  • 程序调用 suspend() 方法将该线程挂起。(容易导致死锁)。

阻塞→就绪:

与上一个状态转换一一对应。

  • 经过了调用 sleep() 方法时指定的时间,睡眠结束。
  • 调用的阻塞式 IO 方法已经返回。
  • 成功获得了该线程想要的同步锁。
  • 收到了线程正在等待的通知。
  • 处于挂起状态的线程被调用了 resume() 方法恢复。

运行→死亡:

  • run() 或 call() 方法执行完成,流程正常结束。
  • 线程抛出一个未捕获的异常。
  • 直接调用 stop() 方法结束该线程。(容易导致死锁)。

说明:①可以使用 isAlive() 来判断该线程是否死亡:线程处于就绪、运行、阻塞时返回 true,处于新建、死亡时返回 false。②不能对已死亡的线程再次调用 start() 方法来重新启动,否则会抛出异常。

三、创建线程

Java 使用 Thread 类来表示线程,所有的线程对象都必须是 Thread 类或其子类的实例。三种创建线程的方式如下:

1、继承 Thread 类创建线程类

  • 先定义 Thread 类的子类并重写 run() 方法,run() 方法的方法体即是线程需要完成的任务。
  • 然后创建实例并使用 start() 方法启动该线程即可。

通过继承 Thread 类来创建并启动多线程的示例如下:

//继承 Thread 类创建线程类
public class FirstThread extends Thread{
    
    
	private int i;
	
	//重写 run() 方法
	public void run(){
    
    
		for( ; i < 100; i++){
    
    
			//在 Thread 的子类中,使用 this 即可获取当前线程
			//Thread 对象的 getName() 方法可以返回当前线程的名字
			System.out.printLn(this.getName() + " " + i);
		}
	}
	
	public static void main(String[] args){
    
    
		//调用 Thread 的 currentThread() 方法获取当前线程
		System.out.printIn(Thread.currentThread().getName() + " " + i);
		if(i == 20){
    
    
			//创建并启动第一个线程
			new FirstThread().start();
			//创建并启动第二个线程
			new FirstThread().start();
		}
	}
}
//运行代码可以看到三个独立的 i 的值

说明:默认情况下,主线程的名字为 main,启动的多个线程的名字为 Thread-0、Thread-1、…、Thread-n 等。用户可以通过 setName(String name) 方法为线程设置名字。

2、实现 Runnable 接口创建线程类

  • 先定义 Runnable 接口的实现类,重写该接口的 run() 方法。
  • 创建 Runnable 实现类的实例,将其作为 Thread 的 target 来创建 Thread 对象,然后使用 start() 方法启动该线程即可。

通过实现 Runnable 接口来创建并启动多线程的示例如下:

//实现 Runnable 接口创建线程类
public class SecondThread implement Runnable{
    
    
	private int i;
	
	//重写 run() 方法
	public void run(){
    
    
		for( ; i < 100; i++){
    
    
			//在实现 Runnable 接口时,只能使用 Thread.currentThread() 方法获取当前线程
			System.out.printLn(Thread.currentThread().getName() + " " + i);
		}
	}
	
	public static void main(String[] args){
    
    
		System.out.printIn(Thread.currentThread().getName() + " " + i);
		if(i == 20){
    
    
			SecondThread st = new SecondThread();
			//通过 new Thread(target, name) 方法创建新线程
			new Thread(st, "新线程2").start();
			new Thread(st, "新线程2").start();
		}
	}
}
//运行代码可以看到两个子线程共用一个 i 值

说明:采用 Runnable 接口的方式创建的多个线程可以共享线程类的实例变量。因为这种方式下,程序所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target,所以多个线程可以共享同一个线程类的实例变量。

3、使用 Callable 和 Future 创建线程

Callable 接口提供了一个 call() 方法作为线程执行体,call() 方法比 run() 强大:call() 方法可以有返回值;call() 方法可以声明抛出异常。Java 提供 Future 接口代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该类实现了 Future 接口和 Runnable 接口。

使用 Callable 和 Future 创建线程的步骤:

  • 创建 Callable 接口的实现类,实现 call() 方法,创建 Callable 实现类的实例。(也可直接用 Lambda 表达式创建 Callable 对象)。
  • 使用 FutureTask 类包装 Callable 对象,该 FutureTask 对象封装 call() 方法的返回值。
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程。
  • 使用 FutureTask 对象的 get() 方法来获取子线程执行结束后的返回值。

使用 Callable 和 Future 来创建并启动多线程的示例如下:

public class ThirdThread{
    
    
	public static void main(String[] args){
    
    
		//创建 Callable 对象
		ThirdThread rt = new ThirdThread();
		//使用 Lambda 表达式创建 Callable<Integer> 对象,然后使用 FutureTask 对象封装 Callable 对象
		FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
    
    
			int i = 0;
			for( ; i < 100; i++){
    
    
				System.out.printLn(Thread.currentThread().getName() + " 的循环变量 i 的值" + 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 ex){
    
    
			ex.printStackTrace();
		}
	}
}
//运行程序可以看到主线程和 call() 方法所代表的线程交替执行的情形。

附:创建线程的方式比较

  • 通过 Runnable、Callable 接口实现多线程的时候还可以继承其他父类,而通过继承 Thread 类实现多线程时不能再继承其它父类。
  • 通过 Runnable、Callable 接口实现多线程时,多个线程可以共享同一个 target 对象,适合多个线程处理同份资源的情况。
  • 在访问当前线程时,通过 Runnable、Callable 接口实现的多线程必须使用 Thread.currentThread() 方法,而通过继承 Thread 类实现的多线程可以直接用 this。

四、线程调度

线程的强制运行

当在某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到被调用的线程执行完毕。
join() 方法的重载形式:

  • join():等待被 join 的线程执行完成。
  • join(long millis):等待被 join 的线程的最长时间为 millis 毫秒。超过这个时间则不再等待。

后台线程

运行在后台,为其他线程提供服务的线程称为后台线程,例如垃圾回收线程。当所有的前台线程全部死亡时,后台线程会自动死亡。

  • 调用 Thread 对象的 setDaemon(true) 方法可以将指定线程设置成后台线程。
  • 调用 Thread 对象的 isDaemon() 方法可以判断指定线程是否为后台线程。

说明:将进程设置为后台进程时必须在 start() 方法调用前,否则会抛出异常。

线程睡眠

  • static void sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态。

线程让步

  • void yield():让当前正在执行的线程暂停,并进入就绪状态。(因此该线程有可能被暂停后立即又被系统调度运行)。

改变线程优先级

在线程调度时,优先级高的线程可以获得更多的执行机会。每个线程默认的优先级与创建它的父线程的优先级相同。

  • void setPriority(int newPrioruty):设置线程的优先级,newPriority 为 1-10 之间的整数。
  • int getPriority():获取线程的优先级。

Thread 类定义了三个与优先级相关的静态常量:

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIOROTY:5

由于不同操作系统上的优先级不同,因此使用静态常量设置线程优先级的程序具有更强的可移植性。

五、线程同步

多个线程同时对一个数据进行操作时,需要人为的添加同步操作从防止造成数据破坏。

1、同步代码块

同步代码块的语法如下

synchronized(obj){
    
    
	...
	//此处的代码即为同步代码块
}

obj 即是同步监视器,线程在执行同步代码块之前必须获得对同步监视器的锁定。在任何时刻,最多只有一个线程可以获得对同步监视器的锁定,同步代码块执行完之后,线程会释放对同步监视器的锁定。

2、同步方法

通过对方法添加 synchronized 关键字使之成为同步方法,该同步方法的同步监视器是 this,即调用该同步方法的对象。

附:关于释放同步监视块的锁定

  • 当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器。
  • 当线程执行同步代码块或同步方法时,程序调用 sleep()、yield() 方法来暂停当前线程时,当前线程不会释放同步监视器。
  • 当线程执行同步代码块或同步方法时,其他线程调用该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器。

3、同步锁

同步锁是指 Lock 对象,通过显示定义同步锁对象来实现同步。Java 为 Lock 提供了 ReentrantLock(可重入锁)实现类,其使用格式如下:

class X{
    
    
	//定义锁对象
	private final ReentrantLock lock = new ReentrantLock();
	//...
	//定义需要保证线程安全的方法
	public void m(){
    
    
		//加锁
		lock.lock();
		try{
    
    
			//需要保证线程安全的代码
			//... method body
		}
		//使用 finally 块来保证释放锁
		finally{
    
    
			lock.unlock();
		}
	}
}

ReentrantLock 锁具有可重入性,因此一个线程可以对已被 ReentrantLock 加锁的对象再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套使用。

4、死锁

死锁产生的必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

六、线程通信

1、传统的线程通信

Object 类提供了以下三个方法用于线程间通信:

  • wait():导致当前线程等待,直到被 nofity() 方法或者 nofityAll() 方法唤醒。可以无参数(一直等待),也可以带毫秒或者耗微秒(指定时间后自动苏醒)。线程调用此方法会释放对同步监视器的锁定。
  • nofity():唤醒此同步监视器上等待的单个线程,如果同时有多个线程在等待,则随机唤醒任意一个。
  • nofityAll():唤醒此同步监视器上等待的所有线程。

这三个方法必须由同步监视器对象来调用:

  • 同步方法:该类的默认实例为同步监视器,所以可以在同步方法中直接调用者三个方法。
  • 同步代码块:同步监视器是 synchronized 后括号里的对象,因此必须使用该对象来调用这三个方法。

2、使用 Condition 控制线程通信

Condition 实例绑定在 Lock 对象上,与 Lock 配套使用。其提供以下三个类似传统线程通信的方法:

  • await():类似于 wait()。
  • singal():类似于 nofity()。
  • singalAll():类似于 nofityAll()。

3、使用阻塞队列控制线程通信

Java 提供了一个 BlockingQueue 接口作为线程同步的工具。当生产者试图向 BlockingQueue 中放入元素时,若该队列已满,则该线程被阻塞;当消费者试图从 BlockingQueue 中取出元素时,若队列为空,则该线程被阻塞。
BlockingQueue 包含的方法如下:

抛出异常 不同返回值 阻塞线程 指定超出时长
队尾插入元素 add(e) offer(e) put(e) offer(e, time, uint)
队头删除元素 remove() poll() take() poll(time, uint)
获取、不删除元素 element() peek()

Guess you like

Origin blog.csdn.net/qingyunhuohuo1/article/details/109175921