深入理解计算机进程与线程:串行与并行下载的差异解析

进程

深入理解计算机进程与线程:串行与并行下载的差异解析

电脑存在很多单独运行的程序,每个程序都有一个独立的进程,并且这些进程是相互独立存在的。例如下图中的 Chrome 浏览器等。

线程

进程要执行任务需依赖线程。即进程中的最小执行单位是线程,且一个进程中最少有一个线程。

相对于单条线程执行多个任务而言,我们以下载文件为例:当下载多个文件时,在串行情况下,它是依循一定顺序进行下载的,即必须先完成 A 的下载,而后才能开始下载 B,在时间上它们不可能有重叠。

下载多个文件时会开启多条线程,多个文件能够同时进行下载,这是严格意义上的在同一时刻发生的情况,并行在时间上是相互重叠的。

了解了这两个概念之后,接着来讲讲什么是多线程。例如,我们打开腾讯管家,腾讯管家本身是一个程序,这意味着它是一个进程。它内部具备很多功能,比如我们可以查看下图,它能查杀病毒、清理垃圾、进行电脑加速等众多功能。

从单线程的角度来看,不管你是想要进行垃圾清理,还是想要进行病毒查杀,都必须先完成其中的一件事,之后才能去做另一件事,这里存在着一个执行的顺序。

如果是多线程的情况,我们在清理垃圾的同时,还能够进行查杀病毒的操作,也能够进行电脑加速等其他操作。这些操作是在严格意义上的同一时刻发生的,不存在执行上的先后顺序,即一个进程运行时会产生多个线程。

线程安全

Integer count = 0;
   public void getCount() {
       count ++;
       System.out.println(count);
   }

一段很简单的代码,当多个线程同时进行访问时,我开启了 3 条线程,每条线程都循环 10 次,然后得到了以下结果。

我们可以看到这里出现了两个 26。这种情况的出现显然表明这个方法根本不是线程安全的。出现这种问题的原因有很多。

一种最常见的情况是,A 线程进入方法后,获取到了 count 的值,并且刚将此值读取出来,然而还未对 count 的值进行改变时,线程 B 也进入了。这样就会使得线程 A 和线程 B 拿到的 count 值是相同的。

从这里能看出这确实并非一个线程安全的类,原因是它们都需要对这个共享变量进行操作。实际上要给线程安全问题给出一个确切的定义,是比较复杂的。我们依据这个程序来总结一下什么是线程安全。

当多个线程访问某个方法时,无论以何种调用方式,无论这些线程如何交替执行,在主程序中都无需进行任何同步。只要这个类的结果行为是我们设想的正确行为,那么就可以说这个类是线程安全的。

我们搞清楚了什么是线程安全之后,接下来就来看看在 Java 中确保线程安全最常用的两种方式。首先来看一段代码。

公共的 void 类型的 threadMethod 方法,它带有一个整数类型的参数 j 。 这个方法主要用于执行与线程相关的操作 。 它接收一个整数参数 j ,并在方法内部根据这个参数进行相应的处理 。 例如 , 可能会根据 j 的值来决定创建多少个线程 , 或者对不同的线程进行不同的操作 。 总之 , 这个方法是与线程相关的功能实现的一部分 。
    int i = 1;
    j = j + i;
}

这段代码绝对是线程安全的。我们能看到这段代码没有任何状态,意味着它不包含任何作用域,也不会引用其他类中的域。它所执行的作用范围和执行结果只在其自身这条线程的局部变量中,且只能被正在执行的线程访问。当前线程的访问不会对正在访问同一个方法的另一个线程造成任何影响。

两个线程会同时去访问这个方法。由于不存在共享的数据,所以这两个线程之间的行为,不会对其他线程的操作以及结果产生影响。因此可以说,无状态的对象是线程安全的。

如果我们给这段代码增添一个状态,同时增添一个 count。这个 count 用于记录这个方法被命中的次数。每当请求一次,count 就加 1。如下所示的代码:

public class ThreadDemo {
   int count = 0; // 记录方法的命中次数
公共的 void 类型的 threadMethod 方法,它接收一个整数类型的参数 j 。
       count++ ;
       int i = 1;
       j = j + i;
   }
}

很明显已不再是线程安全的。单线程运行时确实没问题,然而多条线程并发访问该方法时,问题就产生了。先来分析“count + 1”这个操作:进入方法后,首先要读取 count 的值,接着修改 count 的值,最后将修改后的值赋给 count,总共包含三步,即“读取”“修改”“赋值”。由于这个过程是分步的,看下面这张图可知,count 的值并非正确结果。当线程 A 读取到 count 的值但还未修改时,线程 B 已进入,线程 B 读取到的仍是 count 为 1 的值,正因如此,我们的 count 值已出现偏差。这样的程序放在代码中,存在很多隐患。

如何确保线程安全

1、synchronized

synchronized 关键字的作用是控制线程同步。它能够保证在多线程环境中,我们的线程不会被多个线程同时执行。这样就能确保我们数据的完整性。使用该关键字的一般方法是将其加在方法上。

public class ThreadDemo {
   int count = 0; // 记录方法的命中次数
它会按照特定的逻辑对传入的参数 j 进行处理。
       count++ ;
       int i = 1;
       j = j + i;
   }
}

这样能够确保我们的线程保持同步。同时,这里有一个大家平常容易忽略的问题:其一,synchronized 所锁定的是括号内的对象,而非代码;其二,对于非静态的 synchronized 方法,锁定的是对象本身,也就是 this。

当 synchronized 锁住一个对象后,若其他线程想要获取该锁对象,就必须等待此线程执行完毕并释放锁对象之后才行,不然就会一直处于等待状态。

加上 synchronized 关键字,能够使我们的线程变得安全。然而,在使用时,我们需要注意缩小 synchronized 的使用范围。因为如果随意使用,会很影响程序的性能。当其他对象想获取锁时,结果你却没有使用锁,还一直占用着锁,这样就会造成资源的浪费。

2、Lock

先来讲讲它与 synchronized 的区别。Java1.6 引入了 Lock。Lock 的引入使锁具备了可操作性。这意味着在需要时,我们可以手动获取锁和释放锁。并且,我们还能够实现中断获取以及超时获取等同步特性。然而,从使用方面来看,Lock 明显不如 synchronized 使用起来方便快捷。我们先来看下一般是如何使用的:

创建了一个名为 lock 的私有对象,它是通过 new 关键字实例化的 ReentrantLock 类的对象。ReentrantLock 是 Lock 的子类。
这个私有方法接收一个线程对象作为参数。它会对传入的线程进行相关操作或处理。具体的操作细节取决于方法内部的实现逻辑。
       lock.lock(); // 获取锁对象
       try {
System.out.println("获得了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
System.out.println("释放了锁");
           lock.unlock(); // 释放锁对象
       }
   }

获取锁是进入的第一步,接着我们要执行业务代码。与 synchronized 不同的是,Lock 获取的锁对象需要我们自己去释放。为避免代码出现异常,我们将释放锁的操作放在 finally 中,因为 finally 中的代码无论如何都会被执行。

写个主方法,开启两个线程测试一下我们的程序是否正常:

公共静态 void 主函数(字符串数组参数){
创建了一个名为 LockTest 的对象 lockTest,即 LockTest lockTest = new LockTest();
       // 线程1
Thread t1 被创建,它的实现是一个新的 Runnable 接口的实例,这个实例在新创建的线程中执行。
           @Override
这些代码逻辑决定了该方法的行为和功能。
Thread.currentThread() 会返回当前线程的引用。
Thread.currentThread() 被传递给了 lockTest.method 方法。
           }
       }, "t1");
       // 线程2
Thread t2 被创建,创建它的是一个实现了 Runnable 接口的匿名内部类,这个匿名内部类的 run 方法中包含了相应的代码逻辑。
           @Override
           public void run() {
               lockTest.method(Thread.currentThread());
           }
       }, "t2");
       t1.start();
       t2.start();
   }

结果:

可以看出我们的执行,是没有任何问题的。

其实 Lock 存在几种获取锁的方式,我们在此再讲一种。tryLock()这个方法与 Lock()是有区别的。Lock 在获取锁时,若拿不到锁,就会一直处于等待状态,直至拿到锁。然而,tryLock()并非如此。tryLock 有一个 Boolean 的返回值,若未拿到锁,就会直接返回 false,然后停止等待,它不会像 Lock()那样一直等待去获取锁。看以下代码:

这个方法接收一个线程对象作为参数。在方法内部,会对传入的线程对象进行相关操作。具体的操作细节会根据方法的具体实现而定。它可能会启动线程、暂停线程、恢复线程或者执行其他与线程相关的任务。
获取锁对象,即执行 //lock.lock(); 操作。
       if (lock.tryLock()) {
           try {
System.out.println("获得了锁");
线程需休眠 2000 毫秒的时间。
如果捕获到异常 e ,则执行以下操作;当出现异常 e 时,进行相应处理;一旦捕获到异常 e ,就会执行后续代码;遇到异常 e 并将其捕获后,开始执行相关逻辑;当捕获到的异常为 e 时,执行相应步骤;在捕获到异常 e 的情况下,执行后续的处理流程;当捕捉到异常 e 后,开始进行后续的操作;一旦捕捉到了异常 e ,就会执行相应的处理;当出现的异常是 e 时,执行对应的操作;在捕获到的异常为 e 时,执行相关的处理动作。
以下是对该语句的解释:“e.printStackTrace()”是 Java 语言中的一个方法调用,它用于打印出指定异常对象“e”的堆栈跟踪信息。堆栈跟踪信息包含了方法调用的顺序以及在程序执行过程中出现异常的位置等详细信息,这对于调试和定位程序中的错误非常有帮助。通过调用这个方法,开发人员可以了解到程序在出现异常时的执行路径,从而更好地分析和解决问题。
           } finally {
System.out.println("释放了锁");
释放锁对象。通过执行 lock.unlock() 语句来实现。
           }
       }
   }

我们接着用刚才的两个线程来进行测试,能发现,当线程 t1 获取到锁后,线程 t2 立刻进入,接着它发现锁已被占用,此时它就不再继续等待。

这种方法似乎不是很完美。如果第一个线程拿到锁的时间比第二个线程进来的时间还长,那么是不是就拿不到锁对象呢?

我能否用一种方式来加以控制呢?让后面等待的线程能够等待 5 秒。如果 5 秒过后,依然获取不到锁,那么就停止等待。实际上,tryLock()是可以设置等待的相应时间的。

这个私有方法接收一个线程对象作为参数,并抛出 InterruptedException 异常。在方法内部,可能会对传入的线程进行各种操作,比如启动、暂停、恢复等,以实现特定的线程控制逻辑。具体的操作细节取决于方法的具体实现。
       // lock.lock(); // 获取锁对象
       // 如果2秒内获取不到锁对象,那就不再等待
如果 lock.tryLock(2, TimeUnit.SECONDS) 的结果为真,那么就执行以下操作;如果 lock.tryLock(2, TimeUnit.SECONDS) 的结果为假,那么就不执行以下操作。
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
               // 这里睡眠3秒
线程在 3000 毫秒的时间里处于睡眠状态。
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
   }

看上面的代码可知,获取锁对象时能等待 2 秒。然而,线程 t1 获取锁对象后,执行任务却花费了 3 秒。在此情况下,线程 t2 是不在等待的。

我们再来改一下这个等待时间,改为5秒,再来看下结果:

private void method(Thread thread) throws InterruptedException {
       // lock.lock(); // 获取锁对象
       // 如果5秒内获取不到锁对象,那就不再等待
如果锁的 tryLock 方法在 5 秒内成功获取到了锁(以 TimeUnit.SECONDS 为时间单位),那么就会执行下面的代码块。
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
   }

这个时候我们能看到,线程 t2 等待了 5 秒后获取到了锁对象,接着执行了任务代码。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读