目录
多线程
是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1] 。
程序和线程
程序是指令和数据的有序集合,其本身没有任何的含义,是一个静态的概念。
进程是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位
通常在一个进程中可以包含若干线程,当然一个进程周至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位
注:真正多线程是指有多个cpu,即多核,如服务器,如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个线程,因为切换的很快,所以就造成同时执行的错觉。
核心概念
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
- 对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制
- 线程会带来额外的开销。如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
一、Thead类
- 自定义线程类继承Thread类
- 重写**run()**方法,编写线程体
- 创建线程对象,调用**start()**方法启动线程
测试
public class MyThreadTwo extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
new MyThreadTwo().start();
二、Runnable 接口–推荐使用:java单继承的局限性
- 自定义线程类实现Runnable接口
- 重写**run()**方法,编写线程体
- 创建线程对象,调用**start()**方法启动线程
测试
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
new Thread(new MyRunnable()).start();
Callable 接口(返回结果)
1. 实现Callable接口,需要返回值类型
2. 重写call方法,需要抛出异常
3. 创建目标对象
4. 创建执行服务: ExecutorService ser=Executors.newFixedThreadPool(1)
5. 提交只想:Future result1=ser.submit(t1)
6. 获取结果:boolean r1=result1,get()
7. 关闭服务: ser.shutdownNow()
区别:
shutdown() 只是关闭了提交通道,用submit()是无效的;而内部该怎么跑还是怎么跑,跑完再停。
shutdownNow() 能立即停止线程池,正在跑的和正在等待的任务都停下了。
测试
public class MyCallable implements Callable<Boolean> {
@Override
public Objectcall() throws Exception {
int sum=0;
for (int i = 0; i <= 100; i++) {
sum+=i
}
return sum;
}
}
方式一:FutureTask
MyCallable myCallable = new MyCallable();
FutureTask futureTask=new FutureTask(myCallable);
new Thread(futureTask).start();
// 会等待线程执行结束才会去获取结果
Integer i= (Integer) futureTask.get();
方式二:ExecutorService
MyCallable myCallable=new MyCallable();
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Boolean> r1 = executorService.submit(myCallable);
Boolean aBoolean = r1.get();
System.out.println(aBoolean);
executorService.shutdown();
并发
核心: 多个线程共同操作同一个对象
测试
public class MyRunnable implements Runnable {
private int ticket=10;
@Override
public void run() {
while (true){
if (ticket==0){
break;
}
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"ticket:"+ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MyRunnable myRunnable=new MyRunnable();
new Thread(myRunnable,"A").start();
new Thread(myRunnable,"B").start();
new Thread(myRunnable,"C").start();
#### 结果
Bticket:10
Aticket:9
Cticket:10
Cticket:8
Bticket:8
Aticket:8
Bticket:7
Aticket:7
Cticket:6
Aticket:4
Cticket:5
Bticket:4
Bticket:3
Cticket:2
Aticket:1
Lamda表达式
new Thread(()->{System.out.println(1); }).start();
优点
- 避免匿名内部类定义过多
- 可以让你的代码看起来很简洁
- 去掉一推没有意义的代码,只留下核心逻辑
本质
Lambda表达式其核心就是函数式接口,对于函数式接口都可以通过lambda表达式来创建接口对象
函数式接口:任何接口,如果只创建唯一一个抽象方法,那么它就是一个函数式接口
Lambda由来
匿名内部类—>lambda
匿名内部类升级版–>lambda
函数式接口:
public interface MyInterface {
void print();
}
匿名内部类:
MyInterface myInterface1= new MyInterface(){
@Override
public void print() {
System.out.println(1111111);
}
}myInterface1.print();
lambda:
MyInterface myInterface2=()->{
System.out.println(2222222);
};
myInterface2.print();
Lambda 简化写法
public interface MyInterface {
void print(String a);
// void print(String a,int b);
}
去掉参数类型
MyInterface myInterface1=(a)->{
System.out.println(a);
};
myInterface1.print("测试参数");
去掉小括号
前置条件:只有一个参数才可,多参数不行
MyInterface myInterface1=a->{
System.out.println(a);
};
myInterface1.print("测试参数");
去掉花括号
前置条件:方法中只有一行代码
MyInterface myInterface1=a->
System.out.println(a);
myInterface1.print("测试参数");
线程状态
线程停止
推荐线程自己停止—>通过设置flag标识来控制线程的停止
private boolean flag=true:
public void run() {
while (flag){
if (ticket==0){
break;
}
}
}
public void stop(){
this.flag=false
}
线程休眠
- sleep(时间)指定当前线程休眠的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时、倒计时等
- 每一个对象都有一个锁,sleep不会释放锁。
线程礼让–yield
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重现调度,礼让不一定成功,看CPU心情
线程插队— join
- 待此线程结束后,再执行其他线程,其他线程阻塞----vip插队服务
MyThreadThree myThreadThree1=new MyThreadThree();
myThreadThree1.start();
for (int i = 0; i < 1000; i++) {
myThreadThree1.join();
System.out.println("主线程:"+i);
}
线程状态–state
线程状态。线程可以处于以下状态之一
- NEW:尚未启动的线程处于此状态
- RUNNABLE:在Java虚拟机中执行的线程处于此状态
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态
- WAITING:正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING:正在等待另一个线程执行动作达到达到指定时间点的此线程处于此状态
- TERMINATED:已退出的线程处于此状态
线程优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级来决定应该调度哪个线程来执行
- 线程优先级使用数字标识,范围1~10
- 使用以下方式改变或获取优先级
- getPriority()
- setPriority(int X)
优先级只是意味着获得调度的概率低,并不一定优先级低就不会被调用。
守护线程-setDaemon(boolean)
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如 日志、垃圾回收等
虚拟机执行完毕,守护线程结束
实际的意思是把一个线程标记为“守护线程”,就是当他是一个“后台线程”or“内部线程”,
线程同步问题(并发)
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这些对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列。等待前面的线程使用完毕,下一个线程再使用。
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时。也带来了访问冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
存在以下问题:
- 一个线程持有锁会导致其他所需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时引起性能问题
- 如有一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级导致,引起性能问题
同步锁机制synchronized
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问结束后自动解锁,其他线程才可加锁进入
方式一 、synchronized方法
修饰符 synchronized 返回值类型 方法名(形参){}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行。否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁。后面被阻塞的线程才能获得这个锁继续执行
测试:
public class MyRunnable implements Runnable {
private int ticket=10;
@Override
public synchronized void run() {
while (true){
if (ticket<=0){
break;
}
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"ticket:"+ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注:若将一个大的方法什么为synchronized 将会影响效率
方式二、synchronized块
synchronized(同步锁){
访问共享资源核心代码块
}
注:同步锁必须是同一把(同一个对象)
synchronized(Obj){
}
- Obj 同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者class
- 同步监视器的执行过程- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
锁对象使用规范:
建议共享资源作为锁对象,对于实力方法建议使用this作为锁对象
对于静态方法建议是使用字节类.class码对象
测试:
public class MyRunnable implements Runnable {
private Integer ticket = 20;
@Override
public void run() {
synchronized (ticket) {
while (true) {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(20);
System.out.println(Thread.currentThread().getName() + "ticket:" + ticket--);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
方式三、Lock(锁)
- 从JDK5.0开始,java提供了更强大的线程同步机制–通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
- 解锁,finally 中执行,防止发生异常,无法解锁
ReentrantLock
public class Account {
private int card;
private int money;
private ReentrantLock lock=new ReentrantLock();
public Account(int card,int money){
this.card=card;
this.money=money;
}
public void reduceMoney(int money){
try {
lock.lock();
Thread.sleep(1000);
if(this.money<money){
System.out.println("当前余额不足");
}else{
System.out.println(Thread.currentThread().getName()+"取钱成功:"+this.money);
int result= this.money-money;
this.money=result;
System.out.println(Thread.currentThread().getName()+"取钱:"+money);
System.out.println(Thread.currentThread().getName()+"余额为:"+this.money);
}
} catch (InterruptedException e) {
}finally {
lock.unlock();
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
互相持有对方锁,等待对方释放
public class Test {
public static void main(String[] args) {
Mackeup mackeup1=new Mackeup(1);
Mackeup mackeup2=new Mackeup(2);
mackeup1.start();
mackeup2.start();
}
}
class Mirror {
}
class Desk {
}
class Mackeup extends Thread {
static Mirror mirror = new Mirror();
static Desk desk = new Desk();
int choice;
public Mackeup(int choice){
this.choice=choice;
}
@Override
public void run() {
if(choice==1){
synchronized (mirror){
System.out.println("获得镜子所");
synchronized (desk){
System.out.println("获得书桌所");
}
}
}else{
synchronized (desk){
System.out.println("获得书桌所");
synchronized (mirror){
System.out.println("获得镜子所");
}
}
}
}
}
解决:释放对象的锁
if (choice == 1) {
synchronized (mirror) {
System.out.println("获得镜子所");
}
synchronized (desk) {
System.out.println("获得书桌所");
}
} else {
synchronized (desk) {
System.out.println("获得书桌所");
}
synchronized (mirror) {
System.out.println("获得镜子所");
}
}
线程通信
核心:当多个线程共同操作共享资源时,线程之间通过某种方式告知自己的状态来,协调资源,避免无效资源争夺
- wait():表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁。
- wait(long timeout):指定等待的毫秒数
- notify():唤醒一个处于等待状态的线程
- notifyAll():唤醒同一个对象上所有嗲用wait()方法的线程,优先级别高的线程优先调度。
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁的创建销毁、实现重复利用。
优点:
- 提高响应速度
- 降低资源消耗
- 便于线程管理
线程池不是一个提高系统的并发能力的策略,是一个更好的管理线程的方案。
线程池主要解决两个问题:
一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接new一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。
二是线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
线程池的创建
有两种:ThreadPoolExecutor 和 Executors。
ThreadPoolExecutor
构造
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:线程池的核心线程数量,也就是最小线程数。它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue队列里。
- maximumPoolSize:线程池的最大线程数量
- keepAliveTime:当前线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内销毁。
- TimeUtil : 时间单位 。配合keepAliveTime使用
- workQueue:阻塞队列,用来保存被添加到线程池中但尚未执行的任务,一般有:直接提交队列、有界任务队列、无界任务队列、任务优先队列
- threadFactory:线程工厂,用来创建线程,一般用默认的。
- handler:“饱和处理机制”,“拒绝策略”——当任务太多来不及处理时,如何拒绝任务。
需注意:当线程数达到核心数的时候,任务是先入队,而不是先创建最大线程数。
线程池本意只是让核心数量的线程工作着,不论是 core 的取名,还是 keepalive 的设定,所以你可以直接把 core 的数量设为你想要线程池工作的线程数。而任务队列起到一个缓冲的作用。最大线程数这个参数更像是无奈之举,在最坏的情况下做最后的努力,去新建线程去帮助消化任务。
线程池尽可能只维护核心数量的线程,提供任务队列暂存任务,并提供拒绝策略来应对过载的任务。
如果线程数已经达到核心线程数,那么新增加的任务只会往任务队列里面塞,不会直接给予某个线程,如果任务队列也满了,新增最大线程数的线程时,任务是可以直接给予新建的线程执行的,而不是入队。
三种阻塞队列
- SynchronousQueue:无缓冲无界等待队列,超出核心线程个数的任务时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:0。
//2个核心线程最大线程为3的线程池
Executor executors = new ThreadPoolExecutor(
2, 3, 30, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new RejectHandler());
- ArrayBlockingQueue:基于数组的先进先出的有界队列,超出核心线程个数的任务时,将任务加入在此数组,数组大小为创建时指定大小,当任务队列塞满时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:创建时指定队列大小。
//2个核心线程最大线程为3的线程池,阻塞队列大小为2
Executor executors = new ThreadPoolExecutor(
2, 3, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new RejectHandler());
- LinkedBlockingQueue
基于链表的先进先出可选有界队列,超出核心线程个数的任务时,将任务加入在此队列,队列大小为创建时指定大小不指定时为Integer.MAX_VALUE,当任务队列塞满时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数**:默认为Integer.MAX_VALUE否则为创建时指定队列大小。**
Executor executors = new ThreadPoolExecutor(
2, 6, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
new RejectHandler());
整体结构
流程图
Executors
实质:通过Executors的工厂方法来创建线程,其实根本上是调用ThreadPoolExecutor构造方法时传入的参数不同。
阿里开发手册不建议使用线程池,手册上是说线程池的构建不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。 用 Executors 使得用户不需要关心线程池的参数配置,意味着大家对于线程池的运行规则也会慢慢的忽略。这会导致一个问题,比如我们用 newFixdThreadPool 或者 singleThreadPool.允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致 OOM 的风险;而 newCachedThreadPool,允许创建线程数量为 Integer.MAX_VALUE,也可能会导致大量线程的创建出现 CPU 使用过高或者 OOM 的问题。而如果我们通过 ThreadPoolExecutor 来构造线程池的话,我们势必要了解线程池构造中每个参数的具体含义,使得开发者在配置参数的时候能够更加谨慎
线程池也提供了许多可调参数和可扩展性接口,以满足不同情境的需要,可以使用更方便的Executors的工厂方法
比如newCachedThreadPool (缓冲线程池,线程池线程个数最多可达Integer.MAX_ VALUE,线程自动回收)、newFixedThreadPool (固定大小的线程池)、newSingleThreadExecutor (单线程化的线程池)等来创建线程池,当然还可以自定义。
ExecutorService executorService = Executors.newFixedThreadPool(10);
- 实现Runnable
executorService.execute(Runnable runnable); - 实现Callable
executorService.submit()
线程池关闭
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务