1.线程概述
进程:在一个操作系统中,每个独立执行的程序都可以称之为一个线程。
操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后在下一段时间切换到另一个进程中去执行。
线程:在一个进程中,可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条线索,被称为线程。线程也是由CPU控制并轮流执行的。
当一个java程序启动后,就会产生一个进程,该进程中会默认创建有个线程用来执行main函数代码。
2.线程的创建
(1)Thread类实现多线程
步骤:创建一个Thread线程类的子类,同时重写Thread类的run方法;创建该子类的实例对象,并通过调用start方法启动线程。
class MyThread1 extends Thread{
public MyThread1(String name){
super(name);
}
public void run(){
int i=0;
while(i++<5){
System.out.println(Thread.currentThread().getName());//用来获取当前线程对象
}
}
}
public class Example{
public static void main(String[] args){
MyThread1 thread1=new MyThead1("thread1");//创建实例对象
thread1.start();//启动线程
}
}
(2)Runnable接口实现多线程
步骤:创建一个Runnable接口的实现类,同时重写接口中的run方法;创建Runnable接口的实现类对象;使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;调用线程实例的start方法启动线程。
class MyThread2 implements Runnable{
public void run(){
int i=0;
while(i++<5){
System.out.println(Thread.currentThread().getName());//用来获取当前线程对象
}
}
public class Example{
public static void main(String[] args){
MyThread2 myThread2=new MyThread2();
Thread thread=new Thread(myThread2,"thread");
thread1.start();
}
}
(3)Callable接口实现多线程
用来满足既能创建多线程又可以有返回值的需求。
步骤:创建一个Callable接口的实现类,同时重写call方法;
创建Callable接口的实现类对象;
通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
调用线程实例的start方法来启动线程。
import java.util.concurrent.*;
class MyThread3 implements Callable<Object>{
public Object call() throws Exception{
int i=0;
while(i++<5){
System.out.println(Thread.currentThread().getName());//用来获取当前线程对象
}
return i;
}
public class Example{
public static void main(String[] args){
//创建Callable接口的实现类对象
MyThread3 myThread3=new MyThread2();
//使用FutureTask封装Callable接口
FutureTask<Object>ft1=new FutureTask<>(myThread3);
Thread thread3=new Thread(ft1,"thread3");
thread1.start();
//通过FutureTask对象的方法管理返回值
ft1.get();//返回值是6
}
}
(4)三种方法的对比分析
通过实现Runnable和Callable接口的好处:
适合多个线程去处理同一个共享资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想
可以避免java单继承带来的局限性。
(5)后台线程
新创建的线程默认是前台线程,如果某个线程对象在启动之前调用了setDaemon语句,这个线程就变成了一个后台线程。
3.线程的生命周期和状态转换
(1)NEW(新建状态)
创建一个线程对象后,该线程就处于新建状态,此时仅仅由JVM为其分配了内存。
(2)RUNNABLE(可运行状态)
调用start方法后。
READY(就绪状态):调用start方法后等待jvm的调度,此时线程并没有运行;
RUNNING(运行状态):线程对象获得JVM调度。
(3)BLOCKED(阻塞状态)
因为某些原因失去CPU的执行权。JVM不会给线程分配CPU,直至线程恢复为就绪状态。
线程进入阻塞状态的两种情况:
线程A运行过程中,试图获取同步锁,却被线程B获取,此时JVM把当前线程A存到对象的锁池中,线程A就会进入阻塞状态;
当线程运行过程中,发出I/O请求时。
(4)WAITING(等待状态)
当处于运行状态的线程调用了无时间参数的方法后,wait、join等,就会将当前运行中的线程转换成等待状态。
调用wait方法而处于等待状态中的线程,必须等待其他线程调用notify或者notifyAll方法唤醒当前等待中的线程。
调用join方法的,必须等待其他加入的线程中值。
(5)TOME_WAITING(定时等待状态)
运行线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)
调用wait方法而处于等待状态中的线程,必须等待其他线程调用notify或者notifyAll方法唤醒当前等待中的线程,或者直接等时间结束后
(6)TERMINATED(终止状态)
线程的run或者call方法正常执行完毕或者线程抛出一个未捕获的异常、错误,线程就进入终止状态。
4.线程的调度
Java虚拟机按照特定的机制为程序中的每个线程分配CPU的使用权。
线程调度有两种模型:
分时调度模型:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用的时间。
抢占式调度模型:让可运行池中所有就绪状态的线程争抢CPU的使用权,优先级高的线程获取的概率要大于优先级低的。
Java虚拟机默认使用抢占式调度模型。
(1)优先级
优先级用1~10之间的数字来表示。
也可以使用Thread类中提供的三个静态常量表示线程的优先级。
静态常量 | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,10 |
static int MIN_PRIORITY | 表示线程的最低优先级,1 |
static int NORM_PRIORITY | 表示线程的普通优先级,5 |
可以通过Thread类的setPriority(int newPriority)方法对其进行设置。其中的参数接受1~10的整数或者三个静态常量。
(2)线程休眠
使用sleep(long millis)可以人为的使正在执行的线程休眠,该方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出异常。
try{
Thread.sleep(500);//ms
}
catch(InterruptedException e){
e.printStackTrace();
}
(3)线程让步
通过yield方法实现,但该方法不会阻塞该线程,而是将线程转换成就绪状态。
class YieldThread extends Thread{
public YieldThread(String name){
super(name);
}
public void run(){
for(int i=0,i<5,i++){
if(i==2){
System.out.println("线程让步");
Thread.yield();
}
}
}
}
(4)线程插队
使用join方法。当在线程中调用其他线程的join方法时,调用的线程将被阻塞,直至被该方法加入的线程执行完成后它才会继续执行。
5.多线程同步
限制某个资源在同一时刻只能被一个线程访问。
(1)线程安全:由多个线程同时处理共享资源所导致的。
(2)同步代码块
解决线程安全问题,保证处理共享资源的代码在任意时刻只能有一个线程访问。
同步代码块:当多个线程使用同一个资源时,可以将处理共享资源的代码放置在一个使用synchronized关键字来修饰的代码块中。
synchronized(lock){
}
lock是一个锁对象。当线程同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置0.当一个新线程执行到这段代码段时,由于锁对象的标志位是0,因此新线程会发生堵塞。等当前线程执行完同步代码块之后,锁对象的标志位置1,新线程才能执行同步代码块之中的内容。
锁对象可以是任一类型的对象。但是锁对象的创建不能放入到run方法中。
(3)同步方法
在方法面前使用synchronized关键之来修饰,被修饰的方法为同步方法,他能实现和同步代码块同样的功能。
同步方法也有锁,它的锁就是当前调用该方法的对象
java中静态同步方法的锁是该方法所在类的class对象
(4)同步锁
synchronized锁限制:无法中断一个正在等候获得锁的线程;无法通过轮询得到锁;
java从5开始增加了一个Lock锁,它功能相同但是可以让某个线程在持续获取同步锁失败后返回,不再继续等待。
步骤:
定义一个锁对象
final Lock lock=new ReentrantLock();
在加锁的代码块中加入:
lock.lock();
执行完代码块之后释放锁
lock.unlock();
(5)死锁问题
两个线程在运行时都在等待对方的锁,造成了死锁现象
6.多线程通信
线程通信的常用方法:
方法声明 | 功能描述 |
---|---|
void wait() | 使当前线程放弃同步锁并等待,直至其他线程进入此同步锁,并调用notify或notifyAll |
void notify() | 唤醒此同步锁上等待的第一个调用wait方法的线程 |
void notifyAll() | 唤醒此同步锁上调用wait方法的所有线程 |
注:以上三个方法的调用都应该是同步锁对象,否则会抛出IllegalMonitorStartException异常。
import java.util.*;
public class Example{
public static void main(String[] args){
//定义一个集合类,用来模拟存储的商品
List<Object>goods=new ArrayList<>();
//线程开始的时间
long start=System.currentTimeMillis();
//创建一个生产者线程
Thread thread1=new Thread(()->{
int num=0;
while(System.currentTimeMillis()-start<100){//程序执行100ms
//同步商品的生产和消费
synchronized(goods){
if(goods.size()>0){
tyr{
goods.wait();//有商品,生产线程就处于停滞状态
}
catch(InterruptedException e){
e.printStackTrace();
}
}
else{
goods.add(++num);
}
}
}
})
Thread thread2=new Thread(()->{
int num=0;
synchronized(goods){
if(goods.size()<=0){
goods.notify();//商品不足唤醒生产者生产
}
else{
goods.remove(++num);
}
}
})
thread1.start();
thread2.start();
}
}
7.线程池
在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。采用java提供的线程池来创建多线程,可以进一步优化。
(1)Executor接口实现线程管理
步骤:
创建一个Runnable接口或者Callable接口的实现类,同时重写run或者call方法;
创建Runnable接口或者Callable接口的实现类对象
使用Executors线程执行器类来创建线程池
使用ExecutorService执行器服务类的submit方法将Runnable或者Callable接口的实现类对象提交到线程池进行管理
线程任务执行完成后,使用shutdown方法来关闭线程池
Executors创建线程的方法:
方法声明 | 功能描述 |
---|---|
ExecutorService newCachedThreadPool() | 创建一个可扩展线程池的执行器。适用于启动许多短期任务的应用程序 |
ExecutorService newFixedThreadPool(int nThreads) | 创建一个固定线程数量线程池的执行器。可以很好的控制多线程任务 |
ExecutorService newSingleThreadExecutor() | 在特殊需求下创建一个只执行一个任务的单个线程 |
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个定长线程池,支持定时及周期性任务执行 |
import java.util.concurrent.*;
class MyThread implements Callable<Object>{
public Object call() throws Exception{
int i=0;
while(i++<5){
System.out.println(Thread.currentThread().getName());
}
return i;
}
}
public class Example{
public static void main(String[] args){
MyThread mythread =new Mythread();//创建callable接口类的实现对象
ExecutorService executor=Executors.newCachedThreadPool();//使用线程执行器类创建可扩展的线程池
Future<Object>result=executor.submit(mythread);//将callable接口类的实现对象交到线程池进行管理
executor.shutdown();//关闭线程池
}
}
(2)CompletableFuture类实现线程池管理
在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取。但该类在获取结果时采用的是阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。
CompletableFuture类简化异步编程的复杂性。
CompletableFuture对象创建的4个静态方法:
import java.util.concurrent.*;
public class Example{
public static void main(String[] args){
CompletableFuture<Integer>completableFuture=
CompletableFuture.supplyAsync(()->{
int sun=0,i=1;
while(i++<5){
sum+=i
}
return sum;
})
}
}
runAsync和supplyAsync方法的本质区别就是获取的CompletableFuture对象是否带有计算结果。