3. 线程死锁
概述
死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的.
产生条件
- 多个线程
- 存在锁对象的循环依赖
4. 线程的状态
线程的状态:
线程通信
线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:
等待方法:
- void wait() 让线程进入无限等待。
- void wait(long timeout) 让线程进入计时等待
- 以上两个方法调用会导致当前线程释放掉锁资源。
唤醒方法:
- void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
- void notifyAll() 唤醒在此对象监视器上等待的所有线程。
- 以上两个方法调用不会导致当前线程释放掉锁资源。
注意:
等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)
等待和唤醒方法应该使用相同的锁对象调用
5. 线程池
5.1 线程使用存在问题
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。
5.2 线程池介绍
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
5.3 线程池使用的大致流程
创建线程池指定线程开启的数量
提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任。
5.4 线程池的好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
提高响应速度。当任务到达时,任务可以不需要等待线程创建 , 就能立即执行。
提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存 (每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
5.5 Java 提供的线程池
java.util.concurrent.ExecutorService
是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了
获取线程池我们使用工具类 java.util.concurrent.Executors的静态方
public static ExecutorService newFixedThreadPool (int num) : 指定线程池最大线程池数量获取线程池
线程池ExecutorService的相关方法
Future submit(Callable task)
Future<?> submit(Runnable task)
关闭线程池方法(一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)
void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
5.6 线程池处理 Runable 任务
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /* 1 需求 : 使用线程池模拟游泳教练教学生游泳。 游泳馆(线程池)内有3名教练(线程) 游泳馆招收了5名学员学习游泳(任务)。 2 实现步骤: 创建线程池指定3个线程 定义学员类实现Runnable, 创建学员对象给线程池 */ public class Test1 { public static void main(String[] args) { // 创建指定线程的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(3); // 提交任务 threadPool.submit(new Student("小花")); threadPool.submit(new Student("小红")); threadPool.submit(new Student("小明")); // threadPool.shutdown();// 关闭线程池 } } class Student implements Runnable { private String name; public Student(String name) { this.name = name; } @Override public void run() { String coach = Thread.currentThread().getName(); System.out.println(coach + "正在教" + name + "游泳..."); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(coach + "教" + name + "游泳完毕."); } }
5.7 线程池处理 Callable 任务
import java.util.concurrent.*; /* 需求: Callable任务处理使用步骤 1 创建线程池 2 定义Callable任务 3 创建Callable任务,提交任务给线程池 4 获取执行结果 <T> Future<T> submit(Callable<T> task) : 提交Callable任务方法 返回值类型Future的作用就是为了获取任务执行的结果。 Future是一个接口,里面存在一个get方法用来获取值 练一练:使用线程池计算 从0~n的和,并将结果返回 */ public class Test2 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建指定线程数量的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); Future<Integer> future = threadPool.submit(new CalculateTask(100)); Integer sum = future.get(); System.out.println(sum); } } // 使用线程池计算 从0~n的和,并将结果返回 class CalculateTask implements Callable<Integer> { private int num; public CalculateTask(int num) { this.num = num; } @Override public Integer call() throws Exception { int sum = 0;// 求和变量 for (int i = 0; i <= num; i++) { sum += i; } return sum; } }
6. 自定义线程池
该拒绝策略 在 超出(最大线程+队列数)时报错如下:
7. volatile
如何保证变量的可见性?
在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
如何禁止指令重排序?
在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,你通过这个三个方法也可以实现和volatile禁止重排序一样的效果,只是会麻烦一些。
volatile 可以保证原子性么?
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:
读取 inc 的值。
对 inc 加 1。
将 inc 的值写回内存。volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。
线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。
其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock或者AtomicInteger都可以。
8. AtomicInteger
概述:java 从 JDK1.5 开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变
量的类型有很多种,所以在 Atomic 包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解
使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
以上 3 个类提供的方法几乎一模一样,所以本节仅以 AtomicInteger 为例进行讲解,AtomicInteger 的常用方法如下:
// 初始化一个默认值为0的原子型Integer public AtomicInteger() // 初始化一个指定值的原子型Integer public AtomicInteger(int initialValue) // 获取值 int get() // 以原子方式将当前值加1,注意,这里返回的是自增前的值。 int getAndIncrement() // 以原子方式将当前值加1,注意,这里返回的是自增后的值。 int incrementAndGet() // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 int addAndGet(int data) // 以原子方式设置为newValue的值,并返回旧值。 int getAndSet(int value)
8.1 AtomicInteger-内存解析
AtomicInteger原理 : 自旋锁 + CAS 算法
CAS算法:
有3个操作数(内存值V, 旧的预期值A,要修改的值B)
当旧的预期值A == 内存值 此时修改成功,将V改为B
当旧的预期值A!=内存值 此时修改失败,不做任何操作
并重新获取现在的最新值(这个重新获取的动作就是自旋)
8.2 悲观锁和乐观锁
synchronized和CAS的区别 :
相同点在多线程情况下,都可以保证共享数据的安全性。
不同点synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。
如果别人修改过,那么我再次获取现在最新的值。
如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)9. 并发工具类
9. 并发工具类
9.1 Hashtable
Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
9.2 ConcurrentHashMap
ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。
总结 :
HashMap是线程不安全的。多线程环境下会有数据安全问题
Hashtable是线程安全的,但是会将整张表锁起来,效率低下
ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样
ConcurrentHashMap1.7原理
总结 :
- 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表。
- 计算当前元素应存入的索引。
如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性。