多线程
是指从软件或者硬件上实现多个线程并发执行的技术
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
进程:是正在运行的软件
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行
线程:是进程中的单个顺序控制流,是一条执行路径。软件程序中做的事情
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
多线程的实现方案
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable和Future接口方式实现
方案1:继承Thread类
-
定义一个类MyThread继承Thread类
-
在MyThread类中重写run()方法
-
创建MyThread类的对象
-
启动线程
-
run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
-
start():启动线程;然后由JVM调用此线程的run()方法
方式2:实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
方式3:Callable和Future
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
实现Runnable、Callable接口
- 优点:扩展性强,实现该接口的同时还可以继承其他的类。
- 缺点:编程相对复杂,不能直接使用Thread类中的方法
继承Thread类
- 优点:编程比较简单,可以直接使用Thread类中的方法
- 缺点:可以扩展性较差,不能再继承其他的类
线程类的常见方法
获取线程的名字
- String getName():返回此线程的名称
Thread类中设置线程的名字
- void setName(String name):将此线程的名称更改为等于参数 name
- 通过构造方法也可以设置线程名称
获得当前线程的对象
- public static Thread currentThread():返回对当前正在执行的线程对象的引用
线程休眠
- public static void sleep(long time):让线程休眠指定的时间,单位为毫秒。
线程调度
多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务。
线程有两种调度模型
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
- Java使用的是抢占式调度模型
线程的优先级
- public final void setPriority(int newPriority) 设置线程的优先级
- public final int getPriority() 获取线程的优先级
后台线程/守护线程
- public final void setDaemon(boolean on):设置为守护线程
线程生命周期
线程的安全问题
同步代码块:锁多条语句操作共享数据,可以使用同步代码块实现
- 格式:synchronized(任意对象) {多条语句操作共享数据的代码}
- 默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步方法:就是把synchronized关键字加到方法上
- 格式:修饰符 synchronized 返回值类型 方法名(方法参数) { }
同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是什么呢?
- this
同步静态方法:就是把synchronized关键字加到静态方法上
- 格式:修饰符 static synchronized 返回值类型 方法名(方法参数) { }
同步静态方法的锁对象是什么呢?
- 类名.class
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
- ReentrantLock():创建一个ReentrantLock的实例
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
生产者消费者
等待和唤醒的方法
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程
阻塞队列实现等待唤醒机制
BlockingQueue的核心方法:
- put(anObject):将参数放入队列,如果放不进去会阻塞。
- take():取出第一个数据,取不到会阻塞。
常见BlockingQueue:
- ArrayBlockingQueue:底层是数组,有界。
- LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,最大为int的最大值。
阻塞队列继承结构
虚拟机中线程的六种状态:
- 新建状态( NEW )创建线程对象
- 就绪状态( RUNNABLE )start方法
- 阻塞状态( BLOCKED )无法获得锁对象
- 等待状态( WAITING )wait方法
- 计时等待( TIMED_WAITING )sleep方法
- 结束状态( TERMINATED )全部代码运行完毕
线程池
多线程的弊端:用到线程的时候就创建,用完之后线程消失。
创建线程池对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:核心线程数量.不能小于0
参数二:最大线程数.不能小于等于0,最大数量 >= 核心线程数量
参数三:空闲线程最大存活时间.不能小于0
参数四:时间单位.时间单位
参数五:任务队列.不能为null
参数六:创建线程工厂.不能为null
参数七:任务的拒绝策略.不能为null
任务拒绝策略
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
volatile
- 堆内存是唯一的,每一个线程都有自己的线程栈。
- 每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
- 在线程中,每一次使用是从变量的副本中获取的。
Volatile关键字:强制线程每次在使用的时候,都会看一下共享区域最新的值
Synchronized同步代码块
- 线程获得锁
- 清空变量副本
- 拷贝共享变量最新的值到变量副本中
- 执行代码
- 将修改后变量副本中的值赋值给共享数据
- 释放锁
原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
volatile关键字:只能保证线程每次在使用共享数据的时候是最新值。但是不能保证原子性。
原子类AtomicInteger
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
int get(): 获取值int
getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
AtomicInteger原理
自旋锁 + CAS 算法
CAS算法:
- 有3个操作数(内存值V, 旧的预期值A,要修改的值B)
- 当旧的预期值A == 内存值 此时修改成功,将V改为B
- 当旧的预期值A!=内存值 此时修改失败,不做任何操作
- 并重新获取现在的最新值(这个重新获取的动作就是自旋)
自旋+CAS
CAS 算法:
- 在修改共享数据的时候,把原来的旧值记录下来了。
- 如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功。
- 如果现在内存中的值跟原来的旧值不一样了,证明已经有其他线程操作过内存值了。
- 则修改失败,需要获取现在最新的值,再次进行操作,这个重新获取就是自旋。
synchronized和CAS的区别
相同点:在多线程情况下,都可以保证共享数据的安全性。
不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁。(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。如果别人修改过,那么我再次获取现在最新的值。如果别人没有修改过,那么我现在直接修改共享数据的值。(乐观锁)
并发工具类
Hashtable
- Hashtable采取悲观锁synchronized的形式保证数据的安全性
- 只要有线程访问, 会将整张表全部锁起来, 所以Hashtable的效率低下。
ConcurrentHashMap
- HashMap是线程不安全的。多线程环境下会有数据安全问题
- Hashtable是线程安全的,但是会将整张表锁起来,效率低下
- ConcurrentHashMap也是线程安全的,效率较高。
- 在JDK7和JDK8中,底层原理不一样。
ConcurrentHashMap1.7版本原理解析
- 根据键的哈希值计算出Segment数组的索引。
- 如果为空,就创建一个长度默认为2的
- 再次利用键的哈希值计算出在小数组应存入的索引(二次哈希)
- 如果为空,则直接添加。
ConcurrentHashMap1.8版本原理解析
底层结构:哈希表。(数组、链表、红黑树的结合体)。
结合CAS机制 + synchronized同步代码块形式保证线程安全。
- 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。在第一次添加元素的时候创建哈希表
- 计算当前元素应存入的索引。
- 如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
- 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
- 当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性。
CountDownLatch
让某一条线程等待其他线程执行完毕之后再执行。
public CountDownLatch(int count) 参数传递线程数,表示等待线程数量
public void await() 让线程等待
public void countDown() 当前线程执行完毕
Semaphore
可以控制访问特定资源的线程数量。