带你从零学大数据系列之Java篇---第二十一章 多线程高级,2024年最新附相关架构及资料

31 阅读5分钟

在一个线程操作一个资源的时候, 对这个资源进行“上锁”, 被锁住的资源, 其他的线程无法访问。

类似多个人去公共卫生间, 每一个人在进到卫生间的时候, 都会从里面进行反锁。 此时, 其他人如果也需要使用这个卫生间, 就得在门外等待。

21.1.2. 线程锁

线程锁, 就是用来“锁住”一个临界资源, 其他的线程无法访问。 在程序中, 可以分为对象锁类锁

  • 对象锁: 任何的对象, 都可以被当做是一把锁来使用。 但是需要注意, 必须要保证不同的线程看到的锁, 需要是同一把锁才能生效。 如果不同的线程看到的锁对象是不一样的, 此时这把锁将没有任何意义。
  • 类锁: 可以将一个类做成锁, 使用 类.class 来作为锁。

21.1.3. 同步代码段

同步代码段, 是来解决临界资源问题最常见的方式。 将一段代码放入到同步代码段中, 将这段代码上锁。

第一个线程抢到了锁标记后, 可以对这个紧接资源上锁, 操作这个临界资源。 此时其他的线程再执行到synchronized的时候, 会进入到锁池, 直到持有锁的线程使用结束后, 对这个资源进行解锁。 此时, 处于锁池中的线程都可以抢这个锁标记, 哪一个线程抢到了, 就进入到就绪态, 没有抢到锁的线程, 依然处于锁池中。

/**
 * @Description
 */
public class Program {
    public static void main(String[] args) {
        // 做⼀个 Runnable 接口的实现类对象,实现卖票
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (TicketCenter.ticketCount > 0) {
                    /*
                     * 同步代码段,这里的逻辑执行,会被上锁。当这里的逻辑执行结束之后,会自动的解锁。
                     * 小括号中需要写的是:锁。
                     * 这里的锁,可以分为:类锁 和 对象锁
                     */
                    synchronized (Thread.class) {
                        if (TicketCenter.ticketCount <= 0) {
                            break;
                        }
                        System.out.println(String.format("售票员【%s】卖出⼀张票,剩余: %d", Thread.currentThread().getName(), --TicketCenter.ticketCount));
                    }
                }
            }
        };
        // 实例化四个线程,模拟四个售票员
        Thread t1 = new Thread(runnable, "周杰伦");
        Thread t2 = new Thread(runnable, "林俊杰");
        Thread t3 = new Thread(runnable, "蔡依林");
        Thread t4 = new Thread(runnable, "周润发");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class TicketCenter {
    public static int ticketCount = 100;
}

21.1.4. 同步方法

如果在一个方法中, 所有的逻辑, 都需要放到同一个同步代码段中执行。 这样的方法, 可以直接做成同步方法。

同步方法中的所有的逻辑, 都是在一个同步代码段中执行的。

如果是一个静态方法, 使用当前类做类锁; 如果是一个非静态方法, 使用this做对象锁。

/**
 * 使用 synchronized 修饰的方法,就是一个同步方法
 * 此时这个方法, 是一个静态的方法, 则这个方法使用的锁是类锁
 * @return
 */
public static synchronized Chairman getInstance() {
	if (Instance == null) {
		Instance = new Chairman();
 	}
	return Instance; 
}

21.1.5. 单例设计模式

懒汉式单例, 在多线程的环境下, 会出现问题。 由于临界资源问题的存在, 单例对象可能会被实例化多次。

因此, 单例设计模式, 尤其是懒汉式单例, 需要针对多线程的环境进行处理。

/**
 * @Description
 */
public class Boss {
    private Boss() {}
    private static Boss Instance = null;
    public static synchronized Boss getInstance() {
        if (Instance == null) {
            Instance = new Boss();
        }
        return Instance;
    }
}

21.1.6. 死锁

多个线程, 同时持有对方需要的锁标记, 等待对方释放自己需要的锁标记。

此时就是出现死锁。 线程之间彼此持有对方需要的锁标记, 而不进行释放, 都在等待。

/**
 * @Description
 */
public class Program {
    public static void main(String[] args) {
        Runnable runnable1 = () -> {
            synchronized ("a") {
                System.out.println("线程A,持有了a锁,在等待b锁");
                synchronized ("b") {
                    System.out.println("线程A同时持有了a锁和b锁");
                }
            }
        };

        Runnable runnable2 = () -> {
            synchronized ("b") {
                System.out.println("线程B,持有了b锁,在等待a锁");
                synchronized ("a") {
                    System.out.println("线程B同时持有了a锁和b锁");
                }
            }
        };
        new Thread(runnable1, "A").start();
        new Thread(runnable2, "B").start();
    }
}

21.1.7. wait、notify

1. 方法简介

Object类中几个方法如下:

  • wait()

    • 等待,让当前的线程,释放自己持有的指定的锁标记,进入到等待队列。
    • 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。
  • notify()

    • 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。
    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
  • notifyAll()

    • 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。
    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

2. wait和sleep的区别

  • sleep()方法,在休眠时间结束后,会自动的被唤醒。 而wait()进入到的阻塞态,需要被notify/notifyAll手动唤醒。
  • wait()会释放自己持有的指定的锁标记,进入到阻塞态。sleep()进入到阻塞态的时候,不会释放自己持有的锁标记。

3. 注意事项

无论是wait()方法,还是notity()/notifyAll()⽅法,在使用的时候要注意,⼀定要是自己持有的锁标记,才可以做这个操作。否则会出现 IllegalMonitorStateException 异常。

4. 示例代码

/**
 * @Description
 */
public class Program {
    public static void main(String[] args) {
        Runnable runnable1 = () -> {
            synchronized ("a") {
                System.out.println("线程A,持有了a锁,在等待b锁");
                synchronized ("b") {
                    System.out.println("线程A同时持有了a锁和b锁");
                    // 当 "b" 锁使用结束之后,通知另外⼀个线程使用结束了
                    "b".notify();
                }
            }
        };
        Runnable runnable2 = () -> {
            synchronized ("b") {
                System.out.println("线程B,持有了b锁,在等待a锁");
                try {
                    // 释放自己持有的 "b" 锁标记
                    "b".wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized ("a") {
                    System.out.println("线程B同时持有了a锁和b锁");
                }
            }
        };
        new Thread(runnable1, "A").start();
        new Thread(runnable2, "B").start();
    }
}

21.2. 线程池

21.2.1. 线程池的简介

线程池, 其实就是一个容器, 里面存储了若干个线程。

使用线程池, 最主要是解决线程复用的问题。 之前使用线程的时候, 当我们需要使用一个线程时, 实例化了一个新的线程。 当这个线程使用结束后, 对这个线程进行销毁。 对于需求实现来说是没有问题的, 但是如果频繁的进行线程的开辟和销毁, 其实对于CPU来说, 是一种负荷, 所以要尽量的优化这一点。

可以使用复用机制解决这个问题。 当我们需要使用到一个线程的时候, 不是直接实例化, 而是先去线程池中查找是否有闲置的线程可以使用。 如果有, 直接拿来使用; 如果没有, 再实例化一个新的线程。 并且, 当这个线程使用结束后, 并不是马上销毁, 而是将其放入到线程池中, 以便下次继续使用。

21.2.2. 线程池的开辟

img img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取