Bootstrap

【JavaEE初阶】多线程初阶下部


前言

这篇博客博主开始讲述多线程的下部,上部在博主的上一篇博客,需要的柚柚们可以看看~(链接在这里点击即可)。


提示:以下是本篇文章正文内容

一、volatile关键字

volatile 能保证内存可见性

代码在写入 volatile 修饰的变量的时候

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
    代码在读取 volatile 修饰的变量的时候
  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

代码示例:
在这个代码中

  • 创建两个线程 t1 和 t2
  • t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入⼀个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束
static class Counter {
	public int flag = 0;
}
public static void main(String[] args) {
 	Counter counter = new Counter();
 	Thread t1 = new Thread(() -> {
 		while (counter.flag == 0) {
		}
		System.out.println("循环结束!");
 	});
 	
	Thread t2 = new Thread(() -> {
 		Scanner scanner = new Scanner(System.in);
 		System.out.println("输⼊⼀个整数:");
 		counter.flag = scanner.nextInt();
 	});
 	
 	t1.start();
 	t2.start();
 }
  • t1读的是自己的工作内存中的内容
  • 当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
static class Counter {
 	public volatile int flag = 0;
}
  • 此时当用户输入非零值时,t2将flag的值刷回内存,然后t1再从内存中重新读取flag的值,我们可以发现t1线程循环能够立即结束。

二、wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

完成这个协调工作, 主要涉及到三个方法:

  • wait() / wait(long timeout): 让当前线程进入等待状态
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程

注:
wait, notify, notifyAll 都是 Object 类的方法.

2.1 wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足⼀定条件时被唤醒, 重新尝试获取这个锁

注:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify/notifyAll 方法.
  • wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

代码示例:

public static void main(String[] args) throws InterruptedException {
 	Object object = new Object();
 	synchronized (object) {
 		System.out.println("等待中");
 		object.wait();
 		System.out.println("等待结束");
 	}
}

2.2 notify()方法

notify 方法是唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
    完,也就是退出同步代码块之后才会释放对象锁.

代码示例:

  • 创建 WaitTask 类, 对应⼀个线程, run 内部循环调用 wait.
  • 创建 NotifyTask 类, 对应另⼀个线程, 在 run 内部调用一次 notify
  • 注意, WaitTask 和 NotifyTask 内部持有同⼀个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同⼀个 Object.
static class WaitTask implements Runnable {
 	private Object locker;
 	
 	public WaitTask(Object locker) {
 		this.locker = locker;
 	}
 	
 	@Override
 	public void run() {
 		synchronized (locker) {
 			while (true) {
 				try {
 					System.out.println("wait 开始");
 					locker.wait();
 					System.out.println("wait 结束");
 				} catch (InterruptedException e) {
 					e.printStackTrace();
 				}
 			}
	 	}
 	}
}

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(1000);
 	t2.start();
}

2.3 notifyAll()方法

notify方法只是唤醒某⼀个等待线程. 使用notifyAll方法可以⼀次唤醒所有的等待线程(同一个对象锁的所有等待线程),唤醒之后需要重新竞争锁。

2.4 wait 和 sleep 的对比(面试题)

其实理论上 wait 和 sleep 完全是没有可比性的,因为⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间,唯⼀的相同点就是都可以让线程放弃执行⼀段时间.
不同点:

  1. wait 需要搭配 synchronized 使用, sleep 不需要。
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法。

三、多线程案例

单例模式

单例模式是校招中最常考的设计模式之⼀,单例模式能保证某个类在程序中只存在唯⼀⼀份实例, 而不会创建出多个实例,单例模式具体的实现方式有很多. 最常见的是 “饿汉” 和 “懒汉” 两种。

那我们要如何保证一个程序中对象是单例的,用什么方法去保证?
1.人为口头约束,大家不要new这个对象,我给大家提供一个方法,这个方法可以返回一个单例的对象。(但是显然是不可取的,人和人之间的信任不太靠谱)
2.通过语言自身的语法约束,限制一个类只能被实例化一个对象。(把限制的过程交给程序,程序写死了就按照写的逻辑执行,不会改变)

实现过程:
1.要实现单例类,只需要定义一个static修饰的变量,就可以保证这个变量全局唯一(单例)

public class Singleton {
    private static Singleton instance = new Singleton();
    public Singleton getInstance() {
        return instance;
    }
}
class Test{
    public static void main(String[] args){
        Singleton instance1 = new Singleton();
        System.out.println(instance1.getInstance());
        Singleton instance2 = new Singleton();
        System.out.println(instance2.getInstance());
        Singleton instance3 = new Singleton();
        System.out.println(instance3.getInstance());
    }
}
  • private:防止外部对这个变量修改(就会导致instance不唯一)
  • static:保证全局唯一

输出:
在这里插入图片描述
从输出可以看出我们似乎实现啦单例,但是还是有不足的

2.既然是单例,就不想然外部去new这个对象,虽然返回的是同一个对象,已经实现了单例,但代码书写上有歧义,所以我们要将构造方法私有化,但是私有化之后外界就获取不到单例类了,所以我们要给getInstance方法前面加上static,我们就就可以通过类名.方法名的方式调用。修改之后的代码为:

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return instance;
    }
}
class Test{
    public static void main(String[] args){
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance1.getInstance());
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance2.getInstance());
        Singleton instance3 = Singleton.getInstance();
        System.out.println(instance3.getInstance());
    }
}

输出:
在这里插入图片描述
现在我们就真真正正地实现单例啦~

饿汉模式:类加载的同时, 创建实例

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉模式-单线程版:类加载的时候不创建实例. 第⼀次使用的时候才创建实例

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
    	if(instance == null) {
    		instance = new Singleton();
    	}
        return instance;
    }
}

懒汉模式-多线程版:上面的懒汉模式对于多线程的实现是线程不安全的

  • 线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例
  • ⼀旦实例已经创建好了, 后面再多线程环境调用getInstance 就不再有线程安全问题了(不再修改instance 了)
class Singleton {
 	private static Singleton instance = null;
 	private Singleton() {}
 	public synchronized static Singleton getInstance() {
 		if (instance == null) {
 			instance = new Singleton();
 		}
 		return instance;
 	}
}

懒汉模式-多线程版(改进):以下代码在加锁的基础上, 做出了进⼀步改动
• 使用双重 if 判定, 降低锁竞争的频率.
• 给 instance 加上了 volatile.

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;
 	}
}

四、总结-保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的模型
    a. 不需要写共享资源的模型
    b. 使用不可变对象
  3. 直面线程安全(重点)
    a. 保证原子性
    b. 保证顺序性
    c. 保证可见性

五、对比线程和进程

5.1 线程的优点

  1. 创建⼀个新线程的代价要比创建⼀个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

5.2 进程与线程的区别

  1. 进程是系统进行资源分配和调度的⼀个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同⼀进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。

总结

这篇博客博主后续还会更新更多案例,有兴趣地柚柚们可以点赞收藏,方便后续观看~

;