🍇2.3解决线程不安全方案
这个时候我们应该怎么解决这个问题呢? 上锁!(synchronized)
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
public class Test { static class Count{ public int count = 0; //对方法上锁 synchronized public void sum(){ count++; } } public static void main(String[] args) throws InterruptedException { Count count = new Count(); Thread thread1 = new Thread(()->{ //对该线程上锁 synchronized (count) { for (int i = 0; i < 50000; i++) { count.sum(); } } }); Thread thread2 = new Thread(()->{ //对该线程上锁 synchronized (count) { for (int i = 0; i < 50000; i++) { count.sum(); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count.count); } }
🍇2.4Java 标准库中线程安全类与不安全类
Java 标准库中线程不安全类: 使用了一些锁机制来控制
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer(StringBuffer 的核心方法都带有 synchronized )
特例:String:还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
Java 标准库中线程不安全类: 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施
- .ArrayList
- LinkedList
- TreeMap
- HashSet
- TreeSet
- StringBuilder
🍎三.多线程常见关键字
🍇3.1synchronized(锁)
🎈3.1.1互斥
synchronized的底层是使用操作系统的mutex lock实现的.
我们在介绍线程安全问题解决时说过 synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队
理解 “阻塞等待”
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
注意:synchronized是非公平锁
🎈3.1.2刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.
🎈3.1.3可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
理解 “把自己锁死”:
一个线程没有释放锁, 然后又尝试再次加锁.
// 第一次加锁, 加锁成功 lock(); // 第二次加锁, 锁已经被占用, 阻塞等待. lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第
二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无
法进行解锁操作. 这时候就会 死锁
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.
🎈3.1.4synchronized使用案例
1.直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo { public synchronized void methond() { } }
2. 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo { public synchronized static void method() { } }
3. 修饰代码块: 明确指定锁哪个对象
public class SynchronizedDemo { public void method() { } }
4.锁类对象
public class SynchronizedDemo { public void method() { } }
🍇3.2volatile(可见性)
🎈3.2.1volatile与synchronized区别
关键字 | 相同作用 | 不同作用 |
synchronized | 可以保证内容可见性 | 能够保证线程执行的原子性 |
volatile | 保证线程的可见性 | 并不能保证线程执行的原子性 |
🎈3.2.2volatile使用的场景
我峨嵋你来执行一个停止线程的代码程序
public class Test2 { private static int isQuit; private static Object okject = new Object(); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Thread thread = new Thread(()->{ while(isQuit==0){ } System.out.println("该线程执行结束"); }); thread.start(); System.out.println("输入一个不为0的的数停止该线程"); isQuit = scanner.nextInt(); System.out.println("mian结束运行"); } }
我们发现main结束时并没有执行到Thread内的输出"该线程结束",这是因为在在编译器执行时,因为每次快速执行线程的while循环时会被编译器优化掉while判断isQuit忽视这个值导致线程一直在执行并没有在输入非0时的结束操作而停止该线程结束
那我们应该怎样去做呢?
没错就是要用到我们刚刚认识到的volatile关键字(synchronized也可以)
volatile优化版本:
public class Test2 { //volatile 在需要在循环判断的isQuit中加入volatile来防止编译器优化忽略判断isQuit volatile private static int isQuit; private static Object okject = new Object(); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Thread thread = new Thread(()->{ while(isQuit==0){ } System.out.println("该线程执行结束"); }); thread.start(); System.out.println("输入一个不为0的的数停止该线程"); isQuit = scanner.nextInt(); System.out.println("mian结束运行"); } }
synchronized优化版本:
public class Test2 { private static int isQuit; private static final Object okject = new Object(); public static void main(String[] args) throws InterruptedException { Scanner scanner = new Scanner(System.in); Thread thread = new Thread(()->{ synchronized (okject) { while (isQuit == 0) { } System.out.println("该线程执行结束"); } }); thread.start(); System.out.println("输入一个不为0的的数停止该线程"); isQuit = scanner.nextInt(); System.out.println("mian结束运行"); } }
我们发现该程序并没有结束
这也是编译器优化的问题,编译器的优化不仅可以产生内存可见性问题,也会产生重排序问题,就是保证在原有的逻辑下,调整代码的执行顺序,从而提高线程的运行效率从而引起线程的不安全,这个问题我们了解即可.
🍇3.3wait丶notify(等待丶唤醒)
🎈3.3.1wait
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)释放当前的锁满足一定条件时被唤醒, 重新尝试获取这个锁
注意:
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
wait 结束等待的条件:
其他线程调用该对象的 notify 方法.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
代码示例: 观察wait()方法搭配synchronized使用
public class Testified { public static void main(String[] args) throws InterruptedException { Object o = new Object(); Thread thread = new Thread(()->{ synchronized (o){ } //执行一些任务 }); thread.start(); System.out.println("开始等待"); thread.wait(); System.out.println("等待结束"); } }
抛出错误这是因为我们需要搭配notify来进行等待和唤醒
🎈3.3.2notify
notify 方法是唤醒等待的线程
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
代码示例: 使用notify()方法唤醒线程
我们来设置一个通过线程2来等待后被线程1唤醒的操作
class Tack{ public int i ; public void tack(int i){ System.out.println("第"+i+"任务完成了!"); } } public class Test3 { public static void main(String[] args) throws InterruptedException { Object ob = new Object(); Thread thread1 = new Thread(()->{ synchronized (ob){ //开始执行并且执行完成后将wait唤醒 System.out.println("notify开始执行了!"); ob.notify(); Tack tack1 = new Tack(); System.out.println("开始执行第一个任务!"); tack1.tack(1); } }); Thread thread2 = new Thread(()->{ synchronized (ob){ Tack tack2 = new Tack(); System.out.println("开始执行第二个任务!"); System.out.println("wait开始执行!"); try { //开始等待,等待被notify唤醒 ob.wait(); } catch (InterruptedException e) { e.printStackTrace(); } tack2.tack(2); } }); //先开始执行线程2 thread2.start(); //线阻塞0.1s Thread.sleep(100); //开始执行线程1 thread1.start(); } }
🍎四.总结
🍇4.1线程不安全情况总结(重点)
1.线程是抢占行执行,线程充满了随机性[这就是线程不安全的万恶之源!!!]
2.多线程对同一个变量进行修改操作
3.针对的变量操作不是原子性的
4.内存可见性被编译器优化
5.重拍序
这样问题所围绕的解决方法就是加锁!!在进行搭配volatile,wait,notify来进行解决问题和优化