本文介绍Java里并发相关的内容,包括多线程、线程池、锁等。
当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,需要采取同步机制,以确保在某一时刻,方法内只允许有一个线程。
基本概念
并发编程涉及的三个特性:原子性、可见性、有序性
- 原子性:要么全都做,要么全都不做。
- 可见性:多个线程访问同一个变量时,这个变量被修改后,能被其他的线程看到。
- 有序性。
并发与并行:
- 并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- 并行: 单位时间内,多个任务同时执行。
线程的生命周期:初始 、运行(调用start()
方法后)、阻塞(被锁阻塞)、等待(调用wait()
方法)、超时等待(可在指定时间自己返回)、终止。
线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
synchronized
关键字
synchronized
是 Java 中的关键字,是利用锁的机制来实现同步的。如果多个线程访问的是同一个对象,哪个线程先执行带synchronized
关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。
内部机制:synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。 当执行 monitorenter
指令时,线程试图获取锁也就是获取 monitor
(monitor对象存在于每个Java对象的对象头中,synchronized
锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit
指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized
对应两种锁:对象锁和类锁
- 对象锁。每个Java对象都有一个 monitor 对象,即 Java 对象的锁,通常被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
- 类锁。在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
内置锁的特性:
- 可重入性。如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。即同一个线程可以再次获取自己的锁而不会阻塞。比如可以在同步方法里再次调用相同锁的同步方法。
使用说明:
- 同步普通方法,锁的是当前对象。如:
public synchronized void method(){...}
- 同步静态方法,锁的是当前的 Class对象。如:
public static synchronized void method(){...}
- 同步块,锁的是
()
中的对象或Class对象。比如synchronized (this){...}
或synchronized (A.class){...}
。
其他锁对象:Lock、ReenTrantLock等
TODO
volatile
关键字
volatile
,中文含义为“易改变”。用于修饰变量,告知系统该变量易改变,并发处理需直接读写主存,而非各自CPU内的缓存。
volatile
只能保证变量的可见性、有序性,但是不能保证原子性。比如变量的自增操作,包括三个操作,读、加法、写,就是不是原子操作。在执行加法之前,其他线程可能更改了值,这样最终的结果可能不是预期的结果。
局限:
- 不能替代synchronized。无法提供完全同步的能力,它只能提供改变可见性的能力,可称为一种轻量级的同步,多线程写的时候会出现问题。
- 性能低。由于总是读写主存,它的读写性能要低于普通的变量。
一般使用的情景为:一个线程写,多个线程读。 大部分情况下,完全不需要使用这个关键字。
线程相关方法:wait、notify、sleep、interrupt等
Object.wait()
:在同步方法或同步块中调用;将当前线程置入休眠状态,直到接到通知或被中断为止;调用后会并释放锁。Object.notify()
:在同步方法或同步块中调用;通知等待该对象的其他线程,多个线程时随机通知一个;当前程序退出synchronized
代码块后,等待线程程序才能获取锁,即被唤醒。Object.notifyAll()
:与Object.notify()
一样,通知待该对象的所有线程。Thread.sleep()
:暂停执行,超时后继续执行,不释放锁。Thread.interrupt()
:中断睡眠中的线程,并抛出中断异常信息。
wait()
和sleep()
比较:
- 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
- 两者都可以暂停线程的执行。
- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
线程池与Executor 框架
线程池的作用:减少资源消耗,提高资源利用率,提高响应速度。
Executor 框架包括线程池管理、线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。
Executor 框架结构,主要由三大部分组成:
- 任务(
Runnable
/Callable
):执行任务需要实现的Runnable
接口 或Callable
接口。 - 任务的执行(
Executor
):包括任务执行机制的核心接口Executor
,以及继承自Executor
接口的ExecutorService
接口,以及继承自ExecutorService
的ThreadPoolExecutor
类。 - 异步计算的结果(
Future
):**Future
** 接口以及Future
接口的实现类FutureTask
类都可以代表异步计算的结果。当我们把Runnable
接口 或Callable
接口 的实现类提交给ThreadPoolExecutor
或ScheduledThreadPoolExecutor
执行即调用submit()
方法时,会返回一个FutureTask
对象
ThreadPoolExecutor
的构造
1 | /** |
两种方法构造线程池:
- 通过
ThreadPoolExecutor
构造函数实现(推荐) - 通过 Executor 框架的工具类
Executors
来实现。三种特定的线程池FixedThreadPool
SingleThreadExecutor
CachedThreadPool
四种拒绝策略:
- AbortPolicy:任务队列装不下,直接拒绝,抛异常。默认策略
- CallerRunsPolicy:当前线程直接运行任务
- DiscardPolicy:丢弃
- DiscardOldestPolicy:丢弃最旧的任务
ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal
类正是为了解决这样的问题。
Thread
有成员ThreadLocalMap
,而ThreadLocalMap
存储以ThreadLocal
为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal
对象的话,会使用 Thread
内部ThreadLocalMap
存放数据的,ThreadLocalMap
的 key 就是 ThreadLocal
对象,value 就是 ThreadLocal
对象调用set
方法设置的值。
在Android中利用ThreadLocal来保证Looper对象的唯一性:每个线程对象持有一个
ThreadLocalMap
成员,ThreadLocalMap
以ThreadLocal
为key,以实际目标值为value。如果以一个静态ThreadLocal
对象为key,则每个线程里的ThreadLocalMap
都会持有相同的key却互不并不影响,但在线程内部,由于ThreadLocalMap
里的key唯一,则可以确保只有一个值。在Looper创建时则是判断当前线程是否有Looper,有则抛出异常,从而确保唯一。