多线程
- 进程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。某些进程内部还需要同时执行多个子任务。
- 线程
我们把子任务称为线程
- 进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
- 操作系统调度的最小任务单位其实不是进程,而是线程。
- 创建线程的方法:
方法一:通过继承Thread来创建线程,并且重写run方法。
|
方法二:实现 Runnable 接口创建线程,并且重写run方法
|
方法三:创建线程的方法实现Callable接口
Callable接口使用方法和Runnable接口的方法类似不同的一点是Callable接口具有返回值,
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。
但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。 Executor线程池的超类:
执行已提交的 Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(
包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。例如,
可能会使用以下方法,而不是为一组任务中的每个任务调用 new Thread(new(RunnableTask())).start()
|
- 线程优先级
JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
Thread.setPriority(int n) // 1~10, 默认值5
7、JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
8、中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
9、守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?答案是使用守护线程(Daemon Thread)。守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束。
创建守护线程的方法:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
10、线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
- 死锁
Java的线程锁是可重入的锁。
一个线程可以获取一个锁后,再继续获取另一个锁。在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。
- Lock锁
- ReadWriteLock
使用ReadWriteLock可以提高读取效率:
ReadWriteLock只允许一个线程写入;
ReadWriteLock允许多个线程在没有写入时同时读取;
ReadWriteLock适合读多写少的场景。
- StampedLock
StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
StampedLock是不可重入锁。
13、Sync锁
java.util.Collections工具类还提供了一个旧的线程安全集合转换器,可以这么用:
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
但是它实际上是用一个包装类包装了非线程安全的Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用。
14、线程并发问题
多线程并发的安全问题:
产生:当多个线程并发操作时,由于线程切换实际的不确定性,会导致操作资源的代码
顺序为按照设计顺序执行,出现操作混乱的情况,严重时可能导致系统瘫痪。
解决:将并发操作同一资源改为同步运行,即:有先后顺序的操作
同步与异步:
同步:程序运行有先后顺序
异步:程序运行没有先后顺序
- 线程池
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
作用:
重复利用资源
线程池的创建:
Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:
FixedThreadPool:线程数固定的线程池;
CachedThreadPool:线程数根据任务动态调整的线程池;
SingleThreadExecutor:仅单线程执行的线程池。
newCachedThreadPool:
创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。
当线程池中的线程空闲时间超过60s,则会自动回收该线程,当任务超过线程池的线程数则创建新的线程,
线程池的大小上限为Integer.MAX_VALUE,可看作无限大。
newScheduledThreadPool:
创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行
newSingleThreadExecutor:
创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务。