线程关键字、锁、同步集合笔记

Android开发笔记 onGithub

[TOC]

1.原子性、可见性、有序性

1.1 原子性

指一个操作是不可中断的,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

例如 int a = 10这是一个原子操作,但是a++; a = a + 1就不是原子操作。

能够保证原子性的有:Java内存模型的8种操作(参考《深入理解Java虚拟机》或Java八种内存操作)、synchronized

1.2 可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

能够保证可见性的有:volatile、synchronized、final

1.3 有序性

即程序执行的顺序按照代码的先后顺序执行。

能够保证有序性的有:volatile、synchronized、


2.volatile

轻量级

保证可见性、有序性,但不保证原子性

如果volatile保证原子性,必须满足下面两个条件

  • 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
  • 变量不需要与其他的状态变量共同参与不变约束

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5
复制代码

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的;并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

实现原理

4.volatile的原理和实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

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

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

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

可参考

3.锁

3.1 synchronized

重量级

如图,synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。

实现原理

monitorenter、monitorexit指令

参考

3.2 显示锁Lock与ReentrantLock

Lock是一个接口提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显式的。包路径是:java.util.concurrent.locks.Lock。核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。

Condition包括await、signal、signalAll方法。

ReenTrantLock是可重入锁。

Lock与synchronized 的比较:

  • 1,Lock使用起来比较灵活,但是必须有释放锁的动作;
  • 2,Lock必须手动释放和开启锁,synchronized 不需要;
  • 3,Lock只适用与代码块锁,而synchronized 对象之间的互斥关系;

使用 ReentrantLock 的吞吐量通常要比 synchronized 好。换句话说,当许多线程试图访问 ReentrantLock 保护的共享资源时,JVM 将花费较少的时间来调度线程,而用更多时间执行线程。虽然 ReentrantLock 类有许多优点,但是与同步相比,它有一个主要缺点 — 它可能忘记释放锁定。ReentrantLock实在工作中对方法块加锁使用频率最高的。

举个例子

public class ReadAndWriteLock {  
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  
    public void get(Thread thread) {  
        lock.readLock().lock();  
        try{  
            System.out.println("start time:"+System.currentTimeMillis());  
            for(int i=0; i<5; i++){  
                try {  
                    Thread.sleep(20);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                System.out.println(thread.getName() + ":正在进行读操作……");  
            }  
            System.out.println(thread.getName() + ":读操作完毕!");  
            System.out.println("end time:"+System.currentTimeMillis());  
        }finally{  
            lock.readLock().unlock();  
        }  
    }  
      
    public static void main(String[] args) {  
        final ReadAndWriteLock lock = new ReadAndWriteLock();  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                lock.get(Thread.currentThread());  
            }  
        }).start();  
          
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                lock.get(Thread.currentThread());  
            }  
        }).start();  
    }  
}  
复制代码
3.3 信号量Semaphore

计数信号量,本质是一个“共享锁”。通过acquire获取信号量的许可,通过release释放。

public class SemaphoreTest {
    static int time = 0;
    public static void main(String[] args) {
    final ExecutorService executorService = Executors.newFixedThreadPool(3);
    final Semaphore semaphore = new Semaphore(3);
    for (int i = 0; i < 5; i++) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                  try {
                      semaphore.acquire();
                      System.out.println(" 剩余许可 : " + semaphore.availablePermits());
                      Thread.sleep(2000);
                      semaphore.release();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
            }
          });
      }
    }
}
复制代码

输出结果为:

 剩余许可 : 2
 剩余许可 : 1
 剩余许可 : 0
 剩余许可 : 2
 剩余许可 : 1
复制代码
3.4 循环栅栏 CyclicBarrier

CyclicBarrier是一个同步辅助类,允许一组线程相互等待,直到某个公共屏障点,因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。

也就是若干线程等待到达同一个条件后再去执行。

结果:

Thread-0 等待 CyclicBarrier.
Thread-2 等待 CyclicBarrier.
Thread-1 等待 CyclicBarrier.
Thread-3 等待 CyclicBarrier.
Thread-4 等待 CyclicBarrier.
 ---> 满足条件,执行特定操作。 参与者: 5
Thread-4 继续执行.
Thread-1 继续执行.
Thread-3 继续执行.
Thread-0 继续执行.
Thread-2 继续执行.
复制代码

应用场景

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。

CyclicBarrier和CountDownLatch的区别

  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。

参考:

3.5 闭锁 CountDownLatch

CountDownLatch也是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到条件被满足。

结果:

主线程等待.
Thread-0 执行操作.
Thread-3 执行操作.
Thread-4 执行操作.
Thread-1 执行操作.
Thread-2 执行操作.
主线程继续执行
复制代码

易混淆的关键字 transient

transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

静态成员变量属于类不属于对象,所以不会参与序列化和反序列化过程; transient关键字标记的成员变量不参与序列化过程

5.相关同步类

5.1 AtomicInteger等

能够保证原子性,内部使用了volatile关键字

参考

public class AtomicOperationDemo {  
       static AtomicInteger count=new AtomicInteger(0);  
       public static class AddThread implements Runnable{  
        @Override  
        public void run() {  
            for(int k=0;k<1000;k++){  
                count.incrementAndGet();  
            }  
         }   
       }  
       public static void AtomicIntShow(){  
         System.out.println("AtomicIntShow() enter");  
         ExecutorService threadpool =   Executors.newFixedThreadPool(10);  
           
         for(int k=0;k<100;k++){  
             threadpool.submit(new AddThread());  
         }  
           
         try {  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
           
         /* output 
          * AtomicIntShow() enter 
          * result of acumulated sum=100000 
          * AtomicIntShow() exit 
          */  
           
         System.out.println("result of acumulated sum="+count);  
         threadpool.shutdown();  
         System.out.println("AtomicIntShow() exit");  
         return ;  
          
    }  
} 
复制代码

5.2 CopyOnWriteArrayList

适合读多写少的并发

Copy-On-Write是一种基于程序设计中的优化策略,基本思路是,从多个线程共享同一个列表,当某个线程想要修改这个列表的元素时,会把列表中的元素Copy一份,然后进行修改,修改后将新元素设置给这个列表,这是一种延时懒惰策略。

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
复制代码

好处是可以并发的读,而不需要加锁。

可以看到CopyOnWriteArrayList的add方法是同步方法,而读的时候并没有加锁。

public E get(int index) {    
      return (E) elements[index];
}
复制代码

通过写时拷贝的原理可以将读、写分离,使并发场景下对列表的操作效率得到提高,但在添加、移除元素时占用的内存空间翻了一倍。

CopyOnWriteArrayList的add方法在Android API25 中 和J.U.C中实现的不一样,但是总体思路是一样的,就是COW(Copy-On-Write)

5.3 ConcurrentHashMap

ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

在Java 1.5作为Hashtable的替代选择新引入的。在Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的Map,只能在Hashtable和synchronized Map(Collections.synchronizedMap())中选择,因为HashMap并不是线程安全的。CHM不但是线程安全的,而且比HashTable和synchronizedMap的性能要好。相对于Hashtable和synchronizedMap锁住了整个Map,CHM只锁住部分Map。

ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性,代码如下:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
复制代码

根据默认的并发级别(concurrency level),Map被分割成16个部分,并且由不同的锁控制。这意味着,同时最多可以有16个写线程操作Map。

什么时候使用ConcurrentHashMap

CHM适用于读者数量超过写者时 ,当写者数量大于等于读者时,CHM的性能是低于Hashtable和synchronized Map的。这是因为当锁住了整个Map时,读操作要等待对同一部分执行写操作的线程结束。CHM适用于做cache,在程序启动时初始化,之后可以被多个请求线程访问。正如Javadoc说明的那样,CHM是Hashtable一个很好的替代,但要记住,CHM的比Hashtable的同步性稍弱。

5.4 BlockingQueue

5.4.1 ArrayBlockingQueue

ArrayBlockingQueue内部的阻塞队列是通过重入锁ReentrantLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。

部分实现代码:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** 存储数据的数组 */
    final Object[] items;

    /**获取数据的索引,主要用于take,poll,peek,remove方法 */
    int takeIndex;

    /**添加数据的索引,主要用于 put, offer, or add 方法*/
    int putIndex;

    /** 队列元素的个数 */
    int count;


    /** 控制并非访问的锁 */
    final ReentrantLock lock;

    /**notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作 */
    private final Condition notEmpty;

    /**notFull条件对象,用于通知put方法队列未满,可执行添加操作 */
    private final Condition notFull;

    /**
       迭代器
     */
    transient Itrs itrs = null;
    
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
}
复制代码

//offer方法
public boolean offer(E e) {
     checkNotNull(e);//检查元素是否为null
     final ReentrantLock lock = this.lock;
     lock.lock();//加锁
     try {
         if (count == items.length)//判断队列是否满
             return false;
         else {
             enqueue(e);//添加元素到队列
             return true;
         }
     } finally {
         lock.unlock();
     }
 }

//入队操作
private void enqueue(E x) {
    //获取当前数组
    final Object[] items = this.items;
    //通过putIndex索引对数组进行赋值
    items[putIndex] = x;
    //索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;//队列中元素数量加1
    //唤醒调用take()方法的线程,执行元素获取操作。
    notEmpty.signal();
}

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

复制代码
5.4.2 LinkedBlockingQueue

和ArrayBlockingQueue相似,也是通过重入锁ReentrantLock和Condition实现,但是使用了两个ReentrantLock。

5.5 Deque and BlockingDeque

5.5.1 LinkedBlockingDeque

理解了LinkedBlockingQueue就不难理解LinkedBlockingDeque了。

5.6 ConcurrentSkipListMap and ConcurrentSkipListSet

6.锁的种类

其实也算是锁的优化

6.1 自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

6.2 阻塞锁

阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。 JAVA中,能够进入/退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c经常使用)

阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 cpu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。

在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。

理想的情况则是,在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。

6.3 可重入锁

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

可重入锁最大的作用是避免死锁

6.4 偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

6.5 公平锁和非公平锁

公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队吃饭。

非公平锁:每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。

7.CAS的介绍

Compare and Swap,即比较并替换,实现并发算法时常用到的一种技术.

CAS的实现需要硬件指令集的支撑,CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

存在的问题

ABA、自旋时间过长、只能保证一个共享变量的原子操作

8.happens-before规则

从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before原则定义如下:

  • 1,如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

  • 2,两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

  • 【死磕Java并发】-----Java内存模型之happens-before

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值