线程安全问题解决方案

1. synchronized的关键字

1.1 synchronized的特性

1) 互斥

synchronized会起到互斥的效果,当某个线程执行到某个对象的synchronized时,如果其他线程也执行到同一个对象的synchronized就会陷入阻塞等待.synchronized使用的锁是存在于对象里的.

  • 进入synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁
    在这里插入图片描述
    使用synchronized的时候,其实是对某个具体的对象进行加锁,当synchronized直接修饰方法的时候,就相当于针对this(count对象)加锁.
    如果两个线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突(一个线程加锁成功,另外一个线程阻塞等待).如果是两个线程针对不同对象进行加锁,那么就不会产生产生所冲突,也就不存在阻塞等待.但也不会让两个线程按照串行的方式调度,任然会存在线程安全问题.
class Counter {
    public int count = 0;
    public Object locker = new Object();
    //lambda通过变量捕获进行对count进行修改,如果是局部变量,则不能进行局部变量修改
   public void increase1() {
    //两个线程针对针对不同对象加锁,不会阻塞等待,不存在锁竞争,也就会导致线程安全问题
   //    synchronized (this) {
   //         count++;
   //     }
       synchronized (locker) {
            count++;
        }
    }
    public void increase2() {
        synchronized (locker) {
            count++;
        }
    }
}
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase1();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase2();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}
//只有针对同一个变量加锁,会出现阻塞等待,才会解决线程安全问题,如果一个线程加锁,另外一个线程没有加锁,那么也会出现线程安全问题

synchronized用的锁其实是存在Java对象里的,可以粗略理解为当每个对象在内存中存储,都会有一块内存表示当前"锁"的状态,比如厕所里的门锁,当是无人的状态下,那么就可以进入,进入后需要把门锁锁上,标志为"有人"状态.如果时"有人"状态下,其他人就必须等待.
线程阻塞等待

阻塞等待:针对没一把锁,操作系统都维护了一个等待系列,当这个锁被某个线程占用的时候,其他线程尝试加锁,那么就会加锁失败,陷入阻塞等待,一直由上一个线程解锁之后,由操作系统唤醒的线程,再来获取到这个锁

  • 上一个线程解锁之后,下一个线程并不是立马就能获取到锁,而是要靠操作系统"唤醒"."唤醒"也是操作系统线程调度的一部分工作.
  • 假如由A B C三个线程,线程A先拿到锁,然后B获取锁,然后C获取锁,那么B和C都在阻塞队列中等待,当A释放锁之后,虽然说B比先来,但是B不一定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则.

2) 刷新内存

synchronized的工作流程:

  1. 线程进入synchronized代码块或方法,获取锁
  2. 每个线程都会有自己的线程栈,线程在执行的时候,会将变量从主内存中复制到自己的线程栈中
  3. 执行代码
  4. 将修改后的共享数据刷新到主内存中,为了保证多线程中数据一致性.
  5. 释放锁

3) 可重入

可重入锁指的是同一个线程在持有某把锁的情况下,再次请求该锁时可以继续获取,而不会因为已经持有锁而被阻塞.

不可重入锁
lock();//第一次加锁加锁成功
lock();//第二次加锁,锁已经被占用,阻塞等待
当我们第二次进行加锁的时候,会阻塞等待,因为第一次的锁还没被释放掉,只有第一把锁释放掉后,才能获取到第二把锁,但是释放第一把锁也是有该线程完成,结果线程已经结束操作/停止操作,那么就会出现死锁,这种锁称之为不可重入锁

Java中的synchronized关键字就是可重入锁,当进入代码块是就会加锁,出了代码块就会自动解锁.

public class demo{
    public static Object lock = new Object();

    public static void main(String[] args) {
        synchronizedMethod();
    }
    public static void synchronizedMethod() {
        synchronized (lock) {
            System.out.println("第一次进入");
        }
        //第二次进入,由于是同一个线程可继续获取锁,继续执行代码
        synchronized (lock) {
            System.out.println("第二次进入");
        }
    }
}

1.2 synchronized的使用方式

1 )修饰普通方法:锁的的是synchronized对象

 public class demo{
        public synchronized void method() {
            
        }
    }

2 )修饰静态方法:锁的demo类的对象,静态方法和具体的对象无关,是和类有关的(类方法)

class demo{
    public synchronized static void method() {

    }
}

锁类对象

public class demo{
    public void method() {
        synchronized (demo.class) {

        }
    }
}

3 )修饰代码块:明确指定锁哪个对象

public class demo{
    public void method() {
        synchronized (this) {
            
        }
    }
}

2. volatile关键字

2.1 内存可见性

程序在编译的时候,Java编译器和jvm可能会对代码做出"优化",当代码实际程序执行的时候,编译器/jvm就可能会更改代码,但会保持原有逻辑不变的情况下,提高代码执行效率.编译器优化,本质上靠代码,智能的对你写的代码进行分析判断,进行调整.但如果是多线程,此时的优化就可能会出现差错,是程序原有的逻辑发生改变.

//创建两个线程t1和t2
//t1始终在进行while循环,以flag==0作为循环条件
//t2让用户通过控制台输入一个整数,作为isQuit的值
//预期当用户输入非0时,t1线程结束    
public class demo {
    public static int isQuit = 0;
    public static void main(String[] args) {
        //t1读取的是自己弘佐内存中的内容.
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {
                ;
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入isQuit的值:");
            isQuit = scanner.nextInt();
            //在内存中修改了isQuit的值,但是t1线程没有重复读取isQuit的值,
            //t1线程没有感知到t2线程的修改
        });
        t1.start();
        t2.start();
    }
}
//当输入非0值,已经修改了isQuit的值,但是t1线程并没有结束(bug)

在while(isQuit == 0)中本质上是两条指令:1. load(读内存):速度极慢 2.jmp(比较并跳转:程序成立就进入代码块):寄存器操作,速度极快.对于jvm来说,在这个逻辑中,代码快速的读取同一个内存中的值,并且内存中的值每次读取还都是一样的,编译器就会把load操作优化掉,只是第一次执行了load,后续都不再执行,直接拿寄存器中的值行进比较.但是我们在另外一个线程中修改了isQuit中的值,编译器就没办法准确判定t2到底会不会执行/什么时候执行,因此就会导致出错.

使用volatile关键字用来修饰一个变量后,编译器就会明白,这个变量是"易变的",就不会把读操作优化到读寄存器中,于是就可以保证t1在循环的过程中,始终都能读取到内存中的数据了.

//给isQuit加上volatile
public class demo1 {
 public volatile static int isQuit = 0;
}

volatile本质上是保证变量的内存可见性(禁止该变量的读操作被优化到读寄存器中).当一个线程修改了共享变量时,其他线程就会立即读取到这个修改的值.

volatile和synchronized的本质区别就是:synchronized能够保证原子性,volatile保证的是内存可见性.不能保证原子性.

3.wait和notify

在我们前面所了解多线程的执行过程中,知道多线程之间的调度是随机的,我们希望多个线程能够按照我们规定的顺序来执行,完成线程之间的配合工作.其中wait和notify就是用来协调线程调度顺序的工具.

3.1 wait()方法

wait方法的主要作用是使线程进入等待状态,并释放该线程持有的锁,调用wait方法会有以下几个步骤

  1. 当线程执行到wait方法时,首先会检查是否持有了当前对象的锁(即synchronized关键字所保护的锁),如果没有锁就会抛出IllegalMonitorStateException异常
  2. 如果持有锁,那么调用wait方法后,该线程就会立即释放该锁,其他线程可以获取这个锁并继续执行.
  3. 被等待的线程进入等待状态,直到其他线程调用了同一个对象的notify或notifyAll方法.
  4. 等待的线程被唤醒后,会重新尝试获取之前的锁,一旦获取成功,就可以继续执行.

wait是Object的方法,当wait引起线程阻塞后,可以使用interrupt方法,把线程给唤醒,打断当前的阻塞状态

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
        //wait是先解锁,所以要先加上锁
            object.wait();
        }
        System.out.println("wait结束");
    }
}
//此处wait进行,但并没有执行到"wait结束",说明该线程已经阻塞成功,一直阻塞到其他线程进行notify和notifyAll方法.

3.2 notify()方法

notify是一个线程同步方法,用于唤醒等待在同一个对象上等待的线程.调用notify方法会选择一个处于等待状态的线程,通知他继续执行,notify只能唤醒一个线程,如果有多个线程等待,只会唤醒其中的一个,具体唤醒的线程是哪一个不确定.

举个栗子:假如有一家餐厅,有厨师和多个服务员多个线程(厨师表示正在执行的线程),厨师负责烹饪菜品,服务员负责上菜.厨师在厨房中做好了一道菜,然后调用wait()方法进入等待状态,表示没有菜品可以上菜了,这个时候,服务员进入厨房,然后调用notify()方法来将厨师唤醒,表示有新的菜品的需要上菜,厨师就会被唤醒继续烹饪.

class Restaurant {
    private static Object food = new Object();
    public void sever() throws InterruptedException {
        synchronized (food) {
            System.out.println("服务员:菜品准备好,等待上菜");
            food.wait();//服务员等待上菜
            System.out.println("服务员:上菜完成");
        }
    }
    public void cook() throws InterruptedException {
        synchronized (food) {
            Thread.sleep(4000);//假设烹饪需要4秒
            System.out.println("厨师:菜品已做好,通知服务员上菜");
            food.notify();//厨师通知服务员上菜
        }
    }
}
public class demo3 {
    public static void main(String[] args) {
        Restaurant restaurant = new Restaurant();
        Thread t1 = new Thread(() -> {
            try {
                restaurant.cook();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                restaurant.sever();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t2.start();
        t1.start();
    }
}

wait()和notify()方法注意事项

  1. 如果有多个线程等待,那么线程调度器会随机挑选出一个wait状态的线程(没有"先来后到")
  2. 使用notify方法,当前线程不会立马去唤醒等待的线程,而是要等到执行完notify()方法的线程执行完后,再去执行被唤醒的线程.
  3. notify和wait必须是使用同一个对象调用的才能让notify能够顺利的唤醒wait
  4. 虽然notify不涉及解锁操作,但wait和notify都必须要放到synchronized之内,
  5. 如果进行notify的时候,另外一个线程没有处于wait状态,此时notify就相当于"空打一炮",没有任何副作用.

3.3 notifyAll()方法

notify方法只是唤醒某一个等待线程,使用notifyAll方法就可以一次性唤醒所有等待的线程.由于notifyAll会唤醒所有的线程会导致不必要的竞争和资源浪费.
如果我们使用notifyAll,但是想要按照执行的线程执行,那么就可以让不同的线程,使用不同的对象进行wait.想唤醒谁,就可以使用对应的对象来notify.

//按照顺序打印ABC
class Print{
    public int flag = 1;//用于标识当前字符
    public void print1() throws InterruptedException {
        synchronized (this) {
            //防止虚假唤醒,虚假唤醒:没有收到notify和notifyAll信号的情况下从等待状态返回,没有明确的唤醒信号,使用while判断等待条件
            while (flag!=1) {
                wait();//与打印字符不符合陷入阻塞等待
            }
            System.out.print('A');
            flag = 2;
            notifyAll();//唤醒所有线程,重新检测flag的值
        }
    }
    public void print2() throws InterruptedException {
        synchronized (this) {
            while (flag!=2) {
                wait();
            }
            System.out.print('B');
            flag = 3;
            notifyAll();
        }
    }
    public void print3() throws InterruptedException {
        synchronized (this) {
            while (flag!=3) {
                wait();
            }
            System.out.println('C');
            flag = 1;
            notifyAll();
        }
    }
}
public class demo5 {
    public static void main(String[] args) throws InterruptedException {
        Print print = new Print();
        Thread t1 = new Thread(() -> {
            try {
                print.print1();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t2 = new Thread(() -> {
                try {
                    print.print2();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        });
        Thread t3 = new Thread(() -> {
                try {
                    print.print3();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        });
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }
}

3.4 wait和sleep的对比

  1. wait()方法是Object类的方法,可以在任意对象上调用,用来将当前线程置于等待状态.直到有其他线程调用相同对象唤醒.
    sleep()方法是Thread类静态方法,通过线程本身调用,用来将当前线程休眠一段时间,不需要其他线程干涉就能自动恢复.
  2. wait()方法必须在同步代码块(synchronized)或同步方法中使用.
    sleep()方法可以在任意代码块中使用,不需要获取锁.
  3. wait()方法用来控制线程之间的协调
    sleep()方法用来控制线程之间的休眠
  4. wait()方法在等待期间收到中断异常会被唤醒,抛出InterruptedException 异常
    sleep()方法在等待期间收到中断异常,需要将中断标志位设置为true,不会抛出异常,需要手动检查中断状态并处理.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值