08-多线程带来的问题

nobility 发布于 2021-05-10 406 次阅读


多线程带来的问题

线程安全

当多线程访问一个对象时,若无需考虑下面几点(也就是无需做任何额外考虑,就想单线程那样),调用该对象的行为都可以得到正确的结果,则该对象是线程安全的

  • 无需考虑线程在运行时环境下的调度和交替执行
  • 无需考虑额外的同步
  • 无需考虑调用方进行任何其他的协调操作

要是出现下面这几种情况就可能会出现线程安全问题

  • 数据争用:访问共享的变量或资源,由于同时写数据,会造成错误数据
  • 数据捆绑:数据之间存在捆绑关系,由于修改未进行同步,会造成错误数据
  • 竞争条件:依赖时序的操作,由于顺序原因造成的错误,比如写入前就读取了

非原子操作的数据争用

public static int a;

public static void main(String[] args) throws InterruptedException {
  Runnable runnable = () -> {
    //while (a < 10000) a++;  //使用while将无法直接知道a++执行的次数
    for (int i = 0; i < 10000; i++) a++;  //两个线程虽然执行了共执行了a++语句20000次,但a的值最终不一定是20000
  };
  Thread thread1 = new Thread(runnable);
  Thread thread2 = new Thread(runnable);
  thread1.start();
  thread2.start();
  thread1.join();
  thread2.join();
  System.out.println(a);
}

使用下面代码可找出那一步数据写入时出现了数据争用

public static int a;
/*使用原子类,可保证对该数据的操作是原子操作*/
public static AtomicInteger atomicA = new AtomicInteger(0);  //与非原子数据进行对比
public static AtomicInteger errorCount = new AtomicInteger(0);  //记录出现错误的次数
public static boolean[] mark = new boolean[20001];
/*用来让两个线程在某处同时放行的栅栏*/
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

public static void main(String[] args) throws InterruptedException {
  Object lock = new Object();
  mark[0] = true;
  Runnable runnable = () -> {
    for (int i = 0; i < 10000; i++) {
      try {
        /**
         * 保证两个线程同时执行,且都执行完毕a++后再进行标记,
         * 以防出现由于该语句不在同步代码块中,而篡改已进入同步代码块中的线程,使用a值的语句,
         * CyclicBarrier实例的await方法可以做一个栅栏,若线程不够多时不会向下执行
         */
        cyclicBarrier2.reset();  //在等待之前需要重置,以便下次使用重置过了的CyclicBarrier实例
        cyclicBarrier1.await();  //等到两个线程执行到此处才会继续向下执行
        a++;
        cyclicBarrier1.reset();  //在等待之前需要重置
        cyclicBarrier2.await();  //等到两个线程执行到此处才会继续向下执行
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (BrokenBarrierException e) {
        e.printStackTrace();
      }
      atomicA.incrementAndGet();  //参考值原子性的自增
      synchronized (lock) {
        if (mark[a] && mark[a - 1]) {
          /**
           * 由于大部分情况都是未发生冲突的情况,所以大部分情况会让a自增两次,
           * 导致a会越过一项,所以只有前一项和当前项都为true时才是出现冲突,
           * 由于是从0开始进行对比的,所以第一位应该一开始就设置为true,从而保证第一次就发生冲突时就能判断出来
           */
          System.out.println("在[" + a + "]处发生冲突");
          errorCount.incrementAndGet();  //错误次数原子性自增
        }
        mark[a] = true;  //每次赋值成功都将对于位置打上true标记
      }
    }
  };
  Thread thread1 = new Thread(runnable);
  Thread thread2 = new Thread(runnable);
  /*启动两个子线程*/
  thread1.start();
  thread2.start();
  /*让主线程最后执行结果输出语句*/
  thread1.join();
  thread2.join();
  System.out.println("实际次数(" + a + ") + 冲突次数(" + errorCount + ") = 真正次数(" + atomicA + ")");
}

活跃性问题

  • 死锁:两个或多个线程永久阻塞,互相等待对方释放资源
  • 饥饿:一个线程因CPU时间全被其他线程抢走而得不到CPU资源
  • 活锁:死锁的另一种表现,两个或多个线程并未进入阻塞状态,而是互相避让资源让对方先执行

对象发布与初始化

发布:将对象可超过该类范围之外的地方使用,比如下面这几种情况

  • 将一个对象在类是使用public修饰
  • 方法返回一个对象
  • 将对象当作参数传递给一个方法

逸出:对象发布到了不该发布的地方

方法返回私有对象的逸出

返回该私有对象的副本可解决此类线程安全问题

public class ReturnPrivateObject {

  private Map<String, String> map;

  public ReturnPrivateObject() {
    map = new HashMap<>();
    map.put("127.0.0.1", "80");
  }

  public Map<String, String> getMap() {
    return map;  //直接返回该私有对象
  }

  public Map<String, String> getMapImproved() {
    return new HashMap<>(map);  //返回该私有对象的副本
  }

  public static void main(String[] args) {
    ReturnPrivateObject returnPrivateObject = new ReturnPrivateObject();
    Map<String, String> map = returnPrivateObject.getMap();

    System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));
    returnPrivateObject.getMapImproved().remove("127.0.0.1");  //假设是其他线程做的操作
    System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));

    System.out.println("getMap(): " + map.get("127.0.0.1"));
    map.remove("127.0.0.1");  //假设是其他线程做的操作
    System.out.println("getMap(): " + map.get("127.0.0.1"));
  }
}
对象未初始化完就提供外界使用的逸出
构造函数未初始化完就将this赋值
public class Point {
  public static Point point;
  private int x;
  private int y;

  public Point(int x, int y) throws InterruptedException {
    this.x = x;
    point = this;
    Thread.sleep(100);
    this.y = y;
  }

  @Override
  public String toString() {
    return x + "," + y;
  }

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      try {
        new Point(1, 1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();
    /*由于对象初始化在一个线程中使用在另一个线程中,所以导致未初始化完就使用*/
    Thread.sleep(99);  //1,0
    //Thread.sleep(101);  //1,1
    System.out.println(Point.point);
  }
}
构造函数中新建线程
import java.util.HashMap;
import java.util.Map;

public class InitNewThread {
  private Map<String, String> map;

  public InitNewThread() {
    map = new HashMap<>();
    new Thread(() -> {
      try {
        Thread.sleep(100);  //模拟延时初始化
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      map.put("127.0.0.1", "80");  //新建线程中进行初始化
    }).start();
  }

  public Map<String, String> getMap() {
    return map;  //直接返回该对象
  }

  public static void main(String[] args) throws InterruptedException {
    InitNewThread initNewThread = new InitNewThread();
    /*由于对象属性的初始化在另一个线程中,所以导致未初始化完就使用*/
    Thread.sleep(99);
    //Thread.sleep(101);
    System.out.println(initNewThread.getMap().get("127.0.0.1"));
  }
}
监听事件中事件触发时机
@FunctionalInterface
interface Event {  //事件接口
  void onEvent();  //绑定事件方法
}

public class Observer {
  static int count;  //要初始化的属性
  private Event event;

  public void registerEvent(Event event) {
    this.event = event;  //注册事件
  }

  public void trigger() {
    event.onEvent();  //触发事件
  }

  public static void main(String[] args) throws InterruptedException {
    Observer observer = new Observer();
    new Thread(() -> {
      observer.registerEvent(() -> System.out.println(count));  //该该线程注册事件
      try {
        Thread.sleep(100);  //模拟延时初始化
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      count = 100;  //初始化语句
    }).start();
    /*先让注册线程执行注册,count值未初始化好就触发了事件,count返回0,若初始化好就会返回100*/
    Thread.sleep(99);
    //Thread.sleep(101);
    new Thread(() -> {
      observer.trigger();  //该线程中触发事件
    }).start();
  }
}

使用工厂模式可解决此类线程安全问题

@FunctionalInterface
interface Event {  //事件接口
  void onEvent();  //绑定事件方法
}

public class Observer {
  static int count;  //要初始化的属性
  private Event event;

  private Observer() {
  }

  public void registerEvent(Event event) {
    this.event = event;  //注册事件
  }

  public void trigger() {
    event.onEvent();  //触发事件
  }

  public static Observer getInstance(Event event) {  //工厂方法
    Observer observer = new Observer();
    count = 100;  //先进行初始化
    observer.registerEvent(event);  //再注册事件
    return observer;
  }

  public static void main(String[] args) throws InterruptedException {
    Observer observer = Observer.getInstance(() -> System.out.println(count));  //工厂方法将注册和初始化合并
    new Thread(() -> {
      observer.trigger();  //此时事件触发一定会在注册且初始化之后
    }).start();
  }
}

多线程性能问题

上下文切换

上下文切换:主要是发生在线程调度时(当可运行线程数超过CPU数量时就会发生线程调度),操作系统内核在CPU上对进程或线程进行以下活动

  • 挂起一个进程,将该进程在CPU中的状态存储在内存,进程状态就是上下文
  • 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  • 跳转到程序计数器所指向的位置,即跳转到进程被中断时的代码处于,以恢复运行

上下文切换会导致CPU缓存失效,需要重新缓存:程序很大概率会按照一定规律进行数据访问,比如循环语句,CPU会按照一定的算法(LRU算法等)预测下一次要访问的数据进行缓存,从而提高执行效率,一旦发生上下文切换就导致预测失效,就需要重新缓存

频繁抢锁、IO等原因导致频繁阻塞,就会密集型上下文切换

内存同步

为了数据的正确性,同步手段会禁止编译器优化、使CPU内的缓存失效

此作者没有提供个人介绍
最后更新于 2021-05-10