多线程
- 程序:指令和数据的有序集合,其本省没有任何运行的含义,是一个静态的概念。
- 进程:是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位
- 线程:一个进程包含若干个线程,一个进程中至少有一个线程。线程是CPU调度和执行的单位
1.线程创建
1.1 继承Thread类
自定义线程类继承Thread类;重写run()方法,编写线程执行体;创建线程对象,调用start()方法
1 | package threadDemo; |
1.2 实现Runnable接口
定义一个类实现Runnable接口,实现run()方法,编写线程执行体,创建线程对象,调用start()方法启动线程;推荐使用Runnable对象,java单继承有局限性
1 | package threadDemo; |
*TicketDemo
多个线程操作同一个对象
1 | package threadDemo; |
1.3 实现callable接口
- 实现callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService es = Executor.newFixedThreadPool(1);
- 提交执行:Future<T> future = es.submit(t1);
- 获取结果:T result = future.get();
- 关闭服务:es.shutdownNow();
1 | package threadDemo; |
2.线程状态
| 状态名称 | 说明 |
|---|---|
| NEW | 初始状态,线程被构建,但是还没有调用start() |
| RUNNABLE | 运行状态,java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中” |
| BLOCKED | 阻塞状态,表示线程阻塞于锁 |
| WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
| TIME_WAITING | 超时等待状态,该状态不同于waiting,它是可以在指定的时间自行返回的 |
| TERMINATED | 终止状态,表示当前线程已经执行完毕 |


2.1线程方法
| 方法 | 说明 |
|---|---|
| setPriority(int newPriority) | 更改线程的优先级 |
| static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
| void join() | 等待改线程终止 |
| static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
| void interrupt() | 中断线程(不建议使用) |
| boolean isAlive() | 测试线程是否处于活动状态 |
2.2 线程终止
使用标志位,设置一个公开方法该变标志位状态,停止线程
1 | package threadDemo; |
2.3 线程休眠
sleep(long millis)指定当前线程阻塞的毫秒数;
sleep存在异常interruptedException;
sleep时间到达后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等。
每一个对象都有一个锁,sleep不会释放锁;
1 | package threadDemo; |
2.4 线程礼让
- 礼让线程,让当钱正在执行的 线程暂停,但不阻塞;
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功
1 | package threadDemo; |
2.5 join
join合并线程,只有此线程执行完成后,再执行其他线程,其他线程阻塞
1 | package threadDemo; |
2.6 查看线程状态
Thread.state
1 | package threadDemo; |
3.线程优先级
- java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级用数字表示,范围从1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- 使用getPriority()获取 setPriority(int xxx)修改优先级
优先级低只意味着获得调度的概率低,并不是优先级低就不会被调用,都是看CPU的调度
4.守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
后台记录操作日志、监控内存、垃圾回收等待..
1 | package threadDemo; |
5.线程同步
并发:多个线程操作同一个资源
线程同步:一种等待机制,多个需要同时访问此对象的线程进入对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
- 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的锁,独占资源,其他线程必须等待使用后释放锁。存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题;
买票案例
1 | package threadDemo; |
解决方法:使用同步方法,使用synchronized关键字修饰方法和块;
1 | package threadDemo; |
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。(缺陷:若将一个大的方法使用synchronized修饰会影响效率)
方法里需要修改的内容才需要加锁,避免过多浪费资源。
同步块:synchronized(obj){}
obj称为同步监视器
- obj可以是任何对象,最好使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,同步方法的同步监视器就是this,就是对象本身或者是class
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码。
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,释放同步监视器。
- 第二个线程访问,锁定同步监视器并访问,访问完毕后释放同步监视器。
6.死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情况;某一个同步块同时拥有“两个以上对象的锁”时,就可以会发生“死锁”的问题。
1 | package threadDemo; |
产生死锁的必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
7.lock(锁)
- 通过显示定义同步锁对象来实现同步,同步锁使用lock对象充当;
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReenTrantLock,可以显式加锁、释放锁。
1 | package threadDemo; |
synchronized与lock对比:
- lock是显式锁(需要手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放。
- lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用lock锁,jvm将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。
- 优先使用顺序:lock > 同步代码块(已经进入方法体,分配了相应资源)> 同步方法(在方法体外)
8.线程协作
生产者消费者问题:生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,有需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品,以供消费。
- 在生产者消费者问题中,仅有synchronized是不够的;
- synchronized可阻止并发更新同一个共享资源,实现同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
线程通信:
| 方法名 | 作用 |
|---|---|
| wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
| wait(long timeout) | 指定等待的毫秒数 |
| notify() | 唤醒一个处于等待状态的线程 |
| notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级高的线程优先调度 |
均是object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
管程法:
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区;
- 生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
1 | package threadDemo; |
9.线程池
背景:经常创建和销毁、使用量特别大的资源,对性能影响很大。
思路:提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁,实现重复利用。
优点:
提高响应速度(减少创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
- corePoolSize:核心池大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,没有返回值,一般用来执行Callable
- void shutdown():关闭线程池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。