Java并发之线程与线程池

1,435 阅读6分钟

我们经常听到一些大佬说一些概念,比如线程不安全,那到底什么是线程不安全呢?

 线程不安全指的是,我们在多线程的环境下,操作一些共享数据的时候可能会让我们无法得到期望的结果

线程

为什么会出现线程呢?

 线程就好比一个人,俗话说的好,众人拾柴火焰高,而多线程也是这个道理!

我们先来看一下线程6种状态的状态

线程状态

  • New(新生态)

  • Runnable(可运行态)

    在可运行态中又可以分为,Running(运行态)和Ready(就绪态)

    • Running(运行态)

      运行态指的是,该线程已经获取了CPU的时间片,简单来说,就是该线程正在运行

    • Ready(就绪态)

      而就绪态指的是,该线程万事俱备只欠“东风”,就差CPU给他分配时间片了

  • Blocking(阻塞态) 线程在获取锁失败之后,就会进入该状态,当该线程获取了锁就会结束该状态,所有阻塞状态的线程都会放在阻塞队列中。

  • Waiting(等待态)

当处于运行态的线程调用wait(),park(),join()方法后,就会进入该状态,处于等待态的线程,会释放CPU时间片,并且会释放资源(例如锁),这个状态下的线程只能等待其他线程来唤醒它。

  • Timed Waiting(超时等待态)

超时等待态和等待态类似,不过这个状态的线程不需要显式地去唤醒,这个状态的线程在超过一定时间后,将由系统自动唤醒

  • Terminated(结束态) 线程结束后的状态

线程六种状态的转换.png

线程的三种使用方式

线程的使用方式有三种,分别是实现Runnable接口、实现Callable接口、继承Thread类

  • 实现Runnable接口

     创建一个自定义类然后实现该接口,实现该接口的run()方法
     然后在我们需要使用该类的地方直接去实例化即可
    
  • 实现Callable接口

    创建一个自定义类然后实现该接口,实现该接口的call()方法
    然后通过开启线程池服务来创建该类
    
  • 继承Thread类

    创建一个自定义类然后继承该类,需要重写该类的run()方法
    这种方式其实和实现Runnable接口的方式类似,因为Thread类也实现了Runnable接口
    我们在实现的时候直接去实现即可
    

三种方式的对比

其实我们更加推荐实现Runable接口的这种方式,因为相较于其他两种,实现Callable接口这种方式使用起来更加繁琐,而继承Thread类的这种方式是继承,我们都知道在Java中是不支持多重继承的,但是支持多重实现,并且用起来实现Runnable接口这种方式也更加简单

线程之间的协作工作

join()方法

我们在很多情况下,会在一个线程中调用另一个线程的方法,这个时候我们就会使用到join方法了

在这里我们先定义一个线程类,为了方便我们直接去继承Thread类

public class YlOneThread extends Thread {

    @Override
    public void run() {
        System.out.println("我是亚雷1线程");        
    }
    
}

在这里我们又定义了一个线程类,不过在这个线程类中需要使用前面那个线程


public class YlTwoThread extends Thread{

    private YlOneThread thread;

    public YlTwoThread(YlOneThread thread){
        this.thread = thread;
    }
    @Override
    public void run() {
        try {
            thread.join();
        } catch (InterruptedException e) {           
            e.printStackTrace();
        }
        System.out.println("我是亚雷2线程");        
    }    
}

最后我们再创建一个测试类来测试

public class test {
    public static void main(String[] args) {
        YlOneThread Thread1 = new YlOneThread();
        YlTwoThread Thread2 = new YlTwoThread(Thread1);
        Thread2.start();
        Thread1.start();
    }
}

我们可以很明显的看到再线程2中调用了线程1不出意外我们得到了这样的结果

我是亚雷1线程
我是亚雷2线程

在这里虽然是线程2先启动,不过在线程2中调用了线程1,线程2会先等待线程1完成然后继续执行

wait()、notify()、notifyAll()方法.

在这里我就简略的说一下这些方法、因为这些方法并不是来自于JUC包下的而是来自于Object类下面的

  • wait()方法

    这个方法在前面我们也见过了,让线程进入等待态并且也会释放资源(锁)
    
  • notify()、notifyAll()方法

    这两个方法用于唤醒线程、不同的是一个是唤醒所有线程而另一个不是
    

wait()和sleep()

我们经常拿这两个方法进行比较、因为它们在我们浅层的认识中都是使线程进行等待

其实它们大相径庭

wait()方法会释放资源(锁),这在前面我们已经知道了, 而sleep()方法并不会释放资源(锁)

wait()方法是基于Object的方法,而sleep()是基于Thread类的方法

await()、signal()、signalAll()方法

而在JUC中我们使用这些方法来实现线程间的调度

public class AWaitTest {
    private ReentrantLock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void methodOne(){
        lock.lock();
        try {
            System.out.println("方法一执行......");
            condition.signalAll();
        } catch (Exception e) {    

        }finally{
            lock.unlock();
        }        
    }
    public void methodTwo(){
        lock.lock();
        try {
            condition.await();
            System.out.println("方法二执行.......");
        } catch (Exception e) {
            //TODO: handle exception
        }finally{
            lock.unlock();
        }
        
    }
}
    public static void main(String[] args) {

        ExecutorService ThreadPool = Executors.newCachedThreadPool();
        AWaitTest aWaitTest = new AWaitTest();
        ThreadPool.execute(() -> aWaitTest.methodTwo());
        ThreadPool.execute(() -> aWaitTest.methodOne());
    }
    方法一执行......
    方法二执行.......

这种方法很明显wait()那一套更加灵活

线程池

说到线程池,我们不得不提一下线程池的七大参数了。

线程池七大参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize 核心线程数

    当提交一个任务时,线程池创建一个新的线程来执行任务,直到线程数量达到corePoolSize的时候
    这个时候会将线程放入到工作队列当中阻塞
    
  2. maximumPoolSize 最大线程数

    这个参数只在,工作队列是有边界的时候生效,如果工作队列没有边界,这个参数将不生效
    因为会将新的线程一直添加到工作队列当中
    
  3. keepAliceTime 线程空闲的存活时间

    因为线程执行任务执行完毕之后不会立即死亡,会继续存活下来,而这个参数就是在限制线程的存活时间
    还有一点需要注意,这个参数默认只在当前线程数大于核心线程数的情况下生效
    
  4. unit 线程存活时间单位

    见名知意,这个参数作为keepAliceTime的单位
    
  5. workQueue 工作队列

    用于保存等待需要执行任务的新线程
    
  6. threadFactory 线程工厂

    用于创建线程的线程工厂
    
  7. handler 饱和策略

    当阻塞队列满了,并且没有空闲的的工作线程,如果此时还不断提交任务,线程池必须进行处理
    线程池提供了四种饱和策略
    1. AbortPolicy: 直接抛出异常,默认策略
    2. CallerRunsPolicy: 用调用者所在的线程来执行任务
    3. DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务
    4. DiscardPolicy: 直接丢弃任务
    

线程池的执行流程

线程池到底是怎么来创建线程的呢?

我们在提交任务后,
线程池会先判断当前线程数是否大于核心线程数,
如果不大于则直接创建工作线程,
否则则创建线程添加到阻塞队列当中,
然后,如果线程池会再次进行判断阻塞队列是否满
如果不满,则直接添加到阻塞队列当中
否则,会再次判断当前线程数是否大于最大线程数
如果大于则执行拒绝策略,否则就创建线程

线程池的执行流程.png