并行和并发都是多任务处理的概念,但它们的含义不同。
并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。
并发是指两个或多个任务在同一时间段内执行,即在同一时间段内有多个任务在交替进行。在计算机领域,单核 CPU 可以通过轮流执行各个任务来实现并发处理。在并发处理中,任务之间可能会相互影响,需要考虑任务的顺序和优先级,也需要考虑任务之间的同步和通信问题。
简单来说,如果是多个任务同时执行,就是并行;如果是多个任务交替执行,就是并发。并行处理通常需要多核 CPU 来支持,可以提高处理速度;而并发处理可以在单核 CPU 上实现,但需要考虑任务之间的同步和通信问题。
假设有两个线程,线程 1 和线程 2,它们都需要资源 A/B,假设线程 1 占有了 A 资源,线程 2 占有了 B 资源;
由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1 号线程释放了 A 资源占有锁,2 号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。
简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。
单线程创建方式比较简单,一般只有两种方式:
需要注意的问题有:
单线程创建举例:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
thread.start();
}
线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:
减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去
如上图例子:线程 1 己经持有了资源 2,它同时还想申请资源 1,线程 2 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。
死锁的产生必须具备以下四个条件。
//死锁代码
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args){
new DeadLockDemo().deadLock();
}
private void deadLock(){
Thread t1 = new Thread(new Runnable(){
@Override
public void run(){
//线程1获取A的锁
synchronized (A){
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (B){
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run(){
//线程2获取B的锁
synchronized (B){
//A对象已经被线程1持有
synchronized (A){
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有条件和环路等待条件是可以被破坏的。考虑死锁产生的条件:互斥访问、占有并保持、循环等待。针对以上几点,可以:资源一次性分配、占有时可被打断、考虑资源分配顺序。
如上题代码中,在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,..., n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n.
public class DeadLockRelessDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args){
new DeadLockRelessDemo().deadLock();
}
private void deadLock(){
Thread t1 = new Thread(new Runnable(){
@Override
public void run(){
//线程1获取A的锁
synchronized (A){
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (B){
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run(){
//线程2也获取A的锁
synchronized (A){
synchronized (B){
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位.
线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。在 Java 中,当我们启动 main 函数时其实就启动了一个 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
线程是在内核态调用的,协程在用户态调用,避免了上下文切换
使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码, Runable 则没有这个限制。
public class DemoTest extends Thread {
// private int tickets = 20;
private volatile int tickets = 20;
@Override
public void run(){
synchronized (this){
while (tickets > 0){
System.out.println(Thread.currentThread().getName()+"卖出一张票"+ tickets);
tickets--;
}
}
}
public static void main(String[] args){
//实际上一共卖出了80张票,每个线程都有自己的私有的非共享数据。都认为自己有20张票
DemoTest test4 = new DemoTest();
DemoTest test5 = new DemoTest();
DemoTest test6 = new DemoTest();
DemoTest test7 = new DemoTest();
test4.setName("一号窗口:");
test5.setName("二号窗口:");
test6.setName("三号窗口:");
test7.setName("四号窗口:");
test4.start();
test5.start();
test6.start();
test7.start();
}
}
CPU 多级缓存,切换后如何保证一致性?
一种处理一致性问题的办法是使用 Bus Locking(总线锁)。当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。这个时候,所有 CPU 收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的 CPU 就去内存中获取最新数据更新。但是用锁的方式总是避不开性能问题。总线锁总是会导致 CPU 的性能下降。所以出现另外一种维护 CPU 缓存一致性的方式,MESI。
MESI 是保持一致性的协议。它的方法是在 CPU 缓存中保存一个标记位,这个标记位有四种状态:
CPU 的读取遵循下面几点:
安全的单例模式
1.使用 volatile 禁止重排序
2.可以重排序,但重排序对其他线程不可见
初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据 Java 语言规范,在首次发生下列任意一种情况时,一个类或接口类型 T 将被立即初始化。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance=new Instance();
public static Instance getinstance(){
return InstanceHolderinstance; //这里将导致InstanceHolder类被初始化
}
在 Java 的finally
块中通常执行的是一些必须要在退出try
块时执行的代码,不受异常是否抛出的影响。然而,有几种情况下finally
块不会执行:
System.exit()
方法时,Java 虚拟机会立即退出,不再执行任何未完成的代码,包括finally
块。finally
块中执行了一个会使 JVM 崩溃的操作(如死循环或异常死锁等),则finally
块不会完成执行。finally
块中有一个无法停止的线程(如Thread.stop()
方法),finally
块可能无法完成执行。finally
块可能不会执行。try
块中出现一个无限循环,finally
块将不会执行,因为代码永远不会跳出try
块。finally
块中调用了System.exit()
或Runtime.halt()
来退出 JVM,那么 JVM 将立即终止,finally
块的执行也将被中断。finally
块执行完毕。这意味着,如果守护线程中有finally
块,当 JVM 退出时,这些finally
块可能不会被执行。总之,finally
块在大多数情况下都会执行,但在某些极端情况下,如程序终止或 JVM 崩溃等情况下,finally
块可能不会被执行。因此,在使用finally
块时,应该确保其中的代码是可靠的,不依赖于异常处理或 JVM 的状态。
java.lang.Thread
类是 Java 中用于多线程编程的关键类,它提供了管理线程的各种方法。以下是 Thread
类中常用的一些方法:
start()
: 启动线程,使其进入可运行状态。一旦调用了 start()
方法,线程会在适当的时机被调度执行,而不是立即执行。run()
: 线程的主要执行代码应该放在这个方法中。当线程被启动并进入运行状态时,run()
方法会被调用。sleep(long millis)
: 使当前线程休眠指定的毫秒数。在这段时间内,线程不会执行任何操作。join()
: 等待调用此方法的线程结束。如果在线程 A 中调用了线程 B 的 join()
方法,线程 A 将会等待直到线程 B 执行完毕。interrupt()
: 中断线程,给线程设置中断状态。可以在其他线程中调用该方法来中断目标线程。isInterrupted()
: 检查线程是否被中断,但不会清除中断状态。currentThread()
: 静态方法,返回当前正在执行的线程对象。setName(String name)
: 设置线程的名字,便于识别和调试。setPriority(int priority)
: 设置线程的优先级,用于调度决策。优先级范围是 1(最低)到 10(最高)。yield()
: 提示调度器当前线程愿意放弃 CPU 使用权,但不保证会被采纳。isAlive()
: 检查线程是否处于活动状态(已启动但尚未终止)。getState()
: 返回线程的状态,如 NEW、RUNNABLE、BLOCKED、WAITING 等。setDaemon(boolean on)
: 设置线程是否为守护线程。守护线程在非守护线程全部结束后会自动终止。getId()
: 返回线程的唯一标识符。这些方法只是 Thread
类中的一部分,它们允许你控制线程的创建、运行、暂停、中断等操作。在多线程编程中,了解如何使用这些方法非常重要,以确保线程能够按照预期进行协调和执行。
java.lang.Object
是 Java 中所有类的根类,因此它的方法在所有对象中都可用。以下是一些 Object
类中常用的方法:
toString()
: 返回对象的字符串表示。默认情况下,返回的是对象的类名和哈希码的组合。可以在子类中覆盖这个方法以返回更有意义的字符串。equals(Object obj)
: 用于比较对象是否相等。默认情况下,比较的是对象的引用地址。应该在子类中覆盖这个方法,根据业务逻辑来判断对象是否相等。hashCode()
: 返回对象的哈希码,是一个整数。用于在哈希表等数据结构中进行快速查找。通常需要与 equals
方法一起覆盖。getClass()
: 返回对象的运行时类(Class
对象),该对象包含有关类的信息。wait()
, notify()
, notifyAll()
: 用于线程之间的同步与通信。wait()
使线程等待,直到另一个线程调用该对象的 notify()
或 notifyAll()
方法,唤醒等待线程。finalize()
: 在对象被垃圾回收之前调用。通常不建议使用,因为现代 Java 已经提供更好的资源管理方式(如 try-with-resources 语句块)。clone()
: 用于创建对象的浅拷贝(复制引用,不复制对象本身)。需要实现 Cloneable
接口,并在子类中覆盖该方法来实现深拷贝。finalize()
: 已过时,曾经是在对象被垃圾回收之前调用的方法。现在不推荐使用,因为更好的资源管理方式已经可用。这些只是 Object
类中的一些方法。在实际编程中,你会经常使用其中的一些方法,特别是 toString()
、equals()
、hashCode()
等。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。