多线程
1 多线程? 多线程 !
1.1 相关概念
线程是进程的执行单元, 通俗的说: 线程就是来执行代码的
一个进程最少有一个线程, 如果只有一个, 则被称为单线程; 一个进程也可有多个线程, 这时候被称为多线程程序
1.2 并发和并行
并发: 多个事件在同一时间段,交替执行
并行: 多个事件在同一时刻,同时执行
1.3 多线程运行原理
因为cpu快速切换,所以看起来像是同时在运行
1.4 好处
让程序可以"同时"做多件事情
2 多线程的创建 /\重点/\
Java是通过java.lang.Thread 类的对象来代表线程的
2.1 方式一:继承Thread类
2.1.1 详细步骤
1) 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
2) 创建MyThread类的对象
3) 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
2.1.2 优缺点
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
2.2 方式二:实现Runnable接口
2.2.1 详细步骤
1) 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
2) 创建MyRunnable任务对象
3) 把MyRunnable任务对象交给Thread处理
4) 调用线程对象的start()方法启动线程
2.2.2 优缺点
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。
2.2.3 匿名内部类详细步骤
1) 可以创建Runnable的匿名内部类对象
2) 再交给Thread线程对象。
3) 再调用线程对象的start()启动线程。
实例:
new Thread(()->{
pack.getPack();
},"郭靖").start();
2.3 方式三:实现Callable接口
实现Callable接口方式最大的优点:可以返回线程执行完毕后的结果。
2.3.1 详细步骤
1) 创建任务对象
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(线程任务对象)。
//public FutureTask<>(Callable call)
2) 把线程任务对象交给Thread对象。
3) 调用Thread对象的start方法启动线程。
4) 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
//public V get() throws Exception
2.3.2 优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
2.4 多线程注意事项
1) 启动线程必须是调用start方法,不是调用run方法。
2) 不要把主线程任务放在启动子线程之前。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
2.5 多线程可以通过构造器的方式传参
public class Mth {
public long findSum(){
//省略代码
}
}
public class Callable1 implements Callable<Long> {
private Mth mth;
@Override
public Long call() throws Exception {
return mth.findSum();
}
public Callable1(Mth mth){
this.mth = mth;
}
}
3 Thread的常用方法/\重点/\
3.1 常用构造器
- 可以为当前线程指定名称: public Thread(String name)
- 封装Runnable对象成为线程对象: public Thread(Runnable target)
- 封装Runnable对象成为线程对象,并指定线程名称: public Thread(Runnable target, String name)
3.2 常用方法
- 线程的任务方法: public void run()
- 启动线程: public void start()
- 获取当前线程的名称,线程名称默认是Thread-索引: public String getName()
- 为线程设置名称: public void setName(String name)
- 获取当前执行的线程对象: public static Thread currentThread()
- 让当前执行的线程休眠多少毫秒后,再继续执行: public static void sleep(long time)
- 让调用当前这个方法的线程先执行完: public final void join()...
4 线程安全/\重点/\
4.1 定义
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
4.2 原因
1) 存在多个线程在同时执行 2) 同时访问一个共享资源 3) 存在修改该共享资源
4.3 解决办法
线程同步
5 线程同步/\重点/\
5.1 中心思想
加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题
5.2 方式一:同步代码块
5.2.1 作用
把访问共享资源的核心代码给上锁,以此保证线程安全
synchronized(同步锁) {
访问共享资源的核心代码
}
5.2.2 原理
每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行 (上厕所>...<)
5.2.3 注意事项
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
5.2.4 对同步锁对象的要求
1) 对于实例方法建议使用this作为锁对象。 2) 对于静态方法建议使用字节码(类名.class)对象作为锁对象
5.3 方式二:同步方法
5.3.1 作用
对出现问题的核心方法使用synchronized修饰
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
5.3.2 原理
/\ 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。 /\ 如果方法是实例方法:同步方法默认用this作为的锁对象。 /\ 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
5.3.4 对同步锁对象的要求
1) 对于实例方法建议使用this作为锁对象。 2) 对于静态方法建议使用字节码(类名.class)对象作为锁对象
5.4 方式三:Lock锁
5.3.1 作用
Lock锁可以创建出锁对象进行加锁和解锁是接口
Lock不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
public ReentrantLock () //获得Lock锁的实现类对象
5.3.2 常用方法
获得锁: void lock()
释放锁: void unlock()
示范:
public class Pack {
private int i = 3;
private ReentrantLock rl= new ReentrantLock();
public void getPack(){
String name = Thread.currentThread().getName();
rl.lock();
if(i==0){
System.out.println(name+"抱歉, 红包已经被抢完了");
}else {
i--;
System.out.println("恭喜"+name+" ,您成功抢到一个20元的红包");
}
rl.unlock();
}
6 线程通信 (了解)
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
常用方法: wait(), notify(), notifyAll(), 应该使用当前同步锁对象进行调用
7 线程池
7.1 定义
线程池是一个容器,可以保存一些长久存活的线程对象,负责创建、复用、管理线程
7.2 优势
/\ 降低资源消耗,重复利用线程池中线程,不需要每次都创建、销毁 。 /\ 便于线程管理,线程池可以集中管理并发线程的数量。
7.3 提交Runnable任务
(Executors基本上不用, 因为有内存超出隐患, 被ThreadPoolExecutor代替)
7.3.1 Executors工具类创建线程池
创建一个线程池,该线程池固定数量的线程: static ExecutorService newFixedThreadPool(int nThreads)
7.3.2 Executor的常用方法
- 提交Runnable类型的任务: submit (Runnable task)
- 提交Callable类型的任务: submit (Callable<T> task)
- 关闭线程池: void shutdown()
7.3.3 提交Runnable任务的使用步骤
1) 创建线程池 2) 创建Runnable任务 3) 提交任务
7.4 提交Callable任务
7.4.1 Callable接口
public interface Callable<V> {
V call() throws Exception;
}
public interface Runnable {
public abstract void run();
}
7.4.2 好处
1) 有返回值 2) 可以抛异常
7.4.3 线程池的使用步骤
1) 创建线程池 2) 创建Callable任务 3) 提交任务
7.5 ThreadPoolExecutor (替代Executor)
public class test {
public static void main(String[] args) {
Mth mth = new Mth();
long sum = 0;
Callable1 c1 = new Callable1(mth);
ThreadPoolExecutor tpe = new ThreadPoolExecutor(
3,8,100, TimeUnit.SECONDS,new ArrayBlockingQueue<> (3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()
);
/*int 数量 , 核心线程数
int 数量, 总的线程数
int 时间 ,指定临时线程的存活时间
TimeUnit t , 时间单位
WorkQueue, 指定等候区
ThreadFactory, 用于生产线程,
Reject handler拒绝策略*/
Future<Long> submit1 = tpe.submit(c1);
/*此处省略原代码*/
tpe.shutdown();
}
}
8 其它细节知识:线程的生命周期
| 线程状态 | 说明 |
|---|---|
| NEW(新建) | 线程刚被创建,但是并未启动。 |
| Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
| Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。 |
| Waiting(无限等) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
| Timed Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。 |
| Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |