什么是线程安全问题
多线程同时对一个全局变量做读写操作,可能会受到其他线程的干扰从而导致多线程安全问题。
package thread;
public class ThreadCount implements Runnable {
private int count = 10;
@Override
public void run() {
while (count > 1) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
Thread-1,9
Thread-0,9
Thread-0,7
Thread-1,7
Thread-0,6
Thread-1,6
Thread-0,5
Thread-1,4
Thread-0,3
Thread-1,2
Thread-1,0
Thread-0,0
通过上面执行结果:理想很丰满,现实很骨感。
如何解决线程安全问题
核心思想:上锁
在哪上锁?
可能会发生线程安全性问题的
代码
,对象
,方法
上锁。
在同一个jvm中,多个线程去竞争锁,最终只能又有一个线程得到锁一个女孩子,可以被很多人追,但最终结婚的只能一个
。公平锁
Thread-0,9
Thread-0,8
Thread-0,7
Thread-0,6
Thread-0,5
Thread-0,4
Thread-0,3
Thread-0,2
Thread-0,1
虽然上锁解决了资源争抢问题,但是由于线程内是死循环,所以一直被一个线程占用锁,从而导致其他线程处于围观状态,公平吗?公平吗?
这种方法是不讲武德的,作为高级开发我们能允许这样的事情出现吗?不允许!
说好的公平锁,结果被你一人独占,宝,你下次和ta接吻时,记得涂我送你的口红
怎么也得让我有点参与感啊,hhh适度玩梗。
怎么办,怎么办?
缩小上锁范围
this锁
上面的代码存在很大的问题,既然存在问题我们就要解决问题。
我们尝试缩小上锁范围
就这?这就解决了?tg不得好死,拒绝当tg
当线程1执行完count--
后释放锁,线程2介入执行count--
同步代码块内代码执行完毕后自动释放锁
synchronized 锁的基本用法
- 修饰代码块
指定加锁对象
- 修饰实例方法
当前实例加锁
- 修饰静态方法
当前类对象加锁
修饰代码块
多线程情况下,需要同一个对象锁
synchronized(对象锁){
safeCode
}
手动指定一个上锁对象
修饰实例方法
方法锁很简单:只需要把要上锁的内容写到一个单独的方法内,然后在改方法上使用
synchronized
关键字修饰即可
修饰静态方法
静态方法上锁就有一点小窍门了,我们要知道Class记录了一个类的信息,多个源于同一个类的实例,Class一定是相同的,借助这一点我们可以通过
Class
来上锁。
- 如果作用在
实例方法
上,用this
锁 - 如果作用在
静态方法
上,用类名.class
锁
死锁问题
某面试大会
面试官:给我讲下什么是死锁,我就给你offer
程序猿:你给我offer我就给你讲,什么是死锁。
什么是死锁?
一个对象一直拿住另一个对象的锁不放手,并且另一个对象也拿到该对象的锁。从而造成一种僵持的巨变,故取名为DeadLock死锁。
package thread;
public class DeadLock implements Runnable {
private Object thisLock = new Object();
private Object thatLock = new Object();
private int count;
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(deadLock).start();
new Thread(deadLock).start();
}
@Override
public void run() {
while (true) {
count++;
if (count % 2 == 0) {
synchronized (thatLock) {
methodA();
}
} else {
synchronized (thisLock) {
methodB();
}
}
}
}
private void methodA() {
synchronized (thisLock) {
System.out.println("hr:给我讲什么是死锁我就给你offer");
}
}
private void methodB() {
synchronized (thatLock) {
System.out.println("fang:你给我offer我就给你讲什么是死锁");
}
}
}
宝,我们交往吧
好啊这个程序自己停止了我就和你处
然后....再也没有然后了
fang:你给我offer我就给你讲什么是死锁
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
如何避免死锁?
成年人要学会及时止损
不要写嵌套锁,尽量保证锁的原子性
通过jconsole来进行诊断。
打开jconsole
open /Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/bin/jconsole
选中当前进程
检测死锁
如何保证线程同步
- synchronized 锁
- Lock 锁
- Threadlocal
可能存在内存泄露
- 原子类 CAS 非阻塞
在springboot中如何保证线程安全?
springICO 容器默认是
单例模式
,因此会存在线程安全问题。
@RestController
public class SyncController {
private int count;
@RequestMapping("/count")
public synchronized String count() throws InterruptedException {
count++;
Thread.sleep(3000);
return String.valueOf(count);
}
}
以上代码造成浏览器阻塞3秒后再次响应,效率是非常低的。
那么我们取消单例模式试试看
@RestController
// 取消为单例模式
@Scope(value = "prototype")
public class SyncController {
private int count;
@RequestMapping("/count")
public synchronized String count() throws InterruptedException {
count++;
Thread.sleep(3000);
return String.valueOf(count);
}
}
然后问题就解决了,就这?
多线程通信问题
都是需要 配合 synchronized 结合使用
- wait()
释放锁
当前线程进入阻塞状态 - notify()
唤醒沉睡线程,但是不会释放锁
- notifyAll()
唤醒所有沉睡线程,不释放锁
package thread;
public class ThreadCorrespond implements Runnable {
private Object objLock = new Object();
private void doSomething(){
synchronized (objLock){
try {
System.out.println(">1<");
objLock.wait();
System.out.println(">2<");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建子线程 子线程内有阻塞代码
ThreadCorrespond threadCorrespond = new ThreadCorrespond();
new Thread(threadCorrespond).start();
// 主线程 3秒后 唤醒子线程
try {
Thread.sleep(3_000);
synchronized (threadCorrespond.objLock){
// 开始唤醒
threadCorrespond.objLock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
doSomething();
}
}
>1<
>2<
生产者消费者模型
一个简单的生产者消费者模型
package thread;
public class ThreadProduct {
/**
* 共享对象
*/
class Res {
public String userName;
public char sex;
}
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
boolean isOk =false;
while (true) {
if (isOk) {
res.userName = "luckyFang";
res.sex = '男';
} else {
res.userName = "alice";
res.sex = '女';
}
isOk=!isOk;
}
}
}
/**
* 消费者线程
*/
class OutputThread extends Thread{
private Res res;
public OutputThread(Res res){
this.res=res;
}
@Override
public void run() {
while (true){
System.out.println(res.userName+","+res.sex);
}
}
}
public static void main(String[] args) {
new ThreadProduct().print();
}
public void print(){
// global res
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
inputThread.start();
outputThread.start();
}
}
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,女
luckyFang,男
luckyFang,女
luckyFang,男
luckyFang,男
alice,女
通过上面输出结果我们不难看出,这个生产者消费者模型存在很大的问题。
因为两个线程共享一个对象,就可能造成资源争抢问题,本来luckyFang
是男的,结果被另一个线程影响变成了女的
。这是不妥的
通过上锁解决
package thread;
public class ThreadProduct {
/**
* 共享对象
*/
class Res {
public String userName;
public char sex;
}
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
boolean isOk = false;
while (true) {
synchronized (res) {
if (isOk) {
res.userName = "luckyFang";
res.sex = '男';
} else {
res.userName = "alice";
res.sex = '女';
}
isOk = !isOk;
}
}
}
}
/**
* 消费者线程
*/
class OutputThread extends Thread {
private Res res;
public OutputThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
System.out.println(res.userName + "," + res.sex);
}
}
}
}
public static void main(String[] args) {
new ThreadProduct().print();
}
public void print() {
// global res
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
inputThread.start();
outputThread.start();
}
}
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
继续分析上面代码,我们发现虽然解决了资源争抢问题,但是往往你解决了一个bug,会诞生一个新的bug:我们生产者线程本意是交错生产对象,但是现在疯狂只生产一个对象。
这时候我们就需要用到两个线程之间的通信了
生产者:等会,我还没生产完呢
消费者:好的呢,我等等就是了
生产者:好了我生产完了,过来拿吧
消费者:收到
package thread;
public class ThreadProduct {
/**
* 共享对象
*/
class Res {
public String userName;
public char sex;
public boolean isBlock = false;
}
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
boolean isOk = false;
while (true) {
synchronized (res) {
if (res.isBlock) {
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (isOk) {
res.userName = "luckyFang";
res.sex = '男';
} else {
res.userName = "alice";
res.sex = '女';
}
isOk = !isOk;
res.isBlock = true;
// 生产完了 过来消费吧
res.notify();
}
}
}
}
/**
* 消费者线程
*/
class OutputThread extends Thread {
private Res res;
public OutputThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
// 如果拿到了不属于自己的那就放手吧
if (!res.isBlock) {
try {
// 释放锁
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(res.userName + "," + res.sex);
res.isBlock = false;
// 消费完了 通知生产吧
res.notify();
}
}
}
}
public static void main(String[] args) {
new ThreadProduct().print();
}
public void print() {
// global res
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
inputThread.start();
outputThread.start();
}
}
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
具体实现原理也是非常的简单,我们设置一个isBlock
来判断是否是阻塞状态
isBlock状态
- true 消费者开始消费 生产者开始等待
- false 生产者开始生产 消费者开始等待
这样看似没有问题,但是他真的没有问题吗?
要知道 两个线程同时去竞争一把锁,并不是每次都是同一个线程都能拿到锁的。
因此我们要建立合约
如果 isBlock = true 但是这个锁被生产者拿到了,根据合约内容生产者要自动放弃这把锁,因为她不属于你,你退出的同时,你要通知锁的主人(消费者)
如果 isBlock = false 但是这个锁被消费者拿到了,根据合约内容消费者要主动放弃这把锁,因为不属于你的你把持不住你退出的同时,你要通知锁的主人(生产者)
那么我们简单总结下:
- isBlock 决定是生产还是消费
- 但是如果不是你的锁你就要主动放弃
wait
,并且通知锁的主人notify
。