简单了解多线程
是指从软件或者硬件上实现多个线程并发执行的技术
具有多线程的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能
并发和并行
并行:在同一时刻,有多个指令在多个CPU上同时执行
并发:在同一时刻,有多个指令在单个CPU上交替执行
进程和线程
进程:就是操作系统中正在运行的一个应用程序
线程:就是应用程序中做的事情,比如:360系统中的杀毒,扫描木马,清理垃圾
多线程的实现形式
1、实现Thread接口
- 创建一个类MyThread实现Thread接口
- 运行代码需要重写run方法
- 在主方法处新建MyThread对象
start()
方法来开启线程
问:为什么要重写run()
方法?
答:因为run()
方法是用来封装被线程执行的代码
问:run()
方法和start()
方法的区别?
答: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对象作为构造方法的参数
- 启动线程
- 可以调用FutureTask的get方法,就可以获取线程结束之后的结果
4、三种方式的对比
| | 优点 | 缺点 | | —- | —- | —- | | 实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 | | 继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
5、获取和设置线程名称
获取线程名字String getName()
:返回此线程的名称
Thread类中设置线程的名字
void setName(String name)
:将此线程的名称更改为等于参数name- 通过构造方法也可以设置线程名称
6、获得当前线程的对象
public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
7、线程休眠
public static void sleep(long time)
:让线程休眠指定的时间,单位为毫秒
8、线程调度
多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权
才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务
线程有两种调度模型
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的获取的CPU的时间片相对多一些
线程的优先级public final void setPriority(int newPriority)
:设置线程的优先级public final int getPriority()
:获取线程的优先级
java线程优先级默认是5,最大优先级为10,最小优先级为1,不能小于1或者超过10
9、后台线程/守护线程
public final void setDaemon(boolean on)
:设置守护线程
守护线程:当普通线程执行完毕之后,守护线程也会直接结束,不会继续执行
线程安全
当多线程操作共享数据时会出现线程安全问题
如何解决?
基本思想:让程序没有安全问题的环境
怎么实现呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能由一个线程执行即可
- java提供了同步代码块的方式来解决
同步代码块
锁多条语句操作共享数据,可以使用同步代码块来实现
格式: ```java synchronized(锁对象){ 多条语句操作共享数据的代码 }
- 默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
锁对象必须唯一
<a name="a2o3T"></a>
### 同步方法
同步方法:就是把synchronized关键字加到方法上<br />格式:`修饰符 synchronized 返回值类型 方法名(方法参数){}`<br />同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是:`this`<br />同步静态方法:就是把synchronized关键字加到静态方法上<br />格式:`修饰符 static synchronized 返回值类型 方法名(方法参数){}`<br />同步静态方法的锁对象是:`类名.class`
<a name="gEfxl"></a>
### Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock<br />Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作<br />Lock中提供了获得锁和释放锁的方法
- `void lock()`:获得锁
- `void unlock()`: 释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化<br />ReentrantLock的构造方法
- `ReentrantLock()`:创建一个ReentrantLock的实例
应该将unlock方法放入finally中,保证锁的释放
<a name="R7nE9"></a>
### 死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。<br />不要写锁的嵌套就不会出现死锁
<a name="gljMt"></a>
## 生产者和消费者
<a name="bS6Do"></a>
### 原理分析:
消费者步骤:
1. 判断桌子上是否有汉堡包
1. 如果没有就等待
1. 如果有就开吃
1. 吃完之后,桌子上的汉堡包就没有了,叫醒等待的生产者继续生产,汉堡包的总数量减一
生产者步骤
1. 判断桌子上是否有汉堡包,如果有就等待,如果没有才生产
1. 把汉堡包放在桌子上
1. 叫醒等待的消费者开吃
<a name="EuHHF"></a>
### 等待和唤醒的方法
为了体现生产者和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中<br />Object类的等待和唤醒方法:
| **方法名** | **说明** |
| --- | --- |
| `void wait()` | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll()方法 |
| `void notify()` | 唤醒正在等待对象监视器的单个线程 |
| `void notifyAll()` | 唤醒正在等待对象监视器的所有线程 |
<a name="Ysu9A"></a>
### 代码实现:
```java
public class Foodie extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock){
//食物全部吃完就结束循环
if (Desk.foodCount == 0){
break;
}else {
if (Desk.hasFood){
System.out.println("正在吃饭");
Desk.foodCount--;
Desk.hasFood = false;
Desk.lock.notifyAll();
System.out.println("还剩" + Desk.foodCount + "份食物");
}else {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
public class Cooker extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.foodCount == 0){
break;
}else {
if (!Desk.hasFood){
System.out.println("厨师正在做饭");
Desk.hasFood = true;
Desk.lock.notifyAll();
}else {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
public class Desk {
public static boolean hasFood = false;
public static int foodCount = 10;
//锁对象
public static final Object lock= new Object();
}
注意点:锁对象要唯一,所以只能在桌子上创建
并且唤醒和等待都得是锁对象来调用,使某一个线程在锁对象上等待或者唤醒
修改后代码:
public class Desk {
//public static boolean hasFood = false;
private boolean hasFood;
//public static int foodCount = 10;
private int foodCount;
//锁对象
//public static final Object lock= new Object();
private final Object lock= new Object();
public Desk(boolean hasFood, int foodCount) {
this.hasFood = hasFood;
this.foodCount = foodCount;
}
public Desk() {
this(false,10);
}
public boolean isHasFood() {
return hasFood;
}
public void setHasFood(boolean hasFood) {
this.hasFood = hasFood;
}
public int getFoodCount() {
return foodCount;
}
public void setFoodCount(int foodCount) {
this.foodCount = foodCount;
}
public Object getLock() {
return lock;
}
@Override
public String toString() {
return "Desk{" +
"hasFood=" + hasFood +
", foodCount=" + foodCount +
", lock=" + lock +
'}';
}
}
public class Cooker extends Thread{
private Desk desk;
public Cooker(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while (true){
synchronized (desk.getLock()){
if (desk.getFoodCount() == 0){
break;
}else {
if (!desk.isHasFood()){
System.out.println("厨师正在做饭");
desk.setHasFood(true);
desk.getLock().notifyAll();
}else {
try {
desk.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
public class Foodie extends Thread{
private Desk desk;
public Foodie(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while(true){
synchronized (desk.getLock()){
//食物全部吃完就结束循环
if (desk.getFoodCount() == 0){
break;
}else {
if (desk.isHasFood()){
System.out.println("正在吃饭");
desk.setFoodCount(desk.getFoodCount() - 1);
desk.setHasFood(false);
desk.getLock().notifyAll();
System.out.println("还剩" + desk.getFoodCount() + "份食物");
}else {
try {
desk.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
阻塞队列实现等待唤醒机制
阻塞队列继承结构
常见BlockingQueue:
- ArrayBlockingQueue:底层是数组,有界
- LinkedBlockingQueue:底层是链表,无界,但不是真正的无界,最大为int的最大值
代码实现
import java.util.concurrent.ArrayBlockingQueue;
public class Demo {
public static void main(String[] args) {
//创建阻塞队列,容量为1
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1);
Foodie f = new Foodie(list);
Cooker c = new Cooker(list);
f.start();
c.start();
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
private ArrayBlockingQueue<String> list;
public Foodie(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true){
try {
String take = list.take();
System.out.println("吃了一个" + take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Cooker extends Thread{
private ArrayBlockingQueue<String> list;
public Cooker(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true){
try {
list.put("汉堡包");
System.out.println("厨师放了一个汉堡包");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
多线程高级
线程状态
虚拟机中线程的六种状态:
- 新建状态(NEW):创建线程对象
- 就绪状态(RUNNABLE):start方法
- 阻塞状态(BLOCKED):无法获得锁对象
- 等待状态(WAITING):wait方法
- 计时等待(TIMED_WAITING):sleep方法
-
线程池
之前多线程弊端:每次都需要新建线程对象,用完就销毁
改进: 创建一个池子,池子中是空的
- 有任务需要执行时,才会创建线程对象,当任务执行完毕,线程对象归还给池子
代码实现:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
//创建线程池,返回一个控制线程池的对象
ExecutorService executorService = Executors.newCachedThreadPool();
//提交任务到线程池
executorService.submit(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i + "次");
}
});
executorService.submit(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i + "次");
}
});
//关闭线程池
executorService.shutdown();
}
}
1.创建线程池管理对象
static ExecutorService newCachedThreadPool()
:创建一个默认的线程池,最大值为int的最大值static ExecutorService newFixedThreadPool(int nThreads)
:创建一个指定最多线程数量的线程池,最大值为设置的值,初始池子里还是0个线程,但是最多只能有设置的值的线程
2.任务执行:ExecutorService
的submit(Runnable task)
:提交任务,池子会自动的帮我们创建对象,任务执行完毕,也会自动把线程对象归还给池子
3.关闭线程池:ExecutorService
的void shutdown()
创建线程池对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数,最大线程数,空闲线程最大存活时间(值),空闲线程最大存活时间(单位),任务队列,创建线程工厂,任务的拒绝策略);
- 参数一:核心线程数量
- 不能小于0
- 参数二:最大线程数
- 不能小于0,最大数量>=核心线程数量
- 参数三:空闲线程最大存活时间
- 不能小于0
- 参数四:时间单位
- 时间单位
TimeUnit对象
- 时间单位
- 参数五:任务队列
- 让任务在队列中等着,等有线程空闲了,再从这个队列中获取任务并执行
- 参数六:创建线程工厂
- 按照默认的方式创建线程对象
- 参数七:任务的拒绝策略
- 1.什么时候拒绝任务: 当提交的任务 > 池子中最大线程数量 + 队列容量的时候拒绝任务
任务的拒绝策略ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, 10, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
ThreadPoolExecutor.AbortPolicy
:丢弃任务并抛出RejectedExecutionException异常,是默认的策略ThreadPoolExecutor.DiscardPolicy
:丢弃任务,但是不抛出异常,这是不推荐的做法ThreadPoolExecutor.DiscardOldestPolicy
:抛弃队列中等待醉酒==最久的任务然后把当前任务加入队列中ThreadPoolExecutor.CallerRunsPolicy
:调用任务的run()方法绕过线程池直接执行,就是让主函数执行Volatile关键字
- 1.什么时候拒绝任务: 当提交的任务 > 池子中最大线程数量 + 队列容量的时候拒绝任务
- 堆内存是唯一的,每一个线程都有自己的线程栈
- 每一个线程在使用堆内存里面变量的时候,都会先拷贝一份到变量的副本中
- 在线程中,每一次使用都是从变量的副本中获取的
如果A线程修改了堆中共享变量的值,那么其他线程不一定能及时使用最新的值。volatile
关键字:强制线程在每次使用的时候,都会看一下共享区域最新的值
也可以使用Synchronized同步代码块
- 线程获得锁
- 清空变量副本
- 拷贝共享变量最新的值到变量副本中
- 执行代码
- 将修改后变量副本中的值赋值给共享数据
-
原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行,并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体
count++
操作过程 从共享数据中读取数据到本线程栈中
- 修改本线程栈中变量副本的值
- 会把本线程栈中变量副本的值赋值给共享数据
count++
不是一个原子性操作,也就是说他在执行的过程中,有可能被其他的线程打断操作volatile
关键字:只能保证线程每次在使用共享数据的时候是最新值,但是不能保证原子性
同步代码块可以保证原子性
原子类AtomicInteger
public AtomicInteger()
: 初始化一个默认值为0的原子型Integerpublic 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算法:
- 有三个操作数(内存值V, 旧的预期值A ,要修改的值B)
- 当旧的预期值A == 内存值 此时修改成功,将V改为B
- 当旧的预期值A != 内存值 此时修改失败,不做任何操作
- 并重新获取现在的最新值(这个重新获取的动作就是自旋)
理解:
在修改共享数据的时候,把原来的旧值记录下来了
如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功
如果现在内存中的值跟原来的旧值不一样了,证明已经有其他线程操作过内存值了。
则修改失败,需要获取现在最新的值,再次进行操作,这个重新获取就是自旋
synchronized和CAS的区别
相同点:在多线程情况下,都可以保证共享数据的安全性
不同点:
synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改,所以在每次操作共享数据之前,都会上锁(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁,只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据
如果别人修改说,那么我再次获取现在最新的值
如果别人没有修改过,那么我现在直接修改共享数据的值
(乐观锁)
并发工具类
Hashtable
HashMap是线程不安全的(多线程环境下可能会存在问题)
为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下
- Hashtable采取悲观锁synchronized的形式保证数据的安全性
- 只要有线程访问,会将整张表全部锁起来,所以Hashtable的效率低下
ConcurrentHashMap1.7版本原理解析
创建过程:
- 创建一个默认长度为16,默认加载因子为0.75的数组,数组名Segment
这个大数组一旦创建无法扩容 - 再创建一个长度为2的小数组,把地址值赋值给0索引,其他索引都是null