Java进阶-单线程、多线程、线程池

203 阅读9分钟

前言

java程序天生是多线程的,多线程是并发编程必须掌握的基础概念

线程 进程的基本概念

进程是操作系统分配资源的最小单位 包括分配CPU IO等
线程是CPU调度的最小单位
并行(是同一时刻可通行数)、并发(是单位时间内 通行数)
并行:同一时刻运行线程数,比如四车道的马路的并行数就是4。
并发:一时间段的线程数(一般以秒为单位),比如四车道的马路一秒通过的车辆数是100,并发就是100。

并发编程好处

多线程下载快

线程安全问题

因为一个进程下所有线程都是对这个进程的资源进行操作的,这就导致存在线程安全问题,也就可能产生死锁 线程多了也是耗性能的 因为cpu切换也是需要时间周期的 ,所以线程数量是有限制的 这就引入线程池概念。

线程分类

单线程、多线程(多线程又存在线程池的概念)。

1、单线程

比如我们的UI线程 就是一个单线程。

单线程的开启方式

继承Thread、实现runnable接口、实现callable接口 直接上代码!!!

//创建一个单线程
public class SingleThreadDemo {

    private Thread thread2;
    private SingleThread thread1;
    private Thread thread3;

    //创建单线程的几种方式
    public void createThread() {
        //方式1  extend Thread
        thread1 = new SingleThread();
        //开启线程 让线程处于就绪状态 等待cpu调度
        thread1.start();

        //方式二 实现runnable接口
        thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2名字"+thread2.getName()+"id:"+thread2.getId());
            }
        });
        thread2.start();
        //方式3 :实现callable接口 需要用FutureTask将Callable包装
        //FutureTask 是继承Runnable接口的 Callable不是
        //这种方式的区别是执行call后是有返回值的
        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("线程3名字"+thread3.getName()+"id:"+thread3.getId());
                return "你好";
            }
        });
        thread3 = new Thread(futureTask);
        thread3.start();
        try {
            System.out.println("线程3返回值" + futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    class SingleThread extends Thread {
        //重写run方法
        @Override
        public void run() {
            //实现自己的操作
            System.out.println("线程1名字"+thread1.getName()+"id:"+thread1.getId());
        }
    }

}

注意: 1、thread.start方法并不会让线程直接开启,而是单纯的将线程改为就绪状态,真正是否执行还需要等cpu调度。
2、同一个线程不能多次调用start方法,这样会报错的

 public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        // Android-changed: throw if 'started' is true
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

单线程如何结束呢?有哪几种方式

当线程执行完run方法后就会自动结束,如果想人为去结束 那就只有interrupt方法(而且这个方法是协作式的,意思就是说是告诉cpu一个中断信号,而不是强制结束,具体能不能结束它决定不了,比较卑微)。
所以靠谱的结束方式只有interrupt一种。\color{#FF0000}{**所以靠谱的结束方式只有interrupt一种。**}

以下方法不建议用 (不释放锁是非常危险的一个信号)

1、thread.supspend():线程挂起,但是不会释放锁资源 所以会出现死锁。
2、thread.stop():会立马杀死资源,不管线程有没有释放掉,会导致内存泄漏,也会导致资源丢失。
3、thread.onreusme(): 将挂起线程重新开启
4、thread.ondestory():

那单线程中哪些方法建议使用呢? (会释放锁)

1、thread.wait():让线程进入等待状态,,只有等待另外线程的通知或被中断才会返回,会释放锁
2、Thread.sleep(xxx):也是让线程处于等待状态,但是不会释放锁,这点需要注意 3、notify():唤醒线程,但是不能保证具体唤醒哪个线程,所有如果是多个线程的话,尽量采取notifyAll(),因此该方法使用比较少。
4.notifyAll():唤醒所有线程。比较多用
5、thread.interrupt():发出中断线程的信号,中断不中断取决于线程本身 释放锁
6、thread.interrupted()(是static方法):判断中断状态,且将中断状态给重置(从中断状态重置为非中断状态) 判断不怎么用这个方法
7、thread.isInterrupted():判断中断状态,且不将中断状态给重置 判断多用这个方法
8、thread.join():把指定线程加入到当前线程里面去(任务的插队),这种会让2个线程顺序的执行。比较重要的概念了\color{#FF0000}{2个线程顺序的执行。比较重要的概念了}
9、thread.yield():当前的cp线程让出cpu的执行权,但是可以重新选中,又开始执行,不释放锁 用的非常少。

上述方法注意事项

注意1: wait是Object类中的方法,而sleep是Thread类中的静态方法。
注意2: 调用wait方法的线程,不会自己唤醒,需要线程调用,通过notify / notifyAll(这俩个方法不是线程独有的是Oject拥有的)
注意事项3: sleep方法会自动唤醒,如果时间不到,想要唤醒,可以使用interrupt方法强行打断。
注意事项4: 在调用wait()之前,线程必须要获得该对象的对象级别锁

单线程常用方法调用和线程状态图 (非常非常重要!!!)

小结 单线程的内容就讲到这里了

2、多线程

我们开发任何项目都不可能只存在于单线程里面操作,所以多线程是必备掌握的知识。只是我们使用多线程中需要注意的地方还是非常多的,比如怎么样让多个线程对同一数据操作,还得保证安全呢等等之类的问题

线程安全问题

涉及到线程安全问题,我们就需要通过加锁的形式去保证数据安全。那锁分为哪几种呢?
隐式锁(Synchroinzed)、显示锁(lock)

synchronized

当多个线程对同一变量进行累加的时候 如果放任不管 会导致最终的结果数据不是我们理想的数据 如当一个全局数据count值为0 的时候 count= 0, 开启2个线程对数据进行10000次加值操作 发现最后很多时候值不为20000 所以我们需要加锁去保证。

package com.xm.studyproject.java.thread;

public class SyncDemo {
    int count = 0;

    private static Object object = new Object();
    //不加锁
    public void countAdd() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        }).start();
        try {
            Thread.sleep(1000);
            System.out.println("值111:" + count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //加锁
    public void countAddSyn() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                }
            }
        }).start();
        try {
            Thread.sleep(1000);
            System.out.println("值222:" + count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized虽然可以实现多线程同步(synchronized 可以作用在方法、对象、类上) 但是如果多个线程对自己的变量进行操作 怎么保证呢???

ThreadLocal的使用

package com.xm.studyproject.java.thread;

import androidx.annotation.Nullable;

public class ThreadLocalDemo {
    private int count = 0;
    private ThreadLocal<Integer> threadLocal = new ThreadLocal() {
        //重写这个方法
        @Nullable
        @Override
        protected Integer initialValue() {
            return count;
        }
    };

    public void test() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    Integer value = threadLocal.get();
                    value++;
                    threadLocal.set(value);
                    System.out.println(Thread.currentThread().getName()+"xxx :"
                            +threadLocal.get());
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    Integer value = threadLocal.get();
                    value++;
                    threadLocal.set(value);
                    System.out.println(Thread.currentThread().getName()+"xxx :"
                            +threadLocal.get());
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    Integer value = threadLocal.get();
                    value++;
                    threadLocal.set(value);
                    System.out.println(Thread.currentThread().getName()+"xxx :"
                            +threadLocal.get());
                }
            }
        }).start();
    }

}

synchronized关键字去加锁的问题在哪?

问题1: 如果一个操作通过synchronized获取到锁后不释放 如果其他操作也去获取锁 会一直拿不到直到前一个操作释放锁 (如果当前锁 是锁的当前对象 那么其他操作这个对象的锁也是获取不到 如果是类 是一样的)

 //加锁
    public void countAddSyn() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                    System.out.println("sleep1:" + count);
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                    System.out.println("sleep2:" + count);
                }
            }
        }).start();
        try {
            Thread.sleep(0);
            System.out.println("值222:" + count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

问题2: 因为一旦锁了每次就会导致其他操作处于等待操作,而且当是读数据的时候 需要等到第一个读完才能操作 这个是非常影响性能的。

怎么解决synchronized不能释放锁的问题? 采用lock

显示锁lock

三个比较重要的方法:Lock.lock(),Lock.unlock(),Lock.tryLock();

package com.xm.studyproject.java.thread.lock;

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

public class LockDemo {
    //可重入锁  synchronized也是可重入锁
    //当Synchronized递归重复去调用的时候 如果是不可重入锁会导致拿不到锁 把自己锁死
    // 导致后续代码执行不到 也不释放锁 所以Synchronized实现可重入锁
    private Lock lock = new ReentrantLock();

    private int count = 0;
    public void test() {
        for (int i = 0; i < 3; i++) {
            new Thread(new MyRunnable()).start();
        }
    }

    class MyRunnable implements Runnable {

        @Override
        public void run() {
            //加锁
            lock.lock();
            //尝试获取锁 参数可以设置获取锁的时候
            //lock.tryLock();
            for (int i = 0; i < 10000; i++) {
                count++;
            }
            System.out.println("LockDemo:"+count);
            //可释放锁 但是这个释放最好放在finally里面执行 不然担心try的时候执行不到这行代码
            lock.unlock();
        }
    }
}

读写锁

上面方法虽然解决了 锁不释放的问题,但是如果多个操作去读数据 还是必须等到前一个操作读完才能执行 这还是影响操作的,基于这个我们发现还有读写锁这个概念,读写锁(就是针对读的时候 各个操作之间是可以同时进行的 但是写的时候依然只能按操作有序去写,同时读的时候是禁止写的操作,写的操作是禁止读的操作),在我们实际项目中 读的场景是远远多于写的场景
例子

    //读写锁 ReadWriteLock是接口
    private ReadWriteLock locks = new ReentrantReadWriteLock();
    //读锁
    private Lock readLock = locks.readLock();
    private int count = 0;
    private long timeMillis;
    -------------------------
     public void test2() {
        timeMillis = System.currentTimeMillis();
        for (int i = 0; i < 3; i++) {
            new Thread(new MyRunnable2()).start();
        }
    }
    
    class MyRunnable2 implements Runnable {

        @Override
        public void run() {
            //加锁 读锁
            readLock.lock();
            //lock.tryLock();
            for (int i = 0; i < 100000; i++) {
                count++;
            }
            System.out.println("时间长度:"+(System.currentTimeMillis()-timeMillis));
            readLock.unlock();
        }
    }
    

从上面2个例子 你直观会发现读写锁比单纯锁速度会快很多

补充 contadtion的使用

相关方法
Condition condition = lock.newCondtion();
condition.signal()(类似于notify())
condition.signalAll()(类似于notifyAll())
condition.await()(类似于wait())
这里注意下 notify()唤醒线程是没法指定的,但是signal()是可以指定的。

公平锁 就是优先顺序去拿到锁 非公平则不是 常用都是非公平锁 不然cpu很多时候会处于等待状态

线程池(ThreadPoolExector)的使用

一个应用中线程的数量是有限制的,其次线程频繁的创建销毁也是需要时间、消耗性能的。那有没有什么办法做到线程的复用呢,这里我们就引入线程池的概念。

package com.xm.studyproject.java.thread.singlethread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {

    /**
     * 线程池实例
     */
    private ThreadPoolExecutor executor;

    /**
     * 核心线程数
     */
    private int CORE_NUM = 5;
    /**
     * 最大线程数
     */
    private int MAX_NUM = 10;
    /**
     * 阻塞队列 这边采用的是数组阻塞队列 阻塞队列有8种实现方式
     */
    private BlockingQueue blockingQueue = new ArrayBlockingQueue(100,false);
    /**
     * 线程空闲最大时间(空闲时长超过这个值线程会被回收)
     */
    private int KEEP_ALIVE_TIME = 3000;
    /**
     * 线程工厂 主要是命名 使用的不多 而且我这样使用是错误的 看源码发现 这里面是需要创建线程的
     */
//    private ThreadFactory threadFactory = new ThreadFactory() {
//        //这个对象是需要创建一个线程的
//        @Override
//        public Thread newThread(Runnable r) {
//            return Thread.currentThread();
//        }
//    };
    /**
     * 自定义拒绝策略 系统提供了四种策略
     */
    private RejectedExecutionHandler executionHandler = new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        }
    };


    //获取线程池
    public void getThreadPool() {
        if (executor == null) {
            executor = new ThreadPoolExecutor(CORE_NUM, MAX_NUM, KEEP_ALIVE_TIME,
                    TimeUnit.MILLISECONDS, blockingQueue);
        }
    }

    // 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
    private void execute(Runnable runnable) {
        executor.execute(runnable);
    }

    //任务数
    private int taskCount = 1000;
    public void test() {
        for (int i = 0; i < taskCount; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread()+":线程打印处理");
                }
            };
            //为啥会需要做这么个操作呢  就是当taskCount非常大的时候比如10万 而且这10万个任务可以说是一瞬间创建完成的
            //那么我的阻塞队列+我的线程是没法一瞬间处理完的 那就会崩溃
//            try {
//                Thread.sleep(50);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            if (executor != null) {
                execute(runnable);
            }
        }
    }
}

上面注意的就是基于任务数量的耗时去创建对应线程数。

向线程提交任务的方式

提交任务2种方式 一种execute 一种是submit(这个是有返回值的)

我们创建线程的种类

1、固定线程池
创建一个线程池,该线程池重用固定数量的线程来执行任意数量的任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
2、缓存的线程池
该线程池根据需要创建新线程,但在可用时将重用以前构造的线程。如果任务长时间运行,则不要使用此线程池
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
3、调度线程池
创建一个线程池,该线程池可以调度命令在给定延迟后运行或定期执行。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newScheduledThreadPool(10);
4、单线程池
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newSingleThreadExecutor();
5、工作窃取线程池
创建一个线程池,该线程池维护足够的线程来支持给定的并行级别。这里的并行级别是指在多处理器机器上,在单点时间内执行给定任务所使用的线程的最大数量。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newWorkStealingPool(4);

一个任务过来了 是如何添加到线程池的流程 (核心概念!!!!)

添加任务流程 如果任务小于coreSize 那就新建线程处理 直到coreSize处理不完 那就先提交到BlockQueue中 如果BlockQueue满了 那就看MaxSize 大小 继续新建线程 如果maxSize不够 那就进入拒绝模式

拓展1 (8种阻塞队列源码分析)

参考:8种阻塞队列源码分析

拓展2

子线程可以更新Ui 但是不安全 如界面错乱 只是不安全而已
ThreadLocal实现静态变量隔离

后续

多线程是一门比较浩大的学问,针对这个知识还需要了解为啥不能线程池 设计的阻塞队列形式不一样等等问题 后续能力提升再补充 已经多线程的源码分析