Java多线程基础,你可以这样学


这篇是Java多线程的入门基础,主要介绍什么是多线程、多线程的创建方式、常用方法。引发的问题及锁的相关知识。

📌什么是多线程?

⭐️进程与线程的区别:

进程:进程是指运行在电脑内存中的应用程序,一个进程至少有一个线程。例如360杀毒软件、QQ。

在这里插入图片描述

线程:线程指一个进程中的执行流程,例如360杀毒软件是一个进程,我们可以在让它杀毒的同时,也让他清理垃圾,这就开启了两条线程。

⭐️ 普通方法与多线程

在这里插入图片描述
我们都知道,在main()方法里调用其他方法时,会等所调方法执行完,再继续执行main()方法。
而多线程是与其他线程同时执行的。

📌多线程的创建方式

多线程有四种创建方式,这里先说前三种,带大家感受一下什么是多线程。

⭐️继承Thread类,重写run方法

package thread;

/**
 * @ClassName: ExtendsThread
 * @Description: 通过继承Thread创建多线程
 * @Author: Wen
 * @date: 2022/5/11 8:30
 */
//继承Thread类
public class ExtendsThread extends Thread{

    //重写run方法
    @Override
    public void run() {
        //方法体:打印50次“我是线程A---”及次数
        for(int i = 0;i <=50;i++){
            System.out.println("我是线程A---" + i);
        }
    }

    public static void main(String[] args) {
        //我们new一个对象并且让他调用start进入“就绪”状态,等待CPU调度
        new ExtendsThread().start();
        //主函数里我们让其打印1000次“我是主线程”
        for(int i = 0;i<=1000;i++){
            System.out.println("我是主线程---" + i);
        }

        /**
         * 如果是普通方法的话,他的执行顺序应该是:1.打印50次“我是线程A” 2.打印1000次“我是主线程” 
         * 但是我们写的是多线程,预测执行应该是“我是主线程”和“我是线程A”交替打印。
         * ps:因为我们没加入sleep(),所以你可能看到的不是交替打印的。可以多执行几次,或者拉倒最上面看
         */

    }
}

执行结果如下图,可以看出,两条线程是交替乱序执行的。谁抢到CPU谁就执行。如果没有这种效果的小伙伴可以多执行几次代码(这里先不说让线程睡眠)。
在这里插入图片描述

⭐️实现Runnable接口创建多线程(重点,常用)

package thread;

/**
 * @ClassName: ImplementsRunnable
 * @Description: 通过实现Runnable创建多线程
 * @Author: Wen
 * @date: 2022/5/11 8:59
 */
//实现Runnable接口
public class ImplementsRunnable implements Runnable{
    
    //重写run方法
    @Override
    public void run() {
        for(int i = 0;i <=50;i++){
            System.out.println("我是线程A---" + i);
        }
    }

    public static void main(String[] args) {
        //
        ImplementsRunnable implementsRunnable = new ImplementsRunnable();
        new Thread(implementsRunnable).start();
        //主函数里我们让其打印1000次“我是主线程”
        for(int i = 0;i<=1000;i++){
            System.out.println("我是主线程---" + i);
        }
    }
}

执行结果如下图,也是交替执行。

在这里插入图片描述

⭐️实现Callable接口创建有返回值的多线程(了解即可)

package thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @ClassName: ImplementsCallable
 * @Description: 通过实现Callable创建多线程
 * @Author: Wen
 * @date: 2022/5/11 9:50
 */
public class ImplementsCallable implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 0;i <=50;i++){
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        ImplementsCallable implementsCallable = new ImplementsCallable();
        FutureTask futureTask = new FutureTask(implementsCallable);
        new Thread(futureTask).start();
        try{
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

小结:继承Thread类的方法,我们不建议使用,因为Java是单继承。推荐使用实现Runnable接口的方法,因为他避免了单继承的局限性,方便同一个对象被多个线程使用。

📌 线程安全问题

多个线程使用共享数据时,会引发线程安全问题,下面是个小demo,来体验一下。

package com.example.study2.util;

/**
 * @ClassName: ThreadCount
 * @Description: 线程安全问题
 * @Author: Wen
 * @date: 2022/7/29 9:25
 */
public class ThreadCount implements Runnable{
    private int count = 100;

    @Override
    public void run() {
        while (true){
            if (count>=1){
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +" " + count--);
            }
        }
    }

    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
}

上面的代码,我们实现从100递减打印到1,开启两条线程,中间让线程休眠以让另一条线程可以抢到cpu。
在这里插入图片描述
可以看到,两条线程确实交替打印,但是会有重复数据的情况,这便是线程安全问题。
引发的原因很简单也很好理解,两条线程同时读取到count值是100,进行打印。

⭐️解决线程安全问题

我们可以使用锁来解决线程安全问题,锁分为synchronized锁和lock锁,其中synchronized锁属于API级别,lock锁属于jvm级别。

🍺synchronized解决线程安全问题

  • synchronized修饰代码块
package com.example.study2.util;

/**
 * @ClassName: ThreadSyncTest
 * @Description: TODO
 * @Author: Wen
 * @date: 2022/8/2 11:39
 */
public class ThreadSyncTest implements Runnable{
    //全局共享变量
    private static int count = 100;
    //自定义锁
    private String sync = "sync";

    @Override
    public void run() {
        while (true){
            if (count>=1){
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                /**
                 * synchronized修饰代码块,sync是我们声明的String类型对象
                 */
                synchronized (sync){
                    System.out.println(Thread.currentThread().getName() +" " + count--);
                }

            }
        }
    }
    public static void main(String[] args) {
        //两条线程
        new Thread(new ThreadSyncTest()).start();
        new Thread(new ThreadSyncTest()).start();

    }
}

在这里插入图片描述
由上图我们可以看到,线程交替执行,并且没有重复的数据。证明加锁成功。

  • synchronized修饰实例方法
package com.example.study2.util;

/**
 * @ClassName: ThreadSyncTest
 * @Description: TODO
 * @Author: Wen
 * @date: 2022/8/2 11:39
 */
public class ThreadSyncTest implements Runnable{
    //全局共享变量
    private static int count = 100;
    //自定义锁
    private String sync = "sync";

    @Override
    public void run() {
        while (true){
            if (count>=1){
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                print();
            }
        }
    }

    public synchronized void print(){
        System.out.println(Thread.currentThread().getName() +" " + count--);
    }
    public static void main(String[] args) {
        //两条线程
        ThreadSyncTest threadSyncTest = new ThreadSyncTest();
        new Thread(threadSyncTest).start();
        new Thread(threadSyncTest).start();

    }
}

synchronized修饰实例方法相当于this锁,此时new Thread(threadSyncTest).start();中要使用同一个对象,否则还会出现线程安全问题。

synchronized修饰静态方法

    public synchronized static void print(){
        System.out.println(Thread.currentThread().getName() +" " + count--);
    }

修饰静态方法相当于Object.class锁

🍺Lock锁解决线程安全问题

package com.example.study2.util;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName: ThreadSyncTest
 * @Description: TODO
 * @Author: wenlong
 * @date: 2022/8/2 11:39
 */
public class ThreadSyncTest implements Runnable{
    //全局共享变量
    private static int count = 100;
    //自定义锁
    private String sync = "sync";

    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            if (count>=1){
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                print();
            }
        }
    }

    public void print(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() +" " + count--);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }
    public static void main(String[] args) {
        //两条线程
        ThreadSyncTest threadSyncTest = new ThreadSyncTest();
        new Thread(threadSyncTest).start();
        new Thread(threadSyncTest).start();

    }
}

需要注意的是,Lock锁是手动加锁和释放锁,所以我们需要把释放锁写在finally里,防止因为出现异常而没有释放锁,导致死锁问题。

📌线程之间的通讯

线程之间是抢占式执行的,哪条线程先执行,哪条后执行,是CPU说了算,我们如果要让线程按照我们的预期去执行,就必须完成线程之间的通信。最常用的就是wait,notify,notifyall这三个方法。

  • wait():使当前线程阻塞,释放锁。wait必须在synchronized代码块或者方法中使用。
  • notify():唤醒使用当前锁的线程,使其可以抢占CPU
  • notifyAll():唤醒所有线程

📌使用线程池创建线程

⭐️使用ThreadPoolExecutor创建线程池

先看一下ThreadPoolExecutor的七个参数,也是面试常问题。

  • corePoolSize:核心线程,指定线程池的核心数量
  • maximumPoolSize:最大线程数(临时线程数)>=核心线程数,指定线程池所支持的最大线程数
  • keepAliveTime:指定临时线程的最大存活时间,不能小于0
  • unit:时间单位,秒,分,时,天
  • workQueue:指定任务队列。不能为null
  • threadFactory:指定用哪个线程工厂创建线程,不能为null
  • handler:指定线程忙,任务满的时候,新任务来了怎么办,不能为null

问题:什么时候创建临时线程?

当核心线程满了,任务队列也满了,此时会创建临时线程。如果核心线程满了,任务队列也满了,临时线程也满了,此时会执行handler指定的策略。

🍺handler执行策略

  • AbortPolicy:丢弃并抛出异常,是默认策略
  • DiscardPolicy:丢弃任务,不抛出异常,不推荐
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
  • CallerRunsPolicy:由主线程负责调用任务的run方法,从而绕过线程池直接执行

🍺ThreadPoolExecutor创建线程池demo

package com.example.study2.threadPool;

import java.util.concurrent.*;

/**
 * @ClassName: ThreadPool
 * @Description: TODO
 * @Author: wenlong
 * @date: 2022/8/3 11:48
 */
public class ThreadPool {
    public static void main(String[] args) {
//        int corePoolSize,
//        int maximumPoolSize,
//        long keepAliveTime,
//        TimeUnit unit,
//        BlockingQueue<Runnable> workQueue,
//        ThreadFactory threadFactory,
//        RejectedExecutionHandler handler

        ExecutorService executor = new ThreadPoolExecutor(3,5,6,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Runnable testRunnable = new TestRunnable();
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
        executor.execute(testRunnable);
    }
}

⭐️使用Executors(线程池的工具类)创建线程池

  • newCachedThreadPool:线程数量随着任务增加而增加,如果线程任务执行完毕且空闲一段时间,则被回收
  • newFixedThreadPool:创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
  • newSingleThreadExecutor:创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
  • newScheduledThreadPool:创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务

🍺Executors创建线程池demo

package com.example.study2.threadPool;

import java.util.concurrent.*;

/**
 * @ClassName: ThreadPool
 * @Description: TODO
 * @Author: wenlong
 * @date: 2022/8/3 11:48
 */
public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Runnable testRunnable = new TestRunnable();
        executor.execute(testRunnable);
    }
}

总结:阿里java开发手册不允许使用Executors创建线程池,原因如下:使用newFixedThreadPool和newSingleThreadExecutor,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求。使用newCachedThreadPool和newScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程。

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

IDEA上的操作工

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

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

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

打赏作者

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

抵扣说明:

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

余额充值