Java多线程(锁的操作)
线程不安全的原因
首先我们要先了解线程不安全的原因
1)线程在系统中是随机调度的,抢占式执行的
2)多个线程同时修改同一个变量
3)线程针对变量的修改操作,不是“原子”的
4)内存可见性问题,引起的线程不安全
5)指令重排序,引起的线程不安全
关于锁,主要操作
是两个方面
1)加锁 t加上锁之后,t1也尝试加锁,就会阻塞等待(都是系统内核控制)
2)解锁 直到t解锁了之后,t1才有可能拿到锁(加锁成功)
锁的主要特性:互斥
一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(也叫做锁竞争/锁冲突)
3)代码中,可以创建出多个锁
只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁
关键字synchronized
synchronized后面带上(),里面写的就是“锁对象”
{锁对象的用途。有且只有一个,就是用来区分,两个线程是否针对同一个对象加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待.如果不是,就不会出现锁竞争,也就不会阻塞等待}
4)synchronized下面跟着{}
当进入到代码块,就是给上述()锁对象进行了加锁操作,当出了代码块,就是给上述{}锁对象进行了解锁操作
5)修饰普通方法,相当于针对this加锁
修饰静态方法,相当于针对类对象加锁
package Thread;
class Count{private static int count=0;void add(){count++;}int get(){return count;}}
public class Demo12 {public static void main(String[] args) throws InterruptedException {Count count =new Count();Count count1=new Count();Thread t=new Thread(() ->{synchronized (count1) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (count) {System.out.println("t获得了两把锁");}});Thread t1=new Thread(() ->{synchronized(count) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (count1) {System.out.println("t1获得了两把锁");}});t.start();t1.start();t.join();t1.join();}
}
死锁:有两把锁,一把锁把一把锁,锁在了里面,要想获取到第二把锁,就需要执行完第一层大括号,要想执行完第一层大括号,就需要先获取到第二层的锁,这种情况就叫做死锁
死锁有三种比较典型的场景:
场景一:
锁是不可重入锁,并且一个线程针对一个锁对象,连续加锁两次,通过引入可重入锁
场景二:
两个线程两把锁{有线程一和线程二,以及有锁A和锁B,现在线程1和2都需要获取到锁A和锁B,拿到锁A之后,不释放A,继续获取锁B}
场景三:
N个线程,M把锁{哲学家就餐问题(5个人5根筷子)}
如何避免死锁问题?
1)锁具有互斥特性(基本特点,一个线程拿到锁之后,其他线程就得阻塞等待)
2)锁不可抢占(不可被剥夺)一个线程拿到锁之后,除非他自己主动释放锁,否则别人抢不走
3)请求和保持,一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁
4)循环等待。多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A
必要条件:缺一不可,任何一个死锁的场景,都必须同时具备上述4点,只要缺少一个,都不会构成死锁
内存可见性引起的线程的不安全问题
package Thread;import java.util.Scanner;public class Demo13 {private volatile static int count=0;public static void main(String[] args) {Scanner scanner=new Scanner(System.in);Thread t1=new Thread(() ->{while (count==0) {}System.out.println("t1结束了");});Thread t2=new Thread(() ->{System.out.println("请输入一个数字");count = scanner.nextInt();});t1.start();t2.start();}
}
while(count ==0){}这个代码有两部过程
1)load从内存读取数据到cpu寄存器(load速度非常慢)执行一次load消耗的时间,顶几千次,上万次cmp执行的时间
2)cmp(比较。同时会产生跳转)
条件成立,继续顺序执行
条件不成立,就跳转到另外一个地址来执行
于是jvm就把上述的load操作优化掉了,只是第一次真正的进行load,后续再执行到对应的代码,就不再真正load,而是直接读取刚才load过的寄存器的值了。
如果存在IO操作,IO操作是不能被优化掉,IO执行的时间会更慢,就没有优化load操作的必要了
上述问题的本质上还是编译器引起的,优化掉load操作之后,使t2线程的修改,没有被t1线程感知到”内存可见性“问题
如何解决就需要用到volatile关键字
加上这个关键字,编译器就知道这个变量是“反复无常的”
volatile操作和之前synchronized保证原子性,没有任何关系,volatile是专门针对内存可见性的和场景来解决问题的,并不能解决之前,两个线程循环count++的问题