Java进阶之线程
Java进阶之线程
线程的概念
- 可以同步一起执行
- 线程和进程
- 一个进程就是一个应用,而线程是在进程内部衍生出来的
- 也可以说,线程是进程的最小组成单元
- 并行和并发
- 并行意味着一起开启
- 并发意味着不但一起开启,同时CPU同时进行处理
- 并行是并发产生的前序条件
- 并行一般不会对机器产生较大压力,但是并发会产生大量压力
- 降低线程的并发 RabbitMQ
- 程序执行时间比较长--有效优化程序的执行时长(拆解)eg:消息队列
- 底层一些机制 --排队机制
- 同步和异步
- 同步是指发送一个请求,需要等待返回后才能发送下一个请求,有等待过程
- 异步是指发送一个请求后,不需要等待返回,随时可以发送下一个请求,即不需要等待
- 同步是用于确保资源一次只能被一个线程使用,同步相比非同步,性能会相差三到四倍
线程运行机制
- 就绪状态(新生)
- 执行状态(执行)
- 挂起状态
- 阻塞
- 等待
- 超时等待
- 死亡状态(终止)
- 每次在执行java.exe时,都会有一个java虚拟机进程启动,执行程序带代码的任务是由线程来完成的,所以,每一个线程都一个独立的程序计数器 和方法调用栈
- 程序计数器:也称为PC寄存器,当线程执行一个方法时,程序计数器会指向方法区中的下一条要执行的字节码指令
- 方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程
- 每当用Java命令启动一个Java虚拟机进程时,Java虚拟机都会创建一个主线程,该线程也就是main,也就是诚如的入口
使用线程
- 继承Thread类,重写run方法
- 优点:代码简单。
- 缺点:无法继承别的类
public class ExtendsThread extends Thread{ private String name; public ExtendsThread(String name){ this.name = name; } public void run(){ for(int i = 1;i<=100;i++){ System.out.println(name+"跑了"+i+"米"); } } public static void main(String[] args) { ExtendsThread extendsThread1 = new ExtendsThread("李麒麟"); ExtendsThread extendsThread2 = new ExtendsThread("吉祥物"); ExtendsThread extendsThread3 = new ExtendsThread("神兽"); extendsThread1.start(); extendsThread2.start(); extendsThread3.start(); } }
- 实现Runnable接口,实现run方法
- 优点:可以继承其他类,实现同一接口的多个线程对象共享一个任务类对象,即多线程共享一份资源
- 缺点:代码复杂,访问当前线程必须要使用Thread.currentThread()方法
public class ImplementsRunnable implements Runnable{ private String name; public ImplementsRunnable(String name){ this.name = name; } @Override public void run() { for(int i = 1;i<=100;i++){ System.out.println(name+"跑了"+i+"米"); } } public static void main(String[] args) { ImplementsRunnable implementsRunnable1 = new ImplementsRunnable("起名字真是太难了"); ImplementsRunnable implementsRunnable2 = new ImplementsRunnable("起名字真是超级难"); ImplementsRunnable implementsRunnable3 = new ImplementsRunnable("起名字真是难爆了"); Thread thread1 = new Thread(implementsRunnable1); Thread thread2 = new Thread(implementsRunnable2); Thread thread3 = new Thread(implementsRunnable3); thread1.start(); thread2.start(); thread3.start(); } }
- 实现Callable接口
- 优点:可以继承其他类,多线程共享一份资源,还具有返回值,可以跑出返回值的异常
- 缺点:代码复杂,访问当前线程必须要使用Thread.currentThread()方法
- 当需要调用get()方法时,如果线程还未执行完毕,则会阻塞到线程执行完毕后拿到返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ImplementsCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int i = 0; for (i = 0;i<20;i++){ if(i == 10){ break; } } return i; } public static void main(String[] args) { ImplementsCallable implementsCallable = new ImplementsCallable(); FutureTask<Integer> futureTask = new FutureTask<>(implementsCallable); Thread thread1 = new Thread(futureTask,"线程1"); thread1.start(); try { System.out.println(Thread.currentThread().getName()+" "+futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
- 常用方法
- 获取线程名字:getName()
- 获取线程标识:getId()
- 获取线程优先级:getPriority()
- 更改线程优先级:setPriority()
- 判断线程是否存活:isAlive()
- 设置线程为守护线程:setDaemon(true)
- 判断线程是否为守护线程:isDaemon()
- 设定线程名字:setName()
- 让线程进入阻塞状态:sleep()
- 启动线程:start()
- 线程体:run()
- join(),当前执行的线程调用其他线程,让自己进入阻塞状态,当其他线程执行完毕,自己再继续执行
- sleep()方法进入阻塞状态后,在执行时不会释放对象锁,如果被join()方法中断,则会抛出异常,同时清除中断状态
- yield(),一般用于测试,提高线程并发效果,但执行完毕后会让线程优先级高的,或同等级的线程优先执行
线程锁
- 生产消费者模型
- 线程AB共享固定大小缓冲区
- 生产者:A产生数据存放到缓冲区
- 消费者:B从缓冲区读取数据计算
- 在多线程中,如果生产者生产数据的速度过快,导致消费者数据无法快速使用完毕,生产者就必须等待消费者消费完毕数据后,再次生产数据,为了生产消费达到一定的动态平衡,就需要一个缓冲区存放生产者的数据,这就是所谓的生产者-消费者模式
- 特点
- 保证生产者在缓冲区已满的时候不会再向缓冲区内写入数据,消费者也不会在缓冲区为空的时候消费数据
- 当缓冲区满时,生产者会进入休眠,当消费者消耗缓冲区数据后,缓冲区有空余空间,生产者会被唤醒,继续往缓冲区中写入数据,同理,当缓冲区为空时,消费者会进入休眠,当生产者将数据写入缓冲区后,消费者会被唤醒,消费缓冲区内的数据进行计算。
- 锁机制
- 互斥锁synchronized --隐式锁
- 对象锁synchronized("锁A")
- 类锁synchronized(A.class) = public synchronized static void A(){}
- synchronized是关键字,由虚拟机自动释放
- 加锁方式:方法锁
public synchronized void sy(){ //将方法加锁 }
- 加锁方式:锁块
public class TestTrainTickets { static class TrainTickets implements Runnable{ int trainTickents = 100; @Override public void run(){ while (trainTickents>0){ synchronized (this){ if(trainTickents<=0){ System.out.println(Thread.currentThread().getName()+"没票了,请关注下次列车"); break; }else{ System.out.println(Thread.currentThread().getName()+"出售车票第"+(100-trainTickents+1)+"张"); trainTickents--; } try { Thread.sleep(new Random().nextInt(500)+1); } catch (Exception e) { e.printStackTrace(); } } } } } public static void main(String[] args) { TrainTickets trainTickets = new TrainTickets(); Thread thread1 = new Thread(trainTickets,"一号窗口"); Thread thread2 = new Thread(trainTickets,"二号窗口"); Thread thread3 = new Thread(trainTickets,"三号窗口"); Thread thread4 = new Thread(trainTickets,"四号窗口"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
- 重入锁ReentrantLock 显示锁
- ReentrantLoct默认为非公平锁,可以通过构造器更改
<!-- public ReentrantLock(){ //非公平锁 sync = new NonfairSync(); } public ReentrantLock(boolean fair){ //true为公平锁,false为非公平锁 sync = fair ? new FairSync() : new NonfairSync(); }
- 非公平锁NonfairSync()
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发下,可能会出现优先级翻转(两级反转)的情况下
- 公平锁 FairSync()
- 公平锁顾名思义就是先来后到,按照申请顺序公平的获取锁
- lock是方法,加锁后需要手动释放锁
- 获取锁lock()
- 释放锁unlock()
Lock lock = new ReentrantLock(true);//创建锁为公平锁 public void lock(){ //加锁 lock.lock(); //被锁的内容 /释放锁 lock.unlock(); }
- 读写锁read-write lock,又称共享-独占锁
- 读锁,当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功
- 多个加入读锁的线程的程序之间可以并行访问
- 写锁,当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)
- 写锁和读锁之间串行
- 写锁和写锁之间串行
- synchronized和lock的区别
- Synchronized 内置的 Java 关键字;Lock 是一个 Java 类
- Synchronized 无法判断获取锁的状态;Lock 可以判断是否获取到了锁
- Synchronized 会自动释放;Lock 必须要手动释放锁!如果不释放 会产生死锁
- Synchronized 线程 1(获得锁,阻塞),线程 2(等待获取,死等);Lock 锁就不一定会等待下去
- Synchronized 可重入锁,不可以中断的,非公平;Lock 可重入锁,可以判断锁,非公平(可以自己设置)
- Synchronized 适合锁少量的代码同步问题;Lock 适合锁大量的同步代码!
- ConncurrentHashMap
- 支持高并发情况下的map容器,支持复合操作,采用分段锁管理机制
- Hashtbale是独占锁,在高并发下性能不好,不支持符合操作
- 使用Collections.synchronizedList(new ArrayList()),可以将一个非同步集合变成同步集合
- CountDownLatch
- 一个同步容器的辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
- 常用构造器:CountDownLatch(int count) 锁存器的变量值
- 常用方法
- 线程唤醒:await() 当锁存器变量不等于0,一直进行等待状态,当锁存器变量等于0,将等待的线程唤醒
- 递减锁存器的值:countDown() 递减锁存器的值,直到变量为0,释放所有正在等待的线程,用于finally块中,防止有的线程一直在等待
- 锁的应用场景
- 并发导致了一些数据不一致的情况下
- 考虑先后顺序的
- 悲观锁和乐观锁
- 悲观锁:悲观的认为所有操作都会因为并发出现问题,所有操作必须串行
- 乐观锁:乐观的认为读操作一般不会出现问题,写操作才会,所以对读操作不会产生串行效果,对写操作不友好
- 活锁和死锁
- 死锁发生在当两个或多个线程一直在等待另一个线程持有的锁或资源的时候。这会导致一个程序可能会被拖垮或者直接挂掉,因为线程们都不能继续工作了。
- 活锁是另一个并发问题,它和死锁很相似。在活锁中,两个或多个线程彼此间一直在转移状态,而不像我们上个例子中互相等待。结果就是所有线程都不能执行它们各自的任务。
- 自旋锁和阻塞锁
- 自旋锁:是指当线程获取锁失败的时候,去执行一个无意义的循环,循环结束后再重新去竞争锁,如果竞争不到则继续循环。整个过程中线程一直处于运行(running)状态。
- 阻塞锁:和自旋锁相对,指当线程获取锁失败时,线程进入阻塞(blocking)状态,当获取相应的信号时(唤醒,时间),进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
线程的通讯
- 等待:wait()
- 等待一定时间:wait(long)
- 唤醒:notify()
- 唤醒所有:notifyAll()
- wait方法和sleep方法的区别
- wait是Object类的,sleep是Thread类的方法
- wait方法必须放在同步代码块或同步方法在惠普那个,sleep不需要
- wait方法必须有notify,notifyAll方法唤醒,才能继续执行,sleep方法会在执行完时间段后,自动唤醒执行
- wait方法必须妨碍notify方法的后面
- wate方法可以被中断,sleep如果被中断会抛出异常
- wait在执行时会释放锁,sleep在执行过程中不会释放锁
线程池
- 线程池的实现原理
- 提供了一个线程的队列,队列中保存着所有等待状态的线程
- 避免了创建与销毁操作额外的开销,提高了响应速度
- 使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务
- 多线程运行时,系统不断启动和关闭线程,资源会过渡消耗,以及切换线程时会产生危险,从而导致系统资源的崩溃
- 线程池的工作机制
- 在线程池的编程模式下,任务提交给线程池,而不是直接提交给某个线程
- 线程池在拿到任务后,在内部寻找有空闲的线程
- 一个线程同时只能执行一个任务,但是可以向一个线程提交多个任务
- 线程池体系结构
- java.util.concurrent包
- Excutor:负责线程的使用与调度的根接口
- ExcutorService子接口:线程池的主要接口
- ThreadPoolExecutor:线程池的实现类
- ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor , 实现了ScheduledExecutorService接口,负责线程池中的线程调度
- 有些时候,线程不允许使用Excutors去创建,而是通过ThreadPoolExecutor的方式,这样的方式是为了规避资源耗尽的风险
- Executors线程池对象的弊端
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
- CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM
- 常用线程池
- ExecutorService
- ExecutorService
- ExecutorService
- ScheduledExecutorService
- 线程池的应用
- 创建执行的线程池
- submit():给线程池中的线程分配任务,返回值返回的是Future接口
- shutdown()关闭线程池