多线程
让程序充分利用CPU的资源,提高CPU的使用率,解决高并发带来的负载均衡问题
优点:
- 资源得到合理利用
- 程序设计更加简洁
- 程序响应速度更快
缺点:
- 需要更多的内存空间来支持
- 多线程并发带来的数据准确性
- 数据被多线程共享,可能出现死锁情况
进程和线程
进程是计算机正在运行的一个独立应用程序,是一个动态的概念。线程是组成进程的基本单位,一个进程中包含一个或多个线程。进程和线程都是应用程序在执行过程中的概念,如果应用程序没有运行,则不存在进程和线程
区别在于进程运行时内存空间是独立的,每个进程互不干扰,线程是共享内存空间,每个线程的运行相对独立,线程必须依赖进程才能执行,多线程就是一个进程中多个线程同时执行
在单核CPU的情况下多线程并非同时执行,而是交替执行。在多核CPU上,才是真正的同时执行
普通方法与多线程方法区别
普通方法与多线程方法的执行顺序如下

一个进程中可以有多个线程,进程由系统分配
多线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
继承Thread类
- 自定义线程类继承Thread类
- 重写
run()方法,编写线程执行体 - 创建线程对象,调用
start()方法启动线程
/**
* 多线程创建
*/
public class ThreadTest extends Thread {
// 重写
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("多线程..." + i);
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
// 启动线程
threadTest.start();
for (int i = 0; i < 50; i++) {
System.out.println("主方法..." + i);
}
}
}
注意:线程不一定立即执行,由CPU安排调度
Thread类常见方法:
Thread类也是实现了Runnable,因为继承具有局限性(只能单继承),所以推荐使用实现Runnable方式
实现Runnable接口
- 自定义类实现Runnable接口
- 实现
run()方法,编写线程执行体 - 创建线程对象调用
start()方法启动线程
/**
* 多线程
*/
public class ThreadTest2 implements Runnable {
// 重写
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("多线程..." + i);
}
}
public static void main(String[] args) {
ThreadTest1 threadTest2 = new ThreadTest1();
// 启动线程
new Thread(threadTest2).start();
for (int i = 0; i < 50; i++) {
System.out.println("主方法..." + i);
}
}
}
并发问题
例如抢票问题
/**
* 多线程问题
*/
public class Ticket implements Runnable {
// 票数
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到第" + ticketNum-- + "张票");
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "张三").start();
new Thread(ticket, "李四").start();
new Thread(ticket, "王五").start();
}
}
执行结果
王五拿到第10张票
张三拿到第9张票
李四拿到第10张票
张三拿到第8张票
李四拿到第7张票
王五拿到第6张票
张三拿到第3张票
王五拿到第5张票
李四拿到第4张票
王五拿到第2张票
李四拿到第1张票
张三拿到第0张票
数据出现紊乱,线程不安全
实现Callable接口
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务
- 提交执行
- 获取结果
- 关闭服务
/**
* 多线程创建
*/
public class ThreadTest3 implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
// 操作内容
System.out.println(Thread.currentThread().getName());
return false;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest3 item1 = new ThreadTest3();
ThreadTest3 item2 = new ThreadTest3();
ThreadTest3 item3 = new ThreadTest3();
// 创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交执行并获取结果
Boolean result1 = executorService.submit(item1).get();
Boolean result2 = executorService.submit(item2).get();
Boolean result3 = executorService.submit(item3).get();
// 关闭服务
executorService.shutdown();
}
}
三种实现方式的区别
| 方式 | 有点 | 缺点 |
|---|---|---|
| 继承Thread类 | 编程简单,可直接使用Thread中方法 | 可扩展性差,无法获得结果 |
| 实现Runnable接口 | 扩展性强 | 不能直接使用Thread类中方法,无法获得结果 |
| 实现Callable接口 | 扩展性强,可获得结果 | 不能直接使用Thread类中方法,编程复杂 |
线程状态
线程方法
| 方法 | 说明 |
|---|---|
| setPriority(int newPriority) | 更改线程的优先级 |
| static void sleep(long millis) | 线程休眠时间,毫秒 |
| void join() | 等待该线程终止 |
| static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
| void interrupt() | 中断线程 |
| boolean isAlive() | 测试线程是否处于活动状态 |
线程停止
JDK提供的方法不建议使用了,推荐线程自己结束,示例
public class ThreadTest implements Runnable {
// 线程是否结束标识
private boolean flag = true;
@Override
public void run() {
while (flag) {
System.out.println("run...");
}
}
// 对外提供结束方法
public void stop() {
this.flag = false;
}
}
线程休眠
sheep()方法能指定当前线程阻塞多少毫秒,存在InterruptedException异常,当阻塞结束后会进入就绪状态,用于模拟网络延时,倒计时等,每个对象都有一个锁,sheep()不会释放锁
守护进程
// 默认是false表示用户进程,设置为true为守护进程
thread.setDaemon(true);
线程分为用户进程和守护进程,虚拟机不用等待守护进程执行完毕,也就是说,当同一个方法中的用户线程结束后,即便守护进程是个死循环在下一轮监监控也会将守护进程结束
并发
同一个资源(对象、数据)被多个线程同时操作
线程同步
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问的准确性,在访问时会加入锁机制(synchronized),当一个对象获得对象的排它锁,独占资源,其他线程必须等待,使用后再释放锁
如何判断多个线程是否实现了同步,需要看锁的资源在内存中有几份,如果只有一份,多线程需要排队,如果有多份,则不会排队,也不会实现同步
存在问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引发性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置问题,影响性能
同步方法
public synchronized void method() {
// TODO
}
synchronized方法控制对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象锁才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面阻塞的线程才能获得锁,继续执行
缺陷:若将一个大的方法声明为synchronized将会影响效率
锁的对象就是变化的量,需要增删改的对象
同步代码块
synchronized (Obj) {
// TODO
}
Obj称为同步监视器,可以是任何对象,是需要锁的变化的量,同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,即对象本身,或者class
Lock锁
从JDK5.0开始,Java提供了更强大的线程同步锁机制–显式定义同步锁对象来实现同步
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run () {
try {
// 加锁
lock.lock();
} finally {
// 解锁
lock.unlock();
}
}
这是可重入锁,能指定重入时间,在指定时间内能否拿到锁
private final ReentrantLock lock = new ReentrantLock();
try {
// 获取锁,3秒内,即等待3秒是否能得到锁
if (lock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 判断是否是当前线程持有锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
synchronized和lock
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域就自动释放锁
- 使用Lock时JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供了更多的子类)
使用顺序:Lock > 同步代码块 > 同步方法
生产者和消费者
涉及到线程之间的通信问题,生产者生产出来后通知消费者消费,如果消费者消费发现没有就等待
管程法
核心在于需要一缓冲区,来负责商品的控制
// 消费者消费商品
public synchronized void pop() {
// 没有货物
if(countGoods == 0) {
try {
// 等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 等待生产者通知开始消费
countGoods--;
// 消费完,通知生产者生成
this.notifyAll();
}
// 生产者同理
信号灯法
同理
死锁
多个线程因为争夺同一个资源而形成的一种互斥状态,导致每个线程都无法继续进行,程序一直处于卡顿状态
Callable结合FutureTask获取结果
// 查询用户总数
Callable<Integer> callableUserCount = () -> userService.count();
FutureTask<Integer> taskUserCount = new FutureTask<>(callableUserCount);
Thread threadUserCount = new Thread(taskUserCount);
// 查询用户总数(直接写代码方式)
Callable<Long> callableAll = () -> mongoTemplate.count(new Query(), InvitationRegister.COLLECTION_NAME);
FutureTask<Long> taskAllUserNum = new FutureTask<>(callableAll);
Thread threadAllUser = new Thread(taskAllUserNum);
// 查询会员总数
Callable<Integer> callableVipCount = () -> userService.count(wrapperVip);
FutureTask<Integer> taskVipCount = new FutureTask<>(callableVipCount);
Thread threadVipCount = new Thread(taskVipCount);
// 统一启动线程
threadUserCount.start();
threadVipCount.start();
// 获取结果
Integer userCount = taskUserCount.get(10, TimeUnit.SECONDS);
Integer vipCount = taskVipCount.get(10, TimeUnit.SECONDS);