一,多线程的概念
进程:正在运行的程序
线程:是进程中的一个执行单元(一条执行路径),一个进程中至少包含一个线程。如果一个进程中有多个线程,这样的程序就称为多线程程序。
线程的调度:
- 分时调度:所有线程轮流使用CPU,每个线程平均占用CPU的时间
- 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个线程为其分配CPU的资源(随机性),Java 多线程的执行方式就是抢占式的。
注:
- Java 程序在没有开辟新线程的情况下也会有两个线程:主函数所在的主线程、垃圾回收线程
二,创建线程
方式一:继承 Thread 类
- 自定义类继承 Thread 类
- 重写 run() 方法,在 run() 中明确线程执行的功能
- 创建 Thread 的子类对象
- 使用这个子类对象调用 start() 方法开启线程,JVM 会自动调用重写后的 run() 方法
public class Test1 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
MyThread mt2 = new MyThread();
mt2.start();
for(int i = 1;i <= 100;i++){
System.out.println("main方法:"+i);
}
}
}
class MyThread extends Thread{
// 重写run()方法是为了明确线程中所执行的任务
public void run() {
for(int i = 1;i <= 100;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
方式二:实现 Runnable 接口
- 自定义类实现 Runnable 接口
- 重写 run() 方法,在 run() 中明确线程执行的功能
- 创建 Runnable 实现类的对象
public class Test2 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
Thread t2 = new Thread(mr);
t2.start();
for(int i = 1;i <= 300;i++){
System.out.println("main方法:"+i);
}
}
}
class MyRunnable implements Runnable{
public void run() {
for(int i = 1;i<=300;i++){
System.out.println(Thread.currentThread()+"--"+i);
}
}
}
继承 Thread 类 和 实现 Runnable 接口的选用:
使用实现的方式,因为避免了单继承的局限性
注:同一个线程对象不能重复开启,重复开启会发生 IllegalThreadStateException
使用匿名内部类的方式创建线程并开启:
new Thread(){
public void run(){
// 线程执行的任务
}
}.start();
new Thread(new Runnable(){
public void run(){
// 线程执行的任务
}
}).start();
三,线程中的方法
- String getName():返回线程的名称
- static Thread currentThread():返回线程对象,
Thread [ Thread - 0 , 5 , main]
- Thread - 0:线程的名称
- 5:优先级
- main:在主函数中开启的
- static void sleep(long time)
四,线程安全问题
什么是线程安全:如果有多个线程同时执行,这些线程同时操作同一个内容,程序运行后的结果和单线程运行的结果是一样的时候,就称为线程安全。
完成 3 个窗口同时卖票案例时,发现了重复票和负数票的情况,原因是多个线程在操作同一个 ticket 变量,
在某个线程通过了 if 判断后,被其他线程抢夺了 CPU 的执行权,所以 if 后的 ticket-- 操作,在一次判断后重复执行了多次。
解决方式:
- 同步代码块
格式:
synchronized(锁对象){
// 可能发生线程安全问题的代码
}
注:
- 同步代码块中的锁对象可以是任意的
- 必须保证多个线程使用的锁对象是同一个
- 锁对象的作用:将同步代码块锁定,同一时间只允许让一个线程进入同步代码块
- 同步函数
步骤:
- 将可能发生线程安全问题的代码抽取到一个方法中
- 再在这个方法上添加 synchronized 修饰符
格式:
public synchronized 返回类型 方法名(参数){
// 可能发生线程安全问题的代码
}
注:
- 同步函数中有锁吗?有,同步函数中的锁是 this
- 在使用继承 Thread 的方式去创建线程时,使用同步函数不能保证线程同步,因为每一个线程对象都有属于自己的一份 this,所以多个线程使用的不是同一个锁
解决办法:使用 static 修饰同步函数,此时同步函数中的锁还是 this 吗?
不是,因为 static 不能访问 this,锁是 类名.class
- Lock 锁:
- Lock 是一个接口,在 jdk 1.5 后出现,它使用了比 synchronized 更简单更广泛的锁的操作方式
- 方法:lock() 获取锁,unlock() 释放锁
- 步骤:
- 在成员位置创建 ReentrantLock 对象
- 在可能会发生线程安全问题前调用 lock() 获取锁
- 在可能会发生线程安全问题后调用 unlock() 释放锁
同步技术的原理:
同步技术中使用到了锁对象,这个锁对象也称为同步锁
多个线程一起抢夺 CPU 的执行权,谁抢到了,谁就能进入同步代码块,
当一个线程抢夺到了 CPU 的执行权时,会判断是否有锁对象,
如果有锁对象,就会获取到这个锁,进入同步代码块,当这个线程执行完同步代码块,就会释放锁
如果没有锁对象,会处于阻塞状态,等待其他线程执行完同步代码块,等待其他线程释放锁,才能再进行 CPU 执行权的抢夺
使用同步锁会影响程序的执行效率:
因为获取 CPU 执行权的线程,在进入同步代码块之前,需要判断是否有锁,
如果有锁还需要进行获取的动作,
在执行完同步代码块后,需要释放锁。
(判断锁,获取锁,释放锁)
五,线程池
5.1 问题
在每次使用线程时,都需要创建线程对象,操作方式非常简便,但是存在一个问题:
如果并发的线程数量很多,每个线程执行完一个时间很短的任务就结束了,这样频繁创建线程会极大的降低程序的性能。
所以我们需要想一个办法能重复利用已创建的线程,来避免线程的过多创建。
5.2 概念
线程池本质上就是一个可以存放多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作。
5.3 好处
- 降低资源消耗,减少了创建线程的次数,每个线程都可以被重复利用
- 提高程序的响应速度
- 提高了对线程的管理
5.4 实现步骤
- 使用线程池的工厂类 Executors 中的静态方法 newFixedThreadPool 创建一个包含指定线程数量的线程池 ExecutorService
- 创建 Runnable 接口的实现类,重写 run() 方法
- 调用 ExecutorService 中的 submit() 方法,传入实现类对象,该方法用于开启线程并调用 run() 方法
- 调用 ExecutorService 中的 shutdown() 方法,来销毁线程池,一旦被销毁,就无法再从池中获取线程对象