写点什么

快速掌握并发编程 ---Thread 常用方法

用户头像
田维常
关注
发布于: 2020 年 11 月 02 日

关注Java后端技术全栈”**



回复“000”获取大量电子书



今天我们继续分析并发编程知识,今天聊得是Thread(java.lang.Thread)线程。



先看看Thread有些什么东东:









name线程名称



如何获取当前线程名称





public class ThreadMethodsDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程名称:"+Thread.currentThread().getName());
            }
        },"Java后端技术全栈").start();
    }
}




输出:



当前线程名称:Java后端技术全栈



上面只是举例一种方式,还有其他方式也可以获取。我们还是要搞清楚什么地方能设置线程的名称。



注意Thread中有个属性name,这就是线程名称:





其实能设置线程名称的两个地方:



第一个,Thread中构造方法中能设置name都,最后都是调用这个init方法进行name设置的。



private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        //设置name的值,线程的名称
        this.name = name;
        //....省略
    }



第二个,也就是setName(String name);方法



public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        //设置线程name
        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }



使用



public class ThreadMethodsDemo {
    public static void main(String[] args) {
      test1();
      test2();
    }
    private static void test1(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                //输出:当前线程名称:Java后端技术全栈
                System.out.println("当前线程名称:"+Thread.currentThread().getName());
            }
        },"Java后端技术全栈").start();
    }
    private static void test2(){
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //输出:当前线程名称:老田
                System.out.println("当前线程名称:"+Thread.currentThread().getName());
            }
        });
        thread.setName("老田");
        thread.start();
    }
}




建议:我们在使用的时候建议设置线程名称,当出问题的时候可以更好地辨别属于哪个线程,加速问题排除效率。



sleep线程睡眠



Thread.sleep(times)使当前线程从Running状态放弃处理器进入Block状态,休眠times毫秒,再返回Runnable状态。



private static void test3(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                SimpleDateFormat format=new SimpleDateFormat("hh:mm:ss");
                //输出系统时间的时分秒。每2秒显示一次。可能会出现跳秒的情况,因为阻塞1秒过后进入runnable状态,
                //等待分配时间片进入running状态后还需要一点时间
                while(true){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(format.format(new Date()));
                }
            }
        }).start();
}




运行





注意:当一个线程处于睡眠阻塞时,若被其他线程调用.interrupt方法中断,则sleep方法会抛出InterruptedException异常。从上面的try--catch就能看到捕获的是



InterruptedException中断异常区。



看下面这段有意思的代码,启动两个子线程,A线程先睡会,B线程去执行,等B线程执行玩了后中断A线程;



private static void test4() {
         /*
         * 表演者:处于睡眠阻塞的线程。
         * 当一个方法中的局部内部类中需要引用该方法的其他局部变量,那么这个变量必须是final的
         */
        final Thread lin = new Thread() {
            public void run() {
                System.out.println("林:刚美完容,睡觉吧!");
                try {
                    // 当一个线程处于睡眠阻塞时,若被其他线程调用interrupt()方法中断,
                    // 则sleep()方法会抛出 InterruptedException异常
                    Thread.sleep(100000000);
                } catch (InterruptedException e) {
                    System.out.println("林:干嘛呢!都破了相了!");
                }
            }
        };
        /* 表演者:中断睡眠阻塞的线程*/
        Thread huang = new Thread() {
            public void run() {
                System.out.println("黄:开始砸墙!");
                for (int i = 0; i < 5; i++) {
                    System.out.println("黄:80!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println("咣当!");
                System.out.println("黄:搞定!");
                // 中断lin的睡眠阻塞
                lin.interrupt();
            }
        };
        lin.start();
        huang.start();
    }




运行结果





setPriority线程优先级



在Thread类中有个属性、三个常量、setPriority()方法



//优先级
private int priority;
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public final void setPriority(int newPriority) {
      ThreadGroup g;
      checkAccess();
      //newPriority检查,小于1大于10都不行
      if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
          throw new IllegalArgumentException();
      }
      if((g = getThreadGroup()) != null) {
          if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
          }
          setPriority0(priority = newPriority);
      }
}




优先级被划分为1-10,1最低10最高。优先级越高的线程被分配时间片的机会越多,那么被CPU执行的机会就越多。



看下面这段代码



private static void setPriority() {
        // 最高优先级的线程
        Thread max = new Thread() {
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("max "+Thread.currentThread().getName());
                }
            }
        };
        max.setName("max的线程");
        // 最低优先级的线程
        Thread min = new Thread() {
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("min "+Thread.currentThread().getName());
                }
            }
        };
        min.setName("min的线程");
        // 默认优先级的线程
        Thread norm = new Thread() {
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("norm "+Thread.currentThread().getName());
                }
            }
        };
        norm.setName("norm的线程");
        // 设置了优先级也不能100%控制线程调度。
        // 只是最大程度的告知线程调度以更多的几率分配时间片给线程优先级高的线程
        max.setPriority(Thread.MAX_PRIORITY);
        min.setPriority(Thread.MIN_PRIORITY);
        // 这项设置可以省略,默认情况下就是该值
        norm.setPriority(Thread.NORM_PRIORITY);
        min.start();
        norm.start();
        max.start();
    }




运行结果就不贴了,因为每次运行的结果不一样。因为这家伙是个不靠谱的家伙。



Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(不完全正确,请参考下面的“线程优先级的问题”)。



  • 记住当线程的优先级没有指定时,所有线程都携带普通优先级。

  • 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。

  • 记住优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。

  • 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。

  • 由调度程序决定哪一个线程被执行。

  • t.setPriority()用来设定线程的优先级。

  • 记住在线程开始方法被调用之前,线程的优先级应该被设定。

  • 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级。



对于线程优先级,我们需要注意



  • Thread.setPriority()可能根本不做任何事情,这跟你的操作系统和虚拟机版本有关

  • 线程优先级对于不同的线程调度器可能有不同的含义,可能并不是你直观的推测。特别地,优先级并不一定是指CPU的分享。在UNIX系统,优先级或多或少可以认为是CPU的分配,但Windows不是这样。

  • 线程的优先级通常是全局的和局部的优先级设定的组合。Java的setPriority()方法只应用于局部的优先级。换句话说,你不能在整个可能的范围 内设定优先级。(这通常是一种保护的方式,你大概不希望鼠标指针的线程或者处理音频数据的线程被其它随机的用户线程所抢占)

  • 不同的系统有不同的线程优先级的取值范围,但是Java定义了10个级别(1-10)。这样就有可能出现几个线程在一个操作系统里有不同的优先级,在另外一个操作系统里却有相同的优先级(并因此可能有意想不到的行为)。

  • 操作系统可能(并通常这么做)根据线程的优先级给线程添加一些专有的行为(例如”only give a quantum boost if the priority is below X“)。这里再重复一次,优先级的定义有部分在不同系统间有差别。

  • 大多数操作系统的线程调度器实际上执行的是在战略的角度上对线程的优先级做临时操作(例如当一个线程接收到它所等待的一个事件或者I/O),通常操作系统知道最多,试图手工控制优先级可能只会干扰这个系统。

  • 你的应用程序通常不知道有哪些其它进程运行的线程,所以对于整个系统来说,变更一个线程的优先级所带来的影响是难于预测的。例如你可能发现,你有一个预期 为偶尔在后台运行的低优先级的线程几乎没有运行,原因是一个病毒监控程序在一个稍微高一点的优先级(但仍然低于普通的优先级)上运行,并且无法预计你程序 的性能,它会根据你的客户使用的防病毒程序不同而不同。



join线程协同



join()是Thread类的一个方法,根据jdk文档的定义,join()方法的作用,是等待这个线程结束,即当前线程等待另一个调用join()方法的线程执行结束后再往下执行。通常用于在main主线程内,等待其它调用join()方法的线程执行结束再继续执行main主线程。



在Thread类中有三个join方法,可能很多人也就只用过无参数的那个方法,其实Thread里是有三个方法的





public final void join() throws InterruptedException {
  join(0);
}
public final synchronized void join(long millis, int nanos) throws InterruptedException {
  //...
  join(millis);
}
//最终的join方法实现
public final synchronized void join(long millis)throws InterruptedException {
        //当前时间戳
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                //等待0, Object中wait方法,会释放对象的锁
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                //等待delay, Object中wait方法,会释放对象的锁
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
}
   /** 
     * Tests if this thread is alive. A thread is alive if it has
     * been started and has not yet died.
     *
     * @return  <code>true</code> if this thread is alive;
     *          <code>false</code> otherwise.
     */
public final native boolean isAlive();//一看注释便知道




都知道这个方法使用了同步锁修饰,然后用到了wait方法。那就必然会想到notify和notifyAll方法。那什么时候执行唤醒这个操作呢?



下面是JVM层面的代码



//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
//里面有一个贼不起眼的一行代码
ensure_join(this);
static void ensure_join(JavaThread* thread) {
  Handle threadObj(thread, thread->threadObj());
  ObjectLocker lock(threadObj, thread);
  thread->clear_pending_exception();
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  java_lang_Thread::set_thread(threadObj(), NULL);
  //同志们看到了没,别的不用看,就看这一句
  //这里就是唤醒操作
  lock.notify_all(thread);
  thread->clear_pending_exception();
}




先来个案例演示一下:



图片下载和图片显示,图片显示需要等待图片下载完成后才可以显示



public class JoinDemo {
    // 判断照片是否下载完成
    private static boolean isFinish = false;
    public static void main(String[] args) {
        // 下载图片的线程
        final Thread download = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("download:开始下载图片");
                for (int i = 0; i <= 100; ) {
                    i = i + 10;//为了演示
                    System.out.println("download:已完成" + i + "%");
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("download:图片下载完毕");
                isFinish = true;
            }
        });
        download.start();
        // 用于显示图片的线程
        Thread showImg = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("show:准备显示图片");
                // 等待下载线程工作结束后,再执行下面的代码,
                try {
                    // 此时显示图片的线程就进入阻塞状态,等待download线程运行结束,
                    // 才能执行下面的代码。注意千万不要在永远也死不了的线程上等待
                    download.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (!isFinish) {
                    throw new RuntimeException("show:图片还没有下载完");
                }
                System.out.println("show:图片显示完成!");
            }
        });
        showImg.start();
    }
}




最后输出





start线程启动



都知道线程的启动是调用start()方法,继续说说这个方法,在Thread中start方法



public synchronized void start() {
    //这里private volatile int threadStatus = 0;初始化的时候就是0
    //如果这里不为0的话就抛异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    //把当前线程加入到线程组中
    //private ThreadGroup group;就是这么个东西
    group.add(this);
    //初始化标记位未启动
    boolean started = false;
    try {
        //下面
        start0();
        //标识为启动状态
        started = true;
    } finally {
        try {
            //如果没开启,标识为启动失败
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
//这是虚拟机里的start0方法
private native void start0();
private Runnable target;
//建议重写此方法或者实现使用Runabble的run方法
//如果重写了Thread的run方法,那么start0方法后面会调用到子类重写的run方法
//如果是实现Runnable接口中的run方法那么就会执行下面的target,run()
//否则啥都不干
@Override
public void run() {
    if (target != null) {
        //调用我们通常实现的业务代码的run方法
        target.run();
    }
}




start0方法是虚拟机提供的,最后会调用run方法,最后执行我们在run方法里的业务代码。这个虚拟机里面的源码写出来篇幅会很大,这里在哪买就不贴了。知道这么回事就ok了。



参考:



https://www.cnblogs.com/duanx...



https://www.cnblogs.com/qin-d...



扫描关注公众号“Java后端技术全栈”



解锁程序员的狂野世界





发布于: 2020 年 11 月 02 日阅读数: 25
用户头像

田维常

关注

关注公众号:Java后端技术全栈,领500G资料 2020.10.24 加入

关注公众号:Java后端技术全栈,领500G资料

评论

发布
暂无评论
快速掌握并发编程---Thread常用方法