- 简介
- 多个进程或线程同时 (或着说在同一段时间内) 访问同一资源会产生并发问题
- 线程状态及其生命周期
- 新建 (New), 就绪(Runnable), 运行(Running), 阻塞(Blocked) 和死亡(Dead)
- 使用线程
- 有三种使用线程的方法:
实现 Runnable 接口;
实现 Callable 接口;
继承 Thread 类.
- 实现接口与继承类的比较
实现接口会更好一些, 因为:
Java 不支持多重继承, 因此继承了 Thread 类就无法继承其它类, 但是可以实现多个接口;
类可能只要求可执行就行, 继承整个 Thread 类开销过大.
-start()方法与 run()方法的区别
线程启动是用 start()方法, start()调用后会运行 run()方法里的代码
- 基础线程机制
-Executor
-Executor 管理多个异步任务的执行, 而无需程序员显式地管理线程的生命周期. 这里的异步是指多个任务的执行互不干扰, 不需要进行同步操作.
- 主要有三种 Executor:
CachedThreadPool: 一个任务创建一个线程;
FixedThreadPool: 所有任务只能使用固定大小的线程;
SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool
-Daemon
- 守护线程是程序运行时在后台提供服务的线程, 不属于程序中不可或缺的部分.
当所有非守护线程结束时, 程序也就终止, 同时会杀死所有守护线程.
main() 属于非守护线程.
使用 setDaemon() 方法将一个线程设置为守护线程.
-sleep()
-Thread.sleep(millisec) 方法会休眠当前正在执行的线程, millisec 单位为毫秒.
sleep() 可能会抛出 InterruptedException, 因为异常不能跨线程传播回 main() 中, 因此必须在本地进行处理. 线程中抛出的其它异常也同样需要在本地进行处理.
-yield()
- 对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分, 可以切换给其它线程来执行. 该方法只是对线程调度器的一个建议, 而且也只是建议具有相同优先级的其它线程可以运行.
- 中断
- 触发条件
- 一个线程执行完毕之后会自动结束, 如果在运行过程中发生异常也会提前结束.
-InterruptedException
- 通过调用一个线程的 interrupt() 来中断该线程, 如果该线程处于阻塞, 等待状态, 那么就会抛出 InterruptedException, 从而提前结束该线程. 但是不能中断 I/O 阻塞和 synchronized 锁阻塞
-interrupted()
- 如果一个线程的 run() 方法执行一个无限循环, 并且没有执行 sleep() 等会抛出 InterruptedException 的操作, 那么调用线程的 interrupt() 方法就无法使线程提前结束.
- 但是调用 interrupt() 方法会设置线程的中断标记, 此时调用 interrupted() 方法会返回 true. 因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态, 从而提前结束线程.
-Executor 的中断操作
- 调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭, 但是如果调用的是 shutdownNow() 方法, 则相当于调用每个线程的 interrupt() 方法.
- 如果只想中断 Executor 中的一个线程, 可以通过使用 submit() 方法来提交一个线程, 它会返回一个 Future<?> 对象, 通过调用该对象的 cancel(true) 方法就可以中断线程.
- 互斥同步
- 概览
-Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问, 第一个是 JVM 实现的 synchronized, 而另一个是 JDK 实现的 ReentrantLock.
-synchronized
- 同步一个代码块
- public void func() {
- synchronized (this) {
- // ...
- }
- }
它只作用于同一个对象, 如果调用两个对象上的同步代码块, 就不会进行同步.
- 同步一个方法
- public synchronized void func () {
- // ...
- }
它和同步代码块一样, 作用于同一个对象.
- 同步一个类
- public void func() {
- synchronized (SynchronizedExample.class) {
- // ...
- }
- }
作用于整个类, 也就是说两个线程调用同一个类的不同对象上的这种同步语句, 也会进行同步.
- 同步一个静态方法
- public synchronized static void fun() {
- // ...
- }
作用于整个类.
-ReentrantLock
-ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁
- 两种锁的比较
- 锁的实现
synchronized 是 JVM 实现的, 而 ReentrantLock 是 JDK 实现的.
性能
新版本 Java 对 synchronized 进行了很多优化, 例如自旋锁等, synchronized 与 ReentrantLock 大致相同.
等待可中断
当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 改为处理其他事情.
ReentrantLock 可中断, 而 synchronized 不行.
公平锁
公平锁是指多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来依次获得锁.
synchronized 中的锁是非公平的, ReentrantLock 默认情况下也是非公平的, 但是也可以是公平的.
锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象.
- 使用选择
- 除非需要使用 ReentrantLock 的高级功能, 否则优先使用 synchronized. 这是因为 synchronized 是 JVM 实现的一种锁机制,
JVM 原生地支持它, 而 ReentrantLock 不是所有的 JDK 版本都支持. 并且使用 synchronized 不用担心没有释放锁而导致死锁问题, 因为 JVM 会确保锁的释放.
- 线程之间的协作
- 概览
- 当多个线程可以一起工作去解决某个问题时, 如果某些部分必须在其它部分之前完成, 那么就需要对线程进行协调
-join()
- 在线程中调用另一个线程的 join() 方法, 会将当前线程挂起, 而不是忙等待, 直到目标线程结束.
-wait() notify() notifyAll()
- 调用 wait() 使得线程等待某个条件满足, 线程在等待时会被挂起, 当其他线程的运行使得这个条件满足时, 其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程.
它们都属于 Object 的一部分, 而不属于 Thread.
只能用在同步方法或者同步控制块中使用, 否则会在运行时抛出 IllegalMonitorStateExeception.
使用 wait() 挂起期间, 线程会释放锁. 这是因为, 如果没有释放锁, 那么其它线程就无法进入对象的同步方法或者同步控制块中, 那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程, 造成死锁.
-wait() 和 sleep() 的区别
-wait() 是 Object 的方法, 而 sleep() 是 Thread 的静态方法;
wait() 会释放锁, sleep() 不会.
-await() signal() signalAll()
-java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调, 可以在 Condition 上调用 await() 方法使线程等待, 其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程. 相比于 wait() 这种等待方式, await() 可以指定等待的条件, 因此更加灵活.
- 线程安全
- 定义
- 一个类在可以被多个线程安全调用时就是线程安全的.
- 分类
- 可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变, 绝对线程安全, 相对线程安全, 线程兼容和线程对立.
- 不可变
- 不可变 (Immutable) 的对象一定是线程安全的, 不需要再采取任何的线程安全保障措施. 只要一个不可变的对象被正确地构建出来, 永远也不会看到它在多个线程之中处于不一致的状态.
多线程环境下, 应当尽量使对象成为不可变, 来满足线程安全.
不可变的类型:
final 关键字修饰的基本数据类型
String
枚举类型
Number 部分子类, 如 Long 和 Double 等数值包装类型, BigInteger 和 BigDecimal 等大数据类型. 但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的.
对于集合类型, 可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合.
- 绝对线程安全
- 不管运行时环境如何, 调用者都不需要任何额外的同步措施.
- 相对线程安全
- 相对线程安全需要保证对这个对象单独的操作是线程安全的, 在调用的时候不需要做额外的保障措施. 但是对于一些特定顺序的连续调用, 就可能需要在调用端使用额外的同步手段来保证调用的正确性.
在 Java 语言中, 大部分的线程安全类都属于这种类型, 例如 Vector,HashTable,Collections 的 synchronizedCollection() 方法包装的集合等.
- 线程兼容
- 线程兼容是指对象本身并不是线程安全的, 但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用, 我们平常说一个类不是线程安全的, 绝大多数时候指的是这一种情况.
Java API 中大部分的类都是属于线程兼容的, 如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等.
- 线程对立
- 线程对立是指无论调用端是否采取了同步措施, 都无法在多线程环境中并发使用的代码. 由于 Java 语言天生就具备多线程特性, 线程对立这种排斥多线程的代码是很少出现的, 而且通常都是有害的, 应当尽量避免.
- 线程安全的实现方案
- 互斥同步
-synchronized 和 ReentrantLock.
- 互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题, 因此这种同步也称为阻塞同步.
互斥同步属于一种悲观的并发策略, 总是认为只要不去做正确的同步措施, 那就肯定会出现问题. 无论共享数据是否真的会出现竞争, 它都要进行加锁(这里讨论的是概念模型, 实际上虚拟机会优化掉很大一部分不必要的加锁)
, 用户态核心态转换, 维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作.
- 非阻塞同步
- 锁优化(JVM 对 synchronized 的优化.)
- 自旋锁
- 互斥同步进入阻塞状态的开销都很大, 应该尽量避免. 旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环 (自旋) 一段时间, 如果在这段时间内能获得锁, 就可以避免进入阻塞状态.
- 缺点: 自旋锁虽然能避免进入阻塞状态从而减少开销, 但是它需要进行忙循环操作占用 CPU 时间, 它只适用于共享数据的锁定状态很短的场景.
- 锁消除
- 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除.
锁消除主要是通过逃逸分析来支持, 如果堆上的共享数据不可能逃逸出去被其它线程访问到, 那么就可以把它们当成私有数据对待, 也就可以将它们的锁进行消除.
- 轻量级锁
- 轻量级锁是相对于传统的重量级锁而言, 它使用 CAS 操作来避免重量级锁使用互斥量的开销. 对于绝大部分的锁, 在整个同步周期内都是不存在竞争的, 因此也就不需要都使用互斥量进行同步, 可以先采用 CAS 操作进行同步, 如果 CAS 失败了再改用互斥量进行同步.
-CAS
-CAS(Compare-and-Swap), 即比较并替换, 是一种实现并发算法时常用到的技术, Java 并发包中的很多类都使用了 CAS 技术.
来源: https://www.cnblogs.com/yonyong/p/9537695.html