Java并发编程系列(一)-线程的基本使用

 

最近在学习java并发编程基础.一切从简,以能理解概念为主.

并发编程肯定绕不过线程.这是最基础的.

那么就从在java中,如何使用线程开始.

继承Thread类

继承Thread类,重写run方法,new出对象,调用start方法.

在新启的线程里运行的就是重写的run方法.

 1 /**
 2  * 集成Thread类 实现run()
 3  */
 4 public class C1 extends Thread {
 5 
 6     @Override
 7     public void run() {
 8         try {
 9             Thread.sleep(100);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         System.out.println(Thread.currentThread().getName() + " run()");
14     }
15 
16     public static void main(String[] args) {
17         C1 c1 = new C1();
18         c1.start();
19     }
20 }

run方法里先睡100毫秒,然后打印当前线程名称+run()

运行结果:

实现Runnable接口

实现Runnable接口run方法
 1 /**
 2  * 实现Runnable接口run()
 3  */
 4 public class C2 implements Runnable {
 5 
 6     @Override
 7     public void run() {
 8         try {
 9             Thread.sleep(100);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         System.out.println(Thread.currentThread().getName() + " run()");
14     }
15 
16     public static void main(String[] args) {
17         C2 c2 = new C2();
18         new Thread(c2, "thread0").start();
19     }
20 }

运行结果:

Lambda表达式

Lambda表达式本质上还是实现了Runnable的run方法,但是写起来相当的方便.注:java8或以上版本才支持

java中Lambda表达式的语法:

1.(parameters) -> expression

2.(parameters) ->{ statements; }

3.对象名::方法名 注:方法名后不加小括号

 1 /**
 2  * Lambda表达式 java8或以上版本
 3  */
 4 public class C3 {
 5 
 6     public void foo() {
 7         try {
 8             Thread.sleep(100);
 9         } catch (InterruptedException e) {
10             e.printStackTrace();
11         }
12         System.out.println(Thread.currentThread().getName() + " foo()");
13     }
14 
15     public static void main(String[] args) {
16         C3 c3 = new C3();
17         new Thread(() -> c3.foo()).start();
18         new Thread(() -> {
19             c3.foo();
20         }).start();
21         new Thread(c3::foo).start();
22     }
23 }

运行结果:

可以看到三种写法是等价的.执行无参数的方法,用第三种双冒号的方式最简单,有参数的可以用第一种方式,执行多行的代码片段只能用第二种方式.

start() run() join()

首先看一段代码吧

 1 /**
 2  * start() run() join()
 3  */
 4 public class C4 {
 5 
 6     public void foo() {
 7         try {
 8             Thread.sleep(100);
 9         } catch (InterruptedException e) {
10             e.printStackTrace();
11         }
12         System.out.println(Thread.currentThread().getName() + " foo()");
13     }
14 
15     public static void main(String[] args) {
16         C4 c4 = new C4();
17         //一个线程不能同时执行多次start() 否侧会抛出IllegalThreadStateException
18         Thread tStart=new Thread(c4::foo);
19         tStart.start();
20         tStart.start();
21         //同时执行多次start()没抛出IllegalThreadStateException 因为不在同一线程内 是new出来的
22         new Thread(c4::foo).start();
23         new Thread(c4::foo).start();
24 
25         try {
26             Thread.sleep(1000);
27         } catch (InterruptedException e) {
28             e.printStackTrace();
29         }
30         System.out.println("---分割线---");
31         //run() 调用的就是重写的那个run 所以没有开启线程 是在主线程里执行的
32         new Thread(c4::foo).run();
33 
34         System.out.println("---分割线---");
35         SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
36         Thread tJoin = new Thread(c4::foo);
37         tJoin.start();
38         System.out.println(simpleDateFormat.format(new Date()));
39         try {
40             //阻塞代码 等待线程执行完
41             tJoin.join();
42         } catch (InterruptedException e) {
43             e.printStackTrace();
44         }
45         System.out.println(simpleDateFormat.format(new Date()));
46     }
47 }
start()

先看main方法里的前5行,在同一时间两次调用同一个线程的start().

运行结果:

抛出了IllegalThreadStateException非法的线程状态异常.也就是说在同一线程里,不能同时多次执行同一段代码.这很好理解了,同时多次执行同一段代码应该用多个线程.

注释掉tStart相关代码,继续运行

运行结果:

第一部分结果说明了同时多次执行同一段代码应该用多个线程.

start方法开启新线程,见源码(删掉源注释):

 1 public synchronized void start() {
 2         //判断线程状态 抛出IllegalThreadStateException
 3         if (threadStatus != 0)
 4             throw new IllegalThreadStateException();
 5 
 6         group.add(this);
 7 
 8         boolean started = false;
 9         try {
10             //这里开启
11             start0();
12             started = true;
13         } finally {
14             try {
15                 if (!started) {
16                     group.threadStartFailed(this);
17                 }
18             } catch (Throwable ignore) {
19             }
20         }
21     }

可以看到调用start首先会判断线程状态,不是0的话,抛出非法的线程状态异常.

真正开启线程是在start0()这里.

转到定义可以看到start调用了本地方法,也就是真正开启线程是c语言写的.那么什么参数都没有.底层这么知道开启线程调用什么方法呢.

最上面,Thread类有个静态构造函数是类在实例化的时候,调用registerNatives本地方法,将自己注册进去的.

run()

第二部分是调用的run方法,显示的线程名是主线程.就是说调用run方法并不会开启新线程.

看看到底是怎么回事.

导航到源码可以看到,Thread类是实现了Runnable接口的.重写了run方法.

如果用继承Thread类的方式写线程类,要重写run方法.先不管源码重写的run是什么,那么你的重写会覆盖掉源码重写的run.这种情况下,new出自己的线程类,然后调用run,当然和线程没有任何关系,就是在主线程里调用了另一个类的普通的方法.

如果用实现Runnable接口的方式写线程类,那么会new一个这个接口的实例,传到new出的Thread对象里.继续往下传到init方法,最终赋值到target变量上

结合上面源码重写的run,那么它还是调用了你传过来的Runnable实例的run方法.

join()

join就是阻塞代码,等待线程执行完后再开继续始执行当前代码

可以看到第三部分结果,第一次输出时间和第二次输出时间相差了109毫秒(线程内睡了100毫秒).如果不用join阻塞,时间一般相差的会很小,当然具体是多少也不一定,得看机器当时的运行情况.用join相差多少毫秒也不一定,但至少一定不会小于100毫秒.(可自行多次尝试).