Java多线程基础
知识点分割:
- 多线程概述
- 线程的实现:继承Thread、实现Runnable、实现Callable、Lamdb表达式
- 并发初识
- 线程的一些操作
- 线程锁
- 高级主题
1、多线程概述
Process 进程
一个进程可以有多个线程。
Thread 线程
- 线程就是独立的执行路径
- 在程序运行时,几时没有自己创建线程,后台也会有多个线程如主线程、gc线程
- main()称之为主线程,系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统锦咪相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销如cpu调度时间、并发控制
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2、线程的实现
继承Thread
自定义类继承Thread,重写run()方法。线程不一定立即执行,由CPU调度。
调用:实例化自定义类,执行run()或者start();
- run方法会执行完再回来执行主线程;
- start方法与主线程“同时”执行
缺点:单继承局限性
实现Runnable(重点)
创建线程类,实现Runnable接口,实现run方法。创建Thread,将这个线程类作为参数传递,并启动。
调用:实例化线程类,然后创建Thread对象,将线程类传入Thread的构造器,执行Thread对象的start方法。
模拟抢票系统,多线程操纵同一个资源
/**
* 抢车票
* @author: stone
* @create: 2020-08-13 15:14
*/
public class TestRunnable01 implements Runnable {
//车票总数
private int ticketTotal = 50;
@Override
public void run() {
while (true) {
if (ticketTotal <= 0) {
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到第" + ticketTotal-- + "票");
}
}
public static void main(String args[]){
TestRunnable01 ticket = new TestRunnable01();
new Thread(ticket,"小石").start();
new Thread(ticket,"周杰伦").start();
new Thread(ticket,"黄牛").start();
}
}
#结果
周杰伦抢到第50票
黄牛抢到第49票
小石抢到第49票
黄牛抢到第47票
周杰伦抢到第48票
小石抢到第46票
周杰伦抢到第45票
......
实现Callable(了解)
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser=Executors.newFixedThreadPool(1);
- 提交执行:Funture result1=ser.submit(t1);
- 获取执行结果:boolean r1 = result1.get();
- 关闭服务:ser.shutdownNow();
优点:含有返回值,可以抛出异常;
Lamda表达式
概念:简化代码,去除临时变量,让代码核心部分更突出。
函数式接口:只含有一个抽象方法的接口,称之为函数式接口。
进化过程:
- 自定义类实现函数式接口
- 编写内部类实现函数式接口
- 静态内部类实现函数式接口
- 匿名内部类,直接实例化接口并且实现抽象方法
- lambda表达式,写方法体
Thread thread = new Thread(()->{
System.out.println("线程体");
});
3、静态代理
代理模式概念:
- 真是对象和代理对象都要事先统一个接口
- 代理对象代理真是对象
- 优点:
- 代理对象可以做很多真实对象做不了的事情
- 真是对象专注做自己的事情
//真实类实现Runnable接口
class TrulyObj implement Runnable(){}
//线程类实现了Runnable接口 (代理)
class Thread implement Runnable(){}
//使用代理类去执行真实类
new Thread(trulyObj).start();
//start也是执行了run方法,而run方法在真实类中重写了
4、线程操作
线程生命周期:
常用API:
/**
* 测试stop
* 1、推荐使用flag标示,让线程自行停止,不建议死循环
* 2、推荐使用次数控制
* 3、不推荐使用stop()和destroy()
* @author: stone
* @create: 2020-08-13 16:48
*/
public class TestStop implements Runnable {
private boolean flag = true;
public void setFlag(boolean f) {
if(this.flag){
System.out.println("子线程停止======");
}
this.flag = f;
}
@Override
public void run() {
while (flag) {
System.out.println("子线程在跑----------------");
}
}
public static void main(String args[]) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 30; i++) {
if (i >= 10) {
testStop.setFlag(false);
}
System.out.println("主线程在跑:" + i);
}
}
}
-
线程休眠
Thread.sleep(time);
- 使当前线程休眠指定毫秒数
- 存在异常InterruptedException
- sleep时间达到后程序进入就绪状态
- sleep可以模拟测试网络延迟、倒计时等(增大事件发生几率)
- 每个对象都有一把锁,sleep不会施放锁
-
线程礼让
Thread.yield();
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态,使其重新去排队获取CPU
- 礼让只是让CPU重新调度,不一定是挂起当前线程执行别的线程
-
线程插队
thread.join();
- 待此线程执行完成后,在执行其它线程,其它线程阻塞
-
线程状态
Thread.State enum
thread.getState();
- NEW 线程未开启
- RUNNABLE 线程在JVM中运行
- BLOCKED 被阻塞等待
- WAITING 等待另一个线程执行特定动作
- TIMED WAITNG 线程等待指定时间
- TERMINATED 结束
-
线程优先级
-
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定先调度哪个线程执行。
-
线程的优先级用数字表示,范围:1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
-
获取优先级:thread.getPriority();
-
设置优先级:thread.setPriority(int x);
-
main方法优先级为5;
-
先设置优先级,再启动;
-
优先级高不一定早启动
-
-
守护线程(daemon)
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 举例:后台记录操作日志线程、监控内存线程、垃圾回收等待线程
thread.setDaemon(true);//设置为守护线程
5、线程同步
并发:多个线程同时操作同一个对象
队列和锁 synchronized
排队,当一个线程获得对象的排他锁,独占资源,其它线程必须等待使用者释放锁后才能继续进行
-
一个线程持有锁会导致其它线程挂起;
-
多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引发性能问题
-
如果一个优先级高的线程在等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
-
同步块:syschronized(Obj ){}
-
Obj称之为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this当前对象本身class
-
同步监视器的执行过程
- 第一个线程访问,获得同步监视器的锁,执行其中代码
- 第二个线程访问,等待第一个线程施放同步监视器的锁,无法访问
- 第一个线程执行完毕,施放同步监视器的锁
- 第二个线程访问,获得同步监视器的锁,执行其中代码
6、死锁
多线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能执行,而导致两个或多个线程进入等待。
产生死锁的原因:
- 互斥条件:一个资源每次只能被一个进程执行(锁)
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能抢行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
7、Lock(锁)
- JDK5.0开始,java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁
//定义lock锁
private final ReentrantLock look = new ReentrantLock();
try{
lock.lock();//加锁
......
}finnally{
lock.unlock();//解锁
}
synchronized与Lock的对比
synchronized | Lock |
---|---|
隐式锁,出了作用域自动施放 | 显示锁,手动开启和关闭 |
JVM将花费较少的时间来调度线程,性能更好 | |
拓展性更好,提供更多的子类 |
8、线程通信
wait();//挂起等待,直到其它线程通知,wait会施放锁
wait(long timeout);//指定等待的毫秒数
notify();//唤醒一个处于等待状态的线程
notifyAll();//唤醒同一个对象上所有调用wait()方法的线程,优先级高的先调度
PS:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
并发协作模型:“生产者/消费者模型 ”
生产者 – > 产品 --> 消费者
管程法
-
生产者:负责生产数据的模块(方法、对象、线程、进程)
-
消费者:负责处理数据的模块(方法、对象、线程、进程)
-
缓冲区:消费者不能直接使用生产者的数据,中间建立一个“缓冲区”
生产者将产品放入缓冲区,消费者从缓冲区拿出产品
生产者 --> 缓冲区 --> 消费者
信号灯法
通过共同操作一个标示,作为开关,实现线程之间通信。
生产者完成生产,打开标示,通知消费者使用产品。
消费者使用完产品,关闭标示,通知生产者生产产品。
flag
9、线程池
创建多个线程,放入线程池中,使用时直接获取,使用完后放回线程池
优点:
- 降低资源消耗,提高响应速度(减少创建线程的时间和机器资源)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最常存活时间
使用线程池:
- JDK5.0起提供了线程池API:ExecutorService和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工具类,用于创建并返回不同类型的线程池。
ExecutorService executorService = Executors.newFixedThreadPool(10);