Java SE线程的创建

线程的创建

方式一:继承Thread类

1.1 步骤:

  1. 创建一个子类继承Thread类;
  2. 重写Thread类的run()方法,在该方法中编写该线程要执行的操作;
  3. 创建当前子类的对象;
  4. 通过创建的子类对象调用start()方法,启动线程。

例如:

创建一个线程用于打印1到100间的偶数。

java 复制代码
//1.创建一个子类继承Thread类;
class PrintNumber extends Thread {
	//2. 重写Thread类的run()方法,在该方法中编写该线程要执行的操作;
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }
        }
    }
}

主类调用:

java 复制代码
public class EvenNumberTest extends Thread {
    public static void main(String[] args) {
    	//3. 在主类的main方法中,创建当前子类的对象;
        PrintNumber printNumber = new PrintNumber();
        //4. 通过创建的子类对象调用start()方法,启动线程。
        printNumber.start();
        /*
        * 能否使用printNumber.run()替换printNumber.start()的调用,实现分线程的创建和调用? 不能!
        * start() 方法的作用是启动一个新的线程,并在该线程中执行 run() 方法。
        * 这是真正意义上的多线程执行。
        * 如果调用的是 printNumber.run(),则 run() 方法会在当前线程(即主线程)中执行,并不会创建新的线程,也就无法实现并发执行的效果。
        * */
//        printNumber.run();


        /*
        * 拓展:再提供一个分线程,用于100以内偶数的遍历。
        *
        * 注意:不能让已经start()的线程,再次执行start(),否则报异常IllegalThreadStateException
        * 
        * */
        //此时需要新建一个对象,而不能沿用之前创建的对象
        PrintNumber printNumber1 = new PrintNumber();
        printNumber1.start();
    }
}

练习: 创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。
解决思路: 创建两个类均继承Thread类,分别打印偶数和奇数。

偶数线程类:

java 复制代码
class EvenNumber extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }
        }
    }
}

奇数线程类:

java 复制代码
class OddNumber extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }
        }
    }
}

主线程调用:

写法一:

java 复制代码
public class Test {
    public static void main(String[] args) {
        EvenNumber evenNumber = new EvenNumber();
        OddNumber oddNumber = new OddNumber();
        evenNumber.setName("偶数线程");
		oddNumber.setName("奇数线程");
        evenNumber.start();
        oddNumber.start();
    }
}

写法二:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Thread tEven= new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        };

        Thread tOdd = new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        };
        tEven.setName("偶数线程");
        tEven.start();
        tOdd.setName("奇数线程");
        tOdd.start();
}

写法三:

java 复制代码
public class Test {
    public static void main(String[] args) {
    	//使用匿名对象创建线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }.start();
    }
}

写法四:

java 复制代码
	public class Test {
    public static void main(String[] args) {
        // 使用Lambda表达式创建线程
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                if (i % 2 == 0) {
                    System.out.println(Thread.currentThread().getName() + "  " + i);
                }
            }
        }, "偶数线程").start();

        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                if (i % 2 != 0) {
                    System.out.println(Thread.currentThread().getName() + "  " + i);
                }
            }
        }, "奇数线程").start();
    }
}

方式二:实现Runnable接口

2.1步骤:

  1. 创建一个实现Runnable接口的类;
  2. 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
  3. 创建当前实现类的对象;
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例;
  5. Thread类的实例调用start():1.启动线程;2.调用当前线程的run()方法。
    例如:
    创建一个线程用于打印1到100间的偶数。
java 复制代码
// 1. 创建一个实现Runnable接口的类;
public class MyRunnable implements Runnable{
	// 2. 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }
        }
    }
    public static void main(String[] args) {
    	//3. 创建当前实现类的对象;
        MyRunnable mr = new MyRunnable();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例;
        Thread t1 = new Thread(mr);
        //Thread类的实例调用start()。
        t1.start();
        /*
        * 拓展:再创建一个线程,用于遍历100以内的偶数
        *
        * */
        //此时可以发现,我们无需再创建新的对象,而是使用之前创建的对象即可
        Thread t2 = new Thread(p);
        t2.start();
    }
}

练习: 创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。

使用匿名内部类对象来实现线程的创建和启动

方式一:

java 复制代码
//其实还是上面的写法三的创建形式,只不过在创建时设置其name属性
public class Test {
    public static void main(String[] args) {
        new Thread("偶数线程") {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }.start();
        new Thread("奇数线程") {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }.start();
    }
}

方式二:

java 复制代码
public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }, "偶数线程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                }
            }
        }, "奇数线程").start();
    }
}

两种方式的区别与联系

区别

  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。

实现Runnable接口比继承Thread类所具有的优势

  • 避免了单继承的局限性;
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源;
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

联系

  1. 方式一的Thread类本身就实现了Runnable接口,即:
java 复制代码
public class Thread extends Object implements Runnable
  1. 方式二的Runnable接口的实现类可以作为Thread类的构造器参数;
  2. 方式二的Runnable接口的实现类可以被多个Thread类的实例共享。

值得注意的是,第二种方式在执行start()方法时,执行逻辑与过程是这样的:

首先我们实现runnable接口时重写了run()方法;

start()方法调用的是Thread类中的start()方法,该方法调用了Thread类自己的run()方法,而Thread中的run()方法是这样的:

java 复制代码
@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

它先对target是否为空进行了判断,那么这个target是什么呢,它是

java 复制代码
private Runnable target;

Runnable接口类型的一个成员变量,而我们new的Thread类的构造器是这个:

java 复制代码
public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

而这个构造器就把target属性赋值了,该init函数调用的是:

java 复制代码
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

继续查看调用的init函数:

java 复制代码
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

可以看到在从下往上数第十行的位置,对target进行了赋值操作。

方式三:实现callable接口

3.1 步骤:

  1. 创建一个实现Callable的实现类;
  2. 实现call方法,将此线程需要执行的操作声明在call()中;
  3. 创建Callable接口实现类的对象;
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象;
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start();
  6. 获取Callable中call方法的返回值。

示例: 打印1到100的偶数,并对其求和;

java 复制代码
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
//            Thread.sleep(1000);
        }
        return sum;
    }
}

主测试类中:

java 复制代码
public class CallableTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();

        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);

        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread t1 = new Thread(futureTask);
        t1.start();

//      接收返回值
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

对比Runnable接口

  • 与使用Runnable相比, Callable功能更强大些
    • 相比run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值(需要借助FutureTask类,获取返回结果)
  • Future接口(简单了解)
    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Futrue接口的唯一的实现类
    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。

方式四:使用线程池创建线程

构建一个新的线程是有代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命周期很短的线程,就应该使用线程池 。一个线程池中包含多个准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run()方法,当run()方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数量。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数"固定的"线程池来限制并发线程的总数。
好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止
    • ...

从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorServiceExecutors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
    • void shutdown() :关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

可以采用实现 Runnable 接口 或 Callable接口的形式来使用线程池创建线程。

4.1 步骤:

  1. 创建一个实现 Runnable 接口 或 Callable接口的实现类;
  2. 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
  3. 提供指定线程数量的线程池;
  4. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象;
  5. 关闭连接池。

示例: 使用两个线程分别打印1到100内的奇数和偶数,并对其求和。

java 复制代码
//1). 创建一个实现 Runnable 接口的实现类;
class NumberThread implements Runnable{
	//2). 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}
//1). 创建一个实现 Runnable 接口的实现类;
class NumberThread1 implements Runnable{
	//2). 重写该接口的run()方法,将此线程要执行的操作,声明在此方法体中;
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}
//1). 创建一个实现 Callable 接口的实现类;
class NumberThread2 implements Callable {
//2). 重写call()方法,在该方法中编写该线程要执行的操作;
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }

}

主类中:

java 复制代码
public class ThreadPoolTest {

    public static void main(String[] args) {
        //3).提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //4).执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //5).关闭连接池
        service.shutdown();
    }

}

与此同时,Java 提供了 java.util.concurrent.Executors 工具类来快速创建常见的线程池类型:

1. 固定大小线程池:newFixedThreadPool

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(10);
  • 核心线程数 = 最大线程数 = 10
  • 使用无界队列 LinkedBlockingQueue,所有线程一直存活,适合负载较重、任务量稳定的场景

2. 缓存线程池:newCachedThreadPool

java 复制代码
ExecutorService executor = Executors.newCachedThreadPool();
  • 核心线程数 = 0
  • 最大线程数 = Integer.MAX_VALUE
  • 空闲线程存活时间为 60 秒
  • 使用 SynchronousQueue,适用于大量短生命周期的任务

3. 单线程池:newSingleThreadExecutor

java 复制代码
ExecutorService executor = Executors.newSingleThreadExecutor();
  • 只有一个线程,确保任务串行执行
  • 使用无界队列
  • 保证顺序执行且不会有并发问题

4. 调度线程池:newScheduledThreadPool

java 复制代码
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
  • 支持定时任务和周期性任务执行
  • 例如延迟执行或固定频率重复执行

手动创建线程池

虽然 Executors 提供了快捷方式,但在生产环境中更推荐使用 ThreadPoolExecutor 构造函数手动创建线程池,以便更灵活地配置参数。

ThreadPoolExecutor构造函数签名:

java 复制代码
ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

参数说明:

参数名 含义
corePoolSize 核心线程数(常驻线程数量)
maximumPoolSize 最大线程数(允许的最大线程数量)
keepAliveTime 非核心线程空闲超时时间
unit 超时时间单位
workQueue 存放任务的阻塞队列
threadFactory 线程工厂,用于创建线程(可自定义命名等)
handler 拒绝策略,当任务无法提交时的处理方式

示例代码:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, // corePoolSize
    10, // maximumPoolSize
    60, // keepAliveTime
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100), // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

线程池的工作流程

  • 提交任务后:
    • 若当前运行线程 < corePoolSize,则创建新线程处理任务。
    • 否则,尝试将任务放入工作队列。
  • 如果队列满:
    • 若当前线程数 < maximumPoolSize,创建新线程处理任务。
    • 否则,执行拒绝策略(如抛异常、调用者执行等)。

常见拒绝策略(RejectedExecutionHandler)

策略 行为
AbortPolicy 默认策略,抛出 RejectedExecutionException 异常
CallerRunsPolicy 由提交任务的线程自己执行该任务
DiscardPolicy 默默丢弃任务,不抛异常
DiscardOldestPolicy 丢弃队列中最老的任务,然后尝试重新提交新任务

使用线程池的最佳实践

实践建议 说明
明确任务性质 CPU密集型 vs IO密集型,决定线程池大小
避免无界队列 如 LinkedBlockingQueue 不传容量会导致任务堆积
设置合适的拒绝策略 避免系统崩溃,提供降级处理
自定义线程工厂 方便调试和日志追踪(如命名线程)
关闭线程池 使用 shutdown() 或 shutdownNow() 避免资源泄漏

示例代码:完整使用线程池

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,
            4,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(10),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 0; i < 20; i++) {
            final int taskNo = i;
            executor.execute(() -> {
                System.out.println("正在执行任务 " + taskNo + ",线程:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown(); // 关闭线程池
    }
}

以上就是四种创建线程的方法,其间如果涉及多线程对共享资源的处理,还需要使用同步代码块或同步方法以及使用wait()、notify()/notifyAll()函数来完成进程间通信从而解决多线程对共享资源的使用导致的并发安全问题。

相关推荐
YuTaoShao1 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张31 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx4 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野5 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person5 小时前
Java SE--方法的使用
java·开发语言·算法
小阳拱白菜6 小时前
java异常学习
java
FrankYoou7 小时前
Jenkins 与 GitLab CI/CD 的核心对比
java·docker
麦兜*7 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
KK溜了溜了8 小时前
JAVA-springboot 整合Redis
java·spring boot·redis
天河归来8 小时前
使用idea创建springboot单体项目
java·spring boot·intellij-idea