• 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(),所以你可能看到的不是交替打印的。可以多执行几次,或者拉倒最上面看
             */
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    执行结果如下图,可以看出,两条线程是交替乱序执行的。谁抢到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);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    在这里插入图片描述

    ⭐️实现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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    小结:继承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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    上面的代码,我们实现从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();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

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

    • 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();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

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

    synchronized修饰静态方法

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

    修饰静态方法相当于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();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    需要注意的是,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 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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    ⭐️使用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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

  • 相关阅读:
    Normalizer(归一化)和MinMaxScaler(最小-最大标准化)的区别详解
    java加sqlite3右键菜单版圣经,还没检查错误
    VSCode 使用CMakePreset找不到cl.exe编译器的问题
    扬帆牧哲:跨境电商还有未来吗?
    JavaScript【预定义模式和重复类、量词符和贪婪模式、修饰符和组匹配、正则应用场景、排序之冒泡排序 、 算法之递归、排序之快速排序 】(二十一)
    Rancher集群之间ssh登录问题
    全排列的代码
    python绘制立体玫瑰花
    人的顶级能量从哪里获取?
    ios swift sqlite3 简单使用
  • 原文地址:https://blog.csdn.net/zhang0305/article/details/124702191