多线程的线程安全问题,Java线程安全问题

  多线程的线程安全问题,Java线程安全问题

  本文给大家带来了一些java的知识,主要介绍了多线程的一些相关问题,包括线程的安装,线程锁定和线程不安全的原因,线程安全的标准类等等。希望对你有帮助。

  如何解决写爬虫IP受阻的问题?立即使用。

  本文介绍了Java多线程中的线程安全问题。这里的安全问题不是指黑客攻击导致的安全问题,线程安全问题是指多线程抢先执行导致的bug问题。

  

1.线程安全概述

  

1.1什么是线程安全问题

  首先我们要明白,操作系统中线程的调度是抢占式的,或者说是随机的,这就造成了线程的执行顺序是不确定的。有些代码执行顺序不影响程序的运行结果,但有些代码执行顺序改变了,重写后的运行结果会受到影响,从而导致程序出现bug。在多线程并发中会导致错误的代码称为

  接下来介绍一个线程安全问题的典型例子,整数自增量问题。

  

1.2一个存在线程安全问题的程序

  有一天,老师布置了这样一道题:用两个线程增加变量计数10万次,每个线程承担5万个自增任务,变量计数的初始值为0。

  这个问题很简单,我们可以口头算算最后的结果。答案是10万元。

  小明做事非常迅速,很快就写出了下面的代码:

  类别计数器{

  private int计数;

  公共无效增加(){

  this.count

  }

  public int getCount() {

  返回this.count

  } }公共类Main11 {

  私有静态final int CNT=50000

  私有静态最终计数器Counter=new Counter();

  公共静态void main(String[] args)引发InterruptedException {

  Thread thread1=新线程(()- {

  for(int I=0;我CNTi ) {

  counter . increase();

  }

  });

  Thread thread2=新线程(()- {

  for(int j=0;j CNTj ) {

  counter . increase();

  }

  });

  thread 1 . start();

  thread 2 . start();

  thread 1 . join();

  thread 2 . join();

  system . out . println(counter . get count());

  }}按理说结果应该是10万。让我们来看看运行结果:

  跑的结果不到10万。你可以试着运行这个程序,你会发现每次运行的结果都不一样,但大多数情况下,结果会比预期值小。我们来分析一下为什么会这样。

  

2.线程加锁与线程不安全的原因

  

2.1案例分析

  上面我们用多线程运行了一个程序,把一个值为0的变量增加了10万倍,但是最后的实际结果比我们预期的结果要小。原因是线程调度的顺序是随机的,导致线程间自增长指令集的交叉,导致运行时有两次自增长但只有一次自增长,所以得到的结果会太小。

  我们知道自动增量操作可以包含以下指令:

  将内存中变量的值载入寄存器。您可能希望将此操作记录为LOAD。如果在寄存器中执行自增操作,也可以将此操作记录为add。将寄存器的值保存到内存中,这个操作就像保存一样容易记住。让我们画一个时间线来总结几种常见的情况:

  情况1:线程间指令集,没有交叉,运行结果和预期一样。图中寄存器A表示线程1使用的寄存器,寄存器B表示线程2使用的寄存器,后续情况相同。情况2:线程间存在指令集交集,运行结果低于预期。情况3:线程间指令集完全交叉,实际结果低于预期。

  根据上述情况,发现线程运行中没有交叉指令时运行结果是正常的,但一旦有交叉,自增运算的结果会减1。综上,可以得出一个结论,即由于自增操作不是原子性的,多线程并发执行很可能会导致指令交叉执行,从而产生线程安全问题。

  那怎么解决上面的线程不安全问题呢?当然,那是锁定对象。

  

2.2线程加锁

  

2.2.1什么是加锁

  为了解决“抢先执行”带来的线程安全问题,我们可以锁定被操作对象。当线程获得对象的锁时,它将锁定该对象。如果其他线程需要执行对象的任务,需要等待线程完成对象的任务后才能执行。

  举个例子,假设你要去银行的ATM机存钱或者取钱。每个自动取款机通常都在一个单独的小房子里。这间小房子有一扇门和一把锁。当你进去使用自动取款机时,门会自动锁上。这个时候如果有人要取钱,它是不能进去用ATM的,直到你用完了出来。那么这里的“你”就相当于一根线,ATM就相当于一个物体,小房子就相当于一把锁。

  java中最常用的锁定操作是使用synchronized关键字来锁定。

  

2.2.2如何加锁

  同步会有互斥的效果。当一个线程在一个对象的synchronized中执行时,如果其他线程也在同一个对象的synchronized中执行,它们将阻塞等待。

  进入线程的同步代码块相当于锁定,退出同步代码块相当于解锁。

  java中的加锁操作可以通过使用synchronized关键字来实现,其常见用法如下:

  方式1:使用synchronized关键字来修饰一个公共方法,它会给方法所在的对象添加一个锁。

  比如以上面的自增程序为例,尝试使用synchronized关键字进行锁定。如下,我锁定了increase方法,这个方法实际上锁定了一个对象,这个锁定的对象是这个。本质上,锁定操作是修改这个对象头的标志位。

  类别计数器{

  private int计数;

  同步公共void增加(){

  this.count

  }

  public int getCount() {

  返回this.count

  }}多线程自增的主要方法如下。synchronized的其他用法后面会用同样的栗子介绍,所以后面就不列出这段代码了。

  公共类Main11 {

  私有静态final int CNT=50000

  私有静态最终计数器Counter=new Counter();

  公共静态void main(String[] args)引发InterruptedException {

  Thread thread1=新线程(()- {

  for(int I=0;我CNTi ) {

  counter . increase();

  }

  });

  Thread thread2=新线程(()- {

  for(int j=0;j CNTj ) {

  counter . increase();

  }

  });

  thread 1 . start();

  thread 2 . start();

  thread 1 . join();

  thread 2 . join();

  system . out . println(counter . get count());

  }}}看运行结果:方式2:使用synchronized关键字锁定代码段,但是需要显式指定锁定的对象。

  例如:

  类别计数器{

  private int计数;

  公共无效增加(){

  同步(这){

  this.count

  }

  }

  public int getCount() {

  返回this.count

  }}运行结果:方式3:使用synchronized关键字修饰静态方法相当于锁定了当前类的类对象。

  类别计数器{

  私有静态int计数;

  同步公共静态void增加(){

  数数;

  }

  public int getCount() {

  返回this.count

  }}运行结果:

  这些是常见的用法。对于线程锁(线程取锁),如果两个线程同时取一个对象的锁,就会出现锁竞争。两个线程同时获取两个不同对象的锁,不会发生锁竞争。

  对于synchronized这个关键词,它的英文意思是同步,但是同步在计算机中有很多意思。比如多线程,这里的同步就是“互斥”的意思;在IO或网络编程中,同步指的是“异步”,与多线程无关。

  synchronized 的工作过程:

  获取互斥锁将变量的最新副本从主内存复制到工作内存。执行代码,将更改后的共享变量的值刷新到主内存中。释放互斥锁。同步的同步块可以为同一个线程重新进入,不会出现锁死自己的问题,也就是死锁。关于死锁的后续文章会再次介绍。

  综上所述,同步关键词锁定具有如下性质:互斥、内存刷新和可重入。

  Synchronized关键字也相当于一个监视锁。如果不锁,就用wait方法(线程等待的一种方法,后面会详细介绍),就会抛出非法的monitor异常。这个异常的原因是没有锁。

  

2.2.3再析案例

  锁定自增代码后,我们来分析一下为什么所有线程都是安全的。我们先列出代码:

  类别计数器{

  private int计数;

  同步公共void增加(){

  this.count

  }

  public int getCount() {

  返回this.count

  } }公共类Main11 {

  私有静态final int CNT=50000

  私有静态最终计数器Counter=new Counter();

  公共静态void main(String[] args)引发InterruptedException {

  Thread thread1=新线程(()- {

  for(int I=0;我CNTi ) {

  counter . increase();

  }

  });

  Thread thread2=新线程(()- {

  for(int j=0;j CNTj ) {

  counter . increase();

  }

  });

  thread 1 . start();

  thread 2 . start();

  thread 1 . join();

  thread 2 . join();

  system . out . println(counter . get count());

  }}多线程并发执行的时候,上次分析过没有指令集交叉不会有问题。所以这里只讨论如何保证指令交叉后的线程安全。你最好记住锁定是锁定,解锁是解锁。两个线程的运行过程如下:

  线程1首先获得目标对象的锁,锁定对象,它处于锁定状态。当线程2开始执行自递增操作时,它将被阻塞。在线程1的自递增操作完成并处于解锁状态之前,线程2将准备好执行线程2的自递增操作。

  锁后线程化是串行执行,和单线程区别不大。多线程没用吗?但是方法被锁定后,线程运行方法时会被锁定,方法运行后会自动解锁。此外,大多数操作的并发执行不会引起线程安全,只有少数修改操作可能引起线程安全问题。所以多线程的运行效率整体上要比单线程高很多。

  

2.3线程不安全的原因

  首先,线程不安全的根源在于线程之间的调度充满了随机性,导致原有的逻辑被改变,从而产生线程不安全。这个问题解决不了,我们也无能为力。

  多个线程写入(修改)同一个资源,对资源的修改不是原子性的,这可能导致线程不安全,类似于数据库事务。

  由于编译器优化,无法保证内存可见性,即当一个线程频繁读取同一个变量时,会直接从寄存器中读取值,而不是从内存中读取,这样当内存值被修改时,线程就不会感知到变量被修改了,从而导致线程安全问题(这是编译器优化的结果,现代编译器也有超越Java的类似优化),因为与寄存器相比, 从内容中读取数据的效率要低得多,所以编译器会尽可能用相同的逻辑优化代码。 在单线程的情况下不会翻车,多线程就不一定了,比如下面这段代码:

  导入Java . util . scanner;公共类Main12 {

  private static int isQuit

  公共静态void main(String[] args) {

  Thread thread=新线程(()- {

  while (isQuit==0) {

  }

  System.out.println (thread线程执行完毕!);

  });

  thread . start();

  Scanner sc=新扫描仪(system . in);

  System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);

  is quit=sc . nextint();

  System.out.println(主线程执行完毕!);

  }}运行结果:

  从运行结果可以知道,在isQuit输入后线程并没有停止,这也是编译器优化导致线程无法感知内存可见性,从而导致线程不安全的原因。

  我们可以使用volatile关键字来确保内存可见性。

  我们可以用volatile关键字来修饰isQuit,以确保内存可见性。

  导入Java . util . scanner;公共类Main12 {

  volatile私有静态int isQuit

  公共静态void main(String[] args) {

  Thread thread=新线程(()- {

  while (isQuit==0) {

  }

  System.out.println (thread线程执行完毕!);

  });

  thread . start();

  Scanner sc=新扫描仪(system . in);

  System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);

  is quit=sc . nextint();

  System.out.println(主线程执行完毕!);

  }}运行结果:

  synchronized与volatile关键字的区别:synchronized关键字可以保证原子性,但是能否保证内存可见性要看情况(上面的栗子是不可以的),而volatile关键字只能保证内存可见性而不能保证原子性。

  保证内存可见性就是禁止编译器进行上述优化。

  导入Java . util . scanner;公共类Main12 {

  private static int isQuit

  //锁定对象

  私有静态最终对象锁=新对象();

  公共静态void main(String[] args) {

  Thread thread=新线程(()- {

  同步(锁定){

  while (isQuit==0) {

  }

  System.out.println (thread线程执行完毕!);

  }

  });

  thread . start();

  Scanner sc=新扫描仪(system . in);

  System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);

  is quit=sc . nextint();

  System.out.println(主线程执行完毕!);

  }}运行结果:

  编译器优化不仅会导致内存可见的问题,还会导致线程安全问题。指令重排序也是编译器优化之一,即编译器会智能调整代码执行顺序(同时保持原有逻辑不变),从而提高程序运行效率。单线程没问题,多线程可能会翻车。知道原因就好。

  

3.线程安全的标准类

   Java Java标准库是线程不安全的。这些类可能涉及多线程,以在没有任何锁定措施的情况下修改共享数据。比如ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder。

  但也有一些是线程安全的,使用一些锁机制来控制,比如Vector(已弃用)、HashTable(已弃用)、ConcurrentHashMap(推荐)、StringBuffer。

  此外,虽然没有锁定,但它不涉及“修改”,仍然是线程安全的,如String。

  在网上安全问题上,你也可能遇到JMM模式。我想在这里补充一点,JMM实际上是重新包装了操作系统中的寄存器、缓存和内存。在JMM,寄存器和缓存被称为工作存储器,存储器被称为主存储器。

  缓存分为L1、L2和L3。从L1到L3,空间越来越大,最大的比内存空间小,最小的比寄存器空间大,而且访问速度越来越慢,最慢的比内存访问速度快,最快的没有寄存器访问速度快。

  

4.Object类提供的线程等待方法

  除了在线程类中可以实现线程等待的方法,如join、sleep,在对象类中也提供了与线程等待相关的方法。

  当

序号方法说明
1public final void wait() throws InterruptedException释放锁并使线程进入WAITING状态
2public final native void wait(long timeout) throws InterruptedException;相比于方法1,多了一个最长等待时间
3public final void wait(long timeout, int nanos) throws InterruptedException相比于方法2,等待的最长时间精度更大
4public final native void notify();唤醒一个WAITING状态的线程,并加锁,搭配wait方法使用
5public final native void notifyAll();唤醒所有处于WAITING状态的线程,并加锁(很可能产生锁竞争),搭配wait方法使用
在上面介绍synchronized关键字时,如果不锁定线程,就会引起非法监控异常。我们来验证一下:

  公共类TestDemo12 {

  公共静态void main(String[] args)引发InterruptedException {

  Thread thread=新线程(()- {

  尝试{

  thread . sleep(5000);

  } catch (InterruptedException e) {

  e . printstacktrace();

  }

  System.out.println(执行完毕!);

  });

  thread . start();

  system . out . println(" before wait ");

  thread . wait();

  system . out . println(" after wait ");

  }}看运行结果:

  果然抛出了一个IllegalMonitorStateException,因为wait方法的执行步骤是:先释放锁,然后让线程等待。你现在还没锁,那怎么开锁呢?所以这个异常会被抛出,但是执行notify是无害的。

  wait方法通常与notify方法一起使用。前者可以释放锁,让线程等待,后者可以获取锁,让线程继续执行。这个组合拳的流程图如下:

  现在,有两个任务由两个线程执行。假设线程2在线程1之前执行,请写一个多线程程序使任务1在任务2之前完成,其中线程1执行任务1,线程2执行任务2。

  这个需求可以通过使用等待/通知来实现。

  课程任务{

  公共void任务(int i) {

  System.out.println(任务 I 已完成!);

  } }公共类WiteNotify {

  //锁定对象

  私有静态最终对象锁=新对象();

  公共静态void main(String[] args)引发InterruptedException {

  Thread thread1=新线程(()- {

  同步(锁定){

  任务Task 1=new Task();

  task 1 . task(1);

  //通知线程2,线程1的任务已经完成。

  system . out . println( before notify );

  lock . notify();

  system . out . println( after notify );

  }

  });

  Thread thread2=新线程(()- {

  同步(锁定){

  任务Task 2=new Task();

  //等待线程1的任务1完成执行

  system . out . println(" before wait ");

  尝试{

  lock . wait();

  } catch (InterruptedException e) {

  e . printstacktrace();

  }

  task 2 . task(2);

  system . out . println(" after wait ");

  }

  });

  thread 2 . start();

  Thread.sleep(十);

  thread 1 . start();

  }}运行结果:

  推荐:《java视频教程》以上是Java多线程的线程安全的详细内容。更多请关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: