Java虚拟机中使用线程实现并发逻辑,JVM中的线程是和操作系统中的线程对应的。本篇笔记我们介绍如何在Java语言中创建和使用线程。
Java中使用线程首先要创建Thread
线程类,然后执行其start()
方法。创建线程类的方法有2种:一种是实现Runnable
接口,我们可以实现该接口的run()
方法编写线程逻辑,然后将其作为参数传入Thread
类;另一种是继承Thread
类并重写run()
方法。两种方法没有区别,出于软件工程性考虑我们一般使用前者。
方式1:实现Runnable
接口
public class MyThread1 implements Runnable {
@Override
public void run() {
System.out.println("hello from my thread 1");
}
}
方式2:继承Thread
类
public class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("hello from my thread 2");
}
}
下面例子中,我们创建线程并执行它。
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread1());
Thread t2 = new MyThread2();
t1.start();
t2.start();
}
注意Thread
类和Runnable
的关系:Thread
类实现了Runnable
接口,Runnable
接口中只有一个方法run()
,这个方法中的代码就是在新线程中执行的代码。因此使用继承Thread
的方式创建线程,就必须重写run()
方法。
看了上面例子那么问题来了,虽然Thread
实现了Runnable
接口,但是上面代码中创建t1线程,既new
了自定义的MyThread1
,又new了已经实现Runnable
接口的Thread
类,到底哪个类的Runnable
被执行?对于这个问题我们可以看看Thread
类的代码:
public class Thread implements Runnable {
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
原来JDK中Thread
类中有个target
属性,它就是new Thread(Runnable target)
时传入的参数,Thread
的run()
方法啊会判断这个target
,如果它不为null
就会执行target中的run()
。那么,我们是不是也可以这样写呢?
Thread t2 = new Thread(new MyThread2());
注:MyThread2
继承Thread
类。
答案是可以的,但是这样写不好,这不符合软件工程。Runnable
是接口,接口描述一组行为,这样写传入的是一个具有该行为的对象实例,而这个实例除了表示Runnable
的行为以外毫无意义。因此最佳实践:通常情况下,建议使用实现Runnable
接口的方式创建线程。如果仅仅想创建一个在新线程中执行的函数,可以在new Thread
时传入匿名内部类(或JDK8新引入的Lambda表达式)实现Runnable
。
例子:
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello from my thread");
}
});
我们知道多个线程可以共享一些数据,也可以不共享,如果存在共享的数据处理不好就可能产生线程安全问题,此外在Java这种面向对象语言中情况还要更复杂一些,线程和对象实例我们要区分清楚,下面以例子形式进行说明。这里我们定义一个线程类:
MyThread.java
public class MyThread implements Runnable {
private int i = 0;
@Override
public void run() {
i++;
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
没有线程安全问题的写法:
public class Main
{
public static void main(String[] args)
{
Thread t1 = new Thread(new MyThread(), "thread 1");
Thread t2 = new Thread(new MyThread(), "thread 2");
Thread t3 = new Thread(new MyThread(), "thread 3");
t1.start();
t2.start();
t3.start();
}
}
我们实例化了三个MyThread
类,每个MyThread
有自己的局部变量,因此三个线程都有自己的局部变量,不存在线程安全问题。
运行结果:
三个线程都输出1,这是正确的结果。
有线程安全问题的写法:
public class Main
{
public static void main(String[] args)
{
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread, "thread 1");
Thread t2 = new Thread(myThread, "thread 2");
Thread t3 = new Thread(myThread, "thread 3");
t1.start();
t2.start();
t3.start();
}
}
我们只实例化了一个MyThread
类,因此三个线程共享同一个变量,此时就产生了线程安全问题:
运行结果1:
运行结果2:
这种写法多次运行甚至会出现不同的运行结果,这种不确定性显然很容易造成严重问题。线程切换是操作系统决定的,我们并不能控制,不同的线程切换顺序可能造成不同的结果,多个线程修改同一个变量会造成冲突。然而,有时候我们还必须要让多个线程共享一个变量,这该如何解决?答案是使用线程同步,Java中可以使用synchronized
同步代码块或Lock
锁接口来实现线程同步,这将在后面章节中介绍。
多线程编程中,一个比较常见的场景是主线程等待子线程(可能是多个)再继续执行,此时可以使用join()
方法。
package com.gacfox.demo;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread1());
Thread t2 = new Thread(new MyThread2());
// 同时启动t1和t2
t1.start();
t2.start();
// 等待t1和t2全部完成
t1.join();
t2.join();
}
}
上面代码中,我们同时启动了t1和t2两个线程,因此两个线程是并发执行的,之后我们等待两个线程全部执行完毕主线程再返回。
package com.gacfox.demo;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread1());
Thread t2 = new Thread(new MyThread2());
// 启动t1
t1.start();
// 等待t1完成
t1.join();
// 启动t2
t2.start();
// 等待t2完成
t2.join();
}
}
上面代码稍微调整了代码的顺序,线程的调度逻辑就变了,这里我们先执行线程t1,等t1执行完后才开始执行线程t2。
这里我们再展开介绍一些Thread
类相关的概念和具体方法。
Thread.currentThread():该方法返回当前线程的实例引用。例如我们创建线程时,可以给线程命名:
Thread t1 = new Thread(new MyThread(), "thread 1");
此时我们就可以在子线程中用Thread.currentThread()
取得当前的线程名:
Thread.currentThread().getName();
Thread.sleep(long millis):该方法会使当前线程睡眠指定毫秒数。注意:这是一个静态方法,只能在某个线程中调用并睡眠当前线程。
isAlive():该方法返回某个线程是否是活动线程。活动线程指已开始且未执行结束的线程,注意线程sleep
时也属于活动线程。
getId():返回线程的唯一标识。
setPriority(int priority):线程能够设定优先级,优先级取值1-10,或者使用Thread.MAX_PRIORITY
、Thread.MIN_PRIORITY
、Thread.NORM_PRIORITY
这些预定义的常量。理论上,优先级越高的线程,获得CPU执行的时机越多,不过实际上是否优先执行完全由操作系统决定,该方法指定的优先级仅对操作系统有参考意义。此外,一个线程中启动另一个线程,新线程会继承原来线程的优先级。
setDaemon(boolean on):该方法设置线程是否为守护线程,守护线程会在主线程结束时自动结束。创建守护线程时,设置thread.setDaemon(true)
就可以了。
为什么要使用守护线程?实际开发中通常有这样的需求,例如:编写一个编辑器,有拼写检查功能。这个“拼写检查”可以放在子线程中,用一个无限循环判断编辑器里的内容。如果使用普通线程实现拼写检查,普通线程不会在主线程结束时自动结束,子线程不结束,程序无法退出,因此需要主线程手动结束子线程再退出,虽然也可以实现,但这就有点麻烦了,因此出现了守护线程的概念。