如何解决线程不安全问题以及java中两种加锁

Posted 讵有君@

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决线程不安全问题以及java中两种加锁相关的知识,希望对你有一定的参考价值。

要解决线程不安全问题,首先要了解导致线程不安全的因素:

  1. CPU是抢占式执行的(万恶之源)
  2. 多个线程同时修改同一个变量(即共享变量)
  3. 可见性
  4. 原子性
  5. 指令重排序

1、2两种因素,是我们无法改变的,所以我们只能解决剩下的因素。

解决方案

  • 用volatile修饰

    解决可见性:
    每个线程都有自己的工作内存,假设有两个线程,对一个count值进行修改,按一般情况是线程1对count修改为1再放入主内存,线程2再对count修改为-1,再存入主内存,但此时线程1的工作内存中count还是为1(即线程2的修改对线程1是不可见的)。但是对count进行volatile修饰,所有的线程再修改完成之后就会强制清除掉自己工作内存中的count,之后所有线程在自己工作内存中就找不到count了,就都会去找主内存的值,从而解决了内存不可见问题。

    解决指令重排序问题:禁止了指令重排序

  • java中两种加锁(synchronized 和 Lock)

java中加锁

首先要先了解操作锁的流程:
①尝试获取锁;
②使用锁;
③释放锁。
我们主要关注获取锁和释放锁。

1. synchronized(jvm层的解决方案)

synchronized的底层是使用操作系统的mutex lock实现的。
当线程释放锁时,JMM(java内存模型)会把该线程对应的工作内存中的 共享变量刷新到主内存中;
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
synchronized用的锁是存在Java对象头里的

synchronized的3种使用场景:
①可以修饰代码块(可给任意对象加锁)
②可以修饰静态方法(对当前的类进行加锁)
③可以修饰普通方法(对当前类实例进行加锁)

2. Lock手动锁

步骤:
①创建Lock实例

Lock lock = new ReetrantLock();

②加锁

lock.lock();

③释放锁

lock.unlock();

注意事项:
②一定要放在try外面,③一定要放在finally里面。

在java语言中,所有的锁都默认实现方式为非公平锁。
非公平锁调度:当一个线程释放锁之后,刚好有另一个线程执行到获取锁的代码,所以就直接获取锁。
公平锁调度:一个线程释放锁,然后主动唤醒需要得到锁的队列来得到锁,会有性能消耗。
所以非公平锁的性能更高。

synchronized是非公平锁,ReetrantLock也是非公平锁,但是也可以显式的声明为公平锁:

Lock lock = new ReetrantLock(true);

sychronized 和 Lock 的区别:

  1. 关键字不同;
  2. 前者自动加锁和释放锁,后者手动加锁和释放锁;
  3. Lock是java层面来实现,而sychronized是jvm层面来实现;
  4. 适用范围不同:Lock只能修饰代码块,而sychronized可以修饰前面提到的三个场景;
  5. sychronized锁的模式只有公平锁模式。

以上是关于如何解决线程不安全问题以及java中两种加锁的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal知识小结

synchronized ReentrantLock 比较分析

线程安全加锁的代码块的实现

Java的List如何实现线程安全?

面试问题集

如何创建线程?如何保证线程安全?