线程简介
多线程是指从软件或硬件上实现多个线程并发执行的技术,具有多线程能力的极端几因有硬件支持能够在同一时间执行多余一个线程,今儿提升整体的性能.
进程是指系统中正在运行的一个应用程序.
每个进程之间是独立的,每个进程运行在其专用且受保护的内存空间内,
线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行.
一个线程中任务的执行是串行的,如果要在一个线程中执行多个任务,那么只能一个一个的按照顺序执行这些任务,也就是说,同一时间内,一个线程只能执行一个任务.
- 多线程就是在一个进程中多条线程可以并行执行不用的任务
多线程原理
同一时间,cpu只能处理一条线程,只有一个线程在工作,多线程并发执行,其实是cpu在快速的在多条线程之间调度(切换),如果cpu调度的时间足够快,就能造成多线程并发执行的假象,
- 很多多线程是模拟出来的,真正的多线程是有多核cpu,可以真正并发多个线程.
核心概念
- 线程就是独立的执行路径
- 在程序执行时,即使没有自己创建线程,后台也会有多个线程比如main线程,gc线程
- main()称为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程实现
三种创建方式 核心是静态代理
- thread class -> 继承Thread类(重点) thread类也实现了runnable接口
- ★ Runanble接口 -> 实现Runnable接口(重点)
- callable ->实现Callable接口(了解)
Thread
- 自定义线程类继承
Thread
类 - 重写
run()
方法.编写线程执行体 - 创建线程对象,调用
start()
方法启动线程
注意,线程开启不一定立即执行,由cpu来调度
Runnable接口
- 定义
MyRunnable
类实现Runnable
接口 - 实现
run()
方法,编写线程执行体 - 创建线程对象,调用
start()
方法启动线程
推荐使用
Runnable
对象,因为java
单继承的局限性,只能继承一个类,但是可以实现多个接口,
小结
- 继承
Thread
类- 之类集成Thread类具备多线程能力
- 启动线程:
子类对象.start()
- 不建议使用:避免OOP单继承局限性
- 实现
Runnable
接口- 实现接口Runnable具有多线程能力
- 启动线程: 传入目标对象到Thread对象.start()
- 推荐使用,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
实现Callable
接口
- 实现
Callable
接口,需要返回值类型 - 重写
call
方法,需要抛出异常 - 创建目标对象
- 创建执行服务
ExecutorService ser= Executors.newFixedThreadPool(3);
- 提交执行
Future<Boolean> r1=ser.submit(t1);
- 获取结果
r1.get()
- 关闭服务
ser.shutdownNow();
lambda表达式
-
理解Functional Interface(函数式)接口是lambda表达式的关键所在
-
函数式接口的定义
- 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口.
- 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象
线程状态
五大状态,大体上可以分为5个,其他的也有6个7个的,
Thread.State
类中定义了6个状态
- 新建(NEW) :新建了一个线程状态
- 可运行(RUNNABLE):线程对象被创建后,其它线程(比如main)调用该对象的
start()
方法,该状态线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权 - 运行(RUNNING),可运行状态的线程获得了cpu的时间片(timeslice),执行程序代码
- 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu的使用权,即让出了cpu timeslice,暂时停止运行.知道线程进入可运行状态(RUNNable),才有机会再次获得cpu timeslice转到运行状态.阻塞的情况分为三种
- 等待阻塞: 运行中running的线程执行
.wait()
方法.JVM会把该线程放入等待队列(watting queue)中 - 同步阻塞: 运行中的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock poll)中
- 其他阻塞: 运行中的线程执行
Thread.sleep()
方法或者t.join()
方法或者发出了I/O请求时.JVM会把该线程设置为阻塞状态.当sleep状态超时,join等待线程终止或者超市,I/O处理完毕时,线程会重新进入可运行状态
- 等待阻塞: 运行中running的线程执行
- 死亡(DEAD):线程run(),main()执行结束后,或者因为异常退出了
run()
方法,则该线程结束生命周期.死亡的线程不可再次复生.
java.lang.Thrad.State
中定义的几种状态
- NEW
- RUNNABLE
- BLOCK
- WATTING
- TIMED_WATTING
- TERMINATED
sleep 线程休眠
- sleep指定当前线程阻塞的毫秒数
- sleep存在异常
InterruptedException
- sleep时间到达以后线程进入就绪状态
- sleep可以模拟网络延迟,倒计时等
- 每个对象都有一个锁,sleep不会释放锁
sleep()和wait()的区别
- 所属类不用,
sleep()
是Thread
类的方法,wait()
是Object
类的方法 - 时间不同(参数),
sleep()
必须指定时间,wait()
可以指定也可以不指定 - 释放锁不同.
sleep()
释放CPU资源但是不释放同步锁,wait()
释放cpu资源也释放同步锁 - 使用地方不同,
sleep()
可以在任意地方使用,wait()
只能在同步代码方法或者同步代码块中使用
yield 线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程将运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功!看COU心情
join 合并线程
- join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞
- 可以想想成插队(我是VIP,请你靠边,让我先来)
线程状态观测
死亡之后的线程就不能再次启动了,
start()
不能调用两次
daemon 守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行线程
- 比如.操作日志记录,监控内存线程,垃圾回收线程等是守护线程
线程同步
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步.
- 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池行程队列,等待前面的线程使用完毕,下一个线程在使用
多线程访问同一个对象叫做并发问题. 厕所时解释队列和锁最形象的比喻
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时也带来访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加上 锁机制 - synchronized ,当一个线程获得对象的排他锁,独战之源,其它线程必须等待,使用后释放锁即可,但是存在以下问题
- 一个线程持有锁会导致其它所有需要此锁的线程挂起
- 在多锁竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
同步方法
- 由于我们可以通过
private
关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized
关键字,它包括两种用法;synchronized方法和synchronized块
private synchronized void doSomething() {}
synchronized
方法控制对象
的访问,每个对象对应一把锁,每个synchronized
方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,得以继续执行
缺陷 若将一个大的方法声明为
synchronized
将会影响效率
同步块
synchronized (obj) {}
Obj
称之为 同步监视器- obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定访问
死锁
- 多个线程各自占有一些资源,并且互相等待其它线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放锁,都停止执行的情形,某一个同步块同事拥有两个以上对象的锁 时就可能发生死锁的问题
产生死锁的四个必要条件
- 互斥条件;一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放.
- 不剥脱条件:进程已获得资源,在未使用完之前,不能强行剥夺.
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
上面列出了死锁的四个必要条件,我们只要想办法破解其中任意一个或多个条件就可以避免死锁发生
Lock 锁
- 从jdk1.5开始,java提供了更强大的线程同步机制—通过显式定义同步锁对象来实现同步.同步锁使用
Lock
对象充当 java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应该获得Lock
对象ReentrantLock
可重入锁- 类实现了Lock
接口,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比价常用的是ReentrantLock
,它可以显示加锁,释放锁.
class TestLock2 implements Runnable {
int ticketNums = 10;
//定义lock
private final ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticketNums > 0) {
try {
Thread.sleep(1000);
System.out.println(ticketNums--);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}finally {
lock.unlock();
}
}
}
}
synchronized
和lock
对比
Lock
是显式锁(手动打开和关闭,)synchronized
是隐式锁,出了作用域自动释放Lock
只有代码块锁,synchronized
有代码块锁和方法锁- 使用
Lock
锁,JVM
将话费较少的时间来调度线程,性能更好.并且具有更好的扩展性能(提供更多的子类) - 优先使用顺序:
Lock
-> 同步代码块(已经进入了方法体,分配了响应资源)->同步方法(在方法体之外)
线程通信问题
生产者消费者模式
线程通信分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
- 在生产者消费问题中,仅有
synchronized
是不够的synchronized
可阻止并发更新同一个共享资源,实现了同步- 但是``synchronized`不能用来实现不同线程子啊经典额通信(消息传递)
java提供了几个方法来解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程会一直等待,直到其它线程通知,与sleep不同,wait它会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait() 的线程,优先级别高的线程优先调度 |
注意: 上面都是Object
类的方法,都只能在同步火狐或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
线程通信的解决方法–并发协作模型"生产者/消费者模式"
管程法–通过缓冲区解决
- 生产者:负责生产数据的模块(可能是对象,方法,线程,进程)
- 消费者:负责处理数据的模块(可能是对象,方法,线程,进程)
- 缓冲区: 消费者不能直接使用生产者的数据,它们之间有个缓冲区
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
信号灯法 --一般通过标志位解决
线程池
- 背景: 经常创建和销毁线程,使用量特别大的资源,比如并发情况下的线程,对性能阴影响很大
- 思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完返回池中,可以避免频繁创建销毁,实现重复利用,类似生活中的交通工具
- 好处
- 提高响应速度,(减少了重复创建线程的时间)
- 降低资源消耗,利用线程池资源,避免重复创建
- 便于线程管理,
- corePoolSize:核心池的大小
- maximumPoolSize: 最大线程池数
- keepAliveTime: 线程没有任务时最多保持多长时间后终止
使用线程池
- JDK5提供了线程池相关api:
ExecutorService
和Executors
ExecutorService
; 真正的线程池接口.常见子类ThreadPoolExecutor
,它有一个父接口是Executor
void execute(Runnable command);
这是Executor
接口的的方法,<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
void shutdown();
:关闭线程池
Executors
:线程池的工厂类,用来创建不同的线程池,newCachedThreadPool
创建一个可以根据需要创建新线程的线程池,- 池中线程数量没有固定,可达到最大值(INT.MAX_VALUE)
- 池中线程可进行缓存重复利用和回收
- 当池中没有线程时,会重新创建新线程
- 创建方式:
Executors.newCachedThreadPool();
newFixedThreadPool
创建一个可重用固定数量的线程池,- 线程池中数量固定,可以控制并发量
- 线程可以被重复使用,在线程池显式的被关闭前.都讲一直存在
- 超出一定量的线程被提交时需要在队列中等待
Executors.newFixedThreadPool(int nThreads)
;//nThreads为线程的数量Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);
//nThreads为线程的数量,threadFactory创建线程的工厂方式
newSingleThreadExecutor
创建一个使用单个 worker 线程的 ExecutornewScheduleThreadPool
一个给定延迟空期或者定期执行newSingleThreadScheduledExecutor
建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行
常用知识点
ThreadLocal
ThreadLocal是一个线程内部的存储类,可以在指定线程内存中存储数据,只有指定线程可以得到存储数据.
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值
ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复
Synchronized与Lock的区别
-
Synchronized能实现的功能Lock都可以实现,而且Lock比Synchronized更好用,更灵活。
-
Synchronized可以自动上锁和解锁;Lock需要手动上锁和解锁
-
synchronized是在锁的地方加了MONITOR,LOCK是接口,主要实现类是ReentranceLock,Lock 底层是通过 AQS + CAS 机制来实现的。基于AQS.synchronized不是公平锁,lock可以是公平锁,也可以不是。都是可重入锁。
Runnable和Callable的区别
-
Runnable接口中的方法没有返回值;Callable接口中的方法有返回值
-
Runnable接口中的方法没有抛出异常;Callable接口中的方法抛出了异常
-
Runnable接口中的落地方法是call方法;Callable接口中的落地方法是run方法
原子性的保障
- 一个变量简单的读取和赋值操作是原子性的,将一个变量赋值给另外一个变量不是原子性的。
- Java内存模型(JMM)仅仅保障了变量的基本读取和赋值操作是原子性的,其他均不会保证的。如果想要使某段代码块要求具备原子性,就需要使用 synchronized 关键字、并发包中的 Lock 锁、并发包中 Atomic 各种类型的原子类来实现,
- 而 volatile 关键字修饰的变量,恰恰是不能保障原子性的,仅能保障可见性和有序性。