线程不安全的原因
1.调度器随机调度,抢占式执行(无能为力)
举个例子 有一个int变量 叫count 就俩线程同时count++一万次 结果应该为两万 可多次运行程序 这结果每次都不一样(而且小于2w) 是为什么呢
因为count++这行代码是分三步运行的
load 把数据读到cpu
add 在cpu寄存器实现加法运算
save 把运算完成的数据存回cpu
如果count为0 load(线程1) load(线程2) add(线程1) add(线程2) save(线程1) save(线程2) 他们load的时候count都为0 而且save的时候都把1存在了内存中 所以两次运算 count也只是1
2.多个线程修改一个变量(部分规避)
3.修改操作不是原子性的(锁操作)
4.内存可见性(锁操作)
5.指令重排序
后两种都是编译器/JVM/操作系统 误判了的原因 把不该优化的地方给优化了 就导致了bug的出现
可以通过volatile关键字解决(相当于禁止了编译器进行优化)
synchronized加锁的几种方式
先说下加锁的意义,加锁就是:这个被加锁的线程结束,,其他线程才能拿到该资源进行执行线程!
还是用上面的count++操作举例子,如果这个线程被加锁了,那它的运行逻辑就是
(线程1) load add save (线程2)load add save 也就是说只有当这个线程1完成之后,才能运行线程2
public class Main { private static int a = 0; private static int b = 0; private static final int count = 10_0000; public synchronized static void increase(){ for (int i = 0; i < count; i++) { a++; } } public static void increase1(){ for (int i = 0; i < count; i++) { b++; } } public static void main(String[] args)throws InterruptedException{ Thread t1 = new Thread(()->{ increase(); increase1(); }); Thread t2 = new Thread(()->{ increase(); increase1(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("a="+a); System.out.println("b="+b); } }
(要加join main也是一个线程(主线程) 不加join的话 它会在start之后 运行打印)
我们可以看出 加锁的方法 运行正确了 没加锁的方法就不对
1. 修饰普通方法 相当于对this加锁
Public synchronized void increase(){
代码块
}
2.修饰静态方法,相当于对类对象加锁
Public synchronized static void increase(){
代码块
}
3.修饰代码块
public void method(){
Synchronized(this){
代码块}
}
这个this是一个参数,是指对某个对象加锁,可以以实例对象,或者.class作为参数
wait和notify
wait
调用wait的线程会进入阻塞等待状态,同时会释放锁(所以wait要和synchronized搭配使用)
进入调用wait后的流程
1.释放锁
2.等待通知(notify)
3.当通知到达之后 就会被唤醒 并且尝试重新获取锁
notify
唤醒处于wait状态的线程(也要和synchronized搭配使用)
举个例子
ublic static void main(String[] args)throws InterruptedException{ Object lock = new Object(); Thread t1 = new Thread(()->{ synchronized (lock) { try { System.out.println("准备调用wait"); lock.wait(); System.out.println("调用结束"); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); System.out.println("准备notify"); synchronized (lock) { lock.notify(); } System.out.println("notify结束"); }
运行结果为:
我们可以看到是在notify结束之后才输出调用结束,也就是notify之后wait的线程才可以继续运行
可是为什么准备notify是在准备调用wait之前呢,明明t1.start()是在打印准备notify之前调用的,
因为main本身也是一个线程(主线程),在t1线程运行的时候主线程也在运行,打印notify又没被加锁
所以他们的调度顺序就是随机的了
更极端的情况:
我们可以看到"调用结束"甚至没被输出,主线程都运行结束了,都没调度到t1线程的输出"调用结束",为了防止这种情况,可以加一个join,在主线程的最后加join,有效防止这种情况发生
顺便说下synchronized括号里面的参数和调用notify的对象必须是一个 否则会报错
就像这个写法:
notify和wait应该在不同的线程中,因为这个线程wait了只能让其他线程notify(唤醒它)
如果有多个线程被wait,那notify会随机唤醒其中一个(notifyall可以唤醒全部线程)(同一个(被)锁对象),
同步和异步
同步 调用者自己来负责获取调用结果
异步 调用者自己不负责 被调用者主动推送
常见锁策略
乐观锁&悲观锁
乐观锁:乐观的认为操作数据的时候非常乐观,认为别人不会同时修改数据,因此乐观锁默认是不会上锁的,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:操作数据的时候比较悲观,认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。
读取频繁使用乐观锁(多线程读 不设计修改 线程安全),写入频繁使用悲观锁
synchronized既是悲观锁,又是乐观锁,当前锁冲突概率不大 以乐观锁方式运行,一旦发现冲突概率大了 就以悲观锁方式运行
普通互斥锁&读写锁
普通互斥锁 两个加锁操作会发生竞争
读写锁:把加锁操作细化了加锁分成了 加读锁 加写锁
a尝试加读锁
b尝试加读锁
ab不产生竞争 锁相当于没加
a尝试加读锁
b尝试加写锁 ab产生竞争 和普通锁没区别
synchronized是普通互斥锁
重量级锁&轻量级锁
重量级锁 锁开销大 做的工作比较多
轻量级锁 锁开销小 做的工作比较少
悲观锁经常是重量级锁
乐观锁经常是轻量级锁(不绝对)
sychronized是自适应的锁 既是重量级又是轻量级
公平锁&非公平锁
符合先来后到的规则才是公平,非公平锁 机会均等反而是不公平的
synchronized是非公平锁
自旋锁&挂起等待锁
自旋锁(轻量级锁的具体实现 乐观锁)
当发现冲突的时候 不会挂起等待 会迅速再来尝试看这个锁能不能获取到
1.一旦锁被释放 就可以第一时间获取到
2.如果锁一直不释放 就会消耗大量的CPU
挂起等待锁 (重量级锁 悲观锁)
发现锁冲突就挂起等待
1.一旦锁被释放不能第一时间获取到
2.在锁被其他线程占用的时候 会放弃cpu资源
sychronized作为轻量级锁时 内部是自旋锁
sychronized作为重量级锁时 内部是挂起等待锁
可重入锁&不可重入锁
可重入锁:在内部记录这个锁是哪个线程获取到的 如果发现当前加锁的线程和持有锁的线程是用一个 则不挂起等待
同时在内部引入计数器 记录第几次加锁控制什么时候释放锁(不会提前释放锁)
不可重入锁:
private synchronized static void func(){
func1();
}
private synchronized static void func1(){
}
这个func已经获取到锁了 然后func1需要获取锁才能运行 但是func1不运行完func还获取不到 就死锁了