并发编程(3)线程同步的方式及死锁

概述

线程自己因为建立和切换的开销,采用多线程不会提升程序的执行速度,反而会下降速度,可是对于频繁IO操做的程序,多线程能够有效的并发。 对于包含不一样任务的程序,能够考虑每一个任务使用一个线程。这样的程序在设计上相对于单线程作全部事的程序来讲,更为清晰明了,若是是单纯的计算操做,多线程并无单线程的计算效率高,可是对于一些刻意分散使用计算机系统资源的操做,则适合使用多线程。 在实际的开发中对于性能优化的问题须要考虑到具体的场景来考虑是否使用多线程技术。通常来讲一个程序是运行在一个进程中的,进程是具备必定独立功能的程序、它是计算机系统进行资源分配和调度的一个独立单位。而线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位。html

在JMM中,线程能够把变量保存在本地内存(好比机器的寄存器)中,而不是直接在主存中进行读写。这就可能形成一个线程在主存中修改了一个变量的值,而另外一个线程还在继续使用它在寄存器中的变量值的拷贝,形成数据的不一致,这样就会致使线程不安全,下面介绍几种Java中常见的线程同步的方式。java

正文

关于线程不安全的缘由是由于JMM定义了主内存跟工做内存,形成多个线程同事访问同一个资源时致使的不一致问题,那么要想解决这个问题其实也很简单,也是从JMM入手,主要有如下3种方式, 程序员

synchronization

  • 保证每一个线程访问资源的时候获取到的都是资源的最新值(可见性)
  • 当有线程 操做该资源的时候锁定该资源,禁止别的线程访问(锁)
  • 线程本地私有化一份本地变量,线程每次读写本身的变量(ThreadLocal)

synchronized

采用synchronized修饰符实现的同步机制叫作互斥锁机制,它所得到的锁叫作互斥锁。每一个对象都有一个锁标记,当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池,互斥锁分两种一种是类锁,一种是对象锁。 类锁:用于类的静态方法或者一个类的class,一个对象只有一个 对象锁:用于实例化的对象的普通方法,能够有多个数组

下面仍是用程序员改bug这个例子来示范一下synchronized的使用方式缓存

Bug类安全

public class Bug {

    private static Integer bugNumber = 0;

    public static int getBugNumber() {
        return bugNumber;
    }

    //普通同步方法
    public synchronized void addNormal() {
        bugNumber++;
        System.out.println("normalSynchronized--->" + getBugNumber());
    }

    //静态同步方法
    public static synchronized void addStatic() {
        bugNumber++;
        System.out.println("staticSynchronized--->" + getBugNumber());

    }

    //同步代码块
    public synchronized void addBlock() {
        synchronized (bugNumber) {
            this.bugNumber = ++bugNumber;
            System.out.println("blockSynchronized--->" + getBugNumber());

        }
    }
}

复制代码

Runnable性能优化

public class BugRunnable implements Runnable {
    private Bug mBug=new Bug();
    @Override
    public void run() {
        mBug.addNormal();//普通方法同步
//        mBug.addBlock();//同步代码块
//        Bug.addStatic();//静态方法同步
    }
}
复制代码

测试代码bash

public static void main(String[] args) {
        BugRunnable bugRunnable = new BugRunnable();
        for (int i = 0; i < 6; i++) {
            new Thread(bugRunnable).start();
        }
    }
复制代码
同步代码块
//同步代码块
    public synchronized void addBlock() {
        synchronized (bugNumber) {
            this.bugNumber = ++bugNumber;
            System.out.println("blockSynchronized--->" + getBugNumber());

        }
    }
复制代码

测试结果多线程

blockSynchronized--->1
blockSynchronized--->2
blockSynchronized--->3
blockSynchronized--->4
blockSynchronized--->5
blockSynchronized--->6
复制代码
普通方法同步
//普通同步方法
    public synchronized void addNormal() {
        bugNumber++;
        System.out.println("normalSynchronized--->" + getBugNumber());
    }
复制代码

测试结果并发

normalSynchronized--->1
normalSynchronized--->2
normalSynchronized--->3
normalSynchronized--->4
normalSynchronized--->5
normalSynchronized--->6
复制代码
静态方法同步
//静态同步方法
    public static synchronized void addStatic() {
        bugNumber++;
        System.out.println("staticSynchronized--->" + getBugNumber());

    }
复制代码

测试结果

staticSynchronized--->1
staticSynchronized--->2
staticSynchronized--->3
staticSynchronized--->4
staticSynchronized--->5
staticSynchronized--->6
复制代码
对比分析
  • 类的每一个实例都有本身的对象锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其余线程这时若是要访问同一个实例(由于对象能够有多个实例)同步代码块或同步方法,必须等待当前线程释放掉对象锁才能够,若是是访问类的另一个实例,则不须要。
  • 若是一个对象有多个同步方法或者代码块,没有获取到对象锁的线程将会被阻塞在全部同步方法以外,可是能够访问非同步方法
  • 对于静态方法,实际上能够把它转化成同步代码块,就拿上面的静态方法,实际上至关于:
//静态同步方法
    public static synchronized void addStatic() {
        bugNumber++;
        System.out.println("staticSynchronized--->" + getBugNumber());

    }
    //用同步代码块
    public static void changeStatic() {
        synchronized (Bug.class) {
            ++bugNumber;
            System.out.println("blockSynchronized--->" + getBugNumber());

        }
    }
复制代码

下面具体来总结一下三者的区别

  • 同步代码块:同步代码块的范围较小,只是锁定了某个对象,因此性能较高
  • 普通同步方法:给整个方法上锁,性能较低
  • 静态同步方法:至关于整个类的同步代码块,性能较低

ReentrantLock

除了synchronized这个关键字外,咱们还能经过concurrent包下的Lock接口来实现这种效果,ReentrantLock是lock的一个实现类,能够在任何你想要的地方进行加锁,比synchronized关键字更加灵活,下面看一下使用方式 使用方式

//ReentrantLock同步
    public void addReentrantLock() {
        mReentrantLock.lock();//上锁
        bugNumber++;
        System.out.println("normalSynchronized--->" + getBugNumber());
        mReentrantLock.unlock();//解锁
    }
复制代码

运行测试

ReentrantLock--->1
ReentrantLock--->2
ReentrantLock--->3
ReentrantLock--->4
ReentrantLock--->5
ReentrantLock--->6
复制代码

咱们发现也是能够达到同步的目的,看一下ReentrantLock的继承关系

ReentrantLock

ReentrantLock实现了lock接口,而lock接口只是定义了一些方法,因此至关于说ReentrantLock本身实现了一套加锁机制,下面简单分析一下ReentrantLock的同步机制,在分析前,须要知道几个概念:

  • CLH:AbstractQueuedSynchronizer中“等待锁”的线程队列。在线程并发的过程当中,没有得到锁的线程都会进入一个队列,CLH就是管理这些等待锁的队列。
  • CAS:比较并交换函数,它是原子操做函数,也就是说全部经过CAS操做的数据都是以原子方式进行的。

成员变量

private static final long serialVersionUID = 7373984872572414699L;
  /** Synchronizer providing all implementation mechanics */
private final Sync sync;//同步器
复制代码

成员变量除了序列化ID以外,只有一个Sync,那就看一看具体是什么

Sync
Sync有两个实现类,一个是FairSync,一个是NonfairSync,从名字能够大体推断出一个是公平锁,一个是非公平锁,

FairSync(公平锁) lock方法:

final void lock() {
            acquire(1);
        }

复制代码

ReentrantLock是独占锁,1表示的是锁的状态state。对于独占锁而言,若是所处于可获取状态,其状态为0,当锁初次被线程获取时状态变成1,acquire最终调用的是tryAcquire方法

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
           // 当c==0表示锁没有被任何线程占用
        (hasQueuedPredecessors),
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
            //锁已经被线程占用
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
复制代码

tryAcquire主要是去尝试获取锁,获取成功则设置锁状态并返回true,不然返回false

NonfairSync(非公平锁) 非公平锁NonfairSync的lock()与公平锁的lock()在获取锁的流程上是一直的,可是因为它是非公平的,因此获取锁机制仍是有点不一样。经过前面咱们了解到公平锁在获取锁时采用的是公平策略(CLH队列),而非公平锁则采用非公平策略它无视等待队列,直接尝试获取。

final void lock() {
            if (compareAndSetState(0, 1))
           setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
复制代码

lock()经过compareAndSetState尝试设置锁的状态,若成功直接将锁的拥有者设置为当前线程(简单粗暴),不然调用acquire()尝试获取锁,对比一下,公平锁跟非公平锁的区别在于tryAcquire中

//NonfairSync 
  if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
 //FairSync 
 if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
复制代码

公平锁中要经过hasQueuedPredecessors()来判断该线程是否位于CLH队列头部,是则获取锁;而非公平锁则无论你在哪一个位置都直接获取锁。

unlock

public void unlock() {
        sync.release(1);//释放锁
    }

  public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
复制代码

对比分析

等待可中断
  • synchronized:线程A跟线程B同时竞争同一把锁,若是线程A得到锁以后不释放,那么线程B会一直等待下去,并不会释放。

  • ReentrantLock:能够在线程等待了很长时间以后进行中断,不须要一直等待。

锁的公平性

公平锁:是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次得到锁;非公平锁:在锁被释放时,任何一个等待锁的线程都有机会得到锁;

  • synchronized:是非公平锁
  • ReentrantLock:能够是非公平锁也能够是公平锁
绑定条件
  • synchronized中默认隐含条件。
  • ReentrantLock能够绑定多个条件

可见性

volatile

内存语义

因为多个线程方法同一个变量,致使了线程安全问题,主要缘由是由于线程的工做副本的变量跟主内存的不一致,若是可以解决这个问题就能够保证线程同步,而Java提供了volatile关键字,能够帮助咱们保证内存可见性,当咱们声明了一个volatile关键字,实际上有两层含义;

  • 禁止进行指令重排序。
  • 一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操做,也就不会执行线程阻塞,所以volatile变量是一种比synchronized关键字更轻量级的同步机制。

原理

在使用volatile关键字的时候,会多出一个lock前缀指令,lock前缀指令实际上至关于一个内存屏障实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

2)它会强制将对缓存的修改操做当即写入主存;

3)若是是写操做,它会致使其余CPU中对应的缓存行无效。

使用场景

这里须要强调一点,volatile关键字并不必定能保证线程同步,若是非要采用volatile关键字来保证线程同步,则须要知足如下条件:

  • 对变量的写操做不依赖于当前值
  • 该变量没有包含在具备其余变量的不变式中

其实看了一些书跟博客,都是这么写的,按照个人理解实际上就是只有当volatile修饰的对象是原子性操做,才可以保证线程同步,为何呢。

测试代码:

class Volatile {
    volatile static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Volatile.add();
                }
            }).start();
        }

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count--->" + ++count);

    }

    private static void add() {
        count++;
    }
}

复制代码

运行结果

count--->1001
复制代码

理论上是1000才对,可是输出的值是1001,为何呢,这个其实在以前的JMM中已经分析过了,下面再贴一张图

volatile

跟以前同样,咱们每次从主内存中获取到的count确实是最新的,可是因为对count的操做不是原子性操做,假如如今有两个线程,线程1跟线程2,若是线程1读取到了count值是5,而后read--->load进内存了,而后如今被线程2抢占了CPU,那么线程2就开始read--->load,而且完成了工做副本的赋值操做,而且将count 的值回写到主内存中,因为线程1已经进行了load操做,因此不会再去主内存中读取,会接着进行本身的操做,这样的话就出现了线程不安全,因此volatile必须是原子性操做才能保证线程安全。 基于以上考虑,volatile主要用来作一些标记位的处理:

volatile boolean flag = false;
 //线程1
while(!flag){
    doSomething();
}
  //线程2
public void setFlag() {
    flag = true;
}
复制代码

当有多个线程进行访问的时候,只要有一个线程改变了flag的状态,那么这个状态会被刷新到主内存,就会对全部线程可见,那么就能够保证线程安全。

automatic

automatic是JDK1.5以后Java新增的concurrent包中的一个类,虽然volatile能够保证内存可见性,大部分操做都不是原子性操做,那么volatile的使用场景就比较单一,而后Java提供了automatic这个包,能够帮助咱们来保证一些操做是原子性的。

使用方式

替换以前的volatile代码

public static AtomicInteger atomicInteger = new AtomicInteger(0);
 private static void add() {
        atomicInteger.getAndIncrement();
    }
复制代码

测试一下:

AtomicInteger: 1000
复制代码
原理解析

AtomicInteger既保证了volatile保证不了的原子性,同时也实现了可见性,那么它是如何作到的呢?

成员变量

private static final long serialVersionUID = 6214790243416807050L;
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    private volatile int value;
复制代码

运算方式

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
 int compare_and_swap(int reg, int oldval, int newval) {
        ATOMIC();
        int old_reg_val = reg;
        if (old_reg_val == oldval)
            reg = newval;
        END_ATOMIC();
        return old_reg_val;
    }
复制代码

分析以前须要知道两个概念:

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。

compare_and_swap这个才是核心方法,也就是上面提到的CAS,由于CAS是基于乐观锁的,也就是说当写入的时候,若是寄存器旧值已经不等于现值,说明有其余CPU在修改,那就继续尝试。因此这就保证了操做的原子性。

变量私有化

这种方式实际上指的就是ThreadLocal,翻译过来是线程本地变量,ThreadLocal会为每一个使用该变量的线程提供独立的变量副本,可是这个副本并非从主内存中进行读取的,而是本身建立的,每一个副本相互之间独立,互不影响。相对于syncronized的以时间换空间,ThreadLocal恰好相反,能够减小线程并发的复杂度。

简单使用

class ThreadLocalDemo {
    public static ThreadLocal<String> local = new ThreadLocal<>();//声明静态的threadlocal变量

    public static void main(String[] args) {
        local.set("Android");
        for (int i = 0; i < 5; i++) {
            SetThread localThread = new SetThread();//建立5个线程
            new Thread(localThread).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(local.get());

      
    }

    static class SetThread implements Runnable {

        @Override
        public void run() {
            local.set(Thread.currentThread().getName());
        }

    }
}
复制代码

进行 测试

Android
复制代码

虽然我用for循环建立了好几个线程,可是并无改变ThreadLocal中的值,依然是个人大Android,这个就可以说明我赋的值是跟个人线程绑定的,每一个线程有特定的值。

源码分析

成员变量
private final int threadLocalHashCode = nextHashCode();//当前线程的hash值
 private static AtomicInteger nextHashCode =//下一个线程的hash值
        new AtomicInteger();
 private static final int HASH_INCREMENT = 0x61c88647;//hash增加因子
复制代码
构造函数
public ThreadLocal() {
    }
复制代码

空实现。。。。

set方法
public void set(T value) {
        Thread t = Thread.currentThread();//获取到当前线程
        ThreadLocalMap map = getMap(t);//获取一个map
        if (map != null)
        //map不为空,直接进行赋值
            map.set(this, value);
        else
        //map为空,建立一个Map
            createMap(t, value);
    }
       
     ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
复制代码
ThreadLocalMap

上面建立的Map其实是一个ThreadLocalMap,也便是用来保存跟线程绑定的数据的,之间看过HashMap的源码,既然也叫Map,那么其实应该是差很少的

基本方法

ThreadLocalMap

成员变量
private static final int INITIAL_CAPACITY = 16;//初始容量,2的幂
 
        private Entry[] table;//用来存放entry的数组
        private int size = 0;//数组长度
        private int threshold; // 阈值

//Entry继承了WeakReference,说明key弱引用,便于内存回收
  static class Entry extends WeakReference<ThreadLocal<?>> {
           /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

复制代码
构造方法
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table数组
    table = new Entry[INITIAL_CAPACITY];
    // 经过hash值来计算存放的索引
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 建立entry节点
    table[i] = new Entry(firstKey, firstValue);
    // 数组长度由0到1
    size = 1;
    // 将阈值设置成为初始容量
    setThreshold(INITIAL_CAPACITY);
}
复制代码

还有一个构造方法是传一个Map,跟传key-value大同小异就不解释了

getEntry
private Entry getEntry(ThreadLocal<?> key) {
			 //经过key来计算数组下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
            //遍历到直接返回
                return e;
            else
            //没有遍历到就会调用getEntryAfterMiss,继续遍历
                return getEntryAfterMiss(key, i, e);
        }

复制代码
set方法
private void set(ThreadLocal<?> key, Object value) {
     
        Entry[] tab = table;//拿到table数组
        int len = tab.length;//获取table的长度
        int i = key.threadLocalHashCode & (len-1);//计算下标
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                //如便利到相同的能够,那么取而代之
                    e.value = value;
                    return;
                }
                if (k == null) {
                //替换key值为空的entry
                    replaceStaleEntry(key, value, i);//
                    return;
                }
            }
            tab[i] = new Entry(key, value);//进行赋值
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

复制代码
remove方法
private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            //遍历下标寻找i
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);//清理指定的key
                    return;
                }
            }
        }
复制代码

基本上分析到这里已经将ThreadLocal分析清楚了,它的核心是一个ThreadLocalMap,存放了一个entry数组,期中key是ThreadLocal的weakreference,value就是set的值,而后每次set跟get都会对已有的entry进行清理,加商weakreference就能够最大限度的放置内存泄露。

死锁

定义

死锁:是指多个线程因竞争资源而形成的一种僵局(互相等待),若无外力做用,这些进程都将没法向前推动。

下面举一个死锁的例子

public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的全部对象共享的
    private static Object o1 = new Object(), o2 = new Object();
    @Override
    public void run() {
        System.out.println("flag=" + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪一个线程是不肯定的。
        //td2的run()可能在td1的run()以前运行
        new Thread(td1).start();
        new Thread(td2).start();

    }
}
复制代码

无论哪一个线程先启动,启动的线程都会先sleep500ms,让另一个线程得到CPU的使用权,这样一来就保证了线程td1获取到了O1的对象锁,在竞争O2的对象锁,td2获取到了O2的对象锁,在竞争O1的对象锁,呵呵,这就尴尬了,而后互不想让,就卡死了,形成了死锁。

死锁产生的必要条件
  • 1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。若是此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  • 2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对本身已得到的其它资源保持不放。
  • 3)不剥夺条件:指进程已得到的资源,在未使用完以前,不能被剥夺,只能在使用完时由本身释放。
  • 4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链。
预防死锁
  • 打破互斥条件。即容许进程同时访问某些资源。
  • 打破不可抢占条件。即容许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能当即被知足时,它必须释放所占有的所有资源,之后再从新申请。
  • 打破占有且申请条件。能够实行资源预先分配策略。即进程在运行前一次性地向系统申请它所须要的所有资源。
  • 打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会造成环路。全部进程对资源的请求必须严格按资源序号递增的顺序提出。

参考资料

www.importnew.com/18126.html

ifeve.com/introduce-a…

www.blogjava.net/xylz/archiv…

blog.csdn.net/chenssy/art…

相关文章
相关标签/搜索