上次,小编分享到了多线程出现了安全问题。
那么接下来小编来分享下是如何解决这个多线程安全问题的。
上次分享到,多线程安全问题的“罪魁祸首”:线程调度的不确定性。
就是说呢,A、B、C三个线程,然后,A线程执行完,下次系统分配资源执行的时候,不知道是B先执行,还是C先执行,具有不确定性。
就像是在一个房间内,有个领导听A、B、C汇报
A在里面说说话,话没说完呢,就被扯出去了,又随机放 一个人进去房间说话,所以最终导致领导听的汇报出现不一致。
所以,我们可以这样子,这个房间内我加一把锁,A进去之后,锁住房间,期间内不能让其他人进去,等A说完后,再把锁给其他人,按照刚刚的方法。
那么java呢,提供了这样的一个东西,可以保证线程安全。
synchronized
:synchronized
是 Java 中的一个关键字,用于控制多线程对共享资源的访问,确保同一时间只有一个线程可以执行某个代码块或方法,从而避免线程间的竞争条件和数据不一致。
用法如下:
synchronized
(括号中是一个对象){
}
{}内放的是要打包成一整个整体的代码。
此时呢,如若进入代码块,那么就会针对里面的代码进行加锁。
出了代码块就会解锁。
值得注意的是,锁是不可被抢占,一个线程拿到了锁,其他线程要想拿到这个锁,必须等待这个锁被释放。
使用上一篇文章,开头给的例子,它是线程不安全的例子,接下来使用synchronized。
代码实例:
public class Demo18 {public static Object locker=new Object();public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i=0;i<5000;i++){synchronized (locker){count++;}}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count:"+count);}
}
显然,这个代码中,实例化一个Object对象,然后把这个对象名locker作为参数给到t1线程和t2线程中的synchronized。
可见我们的运行结果是正确的。
这里要提到的是,传入给synchronized的参数不一定要求是Object的,可以是其他。
比如String locker=new String()、public static int [] locker=new int[100];
值得注意的是,要对同一个对象加锁才行,对不同的对象加锁,所起的作用是无效的。
那么这个加锁操作只能这样子了吗?
当然不是,还有其他的操作。
比如
synchronized可以修饰方法。
代码实例
class Counter{public static int count=0;synchronized public void add(){count++;}
}
public class Demo14 {public static void main(String[] args) throws InterruptedException {//synchronized修饰方法Counter counter=new Counter();Thread t1=new Thread(()->{for (int i=0;i<5000;i++){synchronized (counter){counter.add();}}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){synchronized (counter){counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println(Counter.count);}
}
那么这个add()方法可以等价于这样写的
public void add(){synchronized (this){count++;}}
里面的参数改为了this,即谁调用该方法,谁就要被锁了。
除了这样的操作,这个synchronized还可以修饰静态方法。
synchronized可以修饰静态方法。
代码实例:
class Counter{public static int count=0;synchronized public static void add(){count++;}
public class Demo14 {public static void main(String[] args) throws InterruptedException {//synchronized修饰方法Counter counter=new Counter();Thread t1=new Thread(()->{for (int i=0;i<5000;i++){synchronized (counter){Counter.add();}}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++){synchronized (counter){Counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println(Counter.count);}
}
同样的add()方法等价于:
public static void add(){synchronized (Demo14.class){count++;}
}
那么里面的Demo14.class是什么意思呢?
Demo14.class用于获取Demo14的Class的对象。
每个主类都只有一个Class对象。
当然也可以这样写的
synchronized (Demo14.class){count++;
}
然而,锁这么好用,但是也不能随便用的,会发生死锁问题的。
死锁问题
那什么 又是死锁呢?
场景一:一个线程一把锁,然后这个线程对此把锁进行连续加锁两次。
来一个代码引入:
public static Object locker=new Object();public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i=0;i<5000;i++){synchronized (locker){count++;synchronized (locker){}}}});t1.start();
}
显然此时,synchronized使用了两次,还是用了同一个locker对象。
那么此时当代码进入第一个sychronized的代码块内部
那么这时候,里面就拿到锁了。
然后当代码执行到第二个scychronized的时候,此时发现,这个locker对象,还是被第一个sychronized持有,因为它还没出代码块呢,所以还没释放这个锁资源。
所以,第二个synchronized想拿到locker对象,但是第一个sychronized还没释放这个锁,
要释放这个锁,就必须把第二个sychronized加锁给执行完,显然,就僵住了,就是说阻塞了,你不让我,我不让你。
但是,看会执行结果,显然运行成功了。
这是为什么呢?
因为这是因为java的synchronized是一个可重入锁。
那什么又是可重入锁?
可重入锁(Reentrant Lock) 是一种同步机制,允许同一个线程多次获取同一把锁。这种锁是可重入的,意味着如果一个线程已经持有了某个锁,它可以再次获取该锁而不会被阻塞。
意思就是说,加锁的时候判定下,当前这个锁是否是被占用状态,是被哪个线程占用了,如若是当前线程对这个锁进行多次加锁,此时后续的加锁将不会进行真正的加锁操作。
所以,以上的例子情况是synchronized对这情况进行了特殊处理了。
场景二:两个线程,两把锁
即有两个线程,t1和t2,锁1,锁2
t1线程一开始对锁1进行加锁,t2线程一开始对锁2加锁
t1线程不释放锁1的情况下,再对锁2进行加锁,t2线程不释放锁2情况下,再对锁1进行加锁
代码引入:
public class Demo15 {public static Object locker1=new Object();public static Object locker2=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{synchronized (locker1){System.out.println("t1加锁locker1完成!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁locker2完成!");}}});Thread t2=new Thread(()->{synchronized (locker2){System.out.println("t2加锁locker2完成!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2加锁locker1完成!");}}});t2.start();t1.start();}
}
运行结果
此时,出现了阻塞,还有两条语句没有进行打印。
可以看出,代码出现了循环依赖咯。
这也是个死锁发生的例子。
当然,上面是两把锁,两个线程。
如若是N个线程,M把锁,此时呢,一旦出现问题,效果也是类似的,就是会阻塞特别严重。
ok,接下来总结下,死锁发生的条件
死锁必要条件
1.锁是互斥的(基本特性)
2.锁是不可抢占的(基本特性)
3.请求和保持(代码结构)
即在拿到A锁情况下,同时不释放锁A,去拿锁B,但此时,锁B也没有释放资源
4.循环等待(代码结构),多个线程获取锁的过程中,出现了循环等待
那么出现了死锁,那就要尝试去解决它。
尝试解决死锁问题
我们可以从条件入手,当然条件1、2显然是不能用了,毕竟是基本特性。
条件3中,我们可以尝试这样解决
1.一次性申请所有资源,即不是在执行过程中,再次逐步申请
2.资源预分配
在任务启动之前,预先分配所有需要的资源。
……
条件4中
有一个简单的办法,即对锁进行排序,按照锁的顺序来,进行一定顺序的加锁,那么此时可以避免死锁问题。
比如,这个例子
代码实例:
public class Demo15 {public static Object locker1=new Object();public static Object locker2=new Object();public static void main(String[] args) {Thread t1=new Thread(()->{synchronized (locker1){System.out.println("t1加锁locker1完成!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁locker2完成!");}}});Thread t2=new Thread(()->{synchronized (locker1){System.out.println("t2加锁locker2完成!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2加锁locker1完成!");}}});t2.start();t1.start();}
}
运行结果
此时呢,加锁顺序都是按照locker1再到locker2的顺序来,显然,运行是没有问题的。
当然,解决死锁问题,还有一个方案
即银行家算法
核心思想:在分配资源之前,系统会检查此次分配是否会导致系统进入不安全状态。如果分配后系统仍然是安全的,才允许分配;否则,拒绝分配。
但是由于日常开发中不是经常使用这个,毕竟不够接地气,所以这里不多介绍了。