多线程:
学习多线程的作用:提高程序的执行效率
并行: 在同一时刻,有多个指令在多个CPU核心上同时执行
并发:在一段时间内,有多个指令在单个CPU核心上交替执行
进程:进程是资源分配的最小单位,就是程序执行的过程。可以理解为正在运行的程序
特点:
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行
线程:线程是CPU调度的最小单位,它是进程的一部分,是进程中的单个控制流,是一条执行路径
一个进程,可以有多条线程,至少有一条线程
注意:线程只能在进程中运行
多线程:指一个进程如果有多条执行路径,则称为多线程程序
多线程的实现方式
- 继承Thread类的方式进行实现,
- 实现Runnable接口的方式进行实现
- 利用Callable和Future接口方式实现
实现多线程方式一:继承Thread类
实现步骤: - 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
启动线程的方法:void start():使此线程开始执行,Java虚拟机会调用run方法();
重写run()方法:因为run()方法是用来封装被线程执行的代码
run()方法与start()方法的区别:
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
实现多线程方式二:实现Runnable接口
实现步骤: - 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
Thread构造方法:
Thread(Runnable target);分配一个新的Thread对象
实现多线程方式三: 实现Callable接口
实现步骤: - 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
常用方法:
V call();计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable callable);创建一个 FutureTask,一旦运行就执行给定的 Callable
V get();如有必要,等待计算完成,然后获取其结果
注意: get()方法的调用一定要在Thread类对象调用start()方法之后
三种实现方式的对比
.1,实现Runnable、Callable接口 - 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
2.继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
三种实现方式的应用场景: 如果不需要线程的返回值就使用Runnable,需要返回值就使用Callable
Thread类常见方法
void setName(String name);//将此线程的名称更改为等于参数name
String getName();//返回此线程的名称
static Thread currentThread();
//返回对当前正在执行的线程对象的引用
static void sleep(long millis);
//使当前正在执行的线程停留(暂停执行)指定的毫秒数
线程优先级:
1.线程调度有哪两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
2.Java使用 抢占式调度模型
3.多线程执行是随机性的
随机性指:假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的。
优先级相关方法
final int getPriority();//返回此线程的优先级
final void setPriority(int newPriority);
//更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
注意:在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定
守护线程
守护线程是程序运行的时候在后台提供一种通用服务的线程
守护线程的特点: 所有用户线程停止,进程才会停掉所有守护线程,然后退出程序
void setDaemon(boolean on);//将此线程标记为守护线程,
//当运行的线程都是守护线程时,Java虚拟机将退出
线程同步:线程安全问题-原因分析
问题描述:多线程执行数据共享时,导致数据重复和数据范围不合理等问题出现
解决思路:让程序没有安全问题的环境
实现:把多条语句操作共享数据的代码锁起来,让任意时刻只能有一个线程执行即可。
Java中提供了 同步代码块 可以用来解决这问题
同步代码块格式:
synchronized(任意对象){ 多条语句操作共享数据的代码};
特点:默认情况下是打开的,只要有一个线程进去执行代码了,锁就会关闭
当线程执行完出来时,锁才会自动打开
好处:解决了多线程的数据安全问题。
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序运行的效率。
同步方法:就是把synchronized关键字加到方法上,
格式:修饰符synchronized返回值类型 方法名(方法参数){ }
同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
同步代码块可以指定锁对象,同步方法不能指定所对象
同步代码方法的锁对象是:this
静态同步方法:就是把synchronized关键字加到静态方法上。
格式: 修饰符 static synchronized 返回值类型 方法名(方法参数){ }
静态同步方法的锁对象是: 类名.class
线程安全问题-必须保证锁对象唯一
Lock锁
JDK5之后出现
为了更清晰的表达如何加锁和释放锁,JDK就提供了Lock锁
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock类中提供了获得和释放锁的方法:
void lock();获得锁;
void unlock();释放锁;
boolean tryLock();尝试获得锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock类来实例化
ReentrantLock类的构造方法: public ReentrantLock(){ }
synchronized与Lock的区别:
synchronized:是Java的关键字,在jvm层面上
1、以获取锁的线程执行完同步代码,释放锁
2、线程执行发生异常,jvm会让线程释放锁
假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
锁的细粒度和灵活度低
Lock:是一个类
在finally中必须释放锁,不然容易造成线程死锁
分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁的细粒度和灵活度高
当synchronized不满足需要时,才选择Lock
**死锁:**线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
产生死锁情况:
- 资源有限
- 同步嵌套
避免死锁:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
等待唤醒机制:主要为了控制线程执行机制
生产者和消费者模式是一个十分经典的多线程协作模式,弄懂生产者消费者的问题能够让我们更加深刻的理解多线程编程。
生产者步骤:判断第三方对象是否有数据,如果有就等待,如果没有就生产—>把数据提供给第三方对象—>叫醒等待的消费者
消费者者步骤:判断第三方对象是否有数据—>如果没有就等待,如果有就消费数据—>线程结束后,第三方对象中的数据就没有了,它就会唤醒等待中的生产者继续生产数据,数据变量总数 -1。
Object类中提供的等待与唤醒的方法:
void wait();导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll();方法
void notify(); 唤醒正在等待对象监视器的单个线程
void notifyAll();唤醒正在等待对象监视器的全部线程
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.getCount() == 0) {
break;
} else {
if (desk.isFlag()) {
System.out.println("消费者在消费数据");
desk.setFlag(false);
desk.getLock().notifyAll();
desk.setCount(desk.getCount() - 1);
} else {
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
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.getCount() == 0) {
break;
} else {
if (desk.isFlag()) {
System.out.println("消费者在消费数据");
desk.setFlag(false);
desk.getLock().notifyAll();
desk.setCount(desk.getCount() - 1);
} else {
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public class Desk {
private int count;
private boolean flag;
private final Object lock = new Object();
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public Object getLock() {
return lock;
}
public Desk(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
public Desk() {
}
@Override
public String toString() {
return "Desk{" +
"count=" + count +
", flag=" + flag +
", lock=" + lock +
'}';
}
}
public static void main(String[] args) {
Desk desk = new Desk();
desk.setCount(12);
Foodie f = new Foodie(desk);
Cooker c = new Cooker(desk);
f.start();
c.start();
}
小套路:
1,while(true)死循环
2,synchronized线程锁, 锁对象唯一
3,判断共享数据是否结束,结束就break;
4,判断共享数据是否结束,没有结束就继续执行。
阻塞队列实现等待唤醒机制:
继承结构:
BlockingQueue类常用的方法:
put(anObject);将参数放入队列,如果放不进去就会阻塞。
take();取出第一个数据,取出时不会阻塞。
ArrayBlockingQueue:底层是数组,有边界。推荐使用
LinkedBlockingQueue:底层是链表,无边界,但是不能超过int的最大值
public static void main(String[] args) {
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1);
Cooker c = new Cooker(list);
Foodie f = new Foodie(list);
Foodie f1 = new Foodie(list);
Foodie f2 = new Foodie(list);
c.start();
f1.start();
f2.start();
f.start();
}
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) {
e.printStackTrace();
}
}
}
}
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) {
e.printStackTrace();
}
}
}
}
线程状态:
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态,同一时刻线程只能存于一种状态
虚拟机中线程的六中状态:
新建状态(NEW) -----> 创建线程对象
就绪状态(RUNNABLE) -----> start方法
阻塞状态(BLOCKED) ------> 遇到锁但无法获得锁对象
等待状态(WAITING) ------> wait方法
计时等待(TIMED_WAITING) ------> sleep方法
结束等待(TERMINATED) -----> 全部代码运行完毕
线程池 :存线程的容器就是线程池
原理:当有执行任务时,就会在线程池里找有没有线程,如果有就拿来用,如果没有就创建。用完之后再还回去,方便下次使用。
1,创建一个池子,池子中是空的。-----> 创建 Executors类 中的静态方法。
方法一:
ExecutorServiceexecutorService=Executors.newCachedThreadPool();
Executors类中的无参数静态方法创建线程池 方法:newCachedThreadPool();
方法二:
ExecutorServiceexecutorService=Executors.newFixedThreadPool(10);
Executors类中的有参数静态方法创建线程池 方法:newFixedThreadPool(int num);
此参数值线程池中最多存在的线程数量。
方法三:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略)
//参数一:核心线程数量
//参数二:最大线程数
//参数三:空闲线程最大存活时间
//参数四:时间单位
//参数五:阻塞队列(任务队列)
//参数六:创建工厂
//参数七:任务的拒绝策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,//参数一:核心线程数量
5,//参数二:最大线程数
2, //参数三:空闲线程最大存活时间
TimeUnit.SECONDS,//参数四:时间单位
new ArrayBlockingQueue<>(10),/参数五:阻塞队列(任务队列)
Executors.defaultThreadFactory(),//参数六:创建工厂
new ThreadPoolExecutor.AbortPolicy());//参数七:任务的拒绝策略
Java的任务拒绝策略有:
AbortPolicy
DiscardPolicy
DiscardOldestPolicy
CallRunsPolicy
ThreadPoolExecutor的执行顺序:
- 如果运行的线程不少于corePoolSize,则Executor始终首选添加新的线程,而不进入队列
- 如果运行的线程等于或大于corePoolSize,则Executor始终首选将请求加入队列,而不是添加新的线程
- 如果无法将请求加入队列,则创建新的线程,除非创建的线程数超出maximumPoolSize,在这种情况下,任务会被拒绝
RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。
ThreadPoolExecutor.AbortPolicy:
//丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy:
//丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy:
//抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy:
//调用任务的run()方法绕过线程池直接执行。
注意:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数
线程池参数设置总结:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
volatile:关键字
java内存模型(Java Memory Model 简称JMM)的由来:
JVM为了屏蔽各个硬件平台和操作系统对内存访问机制的差异化,提出了JMM的概念。
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中共享变量(包括实例(对象)字段,静态字段和构成数组对象的元素)的访问方式。
总结:JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证
线程安全的本质:
在并发编程中,线程安全问题的本质其实就是 原子性、有序性、可见性;
可见性 多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值
原子性 操作是不可中断的,要么全部执行成功要么全部执行失败
有序性 编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的
.实现共享变量的可见性,必须保证两点:
- 线程修改后的共享变量值能够及时从线程工作内存中刷新到主内存中
- 其他线程能够及时把共享变量的在、最新值从主内存更新到自己的工作内存中
java支持的可见性实现的两种方式:synchronized、volatile
volatile实现内存可见性:
(1)线程写volatile变量的过程:
JMM会把该线程对应的工作内存(本地内存)中的共享变量值刷新到主内存
(2)线程读volatile变量的过程:
JMM会把该线程对应的工作内存(本地内存)置为无效。线程接下来将从主内存中读取共享变量
synchronized解决:
1.JMM关于synchronized的两条规定:(synchronized如何实现内存可见性?)
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要时从主内存中重新读取最新的线程值
- 线程释放锁时,必须把共享变量的最新值刷新到主内存中
2.synchronized线程执行互斥代码的过程:
1 ,线程获得锁
2 ,清空变量副本
3 ,拷贝共享变量最新的值到变量副本中
4 ,执行代码
5 ,将修改后变量副本中的值赋值给共享数据
6 ,释放锁
原子性:原子性就是所有操作要么都执行,要么都不执行
- 除了long和double之外的所有原始类型的赋值
- 所有volatile变量的赋值
- java.concurrent.Atomic 类的所有操作
volatile关键字不能保证原子性
java中保证原子性可以使用
- 使用锁
- 利用处理器提供的CAS指令
原子操作类:AtomicInteger
原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。
使用原子的方式更新基本类型Atomic包提供了以下3个类:
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
AtomicInteger的常用方法:
构造方法:
public AtomiclnInteger(){ } 初始化一个默认值为0的原子型Integer
public AtomclnInteger(int initialValue){ } 初始化一个指定值的原子型Integer
常用方法:
int get(); //返回一个int类型的值
int getAndIncrement();
//以原子方式将当前值加1,注意,这里返回的是自增前的值
int incrementAndGet();
//以原子方式将当前值加1,注意,这里返回的是自增后的值
int addAndGet(int data);
//以原子方式将输入的数值与实例中的值(与带参构造)AtomclnInteger(int initialValue)里面的value)相加,并返回结果。
int getAndSet(int value);
//以原子方式设置newValue的值,并返回旧值。
AtomicInteger原理 自旋锁 + CAS 算法
CAS算法:
有3个操作数(内存值V,旧的预期值A,要修改的值B)。
当旧的预期值A == 内存值V 此时修改成功,将V改为B。
当旧的预期值A != 内存值V 此时修改失败 ,不做任何操作
并重新获取现在的最新值(这个重新获取的动作就是自旋);
小结:
CAS算法:在修改共享数据时,把原来的旧值记录下来了
如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功
如果现在的内存中的值跟原来的旧值不一样,说明其他线程已经操作过内存值,
则修改失败,需要重新获取现在的新值,再次进行操作,这个重新获取就是自旋。
乐观锁和悲观锁:
Synchronized和CAS的区别:
相同点:在多线程情况下,都可以保证共享数据的安全性,
不同点:
Synchronized总是从最坏的角度出发,认为每次获取数据的时候,别的线程都有可能修改数据,所以每次操作共享数据之前,都会上锁,在操作完毕之后才会释放锁(悲观锁)
CAS从乐观的角度出发,假设每次获取共享数据别的线程都不会修改,所以不会上锁,但是在真正的修改共享数据时,会检查一下,别的线程有没有修改过这个数据。
如果别的线程修改过,那么它再次获取现在最新的值。如果别的线程没有修改过,那么就可以直接修改共享数据的值,所以效率相对会快一些。(乐观锁)