操作系统 --- 多线程(初阶)

public void run() {

System.out.println(" thread !!! ");

}

}

public class ThreadDemo {

public static void main(String[] args) {

Thread t = new MyThread();

t.start(); //1

t.run(); // 2

}

}

上面代码中start和run的区别.在程序的执行上结果是一样的

在这里插入图片描述

实际上.图解:

在这里插入图片描述

  • 使用 t.start() 会创建一个新的 PCB ,新的 PCB 链接在链表上,然后执行 myThread.run() 方法

  • 使用 t.run() 会直接调用 myThread.run()

3. 使用 jconsole 命令观察线程

=========================================================================================

有如下代码:

class MyRunnable implements Runnable {

@Override

public void run() {

while(true){

System.out.println(“hello thread !”);

try{

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

public class ThreadDemo2 {

public static void main(String[] args) {

Thread t = new Thread(new MyRunnable());

t.start();

while(true){

System.out.println(“hello main !”);

try{

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

找到自己jdk安装目录下的bin目录 然后找到 jconsole.exe

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4. 用代码来观看多线程的优势

===================================================================================

这里我们使用 serial() 来表示串行方式 concurrency()来表示并行方式

  • 串行方式 a和b在同一个线程里分别自增10亿次

  • 并行方式 a和b在两个线程里分别自增10亿次

public class ThreadDemo6 {

private static final long count = 10_0000_0000L;

public static void serial(){

// System.currentTimeMillis(); 获取到当前系统的毫秒级时间戳

long beg = System.currentTimeMillis();

int a = 0;

for (long i = 0; i < count; i++) {

a++;

}

int b = 0;

for (long i = 0; i < count; i++) {

b++;

}

long end = System.currentTimeMillis();

System.out.println("time: "+(end - beg));

}

public static void concurrency() {

long beg = System.currentTimeMillis();

Thread t1 = new Thread(){

@Override

public void run() {

int a = 0;

for (long i = 0; i < count; i++) {

a++;

}

}

};

t1.start();

Thread t2 = new Thread(){

@Override

public void run() {

int b = 0;

for (long i = 0; i < count; i++) {

b++;

}

}

};

t2.start();

try {

//等待线程结束

t1.join();

t2.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

long end = System.currentTimeMillis();

System.out.println("time: "+(end - beg));

}

public static void main(String[] args) {

serial();

concurrency();

}

}

运行结果:

在这里插入图片描述

  • 对于 serial() ,一个线程执行了 20 亿次循环,中间可能调度若干次

  • 对于concurrency(),两个线程分别执行了10亿次循环,中间也可能调度若干次

所以多线程的运行速度加快

5. Thread类 及 常用方法

=====================================================================================

5.1 Thread类的常见构造方法


| 方法 | 说明 |

| :-- | :-- |

| Thread() | 创建线程对象 |

| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |

| Thread(String name) | 创建线程对象,并命名 |

| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |

| 【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |

代码示例:

Thread t1 = new Thread();

Thread t2 = new Thread(new MyRunnable());

Thread t3 = new Thread(“线程命名”);

Thread t4 = new Thread(new MyRunnable(), “线程命名”);

这里演示第四种的使用方法并用jconsole观察:

在这里插入图片描述

在这里插入图片描述

5.2 Thread类的几个常见属性


| 属性 | 获取方法 |

| :-- | :-- |

| ID | getId() |

| 名称 | getName() |

| 状态 | getState() |

| 优先级 | getPriority() |

| 是否后台线程 | isDaemon() |

| 是否存活 | isAlive() |

| 是否被中断 | isInterrupted() |

注:

  • ID是唯一的标识,不同的线程不会重复

  • 名称就是线程的名字

  • 状态就是线程的状态,存在的意义就是辅助进行线程调度

  • 优先级高的更容易被调度

  • 创建的线程,默认不是后台线程,注意:JVM会在一个进程的所有非后台线程结束后,才会结束

  • 是否存活,就是内核中的PCB是不是销毁了,也就是说系统中的线程是不是销毁了.简单理解,为run方法是否运行结束了.

代码示例:

public class ThreadDemo9 {

public static void main(String[] args) {

Thread t = new Thread(new Runnable() {

@Override

public void run() {

while (true) {

// 打印当前线程的名字

// Thread.currentThread() 这个静态方法,获取到当前线程实例

// 哪个线程调用这个方法,就能获取到对应的实例

System.out.println(Thread.currentThread().getName());

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

},“myThread”);

t.start();

// 在这里也打印一下这个线程的属性

System.out.println("id: "+t.getId());

System.out.println("name: "+t.getName());

System.out.println("state: "+t.getState());

System.out.println("priority: "+ t.getPriority());

System.out.println("isDaemon: "+t.isDaemon());

System.out.println("isInterrupted: "+t.isInterrupted());

System.out.println("isAliveL: "+t.isAlive());

}

}

运行结果:

在这里插入图片描述

5.3 中断一个线程


5.3.1 方法一:自定义一个变量来作为循环结束的标志

public class ThreadDemo1 {

private static boolean flg = true;

public static void main(String[] args) throws InterruptedException {

Thread t = new Thread(){

@Override

public void run() {

while(flg){

System.out.println(“线程正在运行…”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println(“线程运行结束!”);

}

};

t.start();

Thread.sleep(3000);

flg = false;

}

}

运行结果:

在这里插入图片描述

5.3.2 使用标准库里内置的标记

| 方法 | 说明 |

| :-- | :-- |

| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |

| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |

| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |

public class ThreadDemo2 {

public static void main(String[] args){

Thread t = new Thread(){

@Override

public void run() {

// 默认情况下 isInterrupted() 是false的

while(!Thread.currentThread().isInterrupted()){

System.out.println(“线程正在运行…”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

// 如果不加break 会触发异常,导致循环无法结束

break;

}

}

}

};

t.start();

//主线程中 通过t.interrupt() 方法来设置这个标记位

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 这个操作就是把 Thread.currentThread().isInterrupted() 设置成true

t.interrupt();

}

}

这里的 interrupt 可能有两种行为:

  1. 如果当前线程正在运行中,此时就会修改 Thread.currentThread().isInterrupted() 标记位为 true
  1. 如果当前线程正在 sleep / wait / 等待锁 ... 此时就会触发 InterruptedException异常

5.3.3 观察标志位是否清除

  1. interrupt() 方法, 把标志位设成 true ,就应该结束循环

  2. interrupted() 方法, 判定标记位的时候,会返回true,同时把标记位改回false,下次再调用就返回false

  3. isInterrupted 方法, 判定标记位的时候,会返回true,不会改回标记位,下次再调用就返回true.

在这里插入图片描述

5.4 等待一个线程


线程和线程之间,调度顺序是完全不确定的…如果想要线程的顺序可控,线程等待就是一种方法.

| 方法 | 说明 |

| :-- | :-- |

| public void join() | 等待线程结束 |

| public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |

| public void join(long millis, int nanos) | 等待线程结束,但可以更高精度 |

当执行到 t.join() 时,这里的线程会阻塞等待.

代码示例:

public class ThreadDemo4 {

public static void main(String[] args) {

Thread t = new Thread(){

@Override

public void run() {

int count = 0 ;

while(count < 3){

count++;

System.out.println(“线程正在运行…”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println(“线程运行结束!”);

}

};

t.start();

try {

System.out.println(“join 执行开始”);

t.join();//只要t线程在运行就会发生线程阻塞 直到t线程执行完毕

System.out.println(“join 执行结束”);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

运行结果:

在这里插入图片描述

5.5 获取当前线程的引用


| 方法 | 说明 |

| — | — |

| public static Thread currentThread(); | 返回当前线程对象的引用 |

在这里插入图片描述

5.6 休眠当前线程


| 方法 | 说明 |

| — | — |

| public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |

| public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |

这个Sleep这个方法,本质上就是把线程PCB从就绪队列,移动到阻塞队列.

注: 当线程调用 sleep / join / wait / 等待锁 … 就会把 PCB放到另一个队列(阻塞队列)

6. 线程的状态

============================================================================

  • NEW: Thread 对象创建了,但是内核没有创建出PCB

  • RUNNABLE: 当前的PCB创建出来了,这个PCB就绪了.这个线程可能在CPU上运行,也可能在就绪队列中排队

  • BLOCKED: 线程中尝试进行加锁,结果发现锁已经被其他线程占用了,此时PCB也会处于阻塞状态.这个等待会在其他线程释放锁之后被唤醒

  • WAITING: PCB处于阻塞状态,(死等)

  • TIMED_WAITING: 表示PCB在阻塞队列中等待,这个等待是有结束时间的等待.

  • TERMINATED: 表示当前PCB已经结束了,但是Thread对象还在,此时调用获取状态,得到的就是这个状态.

注:之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的

yield()的观察

public class ThreadDemo6 {

public static void main(String[] args) {

Thread t1 = new Thread(){

@Override

public void run() {

while (true){

System.out.println(“线程t1正在运行…”);

Thread.yield();

}

}

};

Thread t2 = new Thread(){

@Override

public void run() {

while(true){

System.out.println(“线程t2正在运行…”);

}

}

};

t1.start();

t2.start();

}

}

在这里插入图片描述

结论:

  1. 不使用Thread.yield(),线程t1 和线程t2 打印的结果次数差不多

  2. 使用Thread.yield(),线程t2 的结果远多于 线程 t1的结果次数

7. 线程安全

===========================================================================

7.1 观察线程不安全


这里我们用两个线程分别实现count自增5w次.

public class ThreadDemo7 {

static class Counter{

public int count = 0;

void increase(){

count++;

}

}

public static void main(String[] args) throws InterruptedException {

Counter counter = new Counter();

Thread t1 = new Thread(){

@Override

public void run() {

for (int i = 0; i < 50000; i++) {

counter.increase();

}

}

};

Thread t2 = new Thread(){

@Override

public void run() {

for (int i = 0; i < 50000; i++) {

counter.increase();

}

}

};

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println(counter.count);

}

}

在这里插入图片描述

发现每次的结果不一样 且没有达到预期结果

原因分析:

在这里插入图片描述

在这里插入图片描述

当出现这种情况的时候 count 只改变了一次.

这两个的线程的执行顺序是不确定的.

操作系统调度线程的时候,是使用"抢占式执行"的方式,某个线程啥时候能上CPU执行,啥时候切换出CPU是不确定的.

而且,两个线程在两个不同的CPU上也可以完全并行执行

结论: 两个线程的执行的具体顺序是完全无法预期的.

7.2 线程安全的概念


线程安全的定义是非常复杂的,

我们可以认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的

7.3 线程不安全的原因


线程之间是抢占式执行的

抢占式执行导致两个线程的执行的先后顺序无法判断,我们无法改变他.

这是导致线程不安全的根本原因.

多个线程修改同一个变量

一个线程修改同一个变量,这里没有线程安全问题,没有涉及到并发,执行的结果就是预期的结果

多个线程读取同一个变量,也没有线程安全问题,读取数据只是把数据从内存读取到CPU种,读取不会修改内存数据的内容

多个线程修改不同的变量,也没有线程安全问题.

多个线程修改同一个变量,这个线程安全问题和代码的写法有关.

原子性

++的操作,本质上是三步操作,是一个"非原子"的操作

可以通过加锁的方式,把这个操作变成原子性

可见性

一个线程修改,一个线程读取.

由于编译器的优化,可能把一些中间环节的操作去掉了(循环自增的时候,++操作会比其他操作快,为了提高程序的整体效率,线程就会把①③操作省略掉,这个省略的操作是 编译器(javac)和JVM(java)综合配合达成的效果)

此时读的线程可能读到的是未修改的结果

编译器优化图解:

在这里插入图片描述

指令重排序

一段代码是这样的:

  1. 去前台取下 U 盘

  2. 去教室写 10 分钟作业

  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但 是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代 码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论

8. synchronized 关键字-监视器锁monitor lock

========================================================================================================

8.1 synchronized 的使用示例


直接修饰普通方法

锁的是SynchronizedDemo 对象

public class SynchronizedDemo {

public synchronized void methond() {

}

}

修斯静态方法

锁的是 SynchronizedDemo 类的对象

public class SynchronizedDemo {

public synchronized static void method() {

}

}

修饰代码块

锁当前对象

public class SynchronizedDemo {

public void method() {

synchronized (this) {

}

}

}

锁类对象

public class SynchronizedDemo {

public void method() {

synchronized (SynchronizedDemo.class) {

}

}

}

8.2 synchronized 的特性


a) 互斥性

当一个线程执行到了 synchronized修饰的对象中时,并且其他线程也进入到同一个被 synchronized 修饰的对象中时,就会产生 阻塞等待;

  • 当进入synchronized修饰的对象时,就相当于加锁

  • 当推出synchronized修饰的对象时,就相当于解锁

在这里插入图片描述

synchronized相当于在上厕所时,会锁门,如果不锁门其他人进来的时候就会出事.

在这里插入图片描述

“阻塞等待”:

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待,一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争,并不遵守先来后到的规则.

当两个线程执行到的对象是两个不同的锁时,不会产生线程阻塞

在这里插入图片描述

b) 刷新内存

synchronized 也可以保证内存的可见性.

编译器在执行count++时,是三步操作,编译器在操作时,会优化这里,把中间的一些 LOAD 和 SAVE 的操作省略掉.当有其他线程进行读取的时候,就可能会出错.

synchronized 就会禁止这样的现象,保证每次的操作都是进行了LOAD 和 SAVE操作.(这样就会导致程序变慢~)

c) 可重入

synchronized 允许一个线程针对一把锁,连续加锁

第一次线程没有释放锁,然后又进行加锁,按照锁的设定,第二次加锁的时候,会进行阻塞等待,直到第一次被释放,才能获取第二个锁.但是当第一个锁也是由同一个线程来完成,那么这样就无法解锁,这个时候就变成了死锁

在这里插入图片描述

在synchronized中 实现了可重入,synchronized记录了锁是哪个线程持有的.

8.3 Java 标准库中的线程安全类


Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList

  • LinkedList

  • HashMap

  • TreeMap

  • HashSet

  • TreeSet

  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制

  • Vecter (一般不推荐使用 很多方法都加了synchronized 在单线程下影响操作效率)

  • HashTable(同上不建议使用)

  • ConcurrentHashMap

  • StringBuffer(StringBuffer 的核心方法都带有 synchronized)

  • String (虽然没有加锁, 但是String是不可变对象,不存在两个线程并发修改同一个String)

9. volatile 关键字

===================================================================================

9.1 volatile 可以保证内存可见性


示例代码:

import java.util.Scanner;

public class ThreadDemo2 {

static class Counter{

public int flg = 0;

}

public static void main(String[] args) {

Counter counter = new Counter();

Thread t1 = new Thread(){

@Override

public void run() {

while (counter.flg ==0){

};

System.out.println(“循环结束”);

}

};

Thread t2 = new Thread(){

@Override

public void run() {

Scanner sc = new Scanner(System.in);

System.out.println(“请输入一个整数:”);

counter.flg = sc.nextInt();

}

};

t1.start();

t2.start();

}

}

这里我们可以发现,线程1快速的循环读取flg的值,编译器进行了优化,所以每次读取的时候并不都是内存的值,可能是CPU中进行读内存值了.

当线程2输入了一个整数之后,线程1感知不到线程2对flg数据的变化.

当使用 volatile关键字之后 每次的读取就是在内存上读取了.

在这里插入图片描述

volatile不能保证原子性

9.2 Java内存模型(java memory model)


在这里插入图片描述

代码中在读取一个变量的时候,不一定都是在内存中读取数据的.也有可能这个数据已经在CPU或者是cashe中缓存着了.这个时候就可能绕过内存,直接从cpu或者cashe中来读取这个数据.

JMM就把CPU的寄存器 cache 同称为 “工作内存” 把真正的内存 称为"主内存"

当使用 volatilesynchronized 就能够强制保证 操作是在 主内存中进行的.(相当于强制同步了主内存和工作内存中的内容)

10. wait 和 notify

=====================================================================================

由于线程之间是抢占式执行的,导致线程之间执行的先后顺序非常难以预知.

为了控制线程之间的执行顺序我们可以通过 waitnotify 来完成这个协调的工作.

| 方法 | 作用 |

| :-- | :-- |

| wait() | 让线程进入等待状态 |

| wait(long timeout) | 让线程进入等待状态(这是一个带有timeout参数的版本.可以指定等待的时间) |

| notify() | 唤醒在当前对象上等待的线程 |

| notifyAll() | 唤醒在当前对象上等待的所有线程 |

10.1 wait()方法


wait 做了 三件 事情:

  1. 让当前线程阻塞等待

  2. 释放当前的锁. (wait 需要搭配 synchronized 来使用,没有synchronized使用wait时会抛出异常.

  3. 满足一定条件被唤醒时,重新尝试获取到这个锁.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify() 方法

  • 当使用带参数的 wait(long timeout)方法时,超出指定的时间就会结束等待

  • 当其他的线程调用该线程的 interrupted 方法,导致 wait 抛出了 InterruptedException 异常

10.2 notify()方法


notify的使用

  1. notify()也必须放在synchronized中使用

  2. notify 操作是一次唤醒一个线程.如果有多个线程都在等待中,调用的 notify 相当于 随机唤醒一个.其他线程保持原状

  3. 使用notify()方法后,当前的线程不会马上被释放该对象的锁,而是要等待当前的 synchronized代码块执行完才能释放锁.

10.3 wait()方法 和 notify()方法 的使用示例


  1. 创建一个 WaitTask 类实现 Runnable 接口 ,重写的run()方法,用来执行wait()方法

  2. 创建一个 NotifyTask 类实现 Runnable 接口.发泄run()方法,用来执行notify()方法

public class ThreadDemo {

static class WaitTask implements Runnable{

private Object locker;

public WaitTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized (locker){

System.out.println(“wait() 方法 开始”);

try {

locker.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“wait() 方法 结束”);

}

}

}

static class NotifyTask implements Runnable{

private Object locker;

public NotifyTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized(locker){

System.out.println(“notify() 方法 开始”);

locker.notify();

System.out.println(“notify() 方法 结束”);

}

}

}

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Thread t1 = new Thread(new WaitTask(locker));

Thread t2 = new Thread(new NotifyTask(locker));

t1.start();

Thread.sleep(3000);

t2.start();

}

}

运行结果:

在这里插入图片描述

注意事项:

在这里插入图片描述

10.4 notifyAll() 方法 及 使用示例


notify()方法只是唤醒某一个等待线程,使用notifyAll()方法可以一次唤醒所有的等待线程.

示例(注:还是使用上面那个相同的WaitTask 和 NotifyTask):

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Thread t1 = new Thread(new WaitTask(locker));

Thread t2 = new Thread(new WaitTask(locker));

Thread t3 = new Thread(new WaitTask(locker));

Thread t4 = new Thread(new NotifyTask(locker));

t1.start();

t2.start();

t3.start();

Thread.sleep(3000);

t4.start();

}

运行结果:

在这里插入图片描述

此时发现只唤醒了一个线程.当我们将NotifyTask()中 notify()改为notifyAll()

在这里插入图片描述

这个时候 所有的线程都被唤醒了

注意: 虽然同时唤醒了3个线程,但是这3个线程还是需要竞争锁.也就是说这3个线程并不是同时执行,仍然是有先有后的执行

图解:

在这里插入图片描述

10.5 wait 和 sleep 的 区别 (面试题)


理论上 wait 和 sleep 是没有什么关联关系的.

  1. sleep 操作的是指定一个时间来让线程 阻塞等待, wait 操作可以指定时间等待,也可以无限的等待

  2. wait 唤醒可以通过三个办法:① notify 唤醒 ② 其他线程调用该线程的 interrupted 方法 ③时间到

sleep 唤醒 通过2个办法 ①时间到 ② interrupted 唤醒

  1. wait 需要搭配synchronized 使用,sleep不需要

  2. wait 主要是为了协调线程之间的先后顺序,这样的场景不适合使用sleep.sleep只是让该线程休眠,不涉及到多个线程的配合

  3. wait 是 Object 的方法

sleep 是 Thread 的静态方法

11. 多线程案例

=============================================================================

11.1 单例模式


单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.使用单例模式就是为了限制这个类能有唯一实例

饿汉模式

在类加载阶段就把实例创建出来

class Singleton{

private static Singleton instance = new Singleton();

//防止在类外面调用构造方法,也就禁止了调用者再其他地方创建实例的机会

private Singleton(){}

public static Singleton getInstance(){

return instance;

}

}

懒汉模式 - 单线程版

通过 getInstance 方法 来获取到实例,首次调用该方法的时候,才真正创建实例.(懒加载/延时加载)

class Singleton{

private static Singleton instance = null;

private Singleton(){}

public static Singleton getInstance(){

if(instance == null){

instance = new Singleton();

}

return instance;

}

}

懒汉模式 - 多线程版

由于单线程版,在多线程情况下,如果有多个线程调用 getInstance() 方法,就可能创建出多个实例出来.

class Singleton{

private static Singleton instance = null;

private Singleton(){}

public static Singleton getInstance(){

synchronized(Singleton.class){

if(instance == null){

instance = new Singleton();

}

}

return instance;

}

}

懒汉模式 - 多线程版(改进版)

由于上一个代码 锁竞争的频率非常大, 每次调用getInstance()都会竞争锁.

class Singleton{

private static volatile Singleton instance = null;

private Singleton(){}

public static Singleton getInstance(){

if(instance == null) {

synchronized (Singleton.class) {

if (instance == null) {

instance = new Singleton();

}

}

}

return instance;

}

}

  • 这里用了双重if 来判定,就降低了对锁竞争的频率

  • 这里给 instance 加了 volatile

11.2 阻塞队列


11.2.1 阻塞队列的概念

我们之前学过优先级队列,优先级队列就是一种特殊的队列.遵循队列"先进先出"的原则.

这里的阻塞队列也是一样的.

阻塞队列也是一种特殊的队列.遵循"先进先出"的原则.

阻塞队列是一种线程安全的数据结构.阻塞队列的特性:

  • 队满 时,继续 入队就会阻塞,直到有其他线程从队列中取走元素.

  • 队空时,继续出队也会阻塞,直到有其他线程往队列中插入元素.

阻塞队列的应用场景 最典型的就是 “生产者消费者模型

11.2.2 生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题.

图解:

在这里插入图片描述

生产者和消费者彼此之间不需要直接通讯,而是通过一个阻塞队列来进行通讯,所以生产者就生产完数据之后就不需要再等待消费者处理,直接把数据给阻塞队列就可以了,消费者也不需要找生产者要数据,而是直接通过阻塞队列来获取数据.

阻塞队列的用途

  1. 削峰填谷

  2. 解耦

11.2.3 Java标准库中的阻塞队列

在Java标准库中内置了阻塞队列.

注意事项:

  1. BlockingQueue 是一个 接口.真正实现的类是 LinkedBlockingQueue

  2. 阻塞队列的入队方法是:put() 方法

  3. 阻塞队列的出队方法是:take() 方法

  4. BlockingQueue 也有offer poll peek 等方法,单是这些方法不带有阻塞特性(线程不安全)

使用示例:

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

public class Test {

public static void main(String[] args) throws InterruptedException {

BlockingQueue queue = new LinkedBlockingQueue<>();

queue.put(12312312);

System.out.println(queue.take());

}

}

11.2.4 生产者消费者模型代码实现

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo {

public static void main(String[] args) {

BlockingQueue queue = new LinkedBlockingQueue<>();

Thread customer = new Thread(){

@Override

public void run() {

while (true){

try {

Integer value = queue.take();

System.out.println("消费了: " + value);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

};

customer.start();

Thread producer = new Thread(){

@Override

public void run() {

for (int i = 1; i <= 10000; i++) {

try {

queue.put(i);

System.out.println("生产了: "+ i);

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

};

producer.start();

try {

customer.join();

producer.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

11.2.5 阻塞队列的实现

这里用"循环队列"来实现 阻塞队列

注意事项:

① 有效元素的范围是 [ head , tail) (左闭右开)

② 出队列 就是 把 head下标下的元素拿走 然后head++

③ 入队列 就是 把元素放到 tail下标下, 然后tail++

④ 当 tail == head时,可能是队空 也可能是队满,这里用size记录有效元素个数,来解决这个问题.

在这里插入图片描述

代码实现:

public class MyBlockingQueue {

private int[] items = new int[1000];

private int head = 0;

private int tail = 0;

private int size = 0;

public void put(int item) throws InterruptedException {

synchronized(this){

// 这里的if最好换成while

// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,

// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了

// 就只能继续等待

while (size == items.length){

wait();

}

items[tail] = item;

tail++;

if(tail >= items.length){

tail = 0;

}

size++;

notify();

}

}

public int take() throws InterruptedException {

synchronized(this){

while (size == 0){

wait();

}

int ret = items[head];

head++;

if(head >= items.length){

head = 0;

}

size–;

notify();

return ret;

}

}

}

测试代码:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

2021年Java中高级面试必备知识点总结

在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。

本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。

目录:

(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)

部分内容:

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
6enp6enp6enp6,size_20,color_FFFFFF,t_70,g_se,x_16)

代码实现:

public class MyBlockingQueue {

private int[] items = new int[1000];

private int head = 0;

private int tail = 0;

private int size = 0;

public void put(int item) throws InterruptedException {

synchronized(this){

// 这里的if最好换成while

// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,

// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了

// 就只能继续等待

while (size == items.length){

wait();

}

items[tail] = item;

tail++;

if(tail >= items.length){

tail = 0;

}

size++;

notify();

}

}

public int take() throws InterruptedException {

synchronized(this){

while (size == 0){

wait();

}

int ret = items[head];

head++;

if(head >= items.length){

head = 0;

}

size–;

notify();

return ret;

}

}

}

测试代码:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-3Hi1bFjC-1713706994001)]

[外链图片转存中…(img-dQewTbl5-1713706994001)]

[外链图片转存中…(img-rnZMMgDO-1713706994002)]

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

2021年Java中高级面试必备知识点总结

在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。

本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。

目录:

[外链图片转存中…(img-AzP1E24c-1713706994002)]

(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)

[外链图片转存中…(img-f8DGDzGL-1713706994002)]

部分内容:

[外链图片转存中…(img-6lfJe1UD-1713706994003)]

[外链图片转存中…(img-wqgUGZqS-1713706994003)]

[外链图片转存中…(img-KfXezhQ1-1713706994003)]

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值