本文是学习Java时所记录的学习笔记,本节包含了多线程的基本概念、简单实现和相关的理论知识。提取了部分我觉得是重点的内容。欢迎留言、私信交流~~
文章目录
多线程简介
什么是多线程?
多线程的程序可以包括多个顺序执行流,多个顺序流之间互不干扰。
什么是进程?
一个程序进入内存运行时,就变成一个进程。
进程和线程是什么关系?
线程的调度和管理由进程本身负责完成。运行程序是通过进行中的线程运行。一个进程可以拥有多个线程,一个线程必须有一个父进程。
多线程的好处?
- 进程之间不能共享内存,但线程之间共享内存非常容易
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多,因此使用多线程来实现多任务并发比多进程的效率高。
- Java语言内置了多线程功能支持,而不是单纯作为底层操作系统的调度方式,从而简化了Java的多线程编程。
Java创建线程的方法概述
三种方式可以创建线程:继承Thread或实现Runnable、Callable接口。
- 实现接口创建多线程的好处:只是实现接口,类还可以继承其他类/多线程可以共享同一个参数对象。
- 实现接口创建多线程的坏处:访问当前线程必须使用Thread.currentThread()方法。
- 继承Thread类创建多线程的好处:访问当前线程直接使用this即可。
- 继承Thread类创建多线程的坏处:因为继承了Thread类,无法继承其他类。
- 总结:推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
多线程具体实现
Thread类实现流程
- 使用Thread类创建线程流程:
1.将类声明为Thread的子类。 2.子类重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。 3.声明子类实例。 4.调用线程对象的start()方法来启动该线程。
Runnable接口实现流程
- 使用Runnable接口创建线程流程:
1.声明实现Runnable接口的类 2.实现run方法,该run()方法的方法体就代表了线程需要完成的任务。 3.创建Runnable实现类的实例,并以此实例作为Thread的参数来创建Thread对象,该Thread对象才是真正的线程对象。 4.调用线程对象的start()方法来启动该线程。
Callable接口和Future接口的实现流程
- 使用Callable接口和Future接口创建线程流程:
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且call()方法有返回值,再创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象。 2.使用FutureTask类来包装Callable对象,该Future对象封装了该Callable对象的call()方法的返回值。 3.调用FutureTask对象作为Thread对象的参数创建并启动新线程。 4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
- 例子
import java.util.concurrent.*; class xianchengdemo1{ public static void main(String[] args){ FutureTask<Integer> ft = new FutureTask<Integer>((Callable<Integer>)() ->{ int i = 0; for (;i<8 ;i++ ){ System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i); } return i ; }); Thread a1 = new Thread(ft,"有返回值的线程1"); try{ for (int i = 0;i<20 ;i++ ){ System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i); if (i==10){ a1.start(); //如果没有用FutureTask对象的get()方法,那么有返回值的线程和主线程会同步执行。 //get()方法在获得返回执行结果前,对程序进行了阻塞。 } } } catch (Exception e){ e.printStackTrace(); } } }
多线程的相关知识
线程的生命周期
- 新建(New)
- 使用new关键字创建线程对象后,线程就处于新建状态。Java虚拟机会为其分配内存,并初始化其成员的值。
- 就绪(Runnable)
- 当线程对象调用了start()方法之后,该线程就处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度(调度具有一定的随机性,会由底层平台随机控制切换线程。可以在start()方法前设置当前正在运行的线程Thread.sleep(1),这样系统不会让CPU空闲,从而进行立即执行就绪状态的线程)。(只能对新建状态的线程调用start方法,否则引发IllegalThreadStateException异常)
- 运行(Running)
- 当处于就绪状态的线程获得处理器资源时,该线程进入运行状态。失去资源时则返回就绪状态。
- 阻塞(Blocked)
- 会发生阻塞的5种情况:线程调用sleep()方法主动放弃所占用的处理器资源/线程调用了一个阻塞式IO方法,在该方法返回前,该线程被阻塞/试图获得一个同步监视器,但该同步监视器正被其他线程所持有/线程在等待某个通知(notify)/程序调用了线程的suspend()方法将该线程挂起(调用resume()方法可恢复)。
- 死亡(Dead)
- 线程会以如下三种方式结束,结束后就处于死亡状态:run()或call()方法执行完成,线程正常结束/线程抛出一个未补货的Exception或Error/直接调用该线程的stop()方法来结束该线程。
线程相关类说明
Thread类
- 可以通过继承Thread类,来实现创建线程。所有线程对象都必须是Thread类或其子类的实例。
- 使用Thread类创建线程流程:
1.将类声明为Thread的子类。 2.子类重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。 3.声明子类实例。 4.调用线程对象的start()方法来启动该线程。
- Thread类常用方法
方法名 说明 hread.getName(); 返回当前线程名字 oid setName(String name) 设置线程名字 hread.currentThread(); 获取当前线程。通常是返回当前正在执行的线程对象。 hread.currentThread().getName() //链式编程,currentThread返回当前正在执行的线程对象的应用,然后获取名字。 sAlive() 该方法用于检测当前线程对象是否已经死亡,线程处于就绪、运行、阻塞三种状态时返回true,当线程处于新建、死亡两种状态时返回false。 oin() 当某个程序执行流中调用了其他线程的join()方法时,当前执行流线程被阻塞,直到join()方法加入的join线程执行完成才会继续往后执行。 etDaemon(true) 将调用该方法的Thread对象设置成后台线程,当所有前台线程死亡时,后台线程也随之死亡(无论是否执行完毕)。 sDaemon() 用于判断指定线程是否为后台线程。 tatic void sleep(long millis) 让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。 ield() 该方法可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将该线程转入就绪状态。 etPriority(int newPriority) 该方法用于设置线程的优先级。范围是0~10,也可以使用Thread类的三个静态常量(MAX_PRIORITY:值为10,MIN_PRIORITY:值为1,NORM_PRIORITY:值为5) etPriority() 返回指定线程的优先级。
Runnable接口
- 也可以通过实现Runnable接口来创建并启动多线程。
- 使用Runnable接口创建线程流程:
1.声明实现Runnable接口的类 2.实现run方法,该run()方法的方法体就代表了线程需要完成的任务。 3.创建Runnable实现类的实例,并以此实例作为Thread的参数来创建Thread对象,该Thread对象才是真正的线程对象。 4.调用线程对象的start()方法来启动该线程。
- Runnable常用方法
方法名称 说明 new Thread(st,“新线程1”); 创建Thread对象并指定名字为新线程1
Callable接口和Future接口
- Java5开始,Callable接口提供了一个call()方法可以作为线程执行体。call()方法可以有返回值,call()方法可以声明抛出异常。
- 可以把Callable对象作为Thread的参数,该线程的执行体就是Callable对象的call()方法。但是它并不是Runnable接口的子接口,所以需要把它包装成FutureTask类。
- Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future类,并实现了Runnable类。可以作为Thread类的参数。
- Callable接口和Future接口的使用流程:
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且call()方法有返回值,再创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象。 2.使用FutureTask类来包装Callable对象,该Future对象封装了该Callable对象的call()方法的返回值。 3.调用FutureTask对象作为Thread对象的参数创建并启动新线程。 4.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
- Future类常用方法
方法名称 说明 get() 返回Callable任务里的call()方法的返回值。调动该方法将会导致程序阻塞,必须等到子线程结束后才会得到返回值。 get(long timeout,TimeUnit unit) 返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间还没有返回值,将会抛出TimeoutException异常。 oolean isCancelled() 如果在Callable任务正常返回前被取消,则返回true oolean isDone() 如果Callable任务已完成,则返回true - 例子
import java.util.concurrent.*; class xianchengdemo1{ public static void main(String[] args){ FutureTask<Integer> ft = new FutureTask<Integer>((Callable<Integer>)() ->{ int i = 0; for (;i<8 ;i++ ){ System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i); } return i ; }); Thread a1 = new Thread(ft,"有返回值的线程1"); try{ for (int i = 0;i<20 ;i++ ){ System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i); if (i==10){ a1.start(); //如果没有用FutureTask对象的get()方法,那么有返回值的线程和主线程会同步执行。 //get()方法在获得返回执行结果前,对程序进行了阻塞。 } } } catch (Exception e){ e.printStackTrace(); } } }
ThreadLocal类
- 该类代表一个线程局部变量。这个类的实例相当于是变量(副本变量),它会独立存在于每个线程当中,它线程中相互独立,任意一个线程中改变它不会导致其他线程也跟着改变。
- 语法
ThreadLocal<String> name = new ThreadLocal<>();
Collections类
- 可以使用Collections类的类方法把集合包装成线程安全的集合。
- 例子
HashMap m = Collections.synchronizedMap(new HashMap()); //将普通的HashMap包装成线程安全的类
线程安全的集合类
- Java5开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类。
线程同步
- Java的多线程支持使用同步监视器来解决多线程不同步的问题。
- 有三种方法可以实现线程同步:同步代码块/同步方法/同步锁(Lock)
- 三种实现线程同步方法的语法
//同步代码块:任何线程在进入同步代码块之前,必须获得“被锁定的对象”的锁,如果无法获得则无法修改它。 synchronized(被锁定的对象){ //此处的代码就是同步代码块 } //同步方法:使用synchronized关键字来修饰某个方法。 synchronized关键字 //同步锁: private final ReentrantLock lock = new ReentrantLock();
- 例子
public void m(){ lock.lock();//加锁 try{ //需要保证线程安全的代码 } //使用finally块来保证释放锁 finally{ lock.unlock();//解锁 } }
线程通信
因为线程的调度具有一定的透明性,如果程序中需要根据不同线程的执行情况来确定其他线程的进行状态,Java提供了一些机制来保证线程协调运行。
使用synchronized关键字保证同步的
- 对于使用synchronized关键字的方法,可以使用Object类下的三个方法实现线程通信(这3个方法必须由同步监视器对象来调用):wait()、notify()和notifyALL()。
wait() //该方法导致线程等待,直到其他线程调用该同步监视器的notify()或者notifyALL()方法来唤醒该线程。 notify() //唤醒在此同步监视器上等待的单个线程,如果有多个线程都在此同步监视器等待,则会随意选择其中一个线程唤醒。同时需要当前线程放弃对该同步监视器的锁定后,也就是使用wait()方法后,才会执行被唤醒的线程。 notifyAll //唤醒再次同步监视器上等待的所有线。同时需要当前线程放弃对该同步监视器的锁定后,也就是使用wait()方法后,才会执行被唤醒的线程。
使用Lock保证同步的
- 对于使用Lock对象来保证同步的,因为系统中不存在隐式的同步监视器,所以无法使用wait()、notify()和notifyAll()方法。但可以使用Condition类保持协调通信。
Condition cond = new ReentrantLock().newCondition(); //通过调用Lock对象的newCondition()方法可以获得Condition实例。 await() //类似于隐式同步监视器的wait()方法。 signal() //类似于隐式同步监视器的notify()方法。 signalAll() //类似于隐式同步监视器的notifyAll()方法。
使用阻塞队列控制线程通信
- Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要作用不是作为容器,而是作为线程同步的工具。当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞。如果消费者线程试图从BlockingQueue中取出元素,如果该队列已空,则该线程被阻塞。
- BlockingQueue的5个实现类:
实现类 说明 rrayBlockingQueue 基于数组实现的BlockingQueue队列。 inkedBlockingQueue 基于链表实现的BlockingQueue队列。 riorityBlockingQueue 该实现类中存放的元素会根据元素大小进行排序(实现Comparable接口),也可以自定义排序。默认取出的顺序是队列中最小的。 ynchronousQueue 同步队列。该队列的存、取操作必须交替进行。 elayQueue 底层基于PriorityBlockingQueue实现。不过DelayQueue中的元素会被要求实现Delay接口,然后DelayQueue会根据集合元素的getDelay()方法返回值进行排序。 - BlockingQueue常用方法:
put(E e) //尝试把E元素放入BlockingQueue中,如果该队列元素已满,则阻塞该线程。 take() //尝试从BlockingQueue的头部取出元素,如果该队列元素已空,则阻塞该线程。
线程组
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程进行控制。
线程池
系统启动一个新线程的成本比较高,因为涉及与操作系统交互。这种情况下,可以使用线程池提高性能。尤其是程序中有大量生存期很短暂的线程时。
Executors类
- Java5新增了一个Executors工厂类来产生线程池。
- 使用线程池执行线程任务的步骤:
1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。 2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。 3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例。 4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
- 例子
import java.util.concurrent.*; class ThreadPoolTest{ public static void main(String[] args){ //创建具有固定线程数(6)的线程池 ExecutorService pool = Executors.newFixedThreadPool(6); //使用Lambda表达式创建Runnable对象 Runnable target = () -> { for (int i =0; i<20;i++ ){ System.out.println(Thread.currentThread().getName()+"的i值为:"+i); } }; //向线程池中提交两个线程 pool.submit(target); pool.submit(target); //关闭线程池 pool.shutdown(); } }
ForkJoinPool类
- 为了充分利用多CPU、多核CPU的性能优势。Java7提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算(放到多个处理器核心上并行执行),再把“小任务”的结果合并成总的计算结果。Java8新增了通用池功能。ForkJoinPool是ExecutorService的实现类。
- ForkJoinPool实例执行任务的方法为submit(ForkJoinTask task)。其中的ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask。其中RecursiveTask代表有返回值的任务,而RecursiveAction代表没有返回值的任务。
- 使用ForkJoinPool的步骤通常为:
1.定义一个类,继承RecursiveAction类或者RecursiveTask类。 2.实现compute()方法(内容是怎么拆分任务)。 3.创建一个ForkJoinPool实例对象或者通用池对象。 4.使用submit()方法执行任务。(如果有返回值,需要用Future类接受返回值) 5.当不想提交任何任务时,调用ForkJoinPool对象的shutdown()方法关闭线程池。
- 例子
import java.util.*; import java.util.concurrent.*; import java.util.concurrent.*; class CalTask extends RecursiveTask<Integer>{ //指定每个“小任务”的大小,意思是最多为20个 private static final int THRESHOLD = 20; private int arr[]; private int start; private int end; public CalTask(int[] arr,int start,int end){ this.arr = arr; this.start = start; this.end = end; } @Override protected Integer compute(){ int sum = 0; //如果end-start的结果小于设定的20,则表示是“小任务”,开始进行累加 if ((end -start)<THRESHOLD){ for (int i = start;i<end ;i++ ){ sum +=arr[i]; } return sum; } else{ //将“大任务”拆分为两个小任务 int middle = (start+end)/2; CalTask left = new CalTask(arr,start,middle); CalTask right = new CalTask(arr,middle,end); //执行两个“小任务” left.fork(); right.fork(); //把两个“小任务”的结果合并 return left.join()+right.join(); } } } class Sum{ public static void main(String[] args) throws Exception{ int[] arr = new int[100]; Random rand = new Random(); int total=0; for (int i=0,len=arr.length;i<len ;i++ ){ int tmp = rand.nextInt(20); total +=(arr[i]=tmp); } System.out.println(total); //创建通用池 ForkJoinPool pool = new ForkJoinPool(); Future<Integer> future = pool.submit(new CalTask(arr,0,arr.length)); System.out.println(future.get()); pool.shutdown(); } }
其他
参考资料
- 《疯狂Java讲义(第4版)》 李刚