Java语言】【Java基础】如何解决线程安全问题?

2020-10-21 10:59发布

8条回答
那些年很冒险的梦。
2楼 · 2020-10-21 11:52

同步方法  使用synchronized 修饰方法  ==》 解决线程安全问题
同步代码块  synchronized(被加锁的对象){ 代码 }  ==》 解决线程安全问题
锁机制Lock  ==》 解决线程安全问题   (这个例子在ex2上修改而成的)

隔壁街道小胖子
3楼 · 2020-10-21 14:05

有2种解决方法
第一,是采用原子变量,毕竟线程安全问题最根本上是由于全局变量静态变量引起的,只要保证了对于变量的写操作要么全写要么不写,就可以解决线程安全,定义变量用sig_atomic_t和volatile。
第二,就是实现线程间同步啦,用互斥索,信号量。让线程有序的访问变量就可以啦

freediandianer
4楼 · 2020-10-21 19:37

加锁,这是最直接的方法。

风火轮
5楼 · 2020-10-21 20:30

有2种解决方法
第一,是采用原子变量,毕竟线程安全问题最根本上是由于全局变量静态变量引起的,只要保证了对于变量的写操作要么全写要么不写,就可以解决线程安全,定义变量用sig_atomic_t和volatile。
第二,就是实现线程间同步啦,用互斥索,信号量。让线程有序的访问变量就可以啦

我的网名不再改
6楼 · 2020-10-25 14:56

出现线程安全问题我们如何处理??  ==》同步原理
    1.同步方法synchronized 修饰的方法      ex:public synchronized void test(){}
              弊端:方法中的所有代码,都只允许一个线程访问。
(有一种情况:一个方法中,有一个部分代码不会涉及到线程安全问题,可以允许多个线程同时访问 == 》即下面的2.同步代码块)
    2.同步代码块 : synchronized(被加锁的对象){ 代码 }
    3.锁机制Lock
        ①创建ReentrantLock对象
        ②调用lock方法:加锁
                {代码....}
        ③调用unlock方法:解锁

         注意:可把解锁的unlock方法的调用放在finally{}代码块中,保证一定能解锁

提醒:在同步的时候,其他代码都可以多个线程同时执行!只是被同步的代码不能同时执行!


ex1:  同步方法  使用synchronized 修饰方法  ==》 解决线程安全问题

/** * 线程安全问题 * @author 郑清 */public class Demo { public static void main(String[] args) {  A a = new A();  MyThread t1 = new MyThread(a);  MyThread t2 = new MyThread(a);  t1.start();  t2.start(); }}class A{ private int tickets = 20;//记录车票的数量 /*  * 在方法定义时,添加一个synchronized修饰符  * 效果:    * 使用synchronized修饰的方法就是同步方法  *  特点: 1.效率会变低  *     2.没有线程安全问题了  *  原理:在方法添加synchronized之后,就相等于给当前对象(当前场景是a)加了一把锁.  *  锁的作用:当一个线程进入该方法时,先看锁还在不在,锁在:把锁取走  *  在第一个线程还没有结束时,第二个线程访问该方法,先看锁还在不在?       锁不在==》等待锁回来  *  第一个线程执行结束,把锁还回去,第二个线程发现锁回来了,把锁取走,开始执行方法  */ public synchronized void getTicket(){  if(tickets>0){   try {    Thread.sleep(50);   } catch (InterruptedException e) {    e.printStackTrace();   }   /*    * 打印出来的结果有可能有重复的车票数量    * 甚至有负数,就是发生了线程安全问题    * 原因是:两个线程同时进入了此对象的getBean方法,读取的车票数量是另一个线程修改之前的值    */   System.out.println("车票还剩: "+ (--tickets) + "张 !");  } }}class MyThread extends Thread{ private A a; public MyThread(A a) {  this.a = a; } public void run() {  while(true){   a.getTicket();  }  }}

运行结果图:(如果没有使用synchronized 修饰方法 getTicket( )   ==》 可能就会出现下图2线程安全问题)

                       


ex2 :  同步代码块  synchronized(被加锁的对象){ 代码 }  ==》 解决线程安全问题

/** * 同步代码块 :synchronized(被加锁的对象){ 代码 } * @author 郑清 */public class Demo2 { public static void main(String[] args) {  WC wc = new WC();    Person person1 = new Person(wc);  Thread t1 = new Thread(person1);  t1.setName("张三");    Person person2 = new Person(wc);  Thread t2 = new Thread(person2);  t2.setName("李四");    t1.start();  t2.start(); }}//上厕所class WC{ //定义一个方法实现上厕所 public void test(){  try{   System.out.println(Thread.currentThread().getName()+" : 拿纸...");   Thread.sleep(1000);   /*    * 把上厕所的代码 放在 同步代码块中    * 该代码块中的代码在一个时间点只能被一个线程访问,其他线程需要排队等待    */   synchronized (this) {    System.out.println(Thread.currentThread().getName()+" : 上厕所...");    Thread.sleep(2000);    System.out.println(Thread.currentThread().getName()+" : 出厕所");   }   System.out.println(Thread.currentThread().getName()+" : 上完厕所,真舒服,洗个手 ... end");  }catch (Exception e) {   // TODO: handle exception  } }}class Person implements Runnable{ private WC wc; public Person(WC wc) {  super();  this.wc = wc; } public void run() {  wc.test(); }}

运行结果图: (如果不处理线程安全问题可能就会出现下图2情况:张三和李四同时在一个厕所上厕所 很恶心...哈哈)

     


ex3 : 锁机制Lock  ==》 解决线程安全问题   (这个例子在ex2上修改而成的)

/** * 使用Lock的步骤: * 1.创建Lock实现类的对象 * 2.使用Lock对象的lock方法加锁 * 3.使用Lock对象的unlock方法解锁 * 注意:可把unlock方法的调用放在finally代码块中,保证一定能解锁 * @author 郑清 */public class Demo3 { public static void main(String[] args) {  WC wc = new WC();  //自定义线程步骤③:创建自定义类对象  Person person1 = new Person(wc);  Person person2 = new Person(wc);  //自定义线程步骤④:创建Thread类对象  Thread t1 = new Thread(person1);  Thread t2 = new Thread(person2);  //给线程名称赋值  t1.setName("张三");  t2.setName("李四");  //自定义线程步骤⑤:启动线程  t1.start();  t2.start(); }}// 上厕所class WC { // 1.创建Lock实现类的对象  注意:需在test()方法外执行,如果在test()方法里执行,在test()方法被调用的时候就会被创建很多ReentrantLock对象,即出现很多锁 ReentrantLock lock = new ReentrantLock(); // 定义一个方法实现上厕所 public void test() {  System.out.println(Thread.currentThread().getName() + " : 拿纸...");  // 2.使用Lock对象的lock方法加锁  lock.lock();  try {   System.out.println(Thread.currentThread().getName() + " : 上厕所...");   System.out.println(Thread.currentThread().getName() + " : 出厕所");   System.out.println(Thread.currentThread().getName() + " : 上完厕所,真舒服,洗个手 ... end");  } finally {   // 3.使用Lock对象的unlock方法解锁   lock.unlock();  } }}//自定义线程步骤①:创建自定义类 实现 Runnable接口class Person implements Runnable { private WC wc; public Person(WC wc) {  super();  this.wc = wc; } //自定义线程步骤②:覆写run方法 public void run() {  wc.test(); }}

运行结果图:(这里注意:在lock锁的地方  比如这个例子,图2中 张三上厕所的同时,李四可以去拿纸,但是不能去厕所!!)

  


爱梦 - 拿来吧你
7楼 · 2021-11-09 09:10

首先理解确定springMVC和servlet都是非线程安全的,因为springMVC是一个单例模式,永远都出于共享controller。其解决办法为:添加@Scope("prototype")后我们可以解决问题,但是影响效率问题.从service层解决问题,不影响效率问题可以添加事务锁。了解redis和zookeeper从数据库层面解决问题可以添加数据的“行级锁”。


超甜的布丁
8楼 · 2021-11-09 16:42

基本上所有的并发模式在解决线程安全问题上,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称同步互斥访问。

通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。


慢半拍
9楼 · 2021-11-09 17:29
  • 方法一:使用synchronized关键字,一个表现为原生语法层面的互斥锁,它是一种悲观锁,使用它的时候我们一般需要一个监听对象 并且监听对象必须是唯一的,通常就是当前类的字节码对象。它是JVM级别的,不会造成死锁的情况。使用synchronized可以拿来修饰类,静态方法,普通方法和代码块。比如:Hashtable类就是使用synchronized来修饰方法的。put方法部分源码:

     public synchronized V put(K key, V value) {        // Make sure the value is not null
            if (value == null) {            
            throw new NullPointerException();
            }

    而ConcurrentHashMap类中就是使用synchronized来锁代码块的。putVal方法部分源码:

     else {                V oldVal = null;                synchronized (f) {                    if (tabAt(tab, i) == f) {                        if (fh >= 0) {
                                binCount = 1;

    synchronized关键字底层实现主要是通过monitorenter 与monitorexit计数 ,如果计数器不为0,说明资源被占用,其他线程就不能访问了,但是可重入的除外。说到这,就来讲讲什么是可重入的。这里其实就是指的可重入锁:指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

    其实在使用synchronized时,存在一个锁升级原理。它是指在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。锁升级的目的是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。可能你又会问什么是偏向锁?什么是轻量级锁?什么是重量级锁?这里就简单描述一下吧,能够帮你更好的理解synchronized。

    偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的id会记录在对象的Mark Word中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

    轻量级锁(CAS):就是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。

    重量级锁:虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

  • 方法二:使用Lock接口下的实现类。Lock是juc(java.util.concurrent)包下面的一个接口。常用的实现类就是ReentrantLock 类,它其实也是一种悲观锁。一种表现为 API 层面的互斥锁。通过lock() 和 unlock() 方法配合使用。因此也可以说是一种手动锁,使用比较灵活。但是使用这个锁时一定要注意要释放锁,不然就会造成死锁。一般配合try/finally 语句块来完成。比如:

    public class TicketThreadSafe extends Thread{      private static int num = 5000;      ReentrantLock lock = new ReentrantLock();      @Override
          public void run() {        while(num>0){             try {
                   lock.lock();               if(num>0){
                     System.out.println(Thread.currentThread().getName()+"你的票号是"+num--);
                   }
                  } catch (Exception e) {
                     e.printStackTrace();
                  }finally {
                     lock.unlock();
                  }
                }
          }
    }

    相比 synchronized,ReentrantLock 增加了一些高级功能,主要有以下 3 项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。

    等待可中断是指:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

    公平锁是指:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }

    锁绑定多个条件是指:一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。

    final ConditionObject newCondition() { //ConditionObject是Condition的实现类
                return new ConditionObject();
        }
  • 方法三:使用线程本地存储ThreadLocal。当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。通过set(T value)方法给线程的局部变量设置值;get()获取线程局部变量中的值。当给线程绑定一个 Object 内容后,只要线程不变,就可以随时取出;改变线程,就无法取出内容.。这里提供一个用法示例:

    public class ThreadLocalTest {      private static int a = 500;      public static void main(String[] args) {            new Thread(()->{
                      ThreadLocal local = new ThreadLocal();                  while(true){
                            local.set(++a);   //子线程对a的操作不会影响主线程中的a
                            try {
                                  Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                  e.printStackTrace();
                            }
                            System.out.println("子线程:"+local.get());
                      }
                }).start();
                a = 22;
                ThreadLocal local = new ThreadLocal();
                local.set(a);            while(true){                  try {
                            Thread.sleep(1000);
                      } catch (InterruptedException e) {
                            e.printStackTrace();
                      }
                      System.out.println("主线程:"+local.get());
                }
          }
    }

    ThreadLocal线程容器保存变量时,底层其实是通过ThreadLocalMap来实现的。它是以当前ThreadLocal变量为key ,要存的变量为value。获取的时候就是以当前ThreadLocal变量去找到对应的key,然后获取到对应的值。源码参考如下:

     public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)
                map.set(this, value);        else
                createMap(t, value);
        }
         ThreadLocalMap getMap(Thread t) {        return t.threadLocals; //ThreadLocal.ThreadLocalMap threadLocals = null;Thread类中声明的
        }    void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    观察源码就会发现,其实每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

    初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

    然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找即可。

  • 方法四:使用乐观锁机制。前面已经讲述了什么是乐观锁。这里就来描述哈在java开发中怎么使用的。

    其实在表设计的时候,我们通常就需要往表里加一个version字段。每次查询时,查出带有version的数据记录,更新数据时,判断数据库里对应id的记录的version是否和查出的version相同。若相同,则更新数据并把版本号+1;若不同,则说明,该数据发生了并发,被别的线程使用了,进行递归操作,再次执行递归方法,直到成功更新数据为止。


相关问题推荐

  • 回答 7
    已采纳

    里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一...

  • 回答 8
    已采纳

    心里有个预期,然后看看是以什么目的进这家企业工作,要是赚钱的话,那就多要点,要是学习的话,可以根据情况要一个能养活自己的价格。

  • 回答 4
    已采纳

    Java中有八种数据类型,基础数据类型分别是:byte,short,int,long,float,double,char,boolean,引用数据类型分别是:数组,类和接口。方法传参的时候我们有两种,一种是形式参数(定义方法时写的参数),一种是实际参数(调用方法时给的具体值)。首先...

  • 回答 15
    已采纳

    现在的架构很多,各种各样的,如高并发架构、异地多活架构、容器化架构、微服务架构、高可用架构、弹性化架构等,还有和这些架构相关的管理型的技术方法,如 DevOps、应用监控、自动化运维、SOA 服务治理、去 IOE 等等,还有很多。分布式架构其实就是分布式系...

  • 回答 10

    1、监控GC的状态使用各种JVM工具,查看当前日志,分析JVM参数的设置,分析堆内存快照和GC日志,根据实际的各区域的内存划分和GC的执行时间,判断是否需要进行优化2、分析结果、判断是否需要优化如果各项参数设置合理,系统没有超时的日志出现,GC频率也不高,...

  • 回答 6

    MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以...

  • 回答 6

    学vue应该要先学习javascript 的基础知识和用法。

  • 回答 8

    1、lambda是jdk8的新特性2、使用lambda的前提,必须是一个接口,接口只能有一个抽象方法3、Lambda 表达式的简单例子:// 1. 不需要参数,返回值为 5  () -> 5    // 2. 接收一个参数(数字类型),返回其2倍的值  x -> 2 * x    // 3. 接受2个参数(数...

  • 回答 4

    1、面向对象编程(OOP):OOP最重要的思想是类,类是模板,从类中构造一个对象,即创建了这个类的一个实例;2、封装:是把数据和行为结合在一起,并对对象使用者隐藏数据的实现过程,通常一个对象中的数据叫他的实例字段(instance field) ;3、继承:Java中允许在已...

  • 回答 6

    jdk1.8的新特性包括如下:一、接口的默认方法与静态方法,也就是接口中可以有实现方法二、Lambda 表达式三、函数式接口与静态导入四、Lambda 作用域在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变...

  • 回答 6

    用static修饰,能节省一些内在空间,虽然很有限

  • 回答 6

    这个是在内存中存放的地址,你必须str1.equals(str2)才可以判断的。你可以输出地址看看,你这样比较的是两个地址,当然是false了。第一个直接缓存 数据共享Hello,是true。

  • 回答 3

    采用接口(Interface)的中变量默认为static final的特性在普通类中使用final修饰变量采用了Java 5.0中引入的Enum类型。,也就是所谓的枚举类

  • 回答 6

    其实就一句话:只要是有多个线程访问并修改某个变量或是区域的话,一定要加1653上同步设置(比如同步控制语句,互斥锁,信号量,原子操作等等)。B/S结构中,如果你不编写类似Apache等的HTTP Server,那么你不用考虑线程安全这个问题。如果有多个用户访问数据库...

  • 回答 4

    当多个线程访问同一个共享数据时,有可能会出现线程安全问题

没有解决我的问题,去提问