java多线程

Java多线程并发

什么是多线程
  • 线程:程序内部的一条执行路径(比如说我们之前学的main方法就是一条单独的执行路线)
  • 单线程:程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
  • 多线程:多线程是指从软硬件上实现多条执行流程的技术
为什么要使用多线程

提高CPU的利用率,更好地利用系统资源

举个生活中的例子:假设说我现在的任务有:

  1. 煲汤(假设煲好汤需要两小时)
  2. 学习Java两小时

那么单线程的做法可能是:准备食材->开始煲汤->等待两小时后->开始学习java两小时(耗时四个小时)
而多线程的做法是:准备食材->开始煲汤->煲汤的同时开始学习java两小时(耗时两个小时)

可以看到,在单线程的处理方式中,我有两个小时是什么都没有做的,相当于是浪费了资源
现在将例子中的我看作cpu,煲汤的过程看作io输出打印东西,学习Java看作其他任务,也就是说
单线程中,在io输出的那两个小时中,cpu一直在等待,而多线程中,cpu在等待的过程中去处理了其他任务,相当于同时完成了两个任务,充分提高了cpu的利用率。

如何创建多线程

四种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口( JDK1.5>= )
  4. 线程池方式创建
继承Thread类

创建步骤:

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
public class MyThread extends Thread{//定义一个子类MyThread继承线程类java.lang.Thread
    public void run(){//重写run()方法
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread run");
        }
    }
}

public class Main {
    public static void main(String[] args) {
       Thread mythread=new MyThread();//创建MyThread类的对象
       mythread.start();//调用线程对象的start()方法启动线程(启动后还是执行run方法的)
       run();//主线程的方法
    }

    public static void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("Main run");
        }
    }
}

运行结果:

Main run
MyThread run
MyThread run
MyThread run
MyThread run
MyThread run
Main run
Main run
Main run
Main run

Process finished with exit code 0

可以看到,两个线程并发执行。
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法。

源码:

public class Thread implements Runnable {
    ...
}

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
  1. 为什么不直接调用了run方法,而是调用start启动线程。

直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。

  1. 把主线程任务放在子线程之前了。

这样主线程一直是先跑完的,相当于是一个单线程的效果了。

实现Runnable接口

可能有人会问,既然方式一里Thread都实现Runnable接口了,为什么还要学这种方式?

因为方式一线程类继承Thread后,不能继承其他类,不便于扩展,这也是方式一的缺点

创建步骤:

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程

emmm…相当于多套了一层:封装Runnable对象成为线程对象,Runnable接口里只定义了一个抽象run方法
那我们不妨先看看Thread提供了哪些构造器?
1.jpg

public class MyRunnable implements Runnable{//定义一个线程任务类MyRunnable实现Runnable接口
    public void run(){//重写run()方法
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread run");
        }
    }
}

public class Test01 {
    public static void main(String[] args) {
        Runnable r=new MyRunnable();//创建MyRunnable任务对象
        Thread thread=new Thread(r);//把MyRunnable任务对象交给Thread处理。
        thread.start();
        run();
    }
    public static void run(){//调用线程对象的start()方法启动线程
        for (int i = 0; i < 5; i++) {
            System.out.println("Main run");
        }
    }
}

运行结果:

Main run
Main run
Main run
Main run
MyThread run
MyThread run
MyThread run
MyThread run
MyThread run
Main run

方式二的简化写法:匿名内部类

public class Test01 {
    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("MyThread run");
                }
            }
        }).start();
        run();
    }
    public static void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("Main run");
        }
    }
}

再次简写:lambda表达式

public class Test01 {
    public static void main(String[] args) {

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("MyThread run");
            }
        }).start();
        run();
    }
    public static void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("Main run");
        }
    }
}
JDK 5.0新增:实现Callable接口

为啥子又要新增这样一个接口呢?

可能细心的小伙伴已经发现了,前面两种方式的返回值都是void,他们重写的run方法均不能直接返回结果。因此,不适合需要返回线程执行结果的业务场景。

执行步骤:

  1. 定义类实现Callable接口,重写call方法,封装要做的事情。
  2. 用FutureTask把Callable对象封装成线程任务对象。
  3. 把线程任务对象交给Thread处理。
  4. 调用Thread的start方法启动线程,执行任务
  5. 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

不错,在方法二的基础上又套了一层。

/**
 1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
     2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3、创建Callable任务对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable任务对象 交给 FutureTask 对象
        //  FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //  FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5、交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
            String rs1 = f1.get();
            System.out.println("第一个结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
            String rs2 = f2.get();
            System.out.println("第二个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

底层Callable接口:Callable接口不能直接交给Thread

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

底层FutureTask类:FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口,所以可以交给Thread

public class FutureTask<V> implements RunnableFuture<V> {
    ...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

FutureTask对象的作用

  1. 是Runnable的对象(实现了Runnable接口),可以交给Thread了
  2. 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果

2.jpg

线程池方式创建

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池.

因为线程池这块内容特别多,所以将在这篇文章后续统一讲述,目前知道还可以通过线程池创建线程便可。

多线程所带来的问题:线程安全问题

线程安全问题:多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题

记得小时候看喜羊羊与灰太狼,里面有一个片段是,羊补桥,狼拆桥,双方同时进行,很专注,结果桥怎么都补不好,也拆不完。把羊和狼看作两个线程,桥看作共享资源,这就是一个线程安全问题。

emmm,可能不是很好理解。再举一个经典案例:两个人同时对一个账户进行取钱操作

  1. 判断余额是否大于0
  2. 取钱100元(假设账户里只有100元)
  3. 更改余额
    假设执行流程为:Person1(1)->Person2(1)->Person1(2)->Person1(3)此时余额为0元->Person2(2)->Person2(3)此时余额为-100元

综上:当多个线程同时访问同一个共享资源且存在修改该资源时会导致线程安全问题

那么如何解决这个问题呢

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

如何实现先后依次访问共享资源?

加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

关于锁的知识也很庞大,将在另外一篇博客中记载。

常见方法
sleep

sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时
间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程
的运行也需要时间.
一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。
但在 sleep 的过程中过程中有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行 catch 语句块(可能还有 finally 语句块)以及以后的代码。
注意 sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep()让 t
对象进入 sleep,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程

wait

wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待
此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象
锁定池准备获得对象锁进入运行状态。

sleep和wait比较
  1. 这两个方法来自不同的类分别是 Thread 和 Object
  2. 最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同
    步控制块或者方法。
  3. wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可
    以在任何地方使用(使用范围)
  4. sleep 必须捕获异常,而 wait,notify 和 notifyAll 不需要捕获异常
notify

notify 是 Object 类的方法,notify 方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

notifyAll

notifyAll 是 Object 类的方法,它会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现

为什么wait和notify方法要在同步块中调用

wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经
获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方
法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某
个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。

为什么wait, notify 和 notifyAll这些方法不在thread类里面

JAVA提供的锁是对象级的而不是线程级的

yield

yield()方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。
如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行

join

join 方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执
行结束后,再继续执行当前线程。

有三个线程T1,T2,T3,如何保证顺序执行

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一
个线程,另外一个线程完成该线程继续执行。T3调用T2,T2调用T1,这样T1就会先完成而T3最后完成。

中断线程

(1)使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
(2)通过 return 退出 run 方法
(3)通过对有些状态中断抛异常退出 thread.interrupt() 中断。
(4)使用 stop 方法强行终止线程(过期)

线程池

什么是线程池

线程池就是一个可以复用线程的技术。

为什么要使用线程池?

如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

举个现实生活中的案例:服务员服务客户,假设每来一个客人我就新招一个服务员,那就太亏了。

  1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

谁代表线程池?

JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋千水竹马道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值