线程的创建分为4种方式:
1.继承Thread类,重写run方法
启动线程是调用start方法,这样会创建一个新的线程,并执行线程的任务。
如果直接调用run方法,这样会让当前线程执行run方法中的业务逻辑,但不能得到多线程的效果。
public class MiTest {
public static void main(String[] args) {
MyJob t1 = new MyJob();
t1.start();
for (int i = 0; i < 100; i++) {
System.out.println("main:" + i);
}
}
}
class MyJob extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyJob:" + i);
}
}
}
2.实现Runnable接口 重写run方法
public class MiTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main:" + i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("MyRunnable:" + i);
}
}
}
最常用的方式:
- 匿名内部类方式
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("匿名内部类:" + i);
}
}
});
- lambda方式
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("lambda:" + i);
}
});
3.实现Callable 重写call方法,配合FutureTask
Callable一般用于有返回结果的非阻塞的执行方法
同步非阻塞。
FutureTask底层实现了Ruunable接口,run()中调用了call()
public class MiTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建MyCallable
MyCallable myCallable = new MyCallable();
//2. 创建FutureTask,传入Callable
FutureTask futureTask = new FutureTask(myCallable);
//3. 创建Thread线程
Thread t1 = new Thread(futureTask);
//4. 启动线程
t1.start();
//5. 做一些操作
//6. 要结果
Object count = futureTask.get();
System.out.println("总和为:" + count);
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
4.基于线程池构建线程
线程是非常宝贵的计算资源,在每次需要时创建并在运行结束后销毁是非常浪费系统资源的。我们可以使用缓存策略并通过线程池来创建线程。具体实现过程为创建一个线程池并用该线程池提交线程任务。
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
//提交多个线程任务并执行
threadPool.execute(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"is running");
}
});
}
总结:这四种方法追其底层,就是只有一种,实现Runable接口。
使用继承的方式的好处是方便传参,可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runnable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是FutureTask方式可以 。
5.相关问题:
(1)Thread类中为什么调用start()方法而不是run()方法
先看start()源码,底层调用start0(),它是一个本地方法
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0) // 判断线程的状态
throw new IllegalThreadStateException(); // 抛出异常
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); // 在start()方法中调用start0()
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0(); // 只定义方法名称没有实现,返回类型native
run()的源码
@Override
public void run() {
if (target != null) {
target.run();
}
}
原因就是Java以良好的跨平台可移植性著称,而在不同的操作系统之中他的资源调度算法是不同的,但操作系统会提供底层函数来进行资源调度。所以当我们在Java程序中调用start()方法时,然后start()方法会去调用start0(),而start0()只有定义没有实现,那么实现是由谁来做的呢?是JVM来做,JVM会根据用户系统类型实现相应的start0()方法,以此实现多线程。而直接调用run()方法并不能达到此效果。
run()源码并没有实现多线程,如果是直接调用run(),则是顺序执行的,不是多线程。
(2)实现Callable接口创建线程与实现Runnable接口和继承Thread创建线程有什么区别?
① Callable接口可以有返回值
② Callable接口可以抛出异常
③ 执行方法不同,call()方法和run()方法
如果需要获取一个线程执行完毕的结果, 相比其他方式,使用 Callable 更方便不易出错。比如计算0-100累加的和:
非Callable接口方式:
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
// 等待 t 线程计算完之后唤醒当前阻塞状态
result.lock.wait();
}
System.out.println(result.sum);
}
}
虽然这种方式也获取到了线程执行完毕的结果, 但本质上并不是作为返回值的, 而是两个线程修改了同一个变量, 这个变量是 result 类的成员属性, 并且实现的过程比较复杂
所以如果需要获取一个线程执行完毕的结果, 使用 Callable 更方便不易出错
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
(3)为什么使用Runnable方式只能使用主线程里面被声明为final的变量?
具体代码如下:
public class Main {
public static void main(String[] args) {
// write your code here
int z=2;
System.out.println("1");
new Thread(new Runnable() {
@Override
public void run() {
//这句话会报错:错误:(17, 36) java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量
System.out.println(z++);
}
}).start();
}
这是因为jdk1.8开始,局部变量如果没有用final修饰,但是你没有在局部内部类或匿名内部类中改变其值则也是可以编译通过的。用户自己虽然没有定义final,但是jdk1.8在编译生成class文件后会自动给你生成final关键字,也就相当于增加了final 。
那么本问题本质上其实是局部内部类和匿名内部类为什么只能访问final的局部变量?
因为内部类对象和外部类对象的生命周期是处于同一级别的,内部类对象不会随着定义在代码块中的局部变量的消亡而消亡。这里就会产生问题:当外部类中某个代码块(或方法)结束时,局部变量(形参)就被销毁了,但是内部类对象可能存在,这里就出现了一个矛盾: 内部类就访问了一个不存在的变量!
为了解决这个矛盾,就将局部变量复制了一份作为内部类的成员变量,这样局部变量消亡后,内部类依然可以访问他,实际访问的是局部变量的副本(其实就是拷贝复制)。
问题又出现了:将局部变量赋值为内部类的成员变量时,必须保证这两个变量是一样的,也就是说我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决这个问题呢?
所以就会有将局部变量设置为final的这种办法,让这个变量在初始化后不能再被改变,就可以保证内部类的成员变量和局部变量的一致性,这其实是一种妥协.
参考:https://blog.csdn.net/Elephant_King/article/details/122378061