文章目录
一、Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
Vector
- Vector诞生的太早了,Java1.1时代就存在。内部的设计并不是特别合理,后面ArrayList可以看做进化版本
- Vector虽然线程安全,但是线程安全不一定就是好事,只是给关键方法家里synchronized,这么做不能百分百保证线程安全,如果是单线程环境下,加锁反而是额外的负担,拖慢了效率。
StringBuffer 的核心方法都带有 synchronized .
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
- String
二、 volatile 关键字
2.1volatile 能保证内存可见性
内存可见性:在编译器优化的背景下,一个线程更改内存,另一个线程不能及时感知到。
volatile 修饰的变量, 能够保证 “内存可见性”.
避免让编译器做出不读内存的优化。
优化的目的是为了提升效率,但是准确性是前提!
代码在写入 volatile 修饰的变量的时候
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快,但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
2.1 volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
最终 count 的值仍然无法保证是 100000.
三、 wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.
3.1 wait()方法
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时(其他线程调用notify)被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常.
wait()方法使用:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。
3.2 notify()方法
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行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();
}
注意:要保证加锁的对象和调用wait的对象是同一个,还要保证,调用wait的对象和调用notify的对象是同一个。
3.3 notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
注意:唤醒的是使用同一个锁对象的wait
3.4 ※wait 和 sleep 的对比(面试题)
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
- wait 需要搭配 synchronized 使用, sleep 不需要。
- wait 是 Object 的方法, sleep 是 Thread 的静态方法。
- sleep是通过时间来控制何时唤醒的,wait则是其他线程通过notify来唤醒的。
四、 单例模式
啥是设计模式? 设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路.按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例. 这一点在很多场景上都需要.
比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
饿汉模式
类加载的同时, 创建实例.
class Singleton {
private static Singleton instance=new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton(){}
}
public class SingletonTest{
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
}
}
懒汉模式
类加载的时候不创建实例. 第一次使用的时候才创建实例.
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
public class SingletonLazyTest {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
}
}
饿汉模式只是单纯的“读数据”,不涉及修改
懒汉模式既涉及到读,也涉及到修改,线程不安全!
修改上述懒汉模式代码,使其线程安全。加锁,把多个操作打包成一个原子操作。
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//加锁
synchronized(SingletonLazy.class){
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy() {
}
}
public class SingletonLazyTest {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
}
}
通过这种加锁方式,线程安全问题解决了,但是又引入了新问题。线程不安全只发生在实例创建之前,也就是首次调用getInstance() 方法的时候,一旦实例创建好了,线程就安全了。
上述加锁代码的写法,导致后续线程已经安全的时候,仍然还得加锁。所以在锁的外层再加上一个判断,让这个代码,需要锁的时候才加上锁。
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance==null){
synchronized (SingletonLazy.class){
if(instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class SingletonLazyTest {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
}
}
同时,这里的new操作也可简单分成三个步骤:
- 申请内存,得到内存地址
- 调用构造方法。初始化实例
- 把内存地址赋值给instance
在单线程的角度下,2和3调换顺序效果一样。这个情况下可能编译器会进行“指令重排序”。
假设按照1 3 2 的顺序执行,执行了1 3后,执行 2 之前,创建了内存空间,但是没有赋值给instance,得到了不完全的对象,只有内存,内存上的数据无效。此时 t2 线程调用了getInstance方法,就会认为instance非空,直接返回了instance,并且后续可能会针对instance进行操作。
另一方面,如果线程很多的话,有的线程在读instance,有的线程在修改instance,好像也存在内存可见性问题。
volatile关键字既可以保证内存可见性,还可以解决指令重排序问题。
这里我们给instance加上volatile关键字。
class SingletonLazy {
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance==null){
synchronized (SingletonLazy.class){
if(instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class SingletonLazyTest {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
}
}