|
| 1 | +# Java多线程基础知识篇 |
| 2 | +这篇是Java多线程基本用法的一个总结。 |
| 3 | +本篇文章会从一下几个方面来说明Java多线程的基本用法: |
| 4 | +1. 如何使用多线程 |
| 5 | +2. 如何得到多线程的一些信息 |
| 6 | +3. 如何停止线程 |
| 7 | +4. 如何暂停线程 |
| 8 | +5. 线程的一些其他用法 |
| 9 | + |
| 10 | +所有的代码均可以在[char01](https://github.com/byhieg/JavaTutorial/tree/master/src) |
| 11 | + |
| 12 | +## 如何使用多线程 |
| 13 | +## 启动线程的两种方式 |
| 14 | +Java 提供了2种方式来使用多线程, 一种是编写一个类来继承Thread,然后覆写run方法,然后调用start方法来启动线程。这时这个类就会以另一个线程的方式来运行run方法里面的代码。另一种是编写一个类来实现Runnable接口,然后实现接口方法`run`,然后创造一个Thread对象,把实现了Runnable接口的类当做构造参数,传入Thread对象,最后该Thread对象调用start方法。 |
| 15 | +这里的start方法是一个有启动功能的方法,该方法内部回调run方法。所以,只有调用了start方法才会启动另一个线程,直接调用run方法,还是在同一个线程中执行run,而不是在另一个线程执行run |
| 16 | +此外,start方法只是告诉虚拟机,该线程可以启动了,也就说该线程在就绪的状态,但不代表调用start就立即运行了,这要等待JVM来决定什么时候执行这个线程。也就是说,如果有两个线程A,B ,A先调用start,B后调用start,不代表A线程先运行,B线程后运行。这都是由JVM决定了,可以认为是随机启动。 |
| 17 | +下面我们用实际的代码,来说明两种启动线程的方式: |
| 18 | +第一种,继承Thread |
| 19 | +``` |
| 20 | +public class ExampleThread extends Thread{ |
| 21 | +
|
| 22 | + @Override |
| 23 | + public void run() { |
| 24 | + super.run(); |
| 25 | + System.out.println("这是一个继承自Thread的ExampleThread"); |
| 26 | + } |
| 27 | +} |
| 28 | +``` |
| 29 | +测试的代码可以看test目录下的`ExampleThreadTest`类 |
| 30 | +另一种,实现了Runnable接口 |
| 31 | +``` |
| 32 | +public class ExampleRunable implements Runnable{ |
| 33 | +
|
| 34 | + public void run() { |
| 35 | + System.out.println("这是实现Runnable接口的类"); |
| 36 | + } |
| 37 | +} |
| 38 | +``` |
| 39 | +测试的代码可以看test目录下的`ExampleRunableTest`类。 |
| 40 | + |
| 41 | +## 如何得到多线程的一些信息 |
| 42 | +我们在启动多线程之后,希望能通过一些API得到启动的线程的一些信息。JDK给我们提供了一个Thread类的方法来得到线程的一些信息。 |
| 43 | +- 线程的名字 —— `getName()` |
| 44 | +- 线程的ID —— `getId()` |
| 45 | +- 线程是否存活 —— `isAlive()` |
| 46 | + |
| 47 | +### 得到线程的名字 |
| 48 | +这些方法是属于Thread的内部方法,所以我们可以用两种方式调用这些方法,一个是我们的类继承Thread来使用多线程的时候,可以用过this来调用。另一种是通过`Thread.currentThread()` 来调用这些方法。但是这两个方法在不同的使用场景下是有区别的。 |
| 49 | +我们先简单来看两个方法的使用。 |
| 50 | +第一个`Thread.currentThread()`的使用,代码如下: |
| 51 | +``` |
| 52 | +public class ExampleCurrentThread extends Thread{ |
| 53 | +
|
| 54 | + public ExampleCurrentThread(){ |
| 55 | + System.out.println("构造方法的打印:" + Thread.currentThread().getName()); |
| 56 | + } |
| 57 | +
|
| 58 | + @Override |
| 59 | + public void run() { |
| 60 | + super.run(); |
| 61 | + System.out.println("run方法的打印:" + Thread.currentThread().getName()); |
| 62 | + } |
| 63 | +} |
| 64 | +``` |
| 65 | +测试的代码如下: |
| 66 | +``` |
| 67 | +public class ExampleCurrentThreadTest extends TestCase { |
| 68 | +
|
| 69 | + public void testInit() throws Exception{ |
| 70 | + ExampleCurrentThread thread = new ExampleCurrentThread(); |
| 71 | + } |
| 72 | +
|
| 73 | + public void testRun() throws Exception { |
| 74 | + ExampleCurrentThread thread = new ExampleCurrentThread(); |
| 75 | + thread.start(); |
| 76 | + Thread.sleep(1000); |
| 77 | + } |
| 78 | +} |
| 79 | +``` |
| 80 | +结果如下: |
| 81 | +``` |
| 82 | +构造方法的打印:main |
| 83 | +run方法的打印:Thread-0 |
| 84 | +构造方法的打印:main |
| 85 | +``` |
| 86 | +为什么我们在`ExampleCurrentThread`内部用`Thread.currentThread()`会显示构造方法的打印是main,是因为`Thread.currentThread()`返回的是代码段正在被那个线程调用的信息。这里面很显然构造方法是被main线程执行的,而run方法是被我们自己启动的线程执行的,因为没有给他起名字,所以默认是Thread-0。 |
| 87 | +接下来,我们在看一看继承自Thread,用this调用。 |
| 88 | +``` |
| 89 | +public class ComplexCurrentThread extends Thread{ |
| 90 | +
|
| 91 | + public ComplexCurrentThread() { |
| 92 | + System.out.println("begin========="); |
| 93 | + System.out.println("Thread.currentThread().getName=" + Thread.currentThread().getName()); |
| 94 | +
|
| 95 | + System.out.println("this.getName()=" + this.getName()); |
| 96 | + System.out.println("end==========="); |
| 97 | + } |
| 98 | +
|
| 99 | + @Override |
| 100 | + public void run() { |
| 101 | + super.run(); |
| 102 | + System.out.println("run begin======="); |
| 103 | + System.out.println("Thread.currentThread().getName=" + Thread.currentThread().getName()); |
| 104 | + System.out.println("this.getName()=" + this.getName()); |
| 105 | + System.out.println("run end=========="); |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | +测试代码如下: |
| 110 | +``` |
| 111 | +public class ComplexCurrentThreadTest extends TestCase { |
| 112 | + public void testRun() throws Exception { |
| 113 | + ComplexCurrentThread thread = new ComplexCurrentThread(); |
| 114 | + thread.setName("byhieg"); |
| 115 | + thread.start(); |
| 116 | +
|
| 117 | + Thread.sleep(3000); |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | +结果如下: |
| 122 | +``` |
| 123 | +begin========= |
| 124 | +Thread.currentThread().getName=main |
| 125 | +this.getName()=Thread-0 |
| 126 | +end=========== |
| 127 | +run begin======= |
| 128 | +Thread.currentThread().getName=byhieg |
| 129 | +this.getName()=byhieg |
| 130 | +run end========== |
| 131 | +``` |
| 132 | + |
| 133 | +首先在创建对象的时候,构造器还是被main线程所执行,所以`Thread.currentThread()`得到的就是Main线程的名字,但是this方法指的是调用方法的那个对象,也就是`ComplexCurrentThread`的线程信息,还没有setName,所以是默认的名字。然后run方法无论是`Thread.currentThread()`还是this返回的都是设置了byhieg名字的线程信息。 |
| 134 | +所以Thread.currentThread指的是具体执行这个代码块的线程信息。构造器是main执行的,而run方法则是哪个线程start,哪个线程执行run。这么看来,this能得到的信息是不准确的,因为如果我们在run中执行了this.getName(),但是run方法却是由另一个线程start的,我们是无法通过this.getName得到运行run方法的新城的信息的。而且只有继承了Thread的类才能有getName等方法,这对于Java没有多继承的特性语言来说,是个灾难。所有后面凡是要得到线程的信息,我们都用`Thread.currentThread()`来调用API。 |
| 135 | + |
| 136 | +### 得到线程的ID |
| 137 | +调用getID取得线程的唯一标识。这个和上面的getName用法一致,没什么好说的,可以直接看`ExampleIdThread`和他的测试类`ExampleIdThreadTest`。 |
| 138 | +所有的代码均可以在[char01](https://github.com/byhieg/JavaTutorial/tree/master/src) |
| 139 | + |
| 140 | + |
| 141 | +### 判断线程是否存活 |
| 142 | +方法isAlive()的作用是测试线程是否处于活动状态。所谓活动状态,就是线程已经启动但是没有终止。即该线程start之后,被认为是存活的。 |
| 143 | +我们看一下具体的例子: |
| 144 | +``` |
| 145 | +public class AliveThread extends Thread{ |
| 146 | +
|
| 147 | + @Override |
| 148 | + public void run() { |
| 149 | + super.run(); |
| 150 | + System.out.println("run方法中是否存活" + " " + Thread.currentThread().isAlive()); |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | +测试方法如下: |
| 155 | +``` |
| 156 | +public class AliveThreadTest extends TestCase { |
| 157 | + public void testRun() throws Exception { |
| 158 | + AliveThread thread = new AliveThread(); |
| 159 | + System.out.println("begin == " + thread.isAlive()); |
| 160 | + thread.start(); |
| 161 | + Thread.sleep(1000); |
| 162 | + System.out.println("end ==" + thread.isAlive()); |
| 163 | +
|
| 164 | + Thread.sleep(3000); |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | +结果如下: |
| 169 | +``` |
| 170 | +begin == false |
| 171 | +run方法中是否存活 true |
| 172 | +end ==false |
| 173 | +``` |
| 174 | +我们可以发现在start之前,该线程被认为是没有存活,然后run的时候,是存活的,等run方法执行完,又被认为是不存活的。 |
| 175 | + |
| 176 | +## 如何停止线程 |
| 177 | + |
| 178 | +### 判断线程是否终止 |
| 179 | +JDK提供了一些方法来判断线程是否终止 —— `isInterrupted()和interrupted()` |
| 180 | + |
| 181 | +### 停止线程的方式 |
| 182 | +这个是得到线程信息中比较重要的一个方法了,因为这个和终止线程的方法相关联。先说一下终止线程的几种方式: |
| 183 | +1. 等待run方法执行完 |
| 184 | +2. 线程对象调用stop() |
| 185 | +3. 线程对象调用interrupt(),在该线程的run方法中判断是否终止,抛出一个终止异常终止。 |
| 186 | +4. 线程对象调用interrupt(),在该线程的run方法中判断是否终止,以return语句结束。 |
| 187 | + |
| 188 | +第一种就不说了,第二种stop()方法已经废弃了,因为可能会产生如下原因: |
| 189 | +1. 强制结束线程,该线程应该做的清理工作,无法完成。 |
| 190 | +2. 强制结束线程,该线程已操作的加锁对象强制解锁,造成数据不一致。 |
| 191 | +具体的例子可以看`StopLockThread`以及他的测试类`StopLockThreadTest` |
| 192 | + |
| 193 | +第三种,是目前推荐的终止方法,调用interrupt,然后在run方法中判断是否终止。判断终止的方式有两种,一种是Thread类的静态方法`interrupted()`,另一种是Thread的成员方法`isInterrupted()`。这两个方法是有所区别的,第一个方法是会自动重置状态的,如果连续两次调用`interrupted()`,第一次如果是false,第二次一定是true。而`isInterrupted()`是不会的。 |
| 194 | +例子如下: |
| 195 | +``` |
| 196 | +public class ExampleInterruptThread extends Thread{ |
| 197 | +
|
| 198 | + @Override |
| 199 | + public void run() { |
| 200 | + super.run(); |
| 201 | + try{ |
| 202 | + for(int i = 0 ; i < 50000000 ; i++){ |
| 203 | + if (interrupted()){ |
| 204 | + System.out.println("已经是停止状态,我要退出了"); |
| 205 | + throw new InterruptedException("停止......."); |
| 206 | + } |
| 207 | + System.out.println("i=" + (i + 1)); |
| 208 | + } |
| 209 | + }catch (InterruptedException e){ |
| 210 | + System.out.println("顺利停止"); |
| 211 | + } |
| 212 | +
|
| 213 | +
|
| 214 | + } |
| 215 | +} |
| 216 | +``` |
| 217 | +测试的代码如下: |
| 218 | +``` |
| 219 | +public class ExampleInterruptThreadTest extends TestCase { |
| 220 | + public void testRun() throws Exception { |
| 221 | + ExampleInterruptThread thread = new ExampleInterruptThread(); |
| 222 | + thread.start(); |
| 223 | + Thread.sleep(1000); |
| 224 | + thread.interrupt(); |
| 225 | + } |
| 226 | +} |
| 227 | +``` |
| 228 | +第四种方法和第三种一样,唯一的区别就是将上面的代码中的抛出异常换成return,个人还是喜欢抛出异常,这里处理的形式就比较多,比如打印信息,处理资源关闭或者捕捉之后再重新向上层抛出。 |
| 229 | +注意一点,我们上面抛出的异常是`InterruptedException`,这里简单说一下可能产生这个异常的原因,在原有线程sleep的情况下,调用interrupt终止线程,或者先终止线程,再让线程sleep。 |
| 230 | + |
| 231 | +## 如何暂停线程 |
| 232 | +在JDK中提供了以下两个方法用来暂停线程和恢复线程。 |
| 233 | +- suspend()——暂停线程 |
| 234 | +- resume()——恢复线程 |
| 235 | + |
| 236 | +这两个方法和stop方法一样是被废弃的方法,其用法和stop一样,暴力的暂停线程和恢复线程。这两个方法之所以是废弃的主要由以下两个原因: |
| 237 | +1. 线程持有锁定的公共资源的情况下,一旦被暂停,则公共资源无法被其他线程所持有。 |
| 238 | +2. 线程强制暂停,导致该线程执行的操作没有执行完全,这时访问该线程的数据会出现数据不一致。 |
| 239 | + |
| 240 | +## 线程的一些其他用法 |
| 241 | +线程的其他的一些基础用法如下: |
| 242 | +1. 线程让步 |
| 243 | +2. 设置线程的优先级 |
| 244 | +3. 守护线程 |
| 245 | + |
| 246 | +### 线程让步 |
| 247 | +JDK提供yield()方法来让线程放弃当前的CPU资源,将它让给其他的任务去占用CPU时间,但是这也是随机的事情,有可能刚放弃资源,又马上占用时间片了。 |
| 248 | +具体的例子可以参考`ExampleYieldThread`以及他的测试类`ExampleYieldThreadTest` |
| 249 | + |
| 250 | +### 设置线程的优先级 |
| 251 | +我们可以设置线程的优先级来让CPU尽可能的将执行的资源给优先级高的线程。Java设置了1-10这10个优先级,又有三个静态变量来提供三个优先级: |
| 252 | +``` |
| 253 | + /** |
| 254 | + * The minimum priority that a thread can have. |
| 255 | + */ |
| 256 | + public final static int MIN_PRIORITY = 1; |
| 257 | +
|
| 258 | + /** |
| 259 | + * The default priority that is assigned to a thread. |
| 260 | + */ |
| 261 | + public final static int NORM_PRIORITY = 5; |
| 262 | +
|
| 263 | + /** |
| 264 | + * The maximum priority that a thread can have. |
| 265 | + */ |
| 266 | + public final static int MAX_PRIORITY = 10; |
| 267 | +``` |
| 268 | +我们可以通过setPriority来设置线程的优先级,可以直接传入上诉三个静态变量,也可以直接传入1-10的数字。设置后线程就会有不同的优先级。如果我们不设置优先级,会是什么情况? |
| 269 | +线程的优先级是有继承的特性,如果我们在A线程中启动了B线程,则AB具有相同的优先级。一般我们在main线程中启动线程,就和main线程有一致的优先级。main线程的优先级默认是5。 |
| 270 | + |
| 271 | +下面说一下优先级的一些规则: |
| 272 | +1. 优先级高的线程一般会比优先级低的线程获得更多的CPU资源,但是不代表优先级高的任务一定先于优先级低的任务先执行完。因为不同优先级的线程中run方法内容可能不一样。 |
| 273 | +2. 优先级高的线程一定会比优先级低的线程执行的快。如果两个线程是一样的run方法,但是优先级不一样,确实优先级高的线程先执行完。 |
| 274 | + |
| 275 | +### 线程守护 |
| 276 | +JDK中提供setDaemon的方法来设置一个线程变成守护线程。守护线程的特点是其他非守护线程执行完,守护线程就自动销毁,典型的例子是GC回收器。 |
| 277 | +具体可以看`ExampleDaemonThread`和`ExampleDaemonThreadTest`。 |
| 278 | + |
| 279 | +## 总结 |
| 280 | +这篇文章主要总结了Java线程的一些基本的用法,关于线程安全,同步的知识,放到了第二篇。 |
0 commit comments