Java 中的多线程
Java 的 JDK 提供了 Thread 类和 Runnable 接口,使用 Thread 类或 Runnable 接口可以实现多个线程同时运行。
Thread 类型的对象即为一个线程,该对象可以调用 start() 方法来启动这个线程。一个线程对象只能调用一次 start() 方法,重复调用会抛出 IllegalThreadStateException 异常。
Thread 类实现了 Runnable 接口,Runnable 接口里只有一个 run() 方法,start() 方法调用后即开启新的线程来运行 run() 方法。
一、创建线程
创建线程有两种方法:
一是定义一个 Thread 类的子类,例如
public class ThreadTest { public static void main(String[] args) { Thread t = new MyThread();// 创建线程 t.start();// 启动线程 } // 定义一个 Thread 类的子类,重写 run() 方法 static class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } } }
二是定义一个实现 Runnable 接口的类,创建线程时在构造方法中传入实现 Runnable 接口的对象,例如
public class ThreadTest { public static void main(String[] args) { Thread t = new Thread(new MyRunnable());// 创建线程 t.start();// 启动线程 } // 定义一个实现 Runnable 接口的类 static class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } } }
由于 Runnable 接口只有一个方法,是一个函数式接口,JDK8 及以后支持使用 Lambda 表达式书写代码,如此以来,上述代码可简写为
public class ThreadTest { public static void main(String[] args) { new Thread(() -> { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } }).start();// 创建并启动线程 } }
对于以上两种方法,更建议用第二种方法,因为 Java 不支持多继承,继承了 Thread 类就无法继承其他类,实现 Runnable 接口则没有这个顾虑。
二、Thread 类的一些常用方法
Thread 除了有 start() 方法之外,还有许多常用的方法。
1. getName() 和 setName(String name) 方法:每个线程都有一个自己的名字,一个 Thread 对象可以调用 getName() 方法获取自己的名字,调用 setName(String name) 设置自己的名字,线程可能会有重名的情况,因为 setName(String name) 方法可以设置任意的字符串为自己的名字,包括已有的线程名。也可以在创建线程时再传入一个 String 类型的对象来设置线程名,例如 new Thread(() -> { System.out.println("Hello Thread! "); }, "Hello").start(); ,这样就设置了线程名为 "Hello" 。 注意,setName(String name) 方法如果传入一个 null 值会抛出 NullPointerException 异常。
2. sleep(long millis) 方法:sleep(long millis) 方法是 Thread 类提供的一个静态方法,调用 sleep(long millis) 方法可以使调用该方法的线程睡眠 millis 毫秒的时间,这个线程在这段时间里什么也不干。例如以下程序可以在终端打印 1~10,每隔 1 秒打印一个数字。注意,sleep(long millis) 方法会抛出 InterruptedException 异常,我这里直接抛出去。
public class Demo { public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { System.out.print(i + "\t"); Thread.sleep(1000); } } }
3. currentThread() 方法:currentThread() 方法也是 Thread 类提供的一个静态方法,这个方法返回一个 Thread 类型的对象,返回的这个对象是当前执行这个方法的线程对象。例如以下程序可以在终端打印出执行 main() 方法的线程的名字。不出意外的话,打印出的是 main 。
public class Demo { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
4. getId() 方法:相比于getName() 方法,每个线程都有一个唯一的标识符,使用 getId() 方法可以获取到线程的标识符,此方法返回一个 long 类型的值。
5. interrupt() 方法:如果调用该方法的对象正在睡眠(调用了 Thread.sleep(long millis) 方法),则该对象会抛出 InterruptedException 异常,否则不会发生什么。此方法可以用来结束进程,例如以下程序。
public class Demo { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { System.out.println("I am alive! "); try { Thread.sleep(100);// 执行到这里时如果调用了 interrupt() 方法会抛出异常 } catch (InterruptedException e) { System.out.println("I am died."); return;// 结束线程 } } }); t.start(); Thread.sleep(50); t.interrupt();// 打断线程的睡眠状态 } }
6. setDaemon(boolean on) 方法:调用此方法可以设置线程为 守护线程 或 用户线程。所谓守护线程,指的是一个守护其他线程的线程,如果程序中没有任何其他线程(用户线程),守护线程自动死亡。例如以下程序,当主线程结束后只剩下一个 Hello 线程,并且这个线程是一个守护线程,则这个线程也会随着死亡。注意,setDaemon(boolean on) 方法只能在线程启动之前调用,在线程启动后调用会抛出 IllegalThreadStateException 异常,并且设置无效。
public class Demo { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { System.out.println("I am deamon! "); try { Thread.sleep(500);// 每隔半秒打印一句话 } catch(Exception e) {} } }, "Hello"); t.setDaemon(true); t.start(); Thread.sleep(3000);// 睡眠 3 秒钟后主线程结束 } }
7. join() 方法 和 join(long millis) 方法:Thread 类还有一个 join() 方法,调用此方法可以让某个线程加入当前线程一段时间。注意,调用此方法的线程如果还没开启,则此方法不起作用。例如以下程序。
public class Demo { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println(i); try { Thread.sleep(500);// 每隔半秒打印一次 } catch(Exception e) { return; } } }); t.join();// 此行无效 t.start();// t 线程开启 Thread.sleep(1000);// 主线程睡眠 1 秒 t.join(1100);// t 线程加入主线程 1.1 秒,此时主线程什么也不干,就等着 t 线程 System.out.println("end");// 主线程结束 } }
除了这些以外还有许多其他的一些方法,这里就不一一列举了,可以查看 API 或阅读源码了解更多的方法。
三、线程不安全问题
多个线程同时操作一个数据时很可能会出现数据错乱的问题,这称为线程不安全问题。例如以下程序。
public class Demo { static int i = 0;// 同时要被两个线程使用的数据 public static void main(String[] args) throws InterruptedException { Runnable r = new MyRunnable(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); Thread.sleep(1);// 假设主线程执行一系列操作花了 1 毫秒,然后开启另一个线程 t2.start(); } static class MyRunnable implements Runnable { @Override public void run() { if (i < 10) {// 只有当 i < 10 才会执行下列代码 try { Thread.sleep(4);// 假设执行了一系列操作用了 4 毫秒,这些操作用到了 i 的值,但是没有修改 i 的值 } catch(Exception e) {} System.out.println(i);// 打印 i 的值,因为没有修改过 i 的值,打印出来的 i 必定是小于 10 的。 } i = 20; } } }
按照预期,应该打印出 0 ,但是却打印出了 20 ,这不符合我们的想法。因为 t1 线程在 t2 线程执行期间修改了 i 的值,使得结果不准确,这是十分不安全的。
为了避免出现数据错乱的问题,不应让多个线程同时操作一个数据,这就需要引入线程锁的概念了。线程锁,指的是在运行一段程序时锁定这段程序,使的其他要使用该段代码的程序必须等待其运行完毕后才可运行。也就是排队执行该段程序。
线程锁可以锁定一个方法或者一个代码块。添加线程锁十分简单,只需添加 synchronized 关键字修饰方法,synchronized 意为同步,可以理解为线程锁。例如以下程序是给 run() 方法添加了一个线程锁。
public class Demo { static int i = 0;// 同时要被两个线程使用的数据 public static void main(String[] args) throws InterruptedException { Runnable r = new MyRunnable(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); Thread.sleep(1);// 假设主线程执行一系列操作花了 1 毫秒,然后开启另一个线程 t2.start(); } static class MyRunnable implements Runnable { @Override public synchronized void run() {// 同步方法 if (i < 10) {// 只有当 i < 10 才会执行下列代码 try { Thread.sleep(4);// 假设执行了一系列操作用了 4 毫秒,这些操作用到了 i 的值,但是没有修改 i 的值 } catch(Exception e) {} System.out.println(i);// 打印 i 的值,因为没有修改过 i 的值,打印出来的 i 必定是小于 10 的。 } i = 20; } } }
这样在运行 run() 方法时就会排队运行,只有当 t1 运行结束后 t2 才可以运行,解决了运行中途数据被修改的问题。同步方法可以使得线程安全但是效率不高。有一点要注意的是,必须是在运行同一个方法时才可以使用同步方法来排队运行这个方法,否则如果两个线程调用的不是同一个方法,那么即使加上了 synchronized 关键字也无济于事,两个线程还是会同时进行,没有排队。上述的程序中是先定义了一个 MyRunnable 的实例 r 然后两个线程都传入参数 r ,这样两个线程用的就是同一个 run() 方法,如果像以下方式书写,则两个线程用的不是同一个 run() 方法,这样就达不到排队的效果,线程同样是不安全的。
public class Demo { static int i = 0;// 同时要被两个线程使用的数据 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new MyRunnable());// 两个线程传入的参数不是同一个 Thread t2 = new Thread(new MyRunnable());// 不是同一个 run() 方法 t1.start(); Thread.sleep(1);// 假设主线程执行一系列操作花了 1 毫秒,然后开启另一个线程 t2.start(); } static class MyRunnable implements Runnable { @Override public synchronized void run() {if (i < 10) {// 只有当 i < 10 才会执行下列代码 try { Thread.sleep(4);// 假设执行了一系列操作用了 4 毫秒,这些操作用到了 i 的值,但是没有修改 i 的值 } catch(Exception e) {} System.out.println(i);// 打印 i 的值,因为没有修改过 i 的值,打印出来的 i 必定是小于 10 的。 } i = 20; } } }
除了可以给方法同步添加线程锁以外,还可以给一块代码块添加线程锁。这同样也要用到 synchronized 关键字,不同的是,后面还要传入一个对象,如果有线程在运行这个同步代码块,那么传入的对象就会被标记为 "已上锁" ,其他线程如果要运行同样传入了这个对象的同步代码块,那就得等正在运行的线程运行结束后才可以运行。例如以下程序。
public class Demo { static int i = 0;// 同时要被两个线程使用的数据 static Object o = new Object();// 用来标记的对象,不能是 null public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { synchronized(o) {// 只有对象 o 没有被标记为上锁时才可以运行这块代码块 if (i < 10) {// 只有当 i < 10 才会执行下列代码 try { Thread.sleep(40);// 假设执行了一系列操作用了 40 毫秒,这些操作用到了 i 的值,但是没有修改 i 的值 } catch(Exception e) {} System.out.println("线程 t1 打印的 i 的值: " + i);// 打印 i 的值,因为没有修改过 i 的值,打印出来的 i 必定是小于 10 的。 } }// 代码块运行结束后对象 o 自动解锁 }); Thread t2 = new Thread(() -> { synchronized(o) {// 只有对象 o 没有被标记为上锁时才可以运行这块代码块 i = 20; }// 代码块运行结束后对象 o 自动解锁 });// t1 和 t2 两个线程传入的参数不是同一个, 不是同一个 run() 方法 t1.start(); Thread.sleep(1);// 假设主线程执行一系列操作花了 1 毫秒,然后开启另一个线程 t2.start(); Thread.sleep(50); System.out.println("最后 i 的值: " + i);// 最后打印一下 i 的值 } }
这样即使两个线程不是运行的同一个方法,只要锁的是同一个对象就得排队执行。