Java学习_多线程
-
多线程基础
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
- 和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
- 创建新线程
- 要创建一个新线程非常容易,我们需要实例化一个
Thread实例,然后调用它的start()方法 - 希望新线程能执行指定的代码,有以下几种方法
- 方法一:从
Thread派生一个自定义类,然后覆写run()方法,start()方法会在内部自动调用实例的run()方法。 - 方法二:创建
Thread实例时,传入一个Runnable实例(即匿名内部类) - 在线程中调用
Thread.sleep(),强迫当前线程暂停一段时间 - 调用
Thread实例的start()方法才能启动新线程
- 方法一:从
-
线程的优先级
- 要创建一个新线程非常容易,我们需要实例化一个
-
线程的状态
- Java线程对象
Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated - 当线程启动后,它可以在
Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。 - 一个线程还可以等待另一个线程直到其运行结束。例如,
main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行
- Java线程对象
- 中断线程
- 如果线程处于等待状态,例如,
t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt(),join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。 - 另一个常用的中断线程的方法是设置标志位。我们通常会用一个
running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束 - 对线程间共享的变量用关键字
volatile声明- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
- 如果线程处于等待状态,例如,
- 守护线程(Daemon Thread)
- 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
-
如何创建守护线程呢?方法和普通线程一样,只是在调用
start()方法前,调用setDaemon(true)把该线程标记为守护线程:Thread t = new MyThread(); t.setDaemon(true); t.start();
-
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
-
线程同步
- 多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待
- 通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
-
synchronized(Counter.lock) { // 获取锁 ... } // 释放锁
- 在使用
synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁 - JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。
-
不需要synchronized的操作
- 单条原子操作的语句不需要同步。即基本类型的变量(不包括float和double)和引用类型的变量的赋值
- 同步方法
- 让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把
synchronized逻辑封装起来。 - 用
synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。public void add(int n) { synchronized(this) { // 锁住this count += n; } // 解锁 } public synchronized void add(int n) { // 锁住this count += n; } // 解锁
-
对于
static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。上述synchronized static方法实际上相当于:public class Counter { public static void test(int n) { synchronized(Counter.class) { ... } } }
- 一个类没有特殊说明,默认不是thread-safe
- 让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把
-
死锁
- JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
- 避免死锁的方法:线程获取锁的顺序要一致。假设有两个锁,则不同的方法严格按照相同的获取锁的顺序
- 使用wait和notigy
wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()。wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。- 如何让等待的线程被重新唤醒,然后从
wait()方法返回?答案是在相同的锁对象上调用notify()方法。
-
使用ReentrantLock
java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
-
使用Condition
- 使用
Condition对象来实现wait和notify的功能。 - 使用
Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。
- 使用
- ReadWriteLock
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。
-
使用StampedLock
StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。- 和
ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
-
使用Concurrent集合
-
使用Atomic
-
使用线程池
- 把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
ExecutorService只是接口,Java标准库提供的几个常用实现类有:- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
-
使用Future
-
使用CompletableFuture
- 使用fForkJoin
-
使用ThreadLocal

浙公网安备 33010602011771号