Java-锁相关

线程不安全的原因

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还获取不到 就死锁了

猜你喜欢

转载自blog.csdn.net/chara9885/article/details/130690703