多线程
一、多线程基础
现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务
CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。
操作系统轮流让多个任务交替执行
1、进程
在计算机中,我们把一个任务称为一个进程
某些进程内部还需要同时执行多个子任务。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
操作系统调度的最小任务单位其实不是进程,而是线程。
常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:
多进程模式(每个进程只有一个线程):
多线程模式(一个进程有多个线程):
多进程+多线程模式(复杂度最高):
2、进程 vs 线程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
1、创建进程比创建线程开销大,尤其是在Windows系统上;
2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃 会直接导致整个进程崩溃。
3、多线程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
多线程模型是Java程序最基本的并发模型;
网络、数据库、Web开发等都依赖Java多线程模型。
二、创建新线程
1、通过继承Thread来创建线程
从Thread
派生一个自定义类,然后覆写run()
方法:
注意:start()方法会在内部自动调用实例的
run()方法 2、实现 Runnable 接口创建线程 创建
Thread实例时,传入一个Runnable 3、实现callable接口创建线程 这种实现线程的方式是有返回值的 线程的执行顺序:当run()方法结束时,新线程就结束了。 当main()方法结束时,主线程就结束了。 1、main线程肯定是先打印main start,再打印main end; 2、t线程肯定是先打印
thread run,再打印thread end。
线程的常用方法:
start() 作用:启动新线程
sleep()传入一个参数为毫秒。作用:调整暂停时间的大小
join() 作用:可以等待,指定线程结束后再执行其他线程
Thread.setPriority()作用:设定线程的优先级(1(高)~10(低))
当要模拟并发执行的效果,我们可以在线程中调用Thread.sleep()
,强迫当前线程暂停一段时间
三、线程的状态
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行
run()
方法的Java代码; - Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待; - Terminated:线程已终止,因为
run()
方法执行完毕。
注意:线程在Runnable、Blocked、Waiting、Timed Waiting这几个状态时可以任意转换
直到到达Terminated时,线程终止
总结:
1、Java线程对象Thread
的状态包括:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
和Terminated
;
2、通过对另一个线程对象调用join()
方法可以等待其执行结束;
3、可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
4、对已经运行结束的线程调用join()
方法会立刻返回。
四、中断线程
方法:interrupt()作用:向指定线程发出“中断的请求”,目标线程只要捕获到join()
方法抛出的InterruptedException
,就证明了有其他线程调用了interrupt()方法
线程间共享变量需要使用volatile
关键字标记,确保每个线程都能读取到更新后的变量值
volatile
关键字的目的是告诉虚拟机:
1、每次访问变量时,总是获取主内存的最新值;
2、每次修改变量后,立刻回写到主内存。
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
总结: 1、对目标线程调用interrupt()
方法可以请求中断一个线程,目标线程通过检测isInterrupted()
标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
;
2、目标线程检测到isInterrupted()
为true
或者捕获了InterruptedException
都应该立刻结束自身线程;
3、通过标志位判断需要正确使用volatile
关键字;
4、volatile
关键字解决了共享变量在线程间的可见性问题。
五、守护线程
1、守护线程必须再启动线程之前设置好,需要调用setDaemon(true)将普通线程标记为守护线程
2、守护线程不能持有任何需要关闭的资源
3、守护线程是与普通线程一起执行当普通线程执行完后,守护线程依旧执行到确定普通线程已经执行结束,才会结束
六、线程的同步
同步与异步的区别:
同步:是多个线程执行有一定的先后顺序;异步:是多个线程执行时只要CPU分配时间片就会执行
多线程同步访问共享变量的准确性问题
缺点:性能下降;synchronized内的代码块无法并发执行;加锁和解锁需要消耗一定的时间,执行效率低。
synchronized(Counter.lock) { // 获取锁
…
} // 释放锁
使用在修改共享变量的线程代码块;选择一个共享实例作为锁
注意:锁名可以直接命名,如:synchronized(“买票”) ;在使用此方法是不必担心是否会抛出异常
七、同步方法
Java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized
逻辑封装起来。
可以用一个类来封装容易出错的代码,加锁后,再在其他类上创建此类和线程。
小结:
1、用synchronized
修饰方法可以把整个方法变为同步代码块,synchronized
方法加锁对象是this
;
2、通过合理的设计和数据封装可以让一个类变为“线程安全”;
3、一个类没有特殊说明,默认不是thread-safe;
4、多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。
八、死锁
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
九、wait和notify
wait()
方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object
类的一个native
方法,也就是由JVM的C代码实现的。其次,必须在synchronized
块中才能调用wait()
方法,因为wait()
方法调用时,会释放线程获得的锁,wait()
方法返回后,线程又会重新试图获得锁。
小结
wait
和notify
用于多线程协调运行:- 在
synchronized
内部可以调用wait()
使线程进入等待状态; - 必须在已获得的锁对象上调用
wait()
方法; - 在
synchronized
内部可以调用notify()
或notifyAll()
唤醒其他等待线程; - 必须在已获得的锁对象上调用
notify()
或notifyAll()
方法; - 已唤醒的线程还需要重新获得锁后才能继续执行。
十、ReentrantLock
synchronized 这种锁缺点:1、很重;2、在获取时必须一直等待,没有额外的尝试机制。不需要考虑异常
ReentrantLock 是一种可重入锁,可以尝试获取锁,但必须先获取锁,然后在finally
中正确释放锁。
使用ReentrantLock更安全,线程在tryLock()失败后不会导致死锁。
总结:
1、ReentrantLock可以替代synchronized进行同步;
2、ReentrantLock获取锁更安全;
3、必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
4、可以使用tryLock()尝试获取锁。
十一、Condition
使用Condition
对象来实现wait
和notify
的功能
Condition
提供的await()
、signal()
、signalAll()
原理和
synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
1、await()
会释放当前锁,进入等待状态;
2、signal()
会唤醒某个等待线程;
3、signalAll()
会唤醒所有等待线程;
4、唤醒线程从await()
返回后需要重新获得锁。
此外,和tryLock()
类似,await()
可以在等待指定时间后,如果还没有被其他线程通过signal()
或signalAll()
唤醒,可以自己醒来
小结
1、Condition
可以替代wait
和notify
;
2、Condition
对象必须从Lock
对象获取。
十二、ReadWriteLock
可以解决以下两个问题:- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。
小结:
使用ReadWriteLock
可以提高读取效率:ReadWriteLock
只允许一个线程写入;ReadWriteLock
允许多个线程在没有写入时同时读取;ReadWriteLock
适合读多写少的场景。
是一种悲观锁。
十三、StampedLock
是一种乐观锁。
缺点:1、代码更加复杂;
2、是不可重入锁,不能在一个线程中反复获取同一个锁
小结:
1、StampedLock
提供了乐观读锁,可取代ReadWriteLock
以进一步提升并发性能;
2、StampedLock
是不可重入锁。
锁的本质就是保护一种受限制的资源,保证同一时刻只能有一个线程能访问(ReentrantLock),或者只能有一个线程写入(ReadWriteLock)。
十四、Semaphore
是一种受限资源,它保证同一时刻最多有N个线程能访问。
Semaphore
本质上就是一个信号计数器,用于限制同一时间的最大访问数量。
十五、Concurrent集合
使用java.util.concurrent
包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。
十六、Atomic
使用java.util.concurrent.atomic
提供的原子操作可以简化多线程编程:
原子操作实现了无锁的线程安全;
适用于计数器,累加器等。
- 在
十七、线程池
1、因为ExecutorService
只是接口,Java标准库提供的几个常用实现类有:
FixedThreadPool:线程数固定的线程池;
CachedThreadPool:线程数根据任务动态调整的线程池;
SingleThreadExecutor:仅单线程执行的线程池。
2、创建这些线程池的方法都被封装到Executors此类中。
3、ScheduledThreadPool:这是一种可以定期的反复执行一种任务。如:每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool
。放入ScheduledThreadPool
的任务可以定期反复执行。
4、Future:在执行多个任务的时候,使用Java标准库提供的线程池是非常方便的。我们提交的任务只需要实现Runnable
接口,就可以让线程池去执行。
get()
:获取结果(可能会等待)
get(long timeout, TimeUnit unit)
:获取结果,但只等待指定的时间;
cancel(boolean mayInterruptIfRunning)
:取消当前任务;
isDone()
:判断任务是否已完成。
十八、CompletableFuture
CompletableFuture:是当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
优点:
1、异步任务结束时,会自动回调某个对象的方法;
2、异步任务出错时,会自动回调某个对象的方法;
3、主线程设置好回调后,不再关心异步任务的执行。
十九、ForkJoin
ForkJoin:是把一个大任务拆成多个小任务并行执行。
Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
二十、ThreadLocal
1、多线程是Java实现多任务的基础
2、可以把ThreadLocal
看成一个全局Map<Thread, Object>
:每个线程获取ThreadLocal
变量时,总是使用Thread
自身作为key
3、ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。
特别注意ThreadLocal
一定要在finally
中清除