一. 什么是线程安全问题
多线程同时对同一个全局变量做写的操作,可能会受到其他
线程的干扰,就会发生线程安全性问题。
全局变量----java内存结构
什么是写操作------修改
当多个线程共享同一个全局变量,做写的操作时,可能会受到其他的线程干扰,发生线程
安全问题。
public class ThreadCount implements Runnable {
private static Integer count = 100;
@Override
public void run() {
while (count > 1) {
cal();
}
}
private void cal() {
try {
Thread.sleep(20);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
Thread thread1 = new Thread(threadCount);
Thread thread2 = new Thread(threadCount);
thread1.start();
thread2.start();
}
}
同时执行概念
1.. 多线程如何解决线程安全问题/ 多线程如何实现同步呢
核心思想:上锁 分布式锁
在同一个jvm中,多个线程需要竞争锁的资源,最终只能够有一个线程
能够获取到锁,多个线程同时抢同一把锁,谁(线程)能够获取到锁,
谁就可以执行到该代码,如果没有获取锁成功 中间需要经历锁的升级过程
如果一致没有获取到锁则会一直阻塞等待。
如果线程A获取锁成功 但是线程A一直不释放锁
线程B一直获取不到锁,则会一直阻塞等待。
代码从那一块需要上锁?-----可能会发生线程安全性问题的代码需要上锁。
Juc并发编程 锁 重入锁 悲观锁 乐观锁 公平锁 非公平锁
线程0 线程1 同时获取 this锁,假设线程0 获取到this ,意味着线程1没有获取到锁
则会阻塞等待。等我们线程0 执行完count-- 释放锁之后 就会唤醒 线程1从新进入
到获取锁的资源。
获取锁与释放锁 全部都是有底层虚拟机实现好了。
对一块代码加锁缺点:
可能会影响到程序的执行效率。
如果是同一把锁 在多线程的情况下 最终只能够给一个线程使用。
如果有线程持有了该锁 意味着其他的线程 不能够在继续获取锁
核心思想:当多个线程共享同一个全局变量时,将可能会发生线程安全的代码
上锁,保证只有拿到锁的线程才可以执行,没有拿到锁的线程不可以执行,需要阻塞等待。
- 使用synchronized锁,JDK1.6开始 锁的升级过程 juc 18-25
- 使用Lock锁 ,需要自己实现锁的升级过程。底层是基于aqs实现
- 使用Threadlocal,需要注意内存泄漏的问题。
- 原子类 CAS 非阻塞式
2. synchronized锁的基本用法
在多线程的情况下 需要是同一个对象锁
Synchronized(对象锁){
需要保证线程安全的代码
}
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例 的锁
- 修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁
2.1 修饰代码块
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得 给定对象 的锁。
public class ThreadCount implements Runnable {
private static Integer count = 100;
private String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private void cal() {
synchronized (this) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
Thread thread1 = new Thread(threadCount);
Thread thread2 = new Thread(threadCount);
thread1.start();
thread2.start();
}
}
2.2 修饰实例方法
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例的锁
在实例方法上默认加上synchronized 默认使用this锁。
public class ThreadCount implements Runnable {
private static Integer count = 100;
private String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private synchronized void cal() {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
Thread thread1 = new Thread(threadCount);
Thread thread2 = new Thread(threadCount);
thread1.start();
thread2.start();
}
}
2.3 修饰静态方法
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得 当前类对象的锁
默认使用当前类的类名.class 锁
public class ThreadCount implements Runnable {
private static Integer count = 100;
private static String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private static void cal() {
synchronized (ThreadCount.class) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
}
public static void main(String[] args) {
ThreadCount threadCount1 = new ThreadCount();
ThreadCount threadCount2 = new ThreadCount();
Thread thread1 = new Thread(threadCount1);
Thread thread2 = new Thread(threadCount2);
thread1.start();
thread2.start();
}
}
2.4 synchronized死锁问题
我们如果在使用synchronized 需要注意 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 死锁诊断工具
D:\path\jdk\jdk8\bin\jconsole.exe
线程1 先获取到自定义对象的lock锁,进入到a方法需要获取this锁
线程2 先获取this锁, 进入到b方法需要自定义对象的lock锁
线程1 线程2 是在同时执行
线程1 | 线程2 |
---|---|
先获取到自定义对象的lock锁 | 先获取this锁 |
需要线程2已经持有的this锁 | 线程1已经持有自定义对象的lock锁 |
2.5 springmvc 接口中使用
需要注意:
Spring MVC Controller默认是单例的 需要注意线程安全问题
单例的原因有二:
1、为了性能。
2、不需要多例。
@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";
}
}
2.6 临界区
当多个线程读共享资源 读的过程中,没有任何问题,
在多个线程对共享资源读写操作时发生指令交错,就会发生线程安全问题
在多线程中如果存在对共享资源读写操作,该代码称作为临界区。
public class Thread08 extends Thread {
int count = 0;
@Override
public void run() {
// 该代码就是为临界区
count++ ;
}
}
2.7 竞争条件
多个线程在临界区内执行,由于代码的执行序列不同(指令)而导致结果无法预测,称之为发生了竞态条件
解决办法:
synchronized,Lock、原子类
3. 字节码角度分析线程安全问题
线程安全问题:
- 字节码
- 上下文切换
- Jmm java内存模型
Java源代码 →编译成class文件
3.1 线程安全代码
public class Thread02 extends Thread {
private static int sum = 0;
@Override
public void run() {
sum();
}
public void sum() {
for (int i = 0; i < 10000; i++) {
sum++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread02 t1 = new Thread02();
Thread02 t2 = new Thread02();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum);
}
}
3.2 字节码文件分析
javap -p -v Thread01.class
sum++
Getstatic ### 获取静态变量值sum
iconst_1 ## 准备一个常量1
Iadd ### 自增
Putstatic ### 将修改后的值存入静态变量sum
3.3 Cpu上下文角度分析线程安全问题
分析:
共享变量值 sum=0
假设现在cpu执行到t1线程,t1线程执行到13行 就切换到另外t2线程执行,
t2线程将静态变量sum=0改成=sum=1 有切换回来执行t1线程 t1线程 使用之前获取
Sum=0 +1 赋值给共享变量sum ,则最终结果:sum=1.
但是现在sum++ 执行 最终结果是算了一次。
二. 多线程线程之间通讯
1. 等待/通知机制
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:
- notify() :通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁
- notifyAll():通知所有等待在该对象的线程
- wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.mayikt.thread.days02.Thread03.run(Thread03.java:16)
注意:wait,notify和notifyAll要与synchronized一起使用
2. wait/notify/notifyAll在Object类中
因为我们在使用synchronized锁 对象锁可以是任意对象,所以wait/notify/notifyAll需要放在Object类中。
3. wait/notify/简单的用法
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();
}
}
}
4. 多线程通讯实现生产者与消费者
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();
}
}
/**
* flag 默认值==false
* flag false 输入线程 输入值 输出线程 先拿到锁 释放锁
* flag true 输出线程 输出值
*/
public boolean flag = false;
5. Join/Wait与sleep之间的区别
sleep(long)方法在睡眠时不释放对象锁
join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait封装的,
Wait(long)方法在等待的过程中释放对象锁
6. 三个线程 T1,T2,T3,怎么确保它们按顺序执行?
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1");
Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t2");
Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t3");
t1.start();
t2.start();
t3.start();
public class Thread05 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t1");
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t2");
Thread t3 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
7. Join的底层原理如何实现
public class Thread06 {
private Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread06 thread06 = new Thread06();
Thread thread = thread06.print();
thread.start();
try {
Thread.sleep(3000);
thread.interrupt();
} catch (Exception e) {
}
}
public Thread print() {
Thread thread = new Thread(() -> {
synchronized (object) {
System.out.println("1");
try {
object.wait(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2");
}
});
return thread;
}
}
Join底层原理是基于wait封装的,唤醒的代码在jvm Hotspot 源码中 当
jvm在关闭线程之前会检测线阻塞在t1线程对象上的线程,然后执行notfyAll(),这样t2就被唤醒了。
网友评论