文章目录
- 一.线程安全的概念
- 1.1 线程安全的概念
- 1.2 线程不安全的原因
- 1.3 解决线程不安全
- 二.synchronized-monitor lock(监视器锁)
- 2.1 synchronized的特性
- (1)互斥
- (2)刷新内存
- (3)可重入
- 2.2 synchronied使用方法
- 1.直接修饰普通方法:
- 2.修饰静态方法:
- 3.修饰代码块:
- 三.死锁
- 3.1死锁的情况
- 3.2 死锁的四个必要条件
- 1.互斥使用
- 2.不可抢占
- 3.请求和保持
- 4.循环等待
- 3.3解决死锁的办法
- 四.volatile 关键字
- 五. wait和notify
- 5.1 wait()方法
- 5.2 notify()方法
一.线程安全的概念
先来看一段代码
class Counter{
public int count = 0;
public void add(){
count++;
}
}
public class Thread14 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = "+ counter.count);
}
}
可以看到结果是不确定的
1.1 线程安全的概念
先来说一下非线程安全的概念:非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
则线程安全:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
1.2 线程不安全的原因
先解释上述线代码程不安全的原因:
如果两个线程并发执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异
由于线程的抢占执行,导致当前执行到任意一个指令,线程都可能bei调度走,CPU让别的线程来执行
如下图:
导致下面的结果:
线程安全问题的原因:
1.抢占式执行,随机调度(根本原因)
2.代码结构:多个线程同时修改同一个变量
3.原子性(操作是非原子性,容易出现问题)
4.内存可见性问题(如一个线程读,一个线程改)
5.指令重排序
1.3 解决线程不安全
从原子性入手,通过加锁,把非原子的,转成"原子"的
加了synchronized之后,进入方法就会加锁,出了方法就会解锁,如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程解锁,当前线程才能加锁成功
二.synchronized-monitor lock(监视器锁)
2.1 synchronized的特性
(1)互斥
- 进入sychronized修饰的代码块,相当于加锁
- 退出sychronizde修饰的代码块,相当于解锁
(2)刷新内存
synchronized的工作过程:
1.获得互斥锁
2.从内存拷贝变量的最新副本到工作的内存
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁
(3)可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题(自己可以再次获取自己的内部锁)
理解"把自己锁死"
一个线程没有释放锁,然后又尝试再次加锁
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,而获取不到第一次的锁,就把自己锁死
2.2 synchronied使用方法
1.直接修饰普通方法:
锁的SynchronizedDemo1对象
public class SynchronizedDemo1 {
public synchronized void methond() {
}
}
2.修饰静态方法:
锁SynchronizedDemo2对象
public class SynchronizedDemo2 {
public synchronized void methond() {
}
}
3.修饰代码块:
明确指定锁哪个对象
public class SychronizedDemo{
public void method(){
sychronized(this){
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
三.死锁
3.1死锁的情况
1.一个线程,连续加锁两次,如果锁是不可重入锁,就会死锁
2.两个线程,两把锁,t1和t2各自先针对锁A和锁B加锁,在获取对方的锁
public class Thread15 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2= new Object();
Thread t1 = new Thread(()->{
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("t1把锁1和锁2都获得了");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println("t2把锁1和锁2都获得了");
}
}
;
});
t1.start();
t2.start();
}
}
3.多个线程,多把锁(相当于2的一般情况)
3.2 死锁的四个必要条件
1.互斥使用
线程1拿到了锁,线程2就须等着
2.不可抢占
线程1拿到锁A之后,必须是线程1主动释放
3.请求和保持
线程1拿到锁A之后,在尝试获取锁B,A这把锁还是保持的
4.循环等待
线程1尝试获取到锁A和锁B,线程2尝试获取锁B和锁A,线程1在获取B的时候等待线程2释放B,同时线程2 在获取A的时候等待线程1释放A
3.3解决死锁的办法
给锁编号,然后指定一个固定的顺序来加锁,任意线程加把锁,都让线程遵守上述顺序,此时循环等待自然破除
对于synchronied前三个条件都是锁的基本特性,我们只能对四修改
public class Thread15 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2= new Object();
Thread t1 = new Thread(()->{
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("t1把锁1和锁2都获得了");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("t2把锁1和锁2都获得了");
}
}
;
});
t1.start();
t2.start();
}
}
四.volatile 关键字
volatile 和内存可见性问题密切相关
一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读取到值,不一定是修改之后的值(归根结底是编译器/jvm在多线程下优化时产生了误判)
使用汇编语言解释
1.load,把内存中flag的值,读取到寄存器
2.cmp把寄存器的值和0进行比较,根据比较结果,决定下一不执行.
由于load执行速度太慢(相比于cmp来说),再加上反复load的结果都一样,JVM就不在重复load判定没人改flag值,就只读取一次就好
而给flag加上volatile关键字,告诉编译器变量是"易变"的,不再进行优化
class MyCounter{
volatile public int flag = 0;
}
public class Thread16 {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
Thread t1 = new Thread(() ->{
while (myCounter.flag == 0){
//循环体空着
}
System.out.println("t1循环结束");
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
myCounter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
结果:
五. wait和notify
wait和notify可以协调线程之间的先后顺序
完成这个协调工作, 主要涉及到三个方法
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll():唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法
5.1 wait()方法
wait的操作
1.先释放锁
2.在阻塞等待
3.收到通知之后,重新获取锁,并且在获取锁后,继续往下执行
wait操作需要搭配synchorized来使用
public class Thread17 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
}
无synchorized的情况
wait无参数版本,就是死等
wait带参数版本,指定了等待的最大时间
5.2 notify()方法
notify()方法是唤醒等待线程
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notfiyAll()方法可以一次唤醒所有的等待线程