Java并发编程

2017年冬天,我曾经对多线程有过一段时间的接触,如今由于需要进行深化线程的知识。本章只是精要简单的说下同步的概念,然后延申到Java的同步机制

操作系统有关于信号量的介绍,信号量通过保持P,V操作的原子性来保证进程安全。关于信号量参见:信号量的一点思考。信号量中有一个叫做互斥元(Mutex)的数据结构,如果为非0,则标识当今资源被一个进程占有,请求该资源的进程PCB应该挂在该资源的请求队列上。

Java中的临界资源就是对象,对象实际上是一块代表该对象的一块内存。依照Java面向对象的原则,所有资源单位都是一个对象,因此为每个对象设定一个互斥元信号量,在Java术语中称之为监视器(Monitor).

监视器是一个信号量,非0表示资源已占有,大于0的情况是由于线程多次进入Synchronized方法,每当进入监视值+1.方法返回,监视值-1.可见,当一个线程锁定一块对象,任何线程不得通过synchronized方法访问这个对象。

注意:对象应该是同一个对象,访问同类但不相同的对象不会被阻塞。

原则上,Java线程安全都应使用synchronized关键字或者Lock,任何引起线程不安全的类成员都应该用synchronized修饰,对象成员在有synchronized方法中进行访问,而对象成员用private修饰,否则,public修饰的成员将不受控制地被各种线程访问到中间状态。

可能,很多人都听过volatile(内存可见性),ThreadLocal,原子性类,我觉得
不要搞那么多花里胡哨,要实现线程安全,就用这两个就够了。线程同步的确因为消除部分并行而引起开销,但是如果精确确定临界区的界限,程序的性能还是可以的。

线程不安全的条件例举:

由于线程调度的存在,不应该对处理器和调度程序做任何假设,保证程序运行的绝对正确性。线程出现不安全的条件出现在:死锁,竞争条件。
死锁的问题不属于讨论范畴,只讨论竞争条件和可见性。
为了说明这两个条件,用伪码(不符合Java文法)来描述一下:

竞争条件:
Thinking in java作者给出的是两个或者多个线程响应同一条件使得结果处于不一致的条件。就用简单的加法来看:

    static int i=0;//某个类的类成员
    Thread Count(){
           i++;
     }

现在同时出现两个Count类线程A,B,
线程A线程B分别运行于相同的CPU(成功情况):
线程A对i加1,i=1,
此时线程B被调度,线程上下文切换回将i保存至Cache,B读到正确的值如此交换。

线程A线程B分别运行于相同的CPU(不成功情况)
但是i++在编译成字节码并非一个指令,线程调度虽然在指令执行过程中不能调度,但在指令间进行调度是可以的。Java虚拟机并非采用传统的MIPS的RISC指令架构,而是栈指令架构,操作数保存在SRAM栈中,取栈顶操作数进行运算。
比如:
i++
分解成两个指令

 push(i);
 add(IMM(1));//(IMM表示立即数)

现在回到调度顺序的故意安排:
线程A刚把i=0放到栈顶,线程B就被调度了。线程A因为还没给i加1,线程B完美迭代一次使i变成0,不算过分吧!此时,线程A又被调度了,从断点add指令开始,加完后i=1.看不正确了吧。

因此,即使在同一处理器上运行的两个线程也是不安全的。
那么在不同的处理器上运行的两个线程出现不安全的情况就更好例举了:
线程A对i加1后,没写到内存。现在多核计算机都是共享内存计算机,因为i在缓存里,所以线程B读的值不新鲜,造成它认为i=0,还是不过分。

所以,我认为Java底层实现synchronized肯定是为多线程涉及到的对象维持一堆互斥量,每个互斥量代表这个对象,互斥量采用直通方式更改,即每次更改后写入内存,同时互斥量本身防止同时超过一个线程访问该对象。

并发结论

  • 两个因素影响锁竞争的几率:锁被请求的频率和锁占用时间 [4]

    一个可行的方法是缩小临界区域,尤其是一些阻塞操作(IO)或者耗时长的操作(前提是这些操作并不需要同步)。也就是如果说一个方法里面只有一段需要加锁,那么就不要给整个方法加上synchronized

    锁分裂与锁条带技术能够从不同的方面解释了对锁的一些优化。

    锁分裂是将原来使用一个锁来同步的多个独立的状态变量分成多个锁来管理。
    锁条带就是将概念上的确是一个状态变量的锁,分成多个锁。比如一个集合当中,一个线程访问集合第一个元素,而另一个线程访问集合第二个元素,那么两个线程本该无竞争。锁条带技术意思是将集合切开一个锁保护一个区域的同步。

    下面这串代码来自[4]:
    一个Map切分成16块,线程访问一块之前首先需要获取其访问元素所在的那个区域的锁。

@ThreadSafe
public class StripedMap {
 // Synchronization policy: buckets[n] guarded by locks[n%N_LOCKS]
   private static final int N_LOCKS = 16;
   private final Node[] buckets;
   private final Object[] locks;
   private static class Node { ... }
    public StripedMap(int numBuckets) {
       buckets = new Node[numBuckets];
       locks = new Object[N_LOCKS];
       for (int i = 0; i < N_LOCKS; i++)
            locks[i] = new Object();
   }
   private final int hash(Object key) {
     return Math.abs(key.hashCode() % buckets.length);
   }

   public Object get(Object key) {
       int hash = hash(key);
       synchronized (locks[hash % N_LOCKS]) {
       for (Node m = buckets[hash]; m != null; m = m.next)
       if (m.key.equals(key))
       return m.value;
        }
        return null;
      }
      public void clear() {
         for (int i = 0; i < buckets.length; i++) {
             synchronized (locks[i % N_LOCKS]) {
             buckets[i] = null;
             }
          }
      }
 ...
}
  • 导致并发应用扩展性差的元凶就是互斥资源锁。[4]

历史记录:

  1. 关于线程的总结之一–安全,协调,开销分析
  2. java多线程原理
  3. Thinking in java - Bruce Eckel.
  4. Java Concurrency In Practice

你可能感兴趣的:(Java,EE,SE)