• Java并发之线程管理(线程基础知识)


    因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录。当然,学习书中知识的时候自己的思考和实践是最重要的。说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的。所以想着细致的来学习一下,就从这本实战开始学习。
    疑问点:什么时候会用到多线程?什么情况下使用多线程来解决问题比较合适?

    线程的创建和运行

    就像学习任何知识一样,要学线程,先得学一下线程是怎么声明(创建)和运行起来的。
    一般来说,java创建线程有两种常用的方式(线程池后面再谈):

      1、继承Thread类,并且覆盖run()方法。

      2、实现Runnable接口类,使用带参数的Thread构造器来创建Thread对象,参数就是Runnable接口的一个对象。

    那么创建完了怎么运行呢?调用run()方法?调用run()方法只是简单的 类对象调用自己的成员方法,那么怎么会开启线程呢?而且每一个线程还有自己的信息(线程名字,线程ID,优先级等)。那么应该是怎么运行呢?答案是调用start()方法。
    来个实例更加直观:
    • 继承Thread类
     1 //创建
     2 public class Thread2 extends Thread{
     3     @Override
     4     public void run() {
     5         System.out.println("继承Thread类创建线程");
     6     }
     7 }
     8 //运行
     9 private static void createThread2() {
    10         Thread2 t2 = new Thread2();
    11         Thread thread = new Thread(t2);
    12         thread.start();
    13 }
    • 实现Runnable接口
     1 //创建
     2 public class Thread1 implements Runnable {
     3     @Override
     4     public void run() {
     5         System.out.println("实现Runnable接口创建线程");
     6     }
     7 }
     8 //运行
     9 private static void createThread1() {
    10         Runnable t1 = new Thread1();
    11         Thread thread = new Thread(t1);
    12         thread.start();
    13 }

    线程信息的获取和设置

    Thread类有一些保存信息的属性,这些属性可以用来标识线程,显示线程状态或者控制线程的优先级。其实这些信息在Thread类中都可以找到,也就是Thread类的一些成员变量。
    • ID:保存线程的唯一标识符
    • Name:线程名称
    • Priority:线程对象的优先级,线程优先级从1-10,1是最低优先级,10是最高优先级。

    • Status:线程状态。在Java中,线程的状态有6种:new(创建)、runnbale(运行)、blocked(阻塞)、waiting(等待)、time waiting和terminated(终止)。状态转换图:

     这个图和上面的六种状态有点差异,六种状态是在Thread类中的枚举类State中的六种枚举。
    1 public enum State {
    2         NEW,
    3         RUNNABLE,
    4         BLOCKED,
    5         WAITING,
    6         TIMED_WAITING,
    7         TERMINATED;
    8 }
    在记忆的时候可以像上图中所示的那样,线程的状态转换图总是会忘掉,可以类比来记忆。线程的物种状态 创建、就绪、运行、阻塞和终止状态 类比开车的过程。
    创建--->把车从车库拿出来
    就绪--->坐在驾驶位置上,发动,拉手刹
    运行--->车启动,开始走
    阻塞--->路口红灯需要停车等待
    就绪--->红灯停止,拉手刹开车
    终止--->到达目的地,停车
    例子举的不是很恰当,其实状态转换主要还是就绪、运行和阻塞三个状态的切换。运行状态运行一段时间后因为某些原因转阻塞状态,阻塞状态解除后 不会立马到运行状态而是到就绪状态,只有就绪状态才有可能获取CPU调度然后重新 变为运行状态。

     线程的中断

    就是结束正在运行的线程。中断方式有以下几种:
    • 调用interrupt()方法,当前执行的线程就会被中断。
    1 task.interrupt();
    判断一个线程是否被中断了,有两种方式:
    1. interrupted(),检查当前执行的线程是否被中断
    2. isInterrupted():当interrupt()方法被调用时,Thread类中表名线程是否被中断的属性会被设置为true。isInterrupted()方法返回这个属性的值。
    推荐使用isInterrupted(),因为interrupted()方法是一个静态方法。
    1 public static boolean interrupted() {
    2         return currentThread().isInterrupted(true);
    3 }

    线程中断的控制

    如果线程实现了复杂的算法那并且分布在几个方法中,或者线程中有递归调用的方法,如何去中断线程?因为直接 调用interrupt()方法不能立竿见影。java还提供了InterruptException异常。当检查到线程中断的时候,就抛出异常,然后在run()方法中捕获并处理这个异常。
    run()方法中捕获异常,打印线程信息
     1 @Override
     2     public void run() {
     3         File file = new File(initPath);
     4         if (file.isDirectory()) {
     5             try {
     6                 dirctoryProcess(file);
     7             } catch (InterruptedException e) {
     8                 System.out.printf("%s:the search has been interrupted", Thread.currentThread().getName());
     9             }
    10         }
    11     }
    在dirctoryProcess()方法中抛出异常。
     1 private void dirctoryProcess(File file) throws InterruptedException {
     2         File[] list = file.listFiles();
     3         if (list != null) {
     4             for (int i = 0; i < list.length; i++) {
     5                 if (list[i].isDirectory()) {
     6                     dirctoryProcess(list[i]);
     7                 } else {
     8                     fileProcess(list[i]);
     9                 }
    10             }
    11         }
    12         if (Thread.interrupted()) {
    13             throw new InterruptedException();
    14         }
    15     }

    线程的休眠和恢复

    java里面提供了sleep()方法来休眠线程。需要掌握的知识点:
    • 休眠指定时间后线程自动恢复
    • 线程休眠的方式有两种:
      • Thread.sleep(1000);//休眠一秒,其中这里的单位为ms
      • TimeUnit.SECONDS.sleep(1);//休眠一秒,这里的单位为s

    等待线程的终止(join())

    当一个线程对象的join()方法被调用时,调用它的线程将被挂起,直到这个线程对象完成它的任务。意思就是:如果在线程B中线程A调用了join()方法,那么只有A线程执行完毕后,才会接着执行线程B下面的代码。
    DataSourceLoader睡眠3秒
     1 public class DataSourceLoader implements Runnable {
     2     @Override
     3     public void run() {
     4         System.out.printf("Begining data sources loading: %s
    ",new Date());
     5         try {
     6             TimeUnit.SECONDS.sleep(3);
     7         } catch (InterruptedException e) {
     8             e.printStackTrace();
     9         }
    10         System.out.printf("Data Sources loading has finised:%s
    ",new Date());
    11     }
    12 }
    NetWorkConnectionLoader睡眠6秒
     1 public class NetWorkConnectionLoader implements Runnable {
     2     @Override
     3     public void run() {
     4         System.out.printf("Begining data sources loading: %s
    ",new Date());
     5         try {
     6             TimeUnit.SECONDS.sleep(6);
     7         } catch (InterruptedException e) {
     8             e.printStackTrace();
     9         }
    10         System.out.printf("Data Sources loading has finised:%s
    ",new Date());
    11     }
    12 }
    执行Main
     1 public class Main {
     2     public static void main(String[] args) {
     3         DataSourceLoader dsLoader = new DataSourceLoader();
     4         Thread t1 = new Thread(dsLoader,"DataSourceThread");
     5         NetWorkConnectionLoader ncLoader = new NetWorkConnectionLoader();
     6         Thread t2 = new Thread(ncLoader,"NetWorkConnectionThread");
     7         t1.start();
     8         t2.start();
     9         try {
    10             t1.join();
    11             t2.join();
    12         } catch (InterruptedException e) {
    13             e.printStackTrace();
    14         }
    15         System.out.printf("Main: Configuration has been loaded:%s
    ",new Date());
    16     }
    17 }
    输出结果如下:
    1 Begining data sources loading: Tue Apr 25 14:46:39 CST 2017
    2 Begining data sources loading: Tue Apr 25 14:46:39 CST 2017
    3 Data Sources loading has finised:Tue Apr 25 14:46:42 CST 2017
    4 NetWorkConnectionLoader loading has finised:Tue Apr 25 14:46:45 CST 2017
    5 Main: Configuration has been loaded:Tue Apr 25 14:46:45 CST 2017
    从结果可以看出:对于main函数线程,如果t1,t2没有调用join函数(),则“
    Main: Configuration has been loaded:Tue Apr 25 14:46:45 CST 2017
    ”这句话应该是先打印的,由于t1,t2调用join函数,结果是main要等待t1,t2执行完成后再执行main线程,即使t1,t2睡眠了一段时间。
    java还提供了另外两种形式的join()方法:
    • join(long milliseconds)
    • join(long milliseconds,long nanos)
    当一个线程调用其他线程的join()方法时,如果使用的是第一种join()方式,那么它不必等到被调用线程运行终止,如果参数指定的毫秒时钟已经到达,它将继续运行。例如:thread1中有这样的代码thread2.join(1000),thread1将挂起,知道满足下面两个条件之一:
    • thread2运行已经完成
    • 时钟已经过去了1000毫秒
    当两个条件中的任何一个成立是,join()方法将返回。
    第二种join()方法跟第一种相似,只是需要接受毫秒和纳秒两个参数。
    其实join()方法在底层调用的就是join(long milliseconds)方法,只不过传递的值是0,即:
    1 public final void join() throws InterruptedException {
    2         join(0);
    3 }
    join()底层实现有一个wait()方法,这样看就比较好记忆了
     1 public final synchronized void join(long millis)
     2     throws InterruptedException {
     3         long base = System.currentTimeMillis();
     4         long now = 0;
     5         if (millis < 0) {
     6             throw new IllegalArgumentException("timeout value is negative");
     7         }
     8         if (millis == 0) {
     9             while (isAlive()) {
    10                 wait(0);
    11             }
    12         } else {
    13             while (isAlive()) {
    14                 long delay = millis - now;
    15                 if (delay <= 0) {
    16                     break;
    17                 }
    18                 wait(delay);
    19                 now = System.currentTimeMillis() - base;
    20             }
    21         }
    22     }

    守护线程的创建和运行

    守护线程(Daemon):这种线程的优先级很低,通常来说,当一个应用程序中没有其他线程运行的时候,守护线程才运行(这个是守护线程的特性)。当守护线程是程序中唯一运行的线程时,守护线程执行结束后,JVM也就结束了。一个典型的守护线程是Java的垃圾回收器
    举的例子是一个队列中的数据插入和取出:
    其中WriteTask负责向队列中插入数据,循环一百次,插入的内容为 时间和 字符串类型的 事件
     1 public class WriterTask implements Runnable {
     2     Deque<Event> deque;
     3     
     4     public WriterTask (Deque<Event> deque){
     5         this.deque=deque;
     6     }
     7     
     8     @Override
     9     public void run() {
    10         
    11         // Writes 100 events
    12         for (int i=1; i<100; i++) {
    13             // Creates and initializes the Event objects 
    14             Event event=new Event();
    15             event.setDate(new Date());
    16             event.setEvent(String.format("The thread %s has generated an event",Thread.currentThread().getId()));
    17             
    18             // Add to the data structure
    19             deque.addFirst(event);
    20             try {
    21                 // Sleeps during one second
    22                 TimeUnit.SECONDS.sleep(1);
    23             } catch (InterruptedException e) {
    24                 e.printStackTrace();
    25             }
    26         }
    27     }
    28 }
     
    记录队列中信息的类 Event
     1 public class Event {
     2 
     3     private Date date;
     4     private String event;
     5     
     6     public Date getDate() {
     7         return date;
     8     }
     9     
    10     
    11     public void setDate(Date date) {
    12         this.date = date;
    13     }
    14     
    15     
    16     public String getEvent() {
    17         return event;
    18     }
    19     
    20     public void setEvent(String event) {
    21         this.event = event;
    22     }
    23 }
    取出数据的线程是一个守护线程 ClearTask
     1 public class CleanerTask extends Thread {
     2     private Deque<Event> deque;
     3 
     4     public CleanerTask(Deque<Event> deque) {
     5         this.deque = deque;
     6         // Establish that this is a Daemon Thread
     7         setDaemon(true);
     8     }
     9 
    10     @Override
    11     public void run() {
    12         while (true) {
    13             Date date = new Date();
    14             clean(date);
    15         }
    16     }
    17 
    18     private void clean(Date date) {
    19         long difference;
    20         boolean delete;
    21         
    22         if (deque.size()==0) {
    23             return;
    24         }
    25         
    26         delete=false;
    27         do {
    28             Event e = deque.getLast();
    29             difference = date.getTime() - e.getDate().getTime();
    30             if (difference > 10000) {
    31                 System.out.printf("Cleaner: %s
    ",e.getEvent());
    32                 deque.removeLast();
    33                 delete=true;
    34             }    
    35         } while (difference > 10000);
    36         if (delete){
    37             System.out.printf("Cleaner: Size of the queue: %d
    ",deque.size());
    38         }
    39     }
    40 }
    最后是执行的Main:
     1 public static void main(String[] args) {
     2 
     3         Deque<Event> deque=new ArrayDeque<Event>();
     4         
     5         WriterTask writer=new WriterTask(deque);
     6         for (int i=0; i<3; i++){
     7             Thread thread=new Thread(writer);
     8             thread.start();
     9         }
    10         
    11         CleanerTask cleaner=new CleanerTask(deque);
    12         cleaner.start();
    13     }
    三个写线程,一个取线程,值得注意的是,只有当三个写线程都休眠的时候,取线程才开始工作。

    线程中不可控异常的处理

    java中异常Exception下面分两类,
    • 受检异常(非运行时异常)(Checked Exception)
    • 非受检异常(运行时异常)(UnChecked Exception)
    由于线程的run()方法不能接受抛出异常,对于受检异常来说,可以在编写程序时捕获,对于非受检异常来说,因为不知道会不会抛出异常,这样就比较麻烦。好在Java提供了一种在线程对象里捕获和处理运行时异常的一种机制。具体如下:
    实现用来处理运行时异常的类,这个类实现UnCaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法
     1 public class ExceptionHandler implements UncaughtExceptionHandler {
     2     @Override    
     3     public void uncaughtException(Thread t, Throwable e) {
     4         System.out.printf("An exception has been captured
    ");
     5         System.out.printf("Thread: %s
    ",t.getId());
     6         System.out.printf("Exception: %s: %s
    ",e.getClass().getName(),e.getMessage());
     7         System.out.printf("Stack Trace: 
    ");
     8         e.printStackTrace(System.out);
     9         System.out.printf("Thread status: %s
    ",t.getState());
    10     }
    11 }
    在run()方法里面制造运行时异常:
    1 @Override
    2     public void run() {
    3         // The next instruction always throws and exception
    4         int numero=Integer.parseInt("TTT");
    5 }
    main函数如下:
     1 public static void main(String[] args) {
     2         Task task=new Task();
     3         Thread thread=new Thread(task);
     4         thread.setUncaughtExceptionHandler(new ExceptionHandler());
     5         thread.start();
     6         try {
     7             thread.join();
     8         } catch (InterruptedException e) {
     9             e.printStackTrace();
    10         }
    11         
    12         System.out.printf("Thread has finished
    ");
    13     }
    这样就捕获了运行时异常,运行结果如下:
     1 An exception has been captured
     2 Thread: 10
     3 Exception: java.lang.NumberFormatException: For input string: "TTT"
     4 Stack Trace: 
     5 java.lang.NumberFormatException: For input string: "TTT"
     6     at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
     7     at java.lang.Integer.parseInt(Integer.java:580)
     8     at java.lang.Integer.parseInt(Integer.java:615)
     9     at com.packtpub.java7.concurrency.chapter1.recipe8.task.Task.run(Task.java:16)
    10     at java.lang.Thread.run(Thread.java:745)
    11 Thread status: RUNNABLE
    12 Thread has finished
    如果不捕获异常,那么输出结果如下:
    1 Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: "TTT"
    2     at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    3     at java.lang.Integer.parseInt(Integer.java:580)
    4     at java.lang.Integer.parseInt(Integer.java:615)
    5     at com.packtpub.java7.concurrency.chapter1.recipe8.task.Task.run(Task.java:16)
    6     at java.lang.Thread.run(Thread.java:745)
    7 Thread has finished

    线程局部变量的使用(ThreadLocal)

    多线程中,共享数据是不安全的。为了保证共享数据的安全性有两种思路:
    1. 设置临界区,保证临界区里的数据一次只能有一个线程访问
    2. 为每个线程维护一个该共享数据的局部变量,这样,每个线程各自使用自己的局部变量。ThreadLocal就是这种思路的实现。
    使用ThreadLocal的大致思路是:把共享数据包装在ThreadLocal<T>中。包装完成后ThreadLocal提供了取值和设值的方法。提供的方法有:
    • get():返回此线程局部变量的当前线程副本中的值。
    • set():将次线程局部变量的当前线程副本中的值设置为指定值。
    • remove():移除此线程局部变量当前线程的值。
    • initialValue():返回次线程局部变量的当前线程的“初始值”。线程第一次使用get()方法访问变量时将调用此方法,但如果线程之前调用了set(T)方法,则不会对该线程再调用initialValue()方法。通常,此方法对每个线程最多调用一次,但如果在调用get()后又调用了remove(),则可能再次调用此方法。
    使用方式如下:
     1 public class SafeTask implements Runnable {
     2 
     3     private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
     4         protected Date initialValue(){
     5             return new Date();
     6         }
     7     };
     8     
     9     @Override
    10     public void run() {
    11         // Writes the start date
    12         System.out.printf("Starting Thread: %s : %s
    ",Thread.currentThread().getId(),startDate.get());
    13         try {
    14             TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
    15         } catch (InterruptedException e) {
    16             e.printStackTrace();
    17         }
    18         // Writes the start date
    19         System.out.printf("Thread Finished: %s : %s
    ",Thread.currentThread().getId(),startDate.get());
    20     }
    21 }

    线程的分组

    Java提供了ThreadGroup类表示一组线程。线程组可以包含线程对象,也可以包含其他的线程组对象,它是一个树形结构。
    线程组允许把一个组的线程当做一个单元,对组内线程对象进行访问并操作他们。对一些执行相同任务的线程,只要一个单一的调用,所有这些线程的运行都会被中断。
    例子是这样的:创建十个线程并让他们休眠一个随机时间(这段时间比如执行了一个查询),当其中一个线程查找成功的时候,我们将中断其他的9个线程。
    疑问点:线程组和线程之间是如何绑定的?
     1 ThreadGroup threadGroup = new ThreadGroup("Searcher");
     2 SearchTask searchTask=new SearchTask(result);
     3 for (int i=0; i<5; i++) {
     4     Thread thread=new Thread(threadGroup, searchTask);
     5     thread.start();
     6     try {
     7         TimeUnit.SECONDS.sleep(1);
     8     } catch (InterruptedException e) {
     9         e.printStackTrace();
    10     }
    11 }
    如上所示,这样就绑定了线程searchTask和线程组threadGroup。线程组提供了一些方法,
    activeCount():线程组中活动线程的估计数
    interrupt():中断此线程中的所有线程
    ...其他方法以后用到的时候查看API
    等待线程组中有线程满足结束条件
    1 private static void waitFinish(ThreadGroup threadGroup) {
    2         while (threadGroup.activeCount()>9) {
    3             try {
    4                 TimeUnit.SECONDS.sleep(1);
    5             } catch (InterruptedException e) {
    6                 e.printStackTrace();
    7             }
    8         }
    9     }
    中断线程组中的所有线程
    1 threadGroup.interrupt();

    线程组中不可控异常的处理

    在线程中处理不可控异常,上面的做法是编写一个类继承UncaughtExceptionHandler接口,然后使用thread.setUncaughtExceptionHandler(UncaughtExceptionHandler eh)来实现。因为Thread没有继承UncaughtExceptionHandler接口,而是提供了一个方法:
    1 public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    2         checkAccess();
    3         uncaughtExceptionHandler = eh;
    4     }
    对于线程组来说,线程组直接实现了UncaughtExceptionHandler接口,
    1 class ThreadGroup implements Thread.UncaughtExceptionHandler {
    这样的话,我们就可以声明一个类MyThreadGroup来继承ThreadGroup类,并实现UncaughtExceptionHandler接口唯一的方法:uncaughtException()方法
    1 public class MyThreadGroup extends ThreadGroup {
    2     public MyThreadGroup(String name) {
    3         super(name);
    4     }
    5     @Override
    6     public void uncaughtException(Thread t, Throwable e) {
    7     }
    8 }
    这样的话以后在使用MyThreadGroup的时候,出现了运行时异常,就可以交给已经实现的uncaughtException()方法处理了。

    使用工厂类创建线程

    Java提供了ThreadFactory接口,这个接口实现了线程对象工厂。
    1 public interface ThreadFactory {
    2     Thread newThread(Runnable r);
    3 }
    工厂类就是用来创建对象的,那么如何使用线程工厂类呢?
    其实从上面的接口看到,ThreadFactory定义了唯一的方法:newThread(Runnable r);方法需要一个Runnable类型的参数,这个参数就是我们定义的线程类。回顾一下不用线程工厂类的时候创建线程对象时怎么用的?
    1 public class Thread1 implements Runnable{
    2 @override
    3 public void run(){
    4 }
    5 }
    6 Runnable t1 = new Thread1();
    7 Thread t = new Thread(t1);
    8 t.start();
    使用了工厂之后呢?这么写,继承ThreadFactory接口实现newThread()方法,这里创建线程
    1 public class MyThreadFactory implements ThreadFactory {
    2 @Override
    3     public Thread newThread(Runnable r) {
    4         Thread t=new Thread(r,"thread_name");
    5         return t;
    6     }
    7 }
    然后创建线程的时候这么写:
    1 Task task = new Task();
    2 Thread thread=factory.newThread(task);
    这么写有什么好处呢?创建线程集中在了new Thread()方法中。
    1. 更容易修改类,或者改变创建对象的方式
    2. 更容易为有限资源创建对象的数目,可以限制一个类型的对象的个数
    3. 更容易为创建的对象生成统计数据
  • 相关阅读:
    史上最全分布式数据库概述
    MySQL高可用实现:主从结构下ProxySQL中的读写分离
    【CS231n】斯坦福大学李飞飞视觉识别课程笔记(十七):神经网络笔记3(下)
    【CS231n】斯坦福大学李飞飞视觉识别课程笔记(十六):神经网络笔记3(上)
    20万DBA都在关注的11个问题
    记一次服务器执行MySQL耗时问题
    Python爬虫入门教程 66-100 对微博登录好奇嘛,用Python试试
    挥手洒衰泪
    长戟如霜大旆红
    我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
  • 原文地址:https://www.cnblogs.com/uodut/p/6775217.html
Copyright © 2020-2023  润新知