Java多线程是指在Java编程语言中实现并发的一种方式。它允许程序同时执行多个线程,每个线程都可以独立运行,具有自己的堆栈和程序计数器。
多线程可以提高程序的性能和响应速度。当一个任务需要等待某个操作完成时,单线程程序会一直等待,而多线程程序则可以通过创建新的线程来执行其他任务,从而提高程序的效率。
多线程
多核CPU能同时运行多个任务,使用多线程可以提高程序运行以及资源利用效率,提高程序并发能力和响应速度。
进程
进程是运行中的程序(驻留在内存中)
是系统执行资源分配和调度的(最小)独立单位
每一个进程之间都有独立的存储空间和系统资源,互不共享
线程
线程是运算调度的最小单位(CPU分配的最小单位)
一个进程中可以有多个线程,可以看成进程中顺序控制流(或者执行路径)
死锁
各个进程互相占据对方需要的资源,使得所有进程都处于阻塞状态。
避免死锁
n个任务共享R个资源,每个任务需要k个资源,至少需要多少个资源可以避免死锁
n*(k-1)+1
解释:每个进程分配k-1个资源,再多余一个资源即可让其中一个任务先完成然后释放资源。
生命周期
Java线程的生命周期可以分为以下6个状态:
- 新建状态(New):当我们使用new操作符创建一个Thread对象时,该线程处于新建状态。此时线程尚未启动,不具备运行能力。
- 就绪状态(Runnable):当调用start()方法启动线程之后,线程进入就绪状态。此时线程已经具备了运行能力,并等待CPU的调度执行。
- 运行状态(Running):当线程被选中并执行时,线程进入运行状态。此时线程正在执行其任务代码。
- 阻塞状态(Blocked):当线程在运行过程中因为某些原因无法继续执行时,它将进入阻塞状态。常见的阻塞情况包括等待I/O操作、获取锁失败、调用sleep()方法等。
- 等待状态(Waiting):当线程执行Object.wait()、Thread.join()、LockSupport.park()等方法时,它将进入等待状态。等待状态是一种特殊的阻塞状态,区别在于等待状态需要其他线程唤醒才能重新进入就绪状态。
- 终止状态(Terminated):当线程完成其任务或者出现异常时,它将进入终止状态。此时线程已经结束,不再具有运行能力。
总之,Java线程的生命周期可以分为新建、就绪、运行、阻塞、等待和终止六个状态,这些状态会根据线程的运行情况不断转换。理解Java线程生命周期对于开发高效的多线程应用程序非常重要。
实现方法
1.继承Thread类:创建一个继承自Thread类的子类,在子类中重写run()方法来定义线程要执行的代码。然后创建该子类的实例对象并调用start()方法来启动线程。
1 | class MyThread extends Thread { |
2.实现Runnable接口:创建一个实现了Runnable接口的类,在该类中实现run()方法来定义线程要执行的代码。然后创建该类的实例对象并将其作为参数传递给Thread类的构造函数中,并调用start()方法来启动线程。
1 | class MyRunnable implements Runnable { |
3.使用匿名内部类:可以在创建Thread或Runnable对象时使用匿名内部类来直接定义线程要执行的代码。这种方式通常用于只需要定义较短的线程代码的情况。匿名内部类的对象类型相当于new的子类型或者具体实现【所以可以new 类、抽象类名、接口名】
1 | Thread thread = new Thread(new Runnable(){ |
4.使用线程池:创建一个线程池来管理多个线程,可以重复利用线程对象,提高程序效率。可以使用Executors类中的工厂方法来创建线程池。
1 | ExecutorService executor = Executors.newFixedThreadPool(10); |
5.利用FutureTask类使用回调的方法
1 | FutureTask task=new FutureTask(new Callable(){ |
以上是几种常见的Java多线程实现方式。
Runnable、Callable、Future、FutureTask
使用继承Thread或实现Runnable接口来实现多线程的方法都没有返回值【run方法返回类型为void,只能通过共享变量等方式获取线程处理后的结果】
Runnable、Callable、Future都是接口,而FutureTask是Future和Runnable接口的具体实现。
Future可以有一系列方法比如get等获取线程运行的结果以及判断、改变线程状态等,Runnable以及Callable都可以传递给Future。
Runnable无法返回值以及抛出异常,Callable则有返回值并且可以抛出异常,与FutureTask一般配合线程池使用(传递给FutureTask)。
1 | ExecutorService executor=Executor.neFixedThreadPool(num); |
线程池
Java线程池是一种可以重复使用多个线程的机制。使用线程池可以避免频繁创建和销毁线程,提高系统的性能和稳定性。其核心原理是线程复用,在系统启动时创建一定数量的线程,处于等待状态,当任务队列中有任务需要执行时就从线程池中取出一个线程来执行任务,任务执行完毕后该线程并不会被销毁,而是返回到线程池中。
线程池关键组件:
1.线程池管理器(ThreadPoolExecutor):负责管理线程池中所有线程,通过核心线程数、最大线程数、等待队列等参数来控制线程池的大小和行为。
2.工作线程(WorkerThread):负责执行任务并返回结果,线程执行完任务后会继续保持活动状态,直到被线程池回收。
3.任务队列(BlockingQueue):用于存储等待执行的任务,通常采用阻塞队列来实现,避免因任务过多导致内存溢出或系统崩溃。
4.任务接口(Runnable/Callable):定义任务的执行逻辑和返回结果,可以通过实现Runnable或Callable接口来创建自定义任务。
execute和submit
线程池通过execute和submit执行线程,execute只能提交Runnable类型的任务,submit既可以提交Runnable类型也能提交Callable类型任务。
execute会直接抛出任务执行的异常,submit则不会,需要通过Future的get方法将任务执行时的异常重新抛出。
execute用于执行不需要返回值的任务。
submit则可以用于提交需要返回值的任务,并返回一个Future类型对象。
线程通信
很多时候各个线程所完成的任务是相关联的,如何整合多个线程的执行结果以及同步线程则需要使用到线程通信。
线程之间的通信方式本质上可以分为两种,内存共享、消息传递和管道流
内存共享
线程之间共享程序的公共状态,通过读写内存中的值来隐式通信。
比如使用volatile、原子变量保证数据的可见性。
消息传递
线程之间没有明确的公共状态时,必须通过明确的发送消息来显式通信。
比如使用wait/notify await/signal acquire/release 这些等待通知的方式
以及join等阻塞方式
管道流
可以使用管道流来进行线程之间的通信,一个线程将数据发送到输出管道中,另一个线程从输入管道中读取数据。
Java提供了四个类使得线程间可以通信
字节流:PipeInputStream,PipeOutputStream
字符流:PipeReader,PipeWriter
1 | package pipeInputOutput; |
sleep()和wait()的区别
sleep和wait方法都是Java中用于线程等待的方法。但是有以下区别:
1.wait()是Object类中的方法,而sleep()是Thread类中方法;
2.wait()会释放锁,使其他线程可以获取该锁并执行相应的代码【并不是一起竞争,当前线程需要被唤醒】,sleep不会释放锁;
3.wait()方法必须在synchronized块中调用,否则会抛出异常,sleep()没有限制;
4.wait()可以被notify()或者notifyAll()方法唤醒,也可以使用wait(long timeout),sleep()只能等待一定时间后自动唤醒。
Java中每一个对象都有一个关联的锁,通过synchronized块可以获取该锁保证线程安全。wait()的作用是释放当前线程获取的对象锁,并让线程进入等待状态【由其他线程唤醒】。这就需要在Object类中实现相应方法;
sleep()是线程级别的操作,不涉及任何对象的锁机制【不需要释放锁】,只是让当前线程进入休眠状态。因此作为Thread的静态方法更合适。
TimeUnit
TimeUnit是concurrent下的一个类,提供更具可读性的线程休眠(作用与sleep一致)
TimeUnit.DAYS.sleep(1);
TimeUnit.DAYS.toSeconds(1);//将天数转换为分钟数
TimeUnit.SECONDS.sleep(60);
notify和notifyAll的区别
notify会由调度器随机唤醒一个线程,notifyAll会唤醒所有正在等待此对象监视器的线程公平竞争。
锁类型
公平锁和非公平锁
公平锁:多个线程按申请顺序来获取锁,先来后到
非公平锁:多个线程获取锁的顺序并不是按申请顺序,有可能后申请的线程先获得锁。高并发场景下可能造成优先级翻转或者饥饿的线程。
ReentrantLock通过构造函数指定锁是否公平,默认非公平锁【构造函数传参为true时为公平锁】,因为非公平锁可以有更大的吞吐量【因为可以节省一些线程等待与唤醒时间】。synchronized也是非公平锁。
可重入锁
也叫递归锁,即一个线程占用锁后又调用了其他占用该锁的其他方法,递归锁在进入内层方法时会直接自动获取锁。
1 | //A和B方法都需要资源res,A中调用了B方法,因为是可重入的所以不会造成死锁 |
自旋锁
自旋指一直循环请求资源。好处是减少线程上下文切换的消耗,缺点是会消耗CPU。
可以通过原子变量、Semaphore等实现。
独占锁(写锁)、共享锁(读锁)、互斥锁
独占锁:该锁一次只能被一个线程持有,ReentrantLock和synchronized都是独占锁
共享锁:该锁可以被多个线程持有
互斥:不指定顺序(同步:线程的某种执行顺序),但是同一时刻只能执行一种操作,读写,写读,写写都是互斥的。
1 | import java.util.HashMap; |
volatile
可以用来修饰变量,告诉虚拟机该域可能会被其他线程更新,线程读取变量时从内存而非线程缓存中读取,可以保证可见性。但并不能保证原子性,对于自增等非原子操作仍然是线程不安全的。其只能保证并发线程同时读取到相同的数据。
符合happen before原则,写在读之前且不能指令重排
join
等待调用此方法的线程执行结束再继续当前线程。
比如下面语句会等到t1和t2都执行完后再执行xxx语句,使用countDownLatch也可以更灵活地实现。
t1.join();
t2.join();
xxxx
countDownLatch
适用于:
1.某个线程需要在其他n个线程执行完毕后再向下执行
【每个子线程中都会使用countDown,最后使用await,则会在所有子线程执行完再执行主线程】
2.多个线程并发执行一个任务,完成后进行汇总,提高响应速度
【count值设为1,先启动多个线程,每个线程都使用await方法阻塞,最后使用countDown,让线程同时启动】
使用一个count计数,一个线程完成便会调用countDown方法使的count值减一,只有count减为0(线程都完成),才会进行主线程(main)。
调用await()方法的线程会被阻塞等待,也可以设置最长等待时间,超时会自动唤醒。
countDownLatch比join更加灵活,join的原理是不停检查join线程是否存活thread.isAlive()),存活则让当前线程永远等待。直到所有线程执行结束才向下执行主线程。
Semaphore
控制信号,通过acquire()与release()函数控制线程执行
acquire()当permits大于0时,会继续执行线程,并且把permits减一;等于0时则会阻塞
release()释放一个permits,permits加一
permits的初始数量以及增减数量都是可以通过参数控制的。
Synchronized
同步锁,可以对普通方法、静态方法、对象、代码块使用。
表示锁定某一资源,只有获取相应锁才能执行方法
八锁问题
对于锁执行顺序的相关问题
静态同步方法锁是此类【类对应的字节码对象】
普通同步方法锁是对象实例
1 | synchronized实现同步的基础:java中的每一个对象都可以作为锁。 |
ReentrantLock
ReentrantLock 重入锁 一个持有锁的线程,在释放锁之前。此线程如果再次访问了该同步锁的其他的方法,这个线程不需要再次竞争锁,只需要记录重入次数。重入锁的设计目的是为了解决死锁的问题
synchronized也是可重入锁。
Synchronized和ReentrantLock的区别
1.ReentrantLock显示地获得,释放锁,synchronized隐式获得释放锁
2.ReentrantLock可响应中断,可轮回,synchronized是不可以响应中断的
3.ReentrantLock是API级别的,synchronized是JVM级别的
4.ReentrantLock可以实现公平锁
5.ReentrantLock通过Condition可以绑定多个条件
6.底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略。
7.Lock是一个接口,而synchronized是java中的关键字,synchronized是内置的语言实现
8.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。
- 在资源竞争不激烈的情况下,Synchronized使用方便,自动释放锁,可以选择使用。
- 但是在资源竞争很激烈的情况下,Synchronized的性能会下降很严重,推荐选择ReetrantLock
Condition
Condition可以用于线程之间的通信,它用来替代传统的Object的wait()、notify()实现线程间的协作【都属于对象监视器方法】,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
Condition能够更加精细地根据条件控制线程执行与休眠,wait和notify等只能释放、唤醒当前锁,不能指定线程
对于一个锁可以建立不同的Condition
ReentrantLock lock = new ReentrantLock();
Condition condition1 =lock.newCondition();
Condition condition2 =lock.newCondition();
原子变量
Atomicinteger/AtomicLong/AtomicBoolean
AtomicReferrence…
Java只有对基本类型变量的赋值和读取是原子操作的【i=1是原子操作,i=j以及i++等都不是】,原子变量提供了自增自减等原子操作(由Unsafe类CAS实现)
线程让步
可以使用Thread.yield()、Thread.sleep(0)来实现,都是让出CPU(不释放锁,wait会释放对象锁),使得同优先级的线程获取执行权(并不能保证,而是重新竞争)。
CAS
compare and swap ,比较并交换
CAS操作包含三个操作数,内存位置、预期原值、新值
如果内存位置的值与预期原值一致则更新为新值,否则不做处理。
AQS
Abstract Queued Synchronizer,抽象队列同步器,通过维护一个共享资源状态(volatile int state)和一个先进先出(FIFO)的线程等待队列来实现多线程之间的同步。AQS可以用来构建锁和同步器。比如ReentrantLock、Semaphore、ReentrantReadWriteLock,FutureTask、CountDownLatch都是基于AQS的。
原理
AQS思想的核心是通过一个资源状态和先进先出队列来实现的。
如果有一个线程请求共享资源,且当前资源状态为空闲,则将此线程设置为有效的工作线程。并将资源设置为锁定状态。如果请求的资源被占用,那么就需要一套线程阻塞、等待以及被唤醒时锁的分配机制。这个机制AQS使用CLH队列锁实现(是一个虚拟双向队列自旋锁,虚拟指的是没有队列实例,仅仅是结点之间的关联关系),将暂时获取不到锁的线程加入到队列中。
AQS中共享资源状态使用volatile保证各线程对状态的可见性,有get、set方法以及CAS原子地改变。
AQS定义了两种资源共享的方式Exclusive(独占)以及Share(共享),
Exclusive:同时只有一个线程能占有锁,如ReentrantLock等。
Share:多个线程可以同时占有锁,比如Semaphore、CountDownLatch、ReadWriteLock等。
Unsafe类
一个Java底层类,包括数组操作、内存操作、对象操作、CAS操作、线程操作等,JUC包和一些三方框架都有使用Unsafe类来保证并发安全。比如AtomicInteger等类源码中就使用了Unsafe类.
Unsafe类可以提供一些绕过JVM更底层的功能,以提高效率。但是Unsafe是不安全的,其分配的内存需要手动Free而不是被GC回收。
ThreadLocal
线程变量,只属于当前线程,对其他线程而言是隔离的。
同一个ThreadLocal所包含的对象在不同线程中有不同的副本,每个使用该变量的线程都会初始化一个完全独立的副本。【可以应用于存储用户session等】
1 | public class ThreadLocaDemo { |