1. 什么是线程和进程?
进程(Process):是系统进行资源分配和调度的一个独立单元。它是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统动态执行的基本单元。每个进程都有自己独立的地址空间,包含文本区域、数据区域、堆栈等,并且进程之间不共享内存。
线程(Thread):是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能够独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、寄存器集合和堆栈),但它与同属一个进程的其他线程共享父进程的地址空间。因此,线程之间可以直接读写进程数据段(如全局变量)来进行通信,但也需要一定的同步机制。
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每个线程有自己的程序计数器、寄存器集合和堆栈,用于记录线程运行的位置和状态。线程是独立运行的,它独立于创建它的进程中的其他线程。
2. 为什么使用线程?
使用线程的主要原因包括:
-
提高程序执行效率:多线程程序可以并行处理任务,从而提高程序的执行效率。特别是在多核CPU的计算机上,多线程程序可以充分利用硬件资源,加速程序的执行。
-
改善用户体验:在图形用户界面(GUI)程序中,主线程通常用于处理用户的输入和界面的更新,而耗时操作(如文件读写、网络请求等)可以放在其他线程中执行,避免界面冻结,提高用户体验。
-
简化编程模型:线程提供了一种简化的并发编程模型,使得开发者可以更容易地编写出能够处理多个任务同时进行的程序。
-
有效利用资源:通过线程,程序可以更有效地利用系统资源,如CPU、内存等,尤其是在执行大量I/O操作时。
2. 创建线程的方式?
在Java中,创建线程主要有以下几种方式:
-
继承Thread类:通过继承
java.lang.Thread
类并重写其run()
方法,可以创建一个新的线程。然后,通过创建该类的实例并调用其实例的start()
方法来启动线程。start()
方法会调用run()
方法,但run()
方法不会直接启动线程,而是通过start()
方法间接启动。//创建第一个线程 public class MyThread extends Thread{ //run表示线程启动后执行的业务代码 @Override public void run() { System.out.println("线程执行中"); for (int i =0;i<100;i++){ //getNmae()获取线程名称:默认名称:Thread-n,n表示线程编号 ,该方法必须在Thread类下才能用的方法 System.out.println(Thread.currentThread().getName()+"i="+i); } } }
public class Test01 { public static void main(String[] args) { //创建线程对象 MyThread myThread = new MyThread(); //启动线程--硬件底层调用线程的run方法 myThread.start(); for (int i = 0; i < 10; i++) { System.out.println("main线程:" + i); } } }
public class SellTicket extends Thread{ private static int ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票"); ticket--; } else { break; } } } }
public class Test02 { public static void main(String[] args) { SellTicket st = new SellTicket(); st.start(); SellTicket st02 = new SellTicket(); st02.start(); SellTicket st03 = new SellTicket(); st03.start(); SellTicket st04 = new SellTicket(); st04.start(); } }
-
实现Runnable接口:通过实现
java.lang.Runnable
接口并重写其run()
方法,然后将该实现类的实例传递给Thread
类的构造器,最后创建Thread
类的实例并调用其start()
方法来启动线程。这种方式相较于继承Thread
类更为灵活,因为Java不支持多重继承,但可以实现多个接口。实现Runnable
接口的方式更加灵活,因为Java不支持多重继承,但可以实现多个接口,这使得一个类可以同时继承其他类并实现Runnable
接口。此外,Runnable
接口还可以被用于ExecutorService
等高级线程管理工具中。public class MyRunnable implements Runnable{ @Override public void run() { //线程执行的代码 for (int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName()+" "+i); } } }
public class Test03 { public static void main(String[] args) { //创建线程任务对象 MyRunnable mr = new MyRunnable(); //创建线程对象 Thread t1 = new Thread(mr, "线程1"); Thread t2 = new Thread(mr, "线程2"); t1.start(); t2.start(); for (int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName() + ":" + i); } System.out.println("main线程结束"); } }
public class SellRunnable implements Runnable{ private int ticket = 100; @Override public void run() { while (true){ if (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票"); } else { break; } } } }
public class Test04 { public static void main(String[] args) { SellTicket sellTicket04 = new SellTicket(); Thread thread1 = new Thread(sellTicket04); Thread thread2 = new Thread(sellTicket04); Thread thread3 = new Thread(sellTicket04); Thread thread4 = new Thread(sellTicket04); thread1.setName("窗口1"); thread2.setName("窗口2"); thread3.setName("窗口3"); thread4.setName("窗口4"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
-
使用FutureTask与Thread结合:
FutureTask
是一个实现了Runnable
和Future
接口的类,它允许你为异步计算提供一个结果。你可以通过实现Callable
接口(类似于Runnable
,但有返回值并且可以抛出异常)来创建一个任务,然后使用FutureTask
来包装这个任务,并将FutureTask
实例传递给Thread
的构造器来启动线程。这种方式适合于需要返回值的异步任务。public class MyCallable implements Callable<Double> { //call表示线程的任务代码 @Override public Double call() throws Exception { double sum = 0; for (int i = 0; i < 1000; i+=3) { sum += i; } return sum; } }
public class Test05 { public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable myCallable = new MyCallable(); //把call对象封装成FutureTask类中,而且任务执行的结果是封装在futureTask中 FutureTask task = new FutureTask(myCallable); //把FutureTask对象封装成Thread类中 Thread thread = new Thread(task); thread.start(); //获取callable中call方法返回的结果 Object o = task.get(); System.out.println(o); } }
-
使用线程池:如
ExecutorService
等,这种方式可以更加高效地管理线程的生命周期和资源利用。
3. Thread类中常用的方法?
Thread
类中常用的方法包括:
-
start():启动当前线程,导致此线程开始执行;Java虚拟机调用该线程的
run
方法。 -
run():通常需要重写
Thread
类中的此方法,将创建的线程要执行的操作声明在此方法中。 -
currentThread():静态方法,返回对当前正在执行的线程对象的引用。
-
join():在线程A中调用线程B的
join()
方法,此时线程A进入阻塞状态,直到线程B执行完之后,线程A结束阻塞状态。 -
sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),但不会释放任何监视器。
-
isAlive():测试线程是否处于活动状态。
-
setName(String name) 和 getName():分别用于设置和获取当前线程的名称。
-
yield():释放当前线程的当前CPU执行权,让出CPU给其他线程。
-
setPriority(int newPriority) 和 getPriority():分别用于设置和获取线程的优先级。
4. Runnable和Callable的区别?
Runnable 和 Callable 都是Java中用于多线程编程的接口,但它们之间存在一些关键区别:
- 返回值:
- Runnable 接口的
run()
方法没有返回值(即返回类型为void
)。 - Callable 接口的
call()
方法可以返回一个值,并且返回值的类型可以通过泛型进行指定。
- Runnable 接口的
- 异常处理:
- Runnable 的
run()
方法不能抛出已检查的异常(checked exceptions),但可以抛出运行时异常(unchecked exceptions)。 - Callable 的
call()
方法可以抛出一个已检查的异常(checked exceptions),并且这些异常可以被捕获和处理。
- Runnable 的
- 使用场景:
- Runnable 适用于那些不需要返回值,且不会抛出已检查异常的情况,如简单的打印输出或修改共享变量。
- Callable 适用于那些需要返回值或者需要抛出已检查异常的情况,如处理计算结果、进行网络或IO操作等。
- 与Future结合:
- Runnable 任务不能直接与
Future
一起使用来获取异步执行的结果。 - Callable 任务可以与
Future
一起使用,通过Future
可以获取异步执行的结果,并可以判断任务是否完成、取消任务等。
- Runnable 任务不能直接与
总结来说,Callable 可以看作是 Runnable 接口的补充,它提供了返回值和异常处理的能力,更加适合于复杂的异步编程场景。