进程和线程:
- 进程: 进程是操作系统中的一个执行单元,它包含了程序的代码、数据和系统资源。每个进程都有独立的内存空间,它们之间不能直接访问对方的内存。
- 线程: 线程是进程中的一个执行单元,一个进程可以包含多个线程。线程共享进程的内存空间,因此它们可以直接访问相同进程中的数据。
特性区别:
- 独立性: 进程是独立的执行单元,每个进程有自己的地址空间和资源。线程则共享相同的地址空间和资源,它们更轻量级。
- 通信和同步: 进程之间通信相对复杂,通常需要使用进程间通信(IPC)机制。线程之间可以直接共享数据,但需要考虑同步问题,防止竞态条件。
- 切换开销: 进程切换的开销较大,因为需要切换整个地址空间。线程切换的开销较小,因为它们共享相同的地址空间。
- 容错性: 进程之间的容错性较好,一个进程的崩溃不会影响其他进程。线程之间共享相同的资源,因此一个线程的错误可能影响整个进程。
价值,扩展,利用的空间,场景:
- 价值: 进程提供了更强的隔离性,适用于需要独立执行环境的任务。线程更轻量,适用于需要并发执行的任务。
- 扩展: 进程的扩展受限于系统资源,而线程可以更容易地扩展,因为它们共享相同的资源。
- 利用的空间: 进程间通信的开销较大,但可以充分利用多核处理器。线程更适合在单个核心上执行,并发地完成任务。
- 场景: 进程适用于需要高度隔离和稳定性的任务,如服务器应用。线程适用于需要并发处理、响应时间较短的任务,如图形界面应用和网络通信。
实现线程的方式:
- extends Thread类
通过继承Thread类,可以创建一个新的线程类。子类需要重写Thread类的run()
方法,该方法包含线程的执行逻辑。然后,可以创建该线程类的实例并调用start()
方法启动线程。
public class MyThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println("Value:"+i);
}
}
public static void main(String args[]){
MyThread t1 = new MyThread();
t1.start();
}
}
- implements Runnable 接口 推荐
通过实现Runnable接口,可以将线程的执行逻辑封装在一个实现了run()
方法的类中。然后,创建Thread类的实例时,将实现了Runnable接口的类的实例传递给Thread的构造方法。
public class MyRunnable implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println("Value:"+i);
}
}
public static void main(String args[]){
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
t1.start();
}
}
- implements Callable 接口
实现Callable
接口是Java中另一种实现多线程的方式,相对于Runnable
接口,Callable
接口提供了更强大的功能和更灵活的线程控制。主要区别在于,Callable
接口允许线程执行任务后返回一个结果,并且可以抛出异常。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
public Integer call(){
int sum=0;
for (int i=0;i<10;i++){
sum+=i;
}
return sum;
}
public static void main(String args[]){
Callable<Integer> mc = new MyCallable();
// 将Callable实例包装成FutureTask
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建线程并启动
Thread t1 = new Thread(ft);
t1.start();
try {
// 获取线程执行结果
Integer result = ft.get();
System.out.println("Sum from Callable: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
区别:
-
继承Thread类:
- 优点:简单,直观,易于理解。
- 缺点:由于Java是单继承,继承了Thread类后不能再继承其他类。
-
实现Runnable接口:
- 优点:更灵活,可以实现多个接口,可以作为参数传递给Thread类的构造方法。
- 缺点:稍微繁琐一些,需要额外创建一个实现Runnable接口的类的实例。
选择建议:
-
如果需要在定义线程的同时扩展其他类的功能,或者需要在多个线程之间共享数据,推荐使用实现
Runnable
接口的方式。 -
如果简单地需要定义一个线程,而不用担心单继承的限制,并且不需要共享数据,可以使用继承
Thread
类的方式。
synchronized:
synchronized
是Java中用于实现同步的关键字,它可以应用于方法和代码块。同步是为了确保多个线程在访问共享资源时的安全性,防止并发访问导致的数据不一致或其他问题。下面是对synchronized
的详细介绍:
同步方法:
使用synchronized
修饰方法,确保在同一时间只有一个线程能够访问该方法。这种方式适用于整个方法需要被同步的情况。
public class MyClass {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
}
同步代码块:
使用synchronized
修饰代码块,可以选择性地对一部分代码进行同步,而不是整个方法。这样可以提高程序的性能,因为只有必要的部分受到同步保护。
public class MyClass {
private int count = 0;
private Object lock = new Object();
public void increment() {
// 同步代码块
synchronized (lock) {
count++;
}
}
}
实例级别的锁和类级别的锁:
-
实例级别的锁: 使用
synchronized
修饰非静态方法时,锁是当前对象实例。不同实例的方法调用之间互不影响。
public synchronized void instanceMethod() {
// 同步方法
}
- 类级别的锁: 使用
synchronized
修饰静态方法时,锁是当前类的Class对象。不同实例之间的静态方法调用之间也会受到同步保护。
public static synchronized void staticMethod() {
// 同步静态方法
}
避免死锁:
在使用synchronized
时,要注意避免死锁的情况。死锁发生在两个或多个线程相互等待对方释放锁的情况下。为了避免死锁,要确保线程获取锁的顺序是一致的。
例
创建一百个线程对num进行 ++ 操作 ,每个线程中都是一个死循环,判断跳出循环的条件是num==100w跳出,并打印本线程执行了多少次
public class IncrementThread1 extends Thread {
private static int num = 0;
private static final int TARGET = 1000000;
@Override
public void run() {
int count = 0;
while (num < TARGET) {
num++;
count++;
}
System.out.println(Thread.currentThread().getName() + " executed " + count + " times.");
}
public static void main(String[] args) {
final int THREAD_COUNT = 100;
IncrementThread1[] threads = new IncrementThread1[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new IncrementThread1();
threads[i].start();
}
// 等待所有线程执行完毕
for (int i = 0; i < THREAD_COUNT; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final num value: " + num);
}
}
加上synchronized
public void run() {
int count = 0;
while (num < TARGET) {
synchronized (IncrementThread.class) {
if (num < TARGET) {
num++;
count++;
}
}
}
System.out.println(Thread.currentThread().getName() + " 执行了 " + count + " 次。");
}
死锁问题:
死锁是多线程编程中一种常见的问题,它发生在两个或多个线程相互等待对方释放锁的情况下,导致程序无法继续执行。死锁通常是由于多个线程争夺资源时,每个线程都持有一部分资源并等待其他线程释放它所需要的资源,形成了相互等待的循环。
死锁发生的条件:
-
互斥条件: 至少有一个资源是被独占的,即只能由一个线程同时使用。
-
不可剥夺条件: 一个线程在持有资源的同时可以请求其他资源,而且不释放已经持有的资源。
-
请求与保持条件: 线程已经持有了一些资源,又在请求其他资源,此时不能抢占已经被其他线程持有的资源。
-
循环等待条件: 存在一个线程等待序列,其中每个线程都在等待下一个线程所持有的资源。
两个线程之间互相持有对方需要的锁,两个线程都持有了一把锁,接着都需要再持有一把锁(也是对方目前持有的锁)才可以执行任务
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TA extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Main.object1) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("TA - 持有 O1 Lock");
synchronized (Main.object2) {
System.out.println("TA - 持有 O2 Lock");
Main.num++;
}
}
}
System.out.println("TA- end");
}
}
class RA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (Main.object2) {// synchronized 大括号范围之内 在线程级别都是一个原子性的操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("RA - 持有 O2 Lock");
synchronized (Main.object1) {
System.out.println("RA - 持有 O1 Lock");
Main.lock.lock();
Main.num++;
Main.lock.unlock();
}
}
}
System.out.println("RA- end");
}
}
class Main {
static int num = 0;
static Object object1 = new Object();
static Object object2 = new Object();
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 实例化线程对象
TA ta = new TA();
RA ra = new RA();
ta.start();
Thread t1 = new Thread(ra);
t1.start();
// 等待线程执行完成
try {
ta.join(); // 阻塞方法直到线程执行完成
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(num);
}
}
预防和解决死锁:
-
加锁顺序: 确保所有线程按照相同的顺序获得锁,以减少循环等待的可能性。
-
使用定时锁: 使用
tryLock()
来尝试获取锁,设置超时时间,如果超过时间仍未获得锁,就释放已经持有的锁。 -
避免嵌套锁: 尽量避免在持有锁的情况下再去获取其他锁,以减少死锁的概率。
-
定期检测: 定期检测系统中是否存在死锁,并采取相应的措施,如强制终止其中一个线程。