浅谈Java线程:线程基础知识扫盲
能够编写高效、安全、稳定的多线程程序是 Java
开发的基础要求。本文介绍了 Java
线程的类型、创建线程的方法、线程状态、线程同步、线程安全等知识点,希望能对读者理解和掌握 Java
线程有所帮助
1. 线程类型
在Java中,主要有两种类型的线程:用户线程和守护线程;下面我们来看看这两种类型的区别
1.1 用户线程
用户线程
是指在程序中通过Thread类创建的线程,默认情况下是用户线程。用户线程的生命周期和程序主线程相同,当程序主线程结束时,所有的用户线程也会自动结束,用户线程会组织应用程序的退出
1.2 守护线程
守护线程
是在后台运行的线程,它们不会阻止应用程序的退出,当所有的用户线程都结束后,守护线程会自动结束
下面是Java用户线程和守护线程的示例代码:
public class ThreadExample {
public static void main(String[] args) {
Thread userThread = new Thread(() -> {
System.out.println("This is a user thread.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("User thread finished.");
});
Thread daemonThread = new Thread(() -> {
System.out.println("This is a daemon thread.");
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Daemon thread finished.");
});
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
System.out.println("Main thread finished.");
}
}
在上面的示例中,创建了一个用户线
程和一个守护线程
。在运行程序时,用户线程会运行5s
,而守护线程只会运行30s
。因为守护线程在程序中没有其他用户线程运行时会被自动终止,所以守护线程在运行5s
钟后就结束了。
2. 线程创建方式
2.1 继承Thread类
可以创建一个继承自 Thread 类的子类,在该子类中重写 run()
方法,并在该方法中实现线程要执行的任务。然后可以通过该子类的实例来创建一个新线程,调用start()
方法来启动该线程。
public class MyThread extends Thread {
public void run() {
// 线程要执行的任务
}
}
MyThread myThread = new MyThread();
myThread.start();
2.2 实现 Runnable 接口
通过Runable
创建线程,需要创建一个实现 Runnable
接口的类,在该类中实现 run()
方法。然后可以创建一个该类的实例,将其作为参数传递给 Thread
类的构造方法来创建一个新线程,并调用 start()
方法来启动该线程:
public class MyRunnable implements Runnable {
public void run() {
// 线程要执行的任务
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
2.3 使用Callable和Future
Callable
接口是一个具有类型参数的泛型接口,它的 call()
方法可以返回一个结果,并且可能会抛出异常。Future
接口则是一个表示异步计算的结果的接口,它提供了检查计算是否完成、等待计算完成和获取计算结果的方法:
public class CallableDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
try {
String result = future.get();
System.out.println("Callable result: " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "Hello, Callable!";
}
}
}
在上面的代码中,创建了一个 MyCallable
类实现了 Callable
接口,并在 call()
方法中返回了一个字符串。
然后使用 ExecutorService.submit()
方法提交了这个 Callable
对象,并返回一个 Future 对象。
2.4 Lambda 表达式
使用 Lambda 表达式也可以创建线程,示例代码如下:
Thread thread = new Thread(() -> {
// 线程要执行的任务
});
thread.start();
在上面的代码中,使用 Lambda 表达式来创建了一个实现 Runnable 接口的对象,并在其中实现了线程要执行的任务。然后通过该对象创建了一个新线程,并调用 start() 方法来启动该线程
3. 线程常见的方法
在Java中,ava 线程提供了许多方法来帮助我们管理线程的生命周期、优先级、同步和通信等方面。以下是一些 Java 线程的核心方法:
3.1 start()
start()
:启动线程并调用 run() 方法。在 start() 方法被调用后,新线程会在一个单独的执行路径上运行。
3.2 sleep(long millis)
sleep(long millis)
:暂停当前线程的执行,让其他线程有机会执行。这是一个静态方法,可以在任何地方使用。当线程被暂停时,它将放弃 CPU 控制权,但它的锁
保持不会释放。
3.3 yield()
yield()
:告诉当前线程放弃CPU资源,以便其他线程可以运行。如果没有其他线程等待执行,当前线程将继续运行,不释放锁
资源,由运行状态变为就绪状态.
作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞
3.4 join()/join(long millis)
join()/join(long millis)
:等待线程执行终止,当前线程里调用其它线程的join
方法后,当前线程进入WAITING/TIMED_WAITING
状态,直到被调用线程执行完毕或者 millis 时间到,当前线程进入就绪状态,
注意:在Java
中,join()
方法不会释放锁。如果一个线程在获得了某个对象的锁之后调用了该对象的join()
方法,那么它仍然持有该对象的锁。
3.5 wait() 和 notify()
wait()
和 notify()
:用于实现线程之间的通信。当一个线程等待某些条件时,可以调用 wait()
方法暂停自己的执行,等待其他线程调用 notify()
方法来通知它恢复执行。
注意:wait()
和 notify()
必须在 synchronized
块中使用,以确保线程安全;当前线程调用对象的 wait()
方法,当前线程释放对象锁。
3.6 线程中断
interrupt()
:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()
方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait
系列函数、join
方法或者sleep
方法而被阻塞挂起,这时候若线程B调用线程A的interrupt(
)方法,线程A会在调用这些方法的地方抛出InterruptedException
异常而返回。isInterrupted()
:检测当前线程是否被中断,如果被中断(中断标志位为true), 则返回true,否则返回false。interrupted()
:检测当前线程是否被中断,如果被中断,则返回true,否则返回false。与isInterrupted
不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread
类直接调用。
4. 线程程状态
线程状态的变化取决于它的执行情况和其他线程的操作。线程状态的不同可以帮助我们更好地管理和控制线程的执行,下是Java中线程状态的总结:
- 新建状态(New):当线程对象被创建但是尚未调用
start()
方法启动线程时,该线程处于新建状态。在这个状态下,线程对象已经被创建但是尚未与操作系统的线程关联,此时线程不会被分配 CPU 时间片。 - 运行状态(Runnable):当线程对象调用
start()
方法后,该线程处于运行状态。在这个状态下,线程对象与操作系统的线程关联,可以被分配 CPU 时间片并执行任务。 - 阻塞状态(Blocked):当线程需要等待一些条件满足或者等待某个锁时,它会进入阻塞状态。在这个状态下,线程暂停执行,等待条件满足或者锁被释放后再继续执行。
- 等待状态(Waiting):当线程调用了
Object.wait()
、Thread.join()
或LockSupport.park()
方法时,它会进入等待状态。在这个状态下,线程暂停执行,等待其他线程发出通知后再继续执行。 - 计时等待状态(Timed Waiting):当线程调用了
Thread.sleep()
、Object.wait(long)
、Thread.join(long
) 或LockSupport.parkNanos()
方法时,它会进入计时等待状态。在这个状态下,线程暂停执行,等待指定时间到期后再继续执行。 - 终止状态(Terminated):当线程执行完任务或者因为异常终止时,它会进入终止状态。在这个状态下,线程已经结束执行,不再活动
5. 常见面试问题
5.1 线程和进程的区别是什么?
线程是程序执行的最小单位,是 CPU 调度和分派的基本单位,一个进程可以有多个线程共享进程的资源,线程间的通信和资源共享比进程更方便和快捷。进程是操作系统资源分配的基本单位,每个进程拥有独立的内存空间,进程间的通信和资源共享比线程更复杂和耗费资源。
5.2 什么是线程同步?如何实现线程同步?
线程同步是指多个线程并发访问共享资源时,通过互斥锁等机制,保证同一时刻只有一个线程访问该资源,避免数据不一致、数据覆盖等问题。Java
中实现线程同步的方式有 synchronized
关键字、Lock
接口等。
5.3 什么是死锁?如何避免死锁?
死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行的状态。避免死锁的方式有避免破坏互斥条件、避免破坏占有且等待条件、避免破坏不可抢占条件、避免破坏循环等待条件,可以通过设计良好的算法、合理的资源申请顺序、资源剥夺机制等方式避免死锁。
5.4 什么是线程安全?如何实现线程安全?
线程安全是指多线程并发访问时,不会出现数据不一致、数据覆盖等问题。Java
中提供了多种线程安全的数据结构,比如 ConcurrentHashMap
、CopyOnWriteArrayList
、Atomic
类等,可以通过使用这些线程安全的数据结构或者通过加锁、使用原子操作等方式来实现线程安全
6. 总结
Java
线程是 Java
平台提供的一种多线程机制,能够让多个线程同时执行,提高程序的并发性和效率。创建线程的方式有多种、种,线程同步和线程间通信可以解决多个线程访问共享资源时可能发生的数据不一致、数据覆盖等问题,线程池 可以很好地管理和调度多个线程,避免了线程创建和销毁的开销,同时还能提高程序的响应速度和性能,线程安全是指多线程并发访问时,不会出现数据不一致、数据覆盖等问题,Java 中提供了多种线程安全的数据结构
转载自:https://juejin.cn/post/7210216031536578618