java编程思想学习笔记——21多线程

一、并发简介

实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包含的程序。多任务操作系统可以通过周期性地将CPU 从一个进程切换到另一个进程,来实现同时运行多个进程(程序)。对于进程来说,他们之间没有任何彼此通信的需要,因为他们都是完全独立的。
但是如果将进程当做并发的唯一选择,那么进程来做并发来说的话,也有他的局限性,因为进程通常来说是有数量和开销的限制的,以避免他们在不同的并发系统之间的可应用性
java采用在顺序型语言的基础上提供对线程的支持,即线程机制是在由执行程序表示的单一进程中创建的任务。
并发编程使我们可以将程序划分为多个分离的、独立运行的任务,一个线程就是在进程中的一个单一顺序控制流,因此单个进程可以拥有多个并发执行的任务,其底层机制就是切分cpu时间。

二、基本的线程机制

1.任务

线程可以驱动任务,这可以由Runnable接口来提供,想要定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令

/**
 * @program: java-demo
 * @description: 定义一个任务,将任务附在线程上,使线程驱动任务
 * @author: hs
 * @create: 2020-08-23 16:52
 **/
public class LiftOff implements Runnable{
   
    protected int countDown=10;
    private static int taskCount=0;
    private final int id=taskCount++;
    public LiftOff(){
   }
    public LiftOff(int countDown){
   
        this.countDown=countDown;
    }

    public String status(){
   
        return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+"),";
    }

    @Override
    public void run() {
   

        while (countDown-- >0){
   
            System.out.print(status());
            Thread.yield();
        }
        
    }
}

Thread.yield()是对线程调度器的一种建议
线程调度器:java线程机制的一部分,可以将cup从一个线程转移到另外一个线程

2.Thread类

声明注册一个线程,将Runnable对象转变为工作任务的方式是把他交给一个Thread构造器。调用Thread对象的start方法为该线程执行必需的初始化操作,然后会调用Runnable的run方法,在这个新线程中启动该任务

public class SimpleThread {
   
    /**
     * 使用默认线程启动任务
     * @param arg
     */
    /*public static void main(String []arg){
        LiftOff launch= new LiftOff();
        launch.run();
    }*/

    /**
     * 创建线程,并启动
     * @param arg
     */
/*    public static void main(String []arg){
        Thread t=new Thread(new LiftOff());
        t.start();
        System.out.println("Waiting for ListOff");
    }*/

    public static void main(String []arg){
   
        for(int i=0;i<5;i++){
   
            new Thread(new LiftOff()).start();
        }
        System.out.println("Waiting for LiftOff");
    }
}

在上面程序中调用start方法,start方法迅速的返回了,因为ListOff.run()是由不同的线程执行的,因此可以同时执行main方法内的其他操作

3.使用Executor执行器管理线程

Executor在客户端和任务执行之间提供了一层间接层,Executor循序你管理异步任务的执行,不需要显示地管理线程的生命周期。
ExecutorService是具有服务声明周期的executor

public class ThreadPool {
   
    /**
     * 动态获取线程,Executors.newCachedThreadPool()将为每一个任务都创建一个线程。
     *
     * @param arg
     */
    /*public static void main(String []arg){
        ExecutorService exec= Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
            exec.execute(new LiftOff());
        }
        exec.shutdown();
        System.out.println("结束");
    }*/

    /**
     * 获取固定数量的线程数,Executors.newFixedThreadPool(5)将一次性执行完指定数量的线程分配。
     *
     * @param arg
     */
    /*public static void main(String []arg){
        ExecutorService exec= Executors.newFixedThreadPool(5);
        for(int i=0;i<5;i++){
            exec.execute(new LiftOff());
        }
        exec.shutdown();
        System.out.println("结束");
    }*/

    /**
     * 序列化线程,Executors.newSingleThreadExecutor()箱式线程数量为1的Executors.newFixedThreadPool(1),只会创建一个线程
     *
     * @param arg
     */
    /*public static void main(String []arg){
        ExecutorService exec= Executors.newSingleThreadExecutor();
        for(int i=0;i<5;i++){
            exec.execute(new LiftOff());
        }
        exec.shutdown();
        System.out.println("结束");
    }*/

    /**
     * 以上三种创建线程的方式,单一、可变、定长都有一定问题,
     * 原因是FixedThreadPool和SingleThreadExecutor底层
     * 都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,容易导致OOM。
     * OOM: out of memory,内存超出
     *  所以一般情况下都会采用自定义线程池的方式来定义
     * @param args
     */
    public static void main(String[] args) {
   
        /**
         * 1、corePoolSize线程池的核心线程数
         * 2、maximumPoolSize能容纳的最大线程数
         * 3、keepAliveTime空闲线程存活时间
         * 4、unit 存活的时间单位
         * 5、workQueue 存放提交但未执行任务的队列
         * 6、threadFactory 创建线程的工厂类
         * 7、handler 等待队列满后的拒绝策略
         */
        ExecutorService executor = new ThreadPoolExecutor(10,20,200L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<5;i++){
   
            executor.execute(new LiftOff());
        }
        executor.shutdown();
        System.out.println("结束");
    }

}

exec.shutdown()方法的调用是为了防止新任务被提交给这个Executor,这个程序将在Executor中所有的任务完成之后尽快退出。

4. Callable从任务中返回值

Runnable是执行工作的独立任务,他不会返回任何值。如果我们希望执行任务后返回一个值,这个时候一般使用Callable接口,实现call()方法

public class CallableDemo  {
   

    public static void main(String []arg){
   
        ExecutorService exec= Executors.newCachedThreadPool();
        ArrayList<Future<String>> results=new ArrayList<Future<String>>();
        for(int i=0;i<10;i++){
   
            results.add(exec.submit(new TaskWithResult(i)));
            //isDone()方法用来检查future是否已经完成
            results.get(0).isDone();
        }
        for(Future<String> fs:results){
   
            try{
   
                System.out.println(fs.get());
            }catch(InterruptedException e){
   
                System.out.println(e);
                return;
            }catch (ExecutionException e){
   
                System.out.println(e);
            }finally {
   
                exec.shutdown();
            }
        }
    }

}

class TaskWithResult  implements Callable<String> {
   
    private int id;
    public TaskWithResult(int id){
   
        this.id=id;
    }

    @Override
    public String call() throws Exception {
   
        return "result of TaskWithResult "+id;
    }
}

随手记

有时使用excel导入数据时可能会遇到导入较长数值类型数据,如超过11位的银行卡号或手机号,这时Excel默认使用科学计数法,而这可能和我们的要求有些不符
可以采用这样的方法将科学计数法的数字转换成为正常的数字
BigDecimal bd = new BigDecimal(“3.40256010353E11”);
System.out.println(bd.toPlainString());

5. 休眠

sleep()方法是最简单的一种任务休眠方法,使任务中止执行给定的时间
方法有:Thread.sleep(100)、TimeUnit.MILLISECONDS.sleep(100)

6.优先级

线程的优先级就是将该线程的重要性传递给了调度器,调度器将会倾向于让优先权最高的线程先执行。当前,这并不是意味着优先权较低的线程将得不到执行(也就意味着优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
注:试图操纵线程优先级通常是一个错误
设置优先级方法:Thread.currentThread().setPriority(Integer)
Thread.currentThread():获取驱动该任务的Thread对象引用
优先级需要在任务开始的地方进行设置,即:run()开头部分

7.让步

让步即可以给线程调度器一个暗示,表示可以让其他线程使用CPU了,这个暗示将通过Thread.yield()方法来做出(注:没有任何的机制保证它必须会被采纳)

8.后台线程

所谓后台线程,就是指程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于线程中不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死所有后台线程。
可以使用Thread实例对象的setDaemon()方法来设置该线程是后台线程
所有后台线程创建的任何线程默认被设置成为后台线程
可通过方法isDaemon来判断线程是否是后台线程
当非后台线程全部终止的时候,jvm会关闭所有的后台进程,哪怕后台线程中有finally程序块,也不会执行到finally,直接强硬关闭


/**
 * @author heshuai
 * @title: E_DaemonThread
 * @description: 基本线程机制,演示java编程思想中21.2.8节案例
 *                这里提到了后台(daemon)线程,也称之为服务线程,它是做什么的呢?在这种类型的线程中主要是做一些辅助性工作的,比如开启一个服务线程去时刻检查一个类的状态。
 *                它有几个特性:1、它由setDaemon(true)来设置后台线程,所有由后台线程创建的线程都默认是后台线程;
 *                           2、当所有非后台线程结束后,进程会直接停止,这里不会有序的去关闭后台线程,而是采用直接强制关闭的方式,所以当我们在后台线程的finally语句块中进行一些操作的
 *                           时候,会因为进程的关闭而不会执行到
 * @date 2021年01月30日 23:09
 */
public class E_DaemonThread {
   

    /*
    public static void main(String[] args) {
        for (int i=0;i<5;++i){
            Thread daemon=new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("全部后台线程已启动");
        try {
            TimeUnit.MILLISECONDS.sleep(9);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
*/

    public static void main(String[] args) {
   
        /**
         * Executors 自1.5之后开始使用,可以创建ExecutorService、ScheduledExecutorService、ThreadFactory(默认一种的内部实现方式)、Callable
         * Executors.newCachedThreadPool();
         *          创建线程池对象,线程池即创建若干线程,若线程结束则不会全部杀死,而是保留一部分,当有新需要线程的时候就会直接从池子从拿出已有的线程来使用,这样就不会浪费反复创建线程资源了
         *          这个方法在这个工具类中一共有两个重载方法,若不设置ThreadFactory,则使用默认的线程工厂实现方式(Executors.defaultThreadFactory()),当然也可以和下面一样,使用自己自定义的线程工厂实现方式
         *          其他几种创建线程池的方法基本一样。不过阿里手册推荐使用自定义去创建线程池的方法( new ThreadPoolExecutor()),当然这个方法内部也是这样使用的
         */
        ExecutorService es= Executors.newCachedThreadPool(new DaemonThreadFactory());
        for (int i=0; i<5;++i){
   
            // 执行
            es.execute(new SimpleDaemons());
        }
        System.out.println("全部后台线程已启动");
        try {
   
            TimeUnit.MILLISECONDS.sleep(9);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }

    /**
     * 自定义一个线程工厂,实现ThreadFactory
     *      在ThreadFactory只有一个方法newThread(),用来创建新的线程,当然这里也就是对创建线程的几个参数进行限定,如:priority、name、daemon status等(还有一个线程组,这个在java编程思想一书中提到在jdk5之后就没有用到了)
     *
     *      在这个自定义ThreadFactory中,设置所有新Thread都是后台线程
     */
    static class DaemonThreadFactory implements ThreadFactory {
   

        @Override
        public Thread newThread(Runnable r) {
   
            Thread t=new Thread(r);
            // 定义线程为后台线程
            t.setDaemon(true);
            return t;
        }
    }

    static class SimpleDaemons implements Runnable{
   

        @Override
        public void run(){
   
            try {
   
                System.out.println(Thread.currentThread()+":"+this);
                //isDaemon()判断是否为后台线程
                System.out.println("是否是后台线程:"+Thread.currentThread().isDaemon());
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
   
                e.printStackTrace();
            }finally {
   
                //当非后台线程全部终止的时候,jvm会关闭所有的后台进程,并不会执行到finally,直接强硬关闭
                System.out.println("后台线程将关闭");
            }
        }
    }
}

9. 编码的变体

这部分内容简单点来说就是,上诉部分都是规规矩矩的实现Runnable或者是生成Thread线程的方式来进行执行的,但是有时候我们可能需要更加简单的方式来实现使用多线程

以下只是举一个例子,有兴趣的朋友可以看我的GitHut

  /**
     * 使用匿名内部类的方式声明线程
     */
    class InnerThread2{
   
        private int countDown=5;
        private Thread t;

        public InnerThread2(String name){
   
            t=new Thread(name){
   
                @Override
                public void run(){
   
                    try{
   
                        while(true){
   
                            System.out.print(this);
                            if(--countDown ==0){
   
                                return;
                            }
                            Thread.sleep(10);
                        }
                    }catch (InterruptedException e){
   
                        System.out.print("Interrupted");
                    }
                }
                @Override
                public String toString(){
   
                    return getName()+":"+countDown;
                }
            };
            t.start();
        }

    }

10. join()

此方法是一个Thread实例对象的方法,它的作用是如果在某个线程上调用t.join()。此线程将被挂起,直到目标线程t结束才会继续执行(即t.isAlive()返回false时)

package com.kfcn.concurrent.abasics;

/**
 * @program: concurrentTest
 * @description: Understanding join()
 * @author: hs
 * @create: 2020-06-10 21:51
 **/
public class Joining {
   
    public static void main(String []args){
   
        Sleeper sleepy=new Sleeper("Sleepy",150);
        Sleeper grumpy=new Sleeper("Grumpy",150);
        Joiner dopey=new Joiner("Dopey",sleepy);
        Joiner doc=new Joiner("Doc",grumpy);
        grumpy.interrupt();
    }
}

class Sleeper extends Thread{
   
    private int duration;
    public Sleeper(String name, int sleepTime){
   
        super(name);
        duration=sleepTime;
        start();
    }
    @Override
    public void run(){
   
        try {
   
            sleep(duration);
        }catch (InterruptedException e){
   
            System.out.println(getName()+" was interrupted. "+"isInterrupted(): "+isInterrupted());
            return;
        }
        System.out.println(getName()+"join completed");
    }
}

class Joiner extends Thread{
   
    private Sleeper sleeper;
    public Joiner(String name, Sleeper sleeper){
   
        super(name);
        this.sleeper=sleeper;
        start();
    }
    @Override
    public void run(){
   
        try {
   
            sleeper.join();
        }catch (InterruptedException e){
   
            System.out.println("Interrupted");
        }
        System.out.println(getName()+" join completed");
    }
}

11. 捕获异常

由于线程的本质特性,一般情况下不能捕获从线程中逃逸的异常,一旦异常逃出了任务的run()方法,它就会向外传播到控制台
为此,在JDK1.5之后,Thread线程提供了一个新接口Thread.UncaughtExceptionHandler,它允许你在每一个Thread对象上边都附着一个异常处理器。这个接口中的uncaughtException()方法会在线程因未捕获异常而临近死亡的时候被调用。由此在线程中未捕获的异常是通过uncaughtException()方法来捕获的


/**
 * @author heshuai
 * @title: H_CaughtExceptionThread
 * @description: 基本线程机制,演示java编程思想中21.2.14节案例
 *              捕获异常,我们都知道如果在串行开发的时候如何捕获异常,那么接下来就是如果处理线程抛出的异常
 * @date 2021年03月06日 12:38
 */
public class H_CaughtExceptionThread {
   
    public static void main(String []args){
   
        ExecutorService exec= Executors.newCachedThreadPool(new HandlerThreadFactory());
        exec.execute(new ExceptionThread2());
    }


    static class ExceptionThread2 implements Runnable{
   

        @Override
        public void run(){
   
            Thread t= Thread.currentThread();
            System.out.println("run() by "+t);
            System.out.println("eh= "+t.getUncaughtExceptionHandler());
            throw new RuntimeException();
        }
    }

    /**
     * 继承未捕获异常处理器
     * t:发生异常的线程
     * e:所发生的异常
     */
    static class MyUncaughtException implements Thread.UncaughtExceptionHandler{
   
        @Override
        public void uncaughtException(Thread t,Throwable e){
   
            System.out.println("caught: "+e );
        }
    }

    /**
     * 工厂类,生成可以捕获异常的Thread工厂类
     */
    static class HandlerThreadFactory implements ThreadFactory {
   

        @Override
        public Thread newThread(Runnable r){
   
            System.out.println(this+" creating new Thread ");
            Thread thread=new Thread(r);
            System.out.println("created "+thread);
//            t.setUncaughtExceptionHandler(new MyUncaughtException());
            // lambda表达式
            thread.setUncaughtExceptionHandler((Thread t,Throwable e) -> {
   
                System.out.println("caught: "+e );
            });
            System.out.println("en = "+thread.getUncaughtExceptionHandler());
            return thread;
        }
    }
}

自此你可以根据代码的需要在不同的线程中设置不同的异常处理器,但是如果有一些相同的异常处理方法的话,可以采用设置Thread类中的一个默认异常处理器的静态域的方式。

Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtException());

这个默认异常处理器只有在不存在线程专有未捕获异常处理器的时候才会被调用。

三、共享受限资源

有了并发就可以同时做多件事情了,但是两个或者多个线程彼此相互干涉的问题也就出现了。如果不防范这种冲突,就可能发生两个线程同时试图访问同一个银行账号,或向同一个打印机打印,改变同一个值等诸如此类的问题

1. 解决共享资源竞争

基本上所有的并发模式在线程冲突问题上,都是采用序列化访问共享资源的方案,这也就意味着给定的同一时刻只有一个任务在访问共享资源。
java提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务执行到被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
这里的锁主要分为两种,一种是对象锁(也被称之为监视器),一种是类锁,ava中有类方法,对象方法。这个与前面的锁相互对应。
对象锁,顾名思义就是针对于对象的锁,当前对象如果上锁后,就不可以被其他线程调用当前对象加锁的方法,这种互相排斥的,也被称之为互斥量(mutex,多介绍一个单词mutual)。
类锁,在类的级别上进行上锁,主要针对的是static所修饰的类方法,当前类被上锁后,可以在类的范围中防止其他线程对当前加锁的类方法的调用。
注意:

  • 所有在上synchronized修饰的资源,被称为互斥量
  • 属性不可以被synchronized修饰
  • 互斥量只针对于被synchronized修饰的资源,而不被它所修饰的共享资源并不会影响(不管类是否加锁)
  • 不管object锁还是class锁,它们的范围都是一个类的,也就是说当前类中一个加锁方法被一个线程调用,那么在这个方法退出之前,其他线程不可以调用这个类中的其他加锁方法。

其实上面也解释了工作中一些常识的理论:

  • 现在的类属性都要求是private,就是为了防止可以直接修改类属性,需要通过setget方法来进行修改
  • 类大多数都是单例的,这个在更高的层面上解决了资源竞争问题

大概就想到这么多,这节就到这。

2. Lock显式声明锁

在jdk5中提供一个显式声明互斥锁的机制,但是它和synchronized在使用层面上有明显的不同,Lock对象必须被显式的创建、锁定和释放。在代码量上,它是更多的,但是它也更加的灵活。

这里提供几个常用的方法,具体测试代码可以看我的GitHub

// 创建Lock对象
private Lock lock = new ReentrantLock();
lock.lock(); // 获得锁,若暂时获取不到则会一致等待,也就是串行处理
lock.tryLock() // 获取锁,若获取到则返回true,否则返回false,并不会等待直接返回
lock.tryLock(2,TimeUnit.SECONDS) // 等待特定时间去获取线程,若期间内可以获取则返回true并且获取到锁,否则false
lock.unlock(); // 释放锁,类似于synchronized,加锁几次就需要释放锁几次

基本上这几个方法就足够了,Lock和synchronized功能基本一致,也可以分为对象锁和类锁,并且不会互相影响。这里边有两个方法lock和tryLock,相对而言tryLock更加灵活,若一时获取不到锁,可以选择去执行其他操作,然后再回去继续等待。

3.原子性与易变性

这一节的知识有些晦涩,下面只对原子性以及可视性进行部分记录

突然项目开会,开到23:12,呼,差点都忘了

原子性:即指不可以被分割的操作,就是原子性操作。通常指的是不加锁状态下的原子性操作。
举个例子,通常基本数据类型的赋值和返回操作是原子性的,除了long、double类型,为什么呢,因为这两个类型是64位的,JVM可以将64位(long、double)的读取和写入当做两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同任务可以看到不确定的结果的可能性(这有时被称为字撕裂,因为你可以看到部分你修改的值)。当然如果你对long、double类型的变量加上volatile关键字,那么就可以获得原子性(赋值、返回操作)。

可视性:在如今的多处理器系统中,可视性问题远比原子性问题多得多,即在一个线程中做出了对一个域进行了修改,对其他线程来说可能是不可视的,这就是可视性。
例如:修改后只是暂时性地存储在了本地处理器的缓存中,未向内存中同步,那么不同的线程可能就有不同的结果了。
可以使用volatile关键字还确保应用中的可视性,如果将一个变量声明为volatile,那么只要对这个变量进行了写操作,那么其他线程所有的读操作就都可以看到这个修改。这是因为volatile会立即被写入到主存中,而读取操作就发生在主存中。
为什么会造成不可视问题?那是因为JVM可能会对非volatile的原子性操作做移除写入和读取操作的优化,因此其他线程就看不到最新的值了。volatile就是为了告诉JVM取消这个优化,直接将对值的改变发生在内存中。

4. 原子类

这个就简单的介绍一下有哪些原子类吧,有AtomicInteger、AtomicLong、AtomicReference等,这些原子类一般来说在项目中很少用到,只有在性能调优的时候才会用到。

5. 临界区

临界区提出的意义:有时,你可能只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。这样就可以使用synchronized语句块来将代码隔离出来,这样隔离出来的代码块也叫作临界区(critical section)

synchronized(syncObject) {
   
	// ...
}

在synchronized关键字的括号中,是用来指定某个对象,此对象锁用来对花括号中的代码进行同步控制。
这块多记录一下,这个代码块也被称为同步控制块,如果想进入这个代码块,那么必须得到syncObject对象的锁,没有锁那么就只能等待获取锁了。

很明显,这样做比直接synchronized修饰整个方法可以获得更多的访问。
自然也可以使用显式锁来创建临界区,那么就是下面的代码

private Lock lock = new ReentrantLock();
lock.lock();
try {
   
 //...
} finally {
   
 lock.unlock();
}

6.线程本地存储

这块就很有意思了,有句话是怎么说的,我不能打败你,我就不搭理你。
这个概念就很类似了,防止任务在共享资源上产生冲突的第二种方式就是根除对于变量域的共享。原理呢,是线程本地存储是一种自动化的机制,可以为使用相同变量的每个不同线程都创建不同的存储。具体是使用java.lang.ThreadLocal类来实现的。

public class E_ThreadLocal {
   

    public static void main(String[] args) throws InterruptedException {
   
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0; i<5; i++){
   
            exec.execute(new Accessor(String.valueOf(i)));
        }
        TimeUnit.SECONDS.sleep(1);
        exec.shutdownNow();
    }

    static class Accessor implements Runnable {
   

        private String id;

        public Accessor(String idn){
   
            this.id = idn;
        }

        @Override
        public String toString() {
   
            return "Accessor{" +
                    "id='" + id + '\'' +
                    '}'+"+++++++"+ThreadLocalVariableHolder.get();
        }

        @Override
        public void run() {
   
            while (!Thread.currentThread().isInterrupted()){
   
                ThreadLocalVariableHolder.increment();
                System.out.println(this);
                Thread.yield();
            }
        }
    }

    static class ThreadLocalVariableHolder {
   
        // 通常ThreadLocal对象当作静态域来存储
        private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
   
            private Random random = new Random(47);
            protected Integer initialValue() {
   
                return random.nextInt(50000);
            }
        };
        // 此方法非synchronized,但是因为value为线程一级的变量,那么是可以保障不会出现竞争条件的。
        public static void increment(){
   
            ThreadLocalVariableHolder.value.set(ThreadLocalVariableHolder.value.get()+1);
        }

        public static Integer get(){
   
            return ThreadLocalVariableHolder.value.get();
        }
    }
}

以上案例是演示如何使用ThreadLocal这个类,基本上用到了这个类里边的三个方法,set、get、initialValue,initialValue主要是做初始化值的使用使用的,而且观察他的注释,这个初始化值是属于懒初始化的。
在这里插入图片描述
也就是说,它只有在调用get方法之前会进行一次初始化,如果在调用get之前调用了set,那么将不会进行初始化。还有一点,很有意思,如果调用了remove方法后调用get方法,那么这个值将再此被初始化(多次进行)。类似于下边这种:
在这里插入图片描述

四、终结任务

1. 在阻塞的时候终结

线程的状态

  1. 新建(new):当线程被创建时,它只会短暂的处于这种状态。此时他分配了必须的系统资源,并执行了初始化。因此也代表了此时的线程有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或者阻塞状态。
  2. 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行了,也就是说,在任意时刻,线程可以运行也可以不运行。这不同于阻塞和死亡。
  3. 阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU。直到线程进入就绪的状态,它才有可能执行操作。
  4. 死亡(Dead):处于死亡或者说是终结状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或是不再可运行的。任务死亡通常方式是从run()方法返回,但是任务的线程还可以被中断

有四种可能进入阻塞状态:

  1. 通过调用sleep()使任务进行休眠状态,任务在指定的时间内是不会运行的。
  2. 通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息(signal()或signalAll()消息),线程才会进入就绪状态。
  3. 任务在等待某个I/O完成
  4. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另外一个任务已经获取了这个锁。

2.中断

中断,中断一个正在运行的线程。这里的中断主要是指中断进入阻塞状态下的线程运行

public class A_InterruptedThread {
   

    static class SleepBlocked implements Runnable {
   

        @Override
        public void run() {
   
            try {
   
                TimeUnit.SECONDS.sleep(100);
            }catch (InterruptedException e){
   
                e.printStackTrace();
                System.err.println("程序被打断");
            }
            System.out.println("SleepBlocked 执行完毕");
        }
    }

    static class IOBlocked implements Runnable {
   

        private InputStream in;

        public IOBlocked(InputStream inn) {
   
            this.in = inn;
        }

        @Override
        public void run() {
   
            try {
   
                System.out.println("等待读:");
                in.read();
            }catch (IOException e){
   
                if (Thread.currentThread().isInterrupted()){
   
                    System.err.println("IOBlocked程序被打断");
                }else{
   
                    e.printStackTrace();
                }
            }
            System.out.println("IOBlocked 执行完毕");
        }
    }

    static class SynchronizedBlocked implements Runnable {
   

        public synchronized void f() {
   
            // 忙等待中,永远也不释放锁
            while (true)
                Thread.yield();
        }

        public SynchronizedBlocked() {
   
            new Thread(()->{
   
                // 开启一个线程锁住该对象
                    f();
            }).start();
        }

        @Override
        public void run() {
   
            System.out.println("尝试获取对象锁...");
            f();
            System.out.println("SynchronizedBlocked 执行完毕");
        }
    }

    static class ReentrantLockBlocked implements Runnable {
   

        private Lock lock = new ReentrantLock();

        public void f() throws InterruptedException {
   
            // 尝试获取锁,直到这个线程被中断
            lock.lockInterruptibly();
        }

        public ReentrantLockBlocked() {
   
            new Thread(()->{
   
                // 锁住当前对象
                lock.lock();
            }).start();
        }

        @Override
        public void run() {
   
            System.out.println("尝试获取对象锁...");
            try {
   
                f();
            } catch (InterruptedException e) {
   
                System.out.println("ReentrantLockBlocked 被中断");
            }
            System.out.println("ReentrantLockBlocked 执行完毕");
        }
    }

    private static ExecutorService exec = Executors.newCachedThreadPool();

    private static void test(Runnable r) throws InterruptedException {
   
        // 获得线程上下文
        Future<?> f = exec.submit(r);
        TimeUnit.MILLISECONDS.sleep(10);
        System.out.println("将要打断线程:"+r.getClass().getName());
        // 中断指定线程,这个是中断单个线程的方式
        f.cancel(true);
        System.out.println("Interrupt 已经发送"+r.getClass().getName());
    }

    private static void testNormal() throws InterruptedException {
   
        test(new SleepBlocked());
        test(new IOBlocked(System.in)); // 不可被中断
        test(new SynchronizedBlocked()); // 不可被中断
        System.out.println("将要退出系统");
        System.exit(0);
    }

    private static void testIO() throws IOException, InterruptedException {
   
        ServerSocket socket1 = new ServerSocket(8080);
        ServerSocket socket2 = new ServerSocket(8081);
        InputStream socketInput1 = new Socket("localhost",8080).getInputStream();
        InputStream socketInput2 = new Socket("localhost",8081).getInputStream();
        exec.execute(new IOBlocked(socketInput1));
        exec.execute(new IOBlocked(socketInput2));
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println("中断由exec管理的所有的线程");
        exec.shutdownNow();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("将关闭。。socketInput1资源");
        socketInput1.close(); // 关闭I/O资源来释放阻塞线程
        TimeUnit.SECONDS.sleep(1);
        System.out.println("将关闭。。socketInput2资源");
        socketInput2.close(); // 释放阻塞线程
    }

    public static void testReentrantLock() throws InterruptedException {
   
        exec.execute(new ReentrantLockBlocked());
        TimeUnit.SECONDS.sleep(1);
        System.out.println("将要中断ReentrantLockBlocked");
        exec.shutdownNow();
        System.out.println("中断ReentrantLockBlocked完成");
    }

    public static void main(String[] args) throws Exception {
   
//        testNormal(); // 测试正  常情况下,三种阻塞下响应Interrupted
//        testIO(); // 测试关闭IO资源以 释放阻塞线程
        testReentrantLock(); // 测试互斥状态下,中断阻塞线程
    }
}

上面测试了对于三种不同阻塞下,调用中断操作后的不同反应,由上可以得知任何要求抛出InterruptedException的阻塞都可以直接中断的,而I/O中断就必须释放底层资源后才可以;锁等待的情况,一般情况下是不能进行中断了(但是可以中断正在占有锁的线程),而且在ReentrantLock上阻塞任务是例外的,是可以被中断的。因为可以看出来lockInterruptibly()方法是有所不同的,它是抛出InterruptedException异常的,也就是说基于上面说的它也是可以被中断的。

补充:
ReentrantLock,有四种获取锁的方法,分别是lock、tryLock、tryLock(long time, TimeUnit unit)、lockInterruptibly(),各有特点
lock:机制和synchronized类似,若锁被占用,那么将等待获取锁,直到锁被获取;
tryLock:尝试获取锁,若锁被占用,则返回false,不再等待。若是获取到锁,则返回true
tryLock(long time, TimeUnit unit):与上边相比,有一定的时间,在指定时间内,则等待获取锁,超时后,则返回false
lockInterruptibly:若锁被占用,则等待获取锁,直到锁被获取或者该线程被中断。

3.检查中断

这一节就很有意思了,上面我记住了如何中断线程,并且举例了三种阻塞情况下的中断方法,但是要知道,线程一共是有四种状态的,分别是准备、就绪、阻塞、死亡。只说了阻塞状态下的中断,那么其他三种情况呢?这种也是可以考虑一下,准备和死亡就不用说了,因为这是一个非常短的一个状态,不需要阻塞。就绪呢?
假如有一个Runnable,是一个循环,如果不被中断,那么一直是处于就绪的状态,那么如何打断呢?其实这里可以检查中断状态来判断是否中断。Thread.currentThread().isInterrupted()返回值是 boolean型,该线程被中断则返回true,否则false;
只要将这个作为一个循环条件,那么就可以在调用shutdownNow的时候,判断线程已被中断,然后退出当前线程了。
注意: 有些是操作是会清空中断状态的,比如调用它Thread.currentThread().isInterrupted()就会清空中断状态,所以有需要的话,必须将这个状态存储起来,当然它清空是为了确保并发状态下不会因为某个任务被中断而通知你两次。

五、线程之间的协作

上面提到的都是通过锁来同步两个线程的行为,从而使得一个线程不会干涉另外一个线程的资源。也就是说两个线程在交替使用某个共享资源的时候,可以通过互斥来使得任何时刻只有一个线程是可以访问这项资源的。
基于此,这一节学习的是如何使线程彼此之间进行协作,协作不同于以前的内容了,这一部分不是要解决彼此之间的干涉,而是彼此之间的协调。书中是举了个项目规划的例子,这里就举个生活中的例子吧

做西红柿炒蛋应该都是知道的,不过有一种做法是要把西红柿先用热水煮一下,煮的快熟了的状态,可以去掉外皮。
那么要完成一道西红柿炒蛋,首先可以完成什么?是否这道菜必须一步一步完成呢?哪些步骤可以同时进行?下面说一下我的见解
假设我们有三个灶台、三个锅,那么第一步我们需要将西红柿煮熟还有需要将鸡蛋炒熟,这两件事其实可以同时进行,这样可以大大减少等待时间。还有一件事那么就是需要放油入锅,放煮好的西红柿进行翻炒再放入鸡蛋,很明显这件事必须要第二步进行,而且必须是在西红柿煮熟后以及鸡蛋准备好后进行,所以哪怕我们已经有条件同时进行这三件事,但是因为做第二个步骤所需要的外部条件不符合,第二步骤也是不可以进行的。
那么真实地步骤应该是:将水放入A锅,放入西红柿;将油放入B锅,放入鸡蛋;将油放入C锅,但是因为没有配好西红柿和鸡蛋,那么需要等待(wait),等待A锅和B锅准备好并发出准备好的指令(notifyall),那么C锅就可以放入西红柿进行翻炒,再放入鸡蛋放入配料,这样一道西红柿鸡蛋就做好了。

1.wait()和notifyAll()

上面简单介绍了在什么场景下使用线程之间的协作。在程序中,我们可以通过wait()来将自身挂起,并且等待某个条件发生变化,通常这种变化 的改变超出了当前线程可控制的范围。在未接触wait()的时候,可以通过忙等待 的方式来实现,但是这是一种不良的CPU周期使用方式,因为所谓的忙等待就是一个不断循环检测某个外部条件变化的代码,这样的话,其实是一直在占有锁并且占用CPU时间段。
只有notify()notifyAll()发生时,即表示发生了某些外部条件的改变了,那么这个线程会被唤醒,也就是解除挂起状态。
wait()有三个重载方法,分别是wait(long timeout, int nanos)wait(long timeout)wait(),timeout的单位是milliseconds,意思是挂起后,在一个时间段内若没有被唤醒,则超时后自己唤醒自己。但是这个方法就比较有意思了,wait(long timeout, int nanos),我原以为它是更加对时间这里更加的细化,结果看了它的实现发现并不是这样的。
在这里插入图片描述
这个方法的注释中指出nanos的单位是nanoseconds,并且超时时间应该为1000000*timeout+nanos,这样的单位应该是nanoseconds了,时间粒度来说更加的细了,但是实际实现确实上图所示。这地方就不深究了,用到这个方法的时候再说吧。另外两个方法其实就够用了。

wait方法和sleep方法的区别:

  • 这个区别是很大的,虽然都是阻塞当前线程,但是wait()期间对象锁是释放的,sleep()并不会释放锁;
  • wait()可以通过notify、notifyAll或时间超时来使wait()恢复。

注:一般来说线程都是由一个工厂来管理的,并且会指定线程的核心线程数和最大线程数等参数(ThreadPoolExecutor),那么使用wait挂起线程的时候,它虽然将锁释放了,但是它依然占有着这个线程,也就是当因为wait出现死锁的状态下,并将所有的线程数都占有了,那么是有整个jvm进程停止工作的可能

notify()notifyAll()wait()他们都是属于Object对象的一部分,并且只能在同步控制方法中或者同步控制块中使用这三个方法。否则将会报错。

下面将写一个代码,实现一个打蜡抛光的过程,这个代码一共两个过程,分别是将蜡涂在车上,然后抛光它,下一层打蜡发生在抛光之后,以此反复进行,也就是多次打蜡抛光。

public class A_WaitAndNotifyAll {
   

    static class Car {
   
        private boolean waxOn = false;

        // 打蜡
        public synchronized void waxed(){
   
            this.waxOn = true;
            notifyAll();
        }
        // 抛光
        public synchronized void buffed() {
   
            this.waxOn = false;
            notifyAll();
        }
        // 等待打蜡执行完毕
        public synchronized void waitForWaxing() throws InterruptedException {
   
            while (this.waxOn == false) {
   
                wait();
            }
        }
        // 等待抛光执行完毕
        public synchronized void waitForBuffing() throws InterruptedException {
   
            while (this.waxOn == true) {
   
                wait();
            }
        }
    }
    // 打蜡流程
    static class WaxOn implements Runnable {
   

        private Car car;

        public WaxOn(Car car) {
   
            this.car = car;
        }

        @Override
        public void run() {
   
            try {
   
                while (!Thread.currentThread().isInterrupted()){
   
                    System.out.println("打蜡...");
                    TimeUnit.MILLISECONDS.sleep(200); // 模拟打蜡过程
                    car.waxed();
                    car.waitForBuffing();
                }
            }catch (InterruptedException e) {
   
                System.out.println("WaxOn 被中断");
            }
            System.out.println("WaxOn 执行完毕"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值