多线程

120 阅读4分钟

多线程

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方法而死亡。