1. 线程的创建方式

线程的创建有三种方式:

  1. 继承 Thread 类,重写run方法
  2. 实现 Runable 接口
  3. 实现 Callable 接口

具体 Demo:

  1. public class CreateThreadDemo {
  2. public static void main(String[] args) {
  3. //1.继承Thread
  4. Thread thread = new Thread() {
  5. @Override
  6. public void run() {
  7. System.out.println("继承Thread");
  8. super.run();
  9. }
  10. };
  11. thread.start();
  12. //2.实现runable接口
  13. Thread thread1 = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. System.out.println("实现runable接口");
  17. }
  18. });
  19. thread1.start();
  20. //3.实现callable接口
  21. ExecutorService service = Executors.newSingleThreadExecutor();
  22. Future<String> future = service.submit(new Callable() {
  23. @Override
  24. public String call() throws Exception {
  25. return "通过实现Callable接口";
  26. }
  27. });
  28. try {
  29. String result = future.get();
  30. System.out.println(result);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. } catch (ExecutionException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }
  • 由于 java 不能多继承可以实现多个接口,因此在创建线程的时候尽量多考虑采用实现接口的形式;
  • 实现 callable 接口,提交给 ExecutorService 返回的是异步执行的结果,另外,通常也可以利用 FutureTask(Callable callable) 将 callable 进行包装然后 FeatureTask 提交给 ExecutorsService。(待补充)

2. 线程状态转换

image.png

具体转换过程如上图描述,注意的是:

  • 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.lockslock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。

用一个表格将上面六种状态进行一个总结归纳:
image.png

多线程内存

  • 多线程执行时,在栈内存中,每一个线程都有一片属于自己的栈内存空间,进行方法的压栈和弹栈.
  • 当执行线程的任务结束了,线程自动在栈内存中释放.
  • 当所有的执行线程都结束了,进程才结束

3. Thread 的相关方法

  • Thread.currentThread().getName(): 获得当前线程的名称(主线程:main;自定义线程:Thread-N)。
  • isAlive:判断线程是否未终止
  • getPriority:获得线程的优先级数值
  • setPriority:设置线程的优先级数值
  • setName:设置线程的名字

4. 线程状态的基本操作

线程在生命周期内还有需要基本操作,而这些操作会成为线程间一种通信方式。

4. 1 interrupted

中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。
中断好比其他线程对该线程打了一个招呼。

  • 其他线程可以调用该线程的interrupt()方法对其进行中断操作
  • 同时该线程可以调用 isInterrupted() 来感知其他线程对其自身的中断操作,从而做出响应。
  • 同样可以调用 Thread 的静态方法interrupted()对当前线程进行中断操作,该方法会清除中断标志位。
  • 需要注意的是,当抛出**InterruptedException**时候,会清除中断标志位,也就是说在调用**isInterrupted**会返回**false**

image.png

Eg:

  1. public class InterruptDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. // sleepThread 睡眠 1000ms
  4. final Thread sleepThread = new Thread() {
  5. @Override
  6. public void run() {
  7. try {
  8. Thread.sleep(1000);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. super.run();
  13. }
  14. };
  15. //busyThread一直执行死循环
  16. Thread busyThread = new Thread() {
  17. @Override
  18. public void run() {
  19. while (true) {}
  20. }
  21. };
  22. sleepThread.start();
  23. busyThread.start();
  24. sleepThread.interrupt();
  25. busyThread.interrupt();
  26. while (sleepThread.isInterrupted()) ;
  27. System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
  28. System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
  29. }
  30. }

输出结果

  1. sleepThread isInterrupted: false
  2. busyThread isInterrupted: true

分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。

  • 如果不会中断 sleep、wait、join 等方法,就不会抛 InterruptException 异常,就不会清除中断标志位,isInterrupt() 返回 true
  • 如果中断 sleep、wait、join 等方法,就会抛 InterruptException 异常,就会清除中断标志位,isInterrupt() 返回 false

如果中断 sleep,wait,join 等,就会抛 InterruptException 异常,就会清除中断标志位,那么这种情况应该怎么处理呢?为了保证数据的一致性和完整性,我们需要用 Thread.interrupt() 方法再次中断自己,置上中断标志位。例子如下:

  1. public class test {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread() {
  4. public void run() {
  5. while (true) {
  6. if (Thread.currentThread().isInterrupted()) {
  7. System.out.println("Interruted!");
  8. break;
  9. }
  10. try {
  11. Thread.sleep(2000); // 睡眠时中断会清除中断标志位
  12. } catch (InterruptedException e) {
  13. // 如果少了下面这句,这个线程虽然在外面中断,但是只要中断睡眠中的进程
  14. // 就会清除中断标志位,仍然处于无限循环,会竞争CPU资源
  15. Thread.currentThread().interrupt(); // 再次中断置上中断标记
  16. }
  17. Thread.yield();
  18. }
  19. }
  20. };
  21. t1.start();
  22. Thread.sleep(200);
  23. t1.interrupt();
  24. }
  25. }

Thread.sleep() 方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位。

一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。

4.2 join

用于临时加入一个运算的线程。让该线程执行完,程序才会执行。

  1. Demo d = new Demo();
  2. Thread t1 = new Thread(d);
  3. Thread t2 = new Thread(d);
  4. t1.start();
  5. try{
  6. // 主线程执行到这里,知道t1要加入执行,主线程释放了执行权(仅仅是释放,至于执行权给谁,有cpu随机决定)
  7. // 主线程的执行资格处于冻结状态,直至t1线程执行完恢复
  8. t1.join;
  9. }catch(InterruptException e){}
  10. t2.start();

join 提供了以下方法:

  1. public final synchronized void join(long millis)
  2. public final synchronized void join(long millis, int nanos)
  3. public final void join() throws InterruptedException

Thread类除了提供 join() 方法外,另外还提供了超时等待的方法,如果在等待的时间内线程 thread B 还没有结束的话,thread A 会在超时之后继续执行。

4.3 yield

public static native void yield(); 这是一个静态方法,一旦执行,它会使当前线程让出 CPU,但是,需要注意的是,让出的 CPU 并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了 CPU 时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。

4.4 sleep

public static native void sleep(long millis) 方法显然是 Thread 的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep 方法并不会失去锁。

4.4.1 sleep 和 wait 方法的异同点

  • 相同点
    • 都可以让线程处于冻结状态
  • 不同点
    • sleep 必须指定时间;wait 可以指定时间,也可以不指定时间
    • sleep 时间到,线程处于临时阻塞或者运行;wait 如果没指定时间,必须通过 notify 或者 notifyAll 唤醒。
    • sleep 不一定非要定义在同步中;wait 必须定义在同步中。
    • 都定义在同步中
      • 线程执行到 sleep,不会释放锁
      • 线程执行到 wait,会释放锁

4.5 线程的优先级

操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。

Thread.currentThread.toString: 返回该线程的字符串表示形式,包括『线程名称』、『优先级』、『线程组』
优先级:

  • 用数字标识的0-10;其中默认的初始化优先级是5
  • 最明显的三个优先级 : 1,5,10。
  • Thread.MAX_PRIORITY 线程可以具有的最高优先级。
  • Thread.MIN_PRIORITY 线程可以具有的最低优先级。
  • Thread.NORM_PRIORITY 分配给线程的默认优先级。
  • 得到线程的优先级:getPriority()
  • 更改线程的优先级:setPriority()

sleep() VS yield() 方法

同样都是当前线程会交出处理器资源,而它们不同的是

  • **sleep()** 交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。
  • **yield()** 方法只允许与当前线程具有相同优先级的线程能够获得释放出来的 CPU 时间片。

5. 守护线程 Daemon

守护线程,可以理解为后台线程,一般创建的为前台线程,前后台运行线程的时候都是一样的,获取 cpu 的执行权 限。但是结束的时候有些不同,前台线程和后台线程只要run方法结束,线程结束,但是在所有前台线程结束的时候,后台线程无论处于什么状态都会结束,从而进程结束。进程结束依赖的都是前台线程。
**在后台默默地守护一些系统服务,比如垃圾回收线程,JIT 线程就可以理解守护线程。

方法: setDaemon(boolean on)

  • 该方法必须在线程启动前调用:t.setDaemon(true); t.start; // t 线程设置为了守护线程,否则会报错:Exception in thread "main" java.lang.IllegalThreadStateException
  • on如果为true,该线程标记为守护线程。

Demo:

  1. public class DaemonDemo {
  2. public static void main(String[] args) {
  3. Thread daemonThread = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. while (true) {
  7. try {
  8. System.out.println("i am alive");
  9. Thread.sleep(500);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. } finally {
  13. System.out.println("finally block");
  14. }
  15. }
  16. }
  17. });
  18. daemonThread.setDaemon(true);
  19. daemonThread.start();
  20. //确保main线程结束前能给daemonThread能够分到时间片
  21. try {
  22. Thread.sleep(800);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }

输出结果为:

i am alive finally block i am alive

上面的例子中 daemodThread run 方法中是一个 while 死循环,会一直打印,但是当 main 线程结束后 daemonThread 因为是守护线程就会退出,所以不会出现死循环的情况。

main 线程先睡眠 800ms 保证 daemonThread 能够拥有一次时间片的机会,也就是说可以正常执行一次打印 i am alive 操作和一次 finally 块中 finally block 操作。紧接着 main 线程结束后,daemonThread 退出,这个时候只打印了i am alive并没有打印 finnal 块中的。因此,这里需要注意的是守护线程在退出的时候并不会执行 finnaly 块中的代码,所以将释放资源等操作不要放在 finnaly 块中执行,这种操作是不安全的