问题: 线程安全,线程同步。为什么发生线程安全,线程同步问题。如何解决?
核心思想: 上锁。 代码从哪里上锁?----可能会发生线程安全的地方进行上锁。
通俗的讲就是我们更改数据的地方。那 是锁方法,锁类,锁代码块 ?
锁:分布式锁、公平锁,非公平锁、重入锁、悲观锁、乐观锁。
锁的机制:
在同一个JVM中,多个线程需要竞争锁资源。多个线程同时抢同一把锁,谁拿到锁资源,谁执行相关代码。
如果没有获取成功,中间需要经历锁升级过程,如果一直没有获取到锁就一直阻塞等待。
加锁的缺点:可能影响到程序的执行效率。
加锁的代码:
2、使用 synchronized 关键字,完成上述功能。
注意:synchronized 获取锁与释放锁都由底层虚拟机实现好了。
在多线程的情况下 需要是同一个对象锁:且对象锁不能重复
Synchronized( 对象锁 )
{
需要保证线程安全的代码
}
3、synchronized 3种用法。
1.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
对象可以是。this, 也可以是任意对象。
2.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例 的锁,
加在实例方法上使用的是 this 锁。
加在静态方法上使用的是 当前类名.class
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁
/** 同一把锁 **/
如果是同一把锁 在多线程的情况下 最终只能够给一个线程使用。
如果有线程持有了该锁 意味着其他的线程 不能够在继续获取锁。
核心思想:当多个线程共享同一个全局变量时,将可能会发生线程安全的代码
上锁,保证只有拿到锁的线程才可以执行,没有拿到锁的线程不可以执行,需要阻塞等待。
死锁:
线程1 | 线程2 |
先获取到自定义对象的lock锁 | 先获取this锁 |
需要线程2已经持有的this锁 | 线程1已经持有自定义对象的lock锁 |
代码
synchronized 的死锁问题。
public class DeadlockThread implements Runnable {
private int count = 1;
private String lock = "lock";
@Override
public void run() {
while (true) {
count++;
if (count % 2 == 0) {
// 线程1需要获取 lock 在获取 a方法this锁
// 线程2需要获取this 锁在 获取B方法lock锁
synchronized (lock) {
a();
}
} else {
synchronized (this) {
b();
}
}
}
}
public synchronized void a() {
System.out.println(Thread.currentThread().getName() + ",a方法...");
}
public void b() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ",b方法...");
}
}
public static void main(String[] args) {
DeadlockThread deadlockThread = new DeadlockThread();
Thread thread1 = new Thread(deadlockThread);
Thread thread2 = new Thread(deadlockThread);
thread1.start();
thread2.start();
}
}
检测死锁的工具:
synchronized 死锁诊断工具
jconsole.exe 默认在 jdk 安装目录的 bin 文件夹下
线程1 先获取到自定义对象的lock锁,进入到a方法需要获取this锁
线程2 先获取this锁, 进入到b方法需要自定义对象的lock锁
线程1 线程2 是在同时执行
线程同步:
- 使用synchronized锁,JDK1.6开始 锁的升级过程
-
使用Lock锁 ,需要自己实现锁的升级过程。底层是基于aqs实现
-
使用Threadlocal,需要注意内存泄漏的问题。
-
手写 原子类 cas 非阻塞式锁。
有个升级过程记录下:
synchronized 偏向锁--> 轻量级锁(cas)-->重量级锁。
多线程通信
多线程之间通信:
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:
1.notify() :通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
2.notifyAll():通知所有等待在该对象的线程
3.wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。
代码例子:
public class Thread03 extends Thread {
@Override
public void run() {
try {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ">>当前线程阻塞,同时释放锁!<<");
this.wait();
}
System.out.println(">>run()<<");
} catch (InterruptedException e) {
}
}
public static void main(String[] args) {
Thread03 thread = new Thread03();
thread.start();
try {
Thread.sleep(3000);
} catch (Exception e) {
}
synchronized (thread) {
// 唤醒正在阻塞的线程
thread.notify();
}
}
}
多线程通讯实现生产者与消费者
public class Thread04 {
class Res {
/**
* 姓名
*/
private String userName;
/**
* 性别
*/
private char sex;
/**
* 标记
*/
private boolean flag = false;
}
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (res) {
//flag = false 写入输入 flag = true 则不能写入数据 只能读取数据
try {
// 如果flag = true 则不能写入数据 只能读取数据 同时释放锁!
if (res.flag) {
res.wait();
}
} catch (Exception e) {
}
if (count == 0) {
this.res.userName = "余胜军";
this.res.sex = '男';
} else {
this.res.userName = "小薇";
this.res.sex = '女';
}
res.flag = true;
res.notify();
}
count = (count + 1) % 2;
}
}
}
class OutThread extends Thread {
private Res res;
public OutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
try {
if (!res.flag) {
res.wait();
}
} catch (Exception e) {
}
System.out.println(res.userName + "," + res.sex);
res.flag = false;
res.notify();
}
}
}
}
public static void main(String[] args) {
new Thread04().print();
}
public void print() {
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutThread outThread = new OutThread(res);
inputThread.start();
outThread.start();
}
}
个人理解:
wait()和 notify() : wait 阻塞当前线程,并且释放锁。notify() 唤醒对应释放锁的线程。 应用场景可以用于并行计算,A,B 同时计算。 A计算完毕,需要B 的结果时,阻塞当前线程,并释放锁,返回B 线程中计算完毕后唤醒A线程。
在 spring mvc controller 中使用 sychronized 关键字。
需要注意:
Spring MVC Controller默认是单例的 需要注意线程安全问题
单例的原因有二:
1、为了性能。
2、不需要多例。
3、省内存。
设置多例
@Scope(value = "prototype") 设置为多例子。
@RestController
@Slf4j
//@Scope(value = "prototype")
public class CountService {
private int count = 0;
@RequestMapping("/count")
public synchronized String count() {
try {
log.info(">count<" + count++);
try {
Thread.sleep(3000);
} catch (Exception e) {
}
} catch (Exception e) {
}
return "count";
}
}