Web全栈~32.深入理解synchronized
上一期
基本用法和原理
银行案例
synchronized可以用于修饰类的实例方法、静态方法和代码块。当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程使用
假设场景 当多个用户同时操作一个银行账户。每次取款400元,取款前先检查余额是否足够。如果不够,放弃取款
很显然这个问题我们可以使用多线程的思维去做,但结果往往不是理想中的那样
package com.alvin.test;
import java.util.ArrayList;
import java.util.List;
public class Test extends Thread{public static void main(String[] args) {//创建两个线程
Runnable runnable = new AccountRunnable();
Thread zhangsanThread = new Thread(runnable);
Thread lisi =new Thread(runnable,"李四");
zhangsanThread.setName("张三");
//启动两个线程
zhangsanThread.start();
lisi.start();
}
}
class Account {//设置余额600元
private int balance = 600;
//取款
public void withDraw(int money){this.balance = this.balance - money;
}
//查看余额
public int getBalance(){return balance;
}
}class AccountRunnable implementsRunnable {private Account account = new Account();
@Override
public void run() {//判断余额是否足够,够,取之;不够,不取之;
if(account.getBalance()>=400){try {Thread.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
//取之
account.withDraw(400);
//输出信息
System.out.println(Thread.currentThread().getName()+
"取款成功,现在的余额是"+account.getBalance());
}else{System.out.println("余额不足,"+Thread.currentThread().getName()
+"取款失败,现在的余额是"+account.getBalance());
}
}
}
文章图片
怎么会这样呢?因为使用Thread.sleep()的目的在于模拟线程切换,在一个线程判断完余额后,不是立刻取款,而是让出CPU,这样另外一个线程获取CPU,并且进行余额的判断。线程安全问题就这么产生了。如果保证安全,必须判断余额和取款的语句必须被一个线程执行完才能让另外一个线程执行。
修改代码
publicvoid run() {synchronized(account) {//判断余额是否足够,够,取之;不够,不取之;
if (account.getBalance() >= 400) {try {Thread.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
//取之
account.withDraw(400);
//输出信息
System.out.println(Thread.currentThread().getName() +
"取款成功,现在的余额是" + account.getBalance());
} else {System.out.println("余额不足," + Thread.currentThread().getName()
+ "取款失败,现在的余额是" + account.getBalance());
}
}
}
文章图片
synchronized实例方法实际保护的是同一个对象的方法调用,确保同时只能有一个线程执行。再具体来说,synchronized实例方法保护的是当前实例对象,即this,this对象有一个锁和一个等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待。
synchronized必须是引用数据类型,不能是基本数据类型。在同步代码块中可以改变同步监视器对象的值,不能改变其引用。并且尽量不要使用String和包装类Integer做同步监视器。如果使用了,只要保证代码块中不对其进行任何操作也没有关系 。一般使用共享资源做同步监视器即可,也可以创建一个专门的同步监视器,没有任何业务含义
同步代码块的执行过程 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,重复第一个线程的处理过程(加锁)
线程同步虽然安全但效率低下 可能出现死锁 进一步理解synchronized 可重入性
synchronized有一个重要的特征,它是可重入的,也就是说,对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。比如,在一个syn-chronized实例方法内,可以直接调用其他synchronized实例方法。可重入是一个非常自然的属性,应该是很容易理解的,之所以强调,是因为并不是所有锁都是可重入的,后续章节我们会看到不可重入的锁。
可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
内存可见性
在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。不过,如果只是为了保证内存可见性,使用synchronized的成本有点高,有一个更轻量级的方式,那就是给变量加修饰符volatile
private volatile boolean on;
public boolean getOn() { return on;
}
public void setOn(boolean on) { this.on = on;
}
加了volatile之后,Java会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存的值。
死锁
使用synchronized或者其他锁,要注意死锁。所谓死锁就是类似这种现象,比如,有a、b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a和b陷入了互相等待,最后谁都执行不下去。
public class Test extends Thread{private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {Thread aThread = new Thread() {@Override
public void run() {synchronized (lockA) {try {Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized (lockB) {System.out.println("lockB");
}
}
}
};
aThread.start();
}
private static void startThreadB() {Thread bThread = new Thread() {@Override
public void run() {synchronized (lockB) {try {Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized (lockA) {System.out.println("lockA");
}
}
}
};
bThread.start();
}
public static void main(String[]args) {startThreadA();
startThreadB();
}
}
出现死锁的时候Java并不会主动处理,不过,借助一些工具,我们可以发现运行中的死锁,比如,Java自带的jstack命令会报告发现的死锁。
同步容器及其注意事项 类Collections中有一些方法,可以返回线程安全的同步容器
public static Collection synchronizedCollection(Collection c)
public static List synchronizedList(List list)
public static Map synchronizedMap(Map m)
它们是给所有容器方法都加上synchronized来实现安全的。比如synchronized-Collection。里面有部分代码是这样的
文章图片
文章图片
这里线程安全针对的是容器对象,指的是当多个线程并发访问同一个容器对象时,不需要额外的同步操作,也不会出现错误的结果。加了synchronized,所有方法调用变成了原子操作,当然,在客户端调用的时候也有以下情况需要注意。
复合操作
class EnbancedMap{Map map;
public EnbancedMap(Mapmap){this.map = Collections.synchronizedMap(map);
}
public V putIfAbsent(K key, V value){V old = map.get(key);
if(old != null){return old;
}
return map.put(key,value);
}public V put(K key,V value){return map.put(key, value);
}
}
EnhancedMap是一个装饰类,接受一个Map对象,调用synchronizedMap转换为了同步容器对象map,增加了一个方法putIfAbsent,该方法只有在原Map中没有对应键的时候才添加
map的每个方法都是安全的,但这个复合方法putIfAbsent并不一定安全,这是一个检查然后再更新的复合操作,在多线程的情况下,可能有多个线程都执行完了检查这一步,都发现Map中没有对应的键,然后就会都调用put,这就破坏了putIf-Absent方法期望保持的语义。
伪同步
给刚刚那个方法加上synchronized?
public synchronized V putIfAbsent(K key, V value){V old = map.get(key);
if(old != null){return old;
}
return map.put(key,value);
}
很明显这样同步错对象了。putIfAbsent同步使用的是EnhancedMap对象,而其他方法(如代码中的put方法)使用的是Collections.synchronizedMap返回的对象map,两者是不同的对象。要解决这个问题,所有方法必须使用相同的锁,可以使用EnhancedMap的对象锁,也可以使用map。使用EnhancedMap对象作为锁,则Enhanced-Map中的所有方法都需要加上synchronized。使用map作为锁
publicV putIfAbsent(K key, V value){synchronized(map){V old = map.get(key);
if(old != null){return old;
}
}
return map.put(key,value);
}
线程的协作机制 多线程之间除了竞争访问同一个资源以外,也经常需要相互协作。
生产者/消费者协作模式:这是一种常见的协作模式,生产者线程和消费者线程通过共享队列进行协作,生产者将数据或任务放到队列上,而消费者从队列上取数据或任务,如果队列长度有限,在队列满的时候,生产者需要等待,而在队列为空的时候,消费者需要等待。
应用场景:生产者和消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
分析 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费
在生产者消费者问题中,仅有synchronized是不够的
synchronized可阻止并发更新同一个共享资源,实现了同步
synchronized不能用来实现不同线程之间的消息传递(通信)
方法名 | 作用 |
final void wait() | 表示线程一直等待,直到其它线程通知 |
void wait(long timeout) | 线程等待指定毫秒参数的时间 |
final void wait(long timeout,int nanos) | 线程等待指定毫秒、微妙的时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final void notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行 |
package com.alvin.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {public static void main(String[] args){//创建共享资源对象
Shop shop = new Shop();
//创建生产者
Boss boss = new Boss(shop);
//创建消费者
Customer customer = new Customer(shop);
//启动线程
new Thread(boss).start();
new Thread(customer).start();
}
}//生产者类
class Boss implements Runnable{private Shop shop;
public Boss(Shop shop){this.shop = shop;
}
@Override
public void run() {for(int i = 1;
i < 100;
i++){if(i % 2 == 0){shop.Set("OPPO","reno 3 Pro");
}else{shop.Set("HUWEI","Mate 30 Pro");
}
}
}
}//消费者类
class Customer implements Runnable{private Shop shop;
public Customer(Shop shop){this.shop = shop;
}
@Override
public void run() {for(int i = 1;
i < 100;
i++){shop.Get();
}
}
}//商品类
class Shop{private String brand;
//商品品牌
private String name;
//商品名称
private boolean flag = false;
//默认没有商品// 编写一个赋值的方法同步监视器为Shop类的对象
public synchronized void Set(String brand, String name){//先判断下有没有商品
if(flag){try {//生产者线程等待
super.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
//当生产者线程被唤醒后从wait()之后的代码开始执行
//生产商品
this.brand = brand;
try {Thread.sleep(300);
} catch (InterruptedException e) {e.printStackTrace();
}
this.name = name;
System.out.println("老板: 今天厂里生产了 " + brand + " " + name + " 欢迎下次光临!----------");
//通知消费者
super.notify();
flag = true;
}//编写一个取值的方法
public synchronized void Get(){//先判断下有没有商品
if(!flag){try {super.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
System.out.println("----------顾客: " + brand + " " + name + " 已经买到手,谢谢老板!");
//通知生产者
super.notify();
flag = false;
}
}
线程同步的细节 进行线程通信的多个线程,要使用同一个同步监视器(product),还必须要调用该同步监视器的wait()、notify()、notifyAll();
wait()等待 在【其他线程】调用【此对象】的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。当前线程必须拥有此对象监视器
wait(time) 等待 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。 当前线程必须拥有此对象监视器。
notify() 通知 唤醒 唤醒在【此对象监视器】上等待的【单个】线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。【选择是任意性的】,并在对实现做出决定时发生
notifyAll() 通知所有 唤醒所有 唤醒在【此对象监视器】上等待的【所有】线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程【进行竞争】;
完整的线程生命周期
阻塞状态有三种 普通的阻塞 sleep,join,Scanner input.next()
同步阻塞(锁池队列) 没有获取同步监视器的线程的队列
等待阻塞(阻塞队列) 被调用了wait()后释放锁,然后进行该队列
sleep()和wait()的区别
区别1:sleep() 线程会让出CPU进入阻塞状态,但不会释放对象锁 wait() 线程会让出CPU进入阻塞状态, 【也会放弃对象锁】,进入等待【此对象】的等待锁定池
区别2: 进入的阻塞状态也是不同的队列
【web|Web全栈~32.深入理解synchronized】区别3:wait只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
推荐阅读
- Java从入门到精通(二)|Java中的循环语句
- java|计算平均成绩【JAVA】
- java|阿里面试官(如何测试接口幂等性())
- python|生成对抗网络gans_生成对抗网络gans简介
- SpringBoot|SpringBoot临时属性设置
- SpringBoot|SpringBoot整合第三方技术与MP常用功能
- spring|SpringBoot多环境配置
- Caffeine (史上最全)
- 编程语言|奇舞周刊第 437 期(一文看懂云原生时代 DevOps 如何选型)