Java并发原理学习笔记+总结+实战(2)——线程带来的风险

1、线程带来的风险

    java对线程的支持其实是一把双刃剑。虽然java提供了相应的语言和库,以及一种明确的跨平台内存模型,这些工具简化了并发应用程序的开发,但同时也提高的对开发人员的技术要求,因为更多的程序中会使用线程。

1.1 线程的安全性问题

    线程安全性可能是非常复杂的,在没有充分同步的情况下,多个线程中的操作次序是不可预测的,甚至会产生奇怪的结果。

/**
 * UnsafeSequence:线程不安全例子.
 *
 * @author YUSIR
 * @version 2019-03-01
 */
public class UnsafeSequence {
  private int value;

  /**
   * @return 返回一个唯一的数值.
   */
  public int getNext() {
    return value++;
  }
}

    上面的demo中将会产生一个整数值序列,该序列中的每个值都是唯一的。该类在单线程环境中能正常的工作,但是在多线程环境中则不能。

    UnSafeSequence的问题在于,如果执行时机不对,那么两个线程的调用getNext()时会得到相同的值。虽然递增运算value++看上去是单个操作,安踏事实上包含了三个独立操作:读取value,将value+1,将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时进行读操作,从而使它们得到相同的值,并都将其加一,结果就是不同的线程返回了相同的值。

     图中给出了不同线程之间的一种交替执行情况。执行时序按照从左到右的顺序递增,每行表示一个线程的动作。这些交替执行是给出的最糟糕的执行情况(事实上,由于指令重排序的可能,因此实际情况可能会更糟糕),目的是为了说明,如果错误地假设程序中的操作将按照某种特定顺序来执行,那么会存在各种可能的、不可预知的危险。

以上说明,导致线程安全性问题出现的原因主要有:

  • 多线程环境下
  • 多个线程共享一个资源
  • 对资源进行非原子性操作

解决方案:使用synchronize进行加锁设置,问题是,当线程加锁后,会导致其他线程不可入,就像回到了串行的场景了,多线程就没有意义了。

public class Sequence {

  private int value;

  /**
   * synchronized 放在普通方法上,内置锁就是当前类的实例
   *
   * @return
   */
  public synchronized int getNext() {
    return value++;
  }

  /**
   * 修饰静态方法,内置锁是当前的Class字节码对象
   * Sequence.class
   *
   * @return
   */
  public static synchronized int getPrevious() {
    //		return value --;
    return 0;
  }

  public int xx() {

    // monitorenter
    synchronized (Sequence.class) {

      if (value > 0) {
        return value;
      } else {
        return -1;
      }

    }
    // monitorexit

  }

  public static void main(String[] args) {

    Sequence s = new Sequence();
    //		while(true) {
    //			System.out.println(s.getNext());
    //		}

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

    new Thread(new Runnable() {

      @Override
      public void run() {
        while (true) {
          System.out.println(Thread.currentThread().getName() + " " + s.getNext());
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();

  }

}

synchronize的原理与使用

    同步代码块是由monitorenter和monitorexit指令实现的,当一个对象的monitor被持有后,该对象就处于锁定状态。

    synchronize用的锁是存在java对象头里的。

                     

    在 jdk6之前的版本,synchronize是作为一个重量级的锁存在的(是因为在互斥状态下,一个线程进去了,另一个线程必须在外部等待),在之后的版本引入了几个新的性能较高的概念

  • 偏向锁

        每次获取锁和释放锁时会浪费资源。在很多情况下,竞争所不是由多个线程触发的,而是由一个线程在使用。引入偏向锁是为了在无多线程的情况下减少不必要的轻量级锁执行性路径。

  • 轻量级锁
  • 重量级锁

1.2 活跃性问题

    安全性的含义是“永远不发生糟糕的事情”,二活跃性则关注与另一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,从而使循环之后的代码无法得到执行。线程将带来其他一些活跃性问题。例如,如果线程A在等待线程B释放其所持有的资源时,二线程B永远都不释放该资源,那么A就会永久地等待下去。主要是包括:

  • 死锁
  • 饥饿

    在Java中,下面三个常见的原因会导致线程饥饿:

        1)、高优先级吞噬所有低优先级的CPU时间片

             优先级越高的线程获得的CPU资源越多,线程优先级设置范围在1到10之间。

        2)、线程被永久堵塞在一个等待进入同步块的状态

             java同步代码区对哪个线程执行的次序是没有任何安排的,意味着理论上存在一个试图进入该同步区的线程会被永久堵          塞的风险因为其他线程总是能持续的地先于它访问。

        3)、等待的线程永远不会被唤醒

    如何避免饥饿问题:

        1)、设置合理的优先级

        2)、使用锁来代替synchronize

  • 活锁

1.3 性能问题

    性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,或者可伸缩性较低等。使用多线程技术,在能够提升程序性能的同时,不可避免的也会带来某种程度上的运行时开销。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将跟过地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器的优化,是内存缓存区中的数据无效,以及增加共享内存总线的同步流量。

 

 

 

--

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章