zoukankan      html  css  js  c++  java
  • java多线程

    JMM(Java内存模型)

      来源:为了解决CPU和主存之间速度不匹配导致CPU资源浪费的情况,每个线程都独占一块缓存Cache用于缓存要使用到的主存中的数据。JMM模型如下图所示:  

      

      JMM基本原理:当多个线程修改同一静态变量时,每个线程在修改完变量后,会将线程的缓存中的变量值刷新到主存中;在每个线程启动时,会从主存中将变量加载到线程的缓存。

    主存和缓存数据相互操作流程

    1.   load:将主存中的数据t加载到缓存的变量中。
    2.   assign:在缓存中修改数据t。
    3.   store:将修改后的数据t从缓存中放到主存的变量中。  

    线程安全问题 

       问题:10个线程,每个线程对同一变量t进行100次加1操作。

    public class Test{
        public static int t = 0;
    
        public static void main(String[] args){
    
            Thread[] threads = new Thread[10];
            for(int i = 0; i < 10; i++){
                //每个线程对t进行1000次加1的操作
                threads[i] new Thread(new Runnable(){
                    @Override
                    public void run(){
                        for(int j = 0; j < 1000; j++){
                            t = t + 1;
                        }
                    }
                });
                threads[i].start();
            }
    
            //等待所有累加线程都结束
            while(Thread.activeCount() > 1){
                Thread.yield();
            }
    
            //打印t的值
            System.out.println(t);
        }
    }

       线程安全问题分析:

      对于每个线程,对t的操作有三步:

      1. load:从主存中获取t的值,加载到缓存中。

      2. assign:修改缓存中t的值,t-> t + 1。

      3. store:将修改后的值放回到主存中。

      我们希望在一个线程进行了load--assign--store这一套操作后,另一个线程再进行load--assign--store操作;但是实际上,可能线程A执行了load :t = 1后,在assign执行前,线程B执行了load操作t = 0,这样,两个线程load时t都是0,store时t都是1,这表示有一个线程没有实现该有的t+1操作,造成最后t的结果小于1000,这就是线程不安全问题。

    线程不安全问题原因剖析

    1.   在主存和缓存之间进行数据交互时,线程A在线程B进行store操作之前就从主存中获取共享变量,导致线程B的修改无效,这就是线程不安全问题。
    2.   指令重排序也会造成线程不安全问题。

    线程安全

      子线程多主线程的变量进行操作时,保证了操作的原子性,可见性和有序性。这样的线程就是线程安全的。 

    volatile

      volatile保证了变量的可见性:

        如果一个共享变量被一个线程修改了之后,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取。

      可见性使用了缓存一致性原则:

      线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。

     

    volatile的线程安全问题

      由于volatile只能保持可见性、有序性,无法保证load--assign--store这套操作的原子性,因此会产生线程安全问题。

      

      面试问题:可以创建volatile数组吗?

        Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

        同理,对于 Java POJO 类,使用 volatile 修饰,只能保证这个引用的可见性,不能保证其内部的属性。

    Java四种引用类型

      Java语言中,除了原始的八个基本数据类型外,其他都是引用类型,指向各种不同的对象。不同的引用类型,不同之处在于对象不同的可达性及对垃圾回收的影响。主要分为:

    强引用

      只要引用存在,垃圾回收器永远不会回收。例如:

      Object obj = new Object(); //其中obj就是强引用。通过关键字new创建的对象所关联的引用是强引用。
      特点:JVM内存空间不足时,JVM宁愿抛出OutOfMemoryError(OOM)运行时错误,使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通对象,如果没有其他的引用关系,只要超过了引用的作用域(如超出局部变量作用范围)或者显示将相应(强)引用赋值为null,就可以根据具体的垃圾回收机制被回收。

    软引用  

      软引用通过SoftReference实现,非必须引用,生命周期比强引用短一些,内存溢出之前进行回收。

      应用场景: 软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

    弱引用

      弱引用通过WeakReference类实现,弱引用的生命周期比软引用短,被弱引用关联的对象只能生存到下一次垃圾回收之前。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现具有弱引用的对象,不管当前的内存空间足够与否,都会回收他的内存。(由于垃圾回收器县城是一个优先级很低的线程,因此不一定会很快回收弱引用的对象)。通过get()方法创建对象的强引用。

      应用场景:弱引用同样是很多缓存实现的选择。

    虚引用

      通过PhantomReference类来实现。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

      

    ThreadLocal

    ThreadLocal的作用

      ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

    数据结构

      ThreadLocal类中最终要的是ThreadLocalMap这个静态内部类,k指向弱引用,value就是线程缓存的对象。

    
    
    public class ThreadLocal<T> {
      
    static class ThreadLocalMap {
      private Entry[] table;
      static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
      }
      }
    }

      ThreadLocalMap的初始长度为16。当集合中size数量大于规定长度的1/2()时,则执行resize()操作,扩容到原来两倍。 

    重要方法

      ThreadLocal就是一个类,他有get、set方法,可以起到一个保存、获取某个值的作用。但是这个类的get、set方法有点特殊,各个线程调用时是互不干扰的,就好像线程在操作ThreadLocal对象时是在操作线程自己的私有属性一样。具体原因在于他的方法实现:

      get方法步骤:  

    (1)拿到当前线程,Thread t = Thread.currentThread();
    (2)拿到线程中的成员变量threadLocals,ThreadLocalMap map = getMap(t);
    (3)对map进行判断,不为null则以local为key,获取Entry对象e,e不为null则返回存储的Object对象。
    (4)map为null,则执行setInitialValue方法,初始化map,返回Object对象。

    public T get() {
            Thread t = Thread.currentThread();  //先确定调用我的线程
            ThreadLocalMap map = getMap(t);  //根据调用我的线程,找到这个线程的ThreadLocalMap对象
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);  //以ThreadLocal对象为key,找到对应元素
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;   //讲元素的value返回
                    return result;
                }
            }
            return setInitialValue();  //如果调用我的线程没有ThreadLocalMap对象,则返回初始值
        }

      set方法类似于get方法:

    public void set(T value) {
            Thread t = Thread.currentThread();  //先确定调用我的是哪个线程
            ThreadLocalMap map = getMap(t);  //获取调用我的线程的ThreadLocalMap 
            if (map != null)
                map.set(this, value);  //如果那个线程有map,就将此ThreadLocal对象为key的value设置好
            else
                createMap(t, value);   //如果那个线程还没有map,先创建一个再设置
        }

      由set和get方法可以知道ThreadLocalMap依赖于具体的Thread线程,ThreadLocal是Thread类型的成员变量:

    ThreadLocal.ThreadLocalMap threadLocals = null;

    获取ThreadLocal在Entry中的位置

      由于ThreadLocalMap不是Map类型,而是数组类型,因此无法直接找到ThreadLocal对象的封装对象Entry在Entry数组中的下标。需要通过threadLocalHashCode+开放定址法确认下标。

    threadLocalHashCode

       ThreadLocal对象将threadLocalHashCode成员作为自己的唯一标识。threadLocalHashCode成员相关方法如下所示:

    public class ThreadLocal<T> {
        private final int threadLocalHashCode = nextHashCode();
       private final int threadLocalHashCode = nextHashCode();
        private static AtomicInteger nextHashCode = new AtomicInteger();
        private static final int HASH_INCREMENT = 0x61c88647;
       private static int nextHashCode() {
          return nextHashCode.getAndAdd(HASH_INCREMENT);
       }
    } 

      ThreadLocal在初始化threadLocalHashCode成员时,调用nextHashCode()方法,nextHashCode()方法原子性地返回当前nextHashCode+HASH_INCREMENT值,用于创建该对象唯一标识。

    • 由于nextHashCode对象是static类型的,因此每个ThreadLocal对象都共享这个变量。
    • 由于nextHashCode对西那个是AtomicInteger类型的,因此nextHashCode()的操作是原子性的,是线程安全的,保证了每个ThreadLocal对象的threadLocalHashCode都不一样。

    确定ThreadLocal对象在Entry中的下标

      1. 通过threadLocalHashCode创建对象的下标,若数组中该下标没有存没有元素,则存储该对象,否则使用开放一次向前检测空位置。

    int i = key.threadLocalHashCode & (len-1);

      2. 如果没有空元素,向前依次访问。

    ((i + 1 < len) ? i + 1 : 0)

      

    为什么使用ThreadLocal作为key,而不是线程作为key?

      需要注意的是,每次set/get值,不直接用线程id来作为ThreadLocalMap的key,因为若直接用线程id当作key,无法区分放入ThreadLocalMap中的多个value。所以是使用ThreadLocal作为key,因为每一个ThreadLocal对象都可以由threadLocalHashCode属性(final修饰,每次实例创建后就不会更改了)唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同value。

    ThreadLocal与Thread的引用关系 

      Entry对象中的key弱引用ThreadLocal对象。具体引用关系如下图: 

     

    key弱引用ThreadLocal对象原因及解决方法

      如果key强引用ThreadLocal对象,当栈中不需要ThreadLocal对象并将ThreadLocal引用置为null,让gc回收时,发现不能回收ThreadLocal,因为此时线程通过key强引用threadLocal,在线程结束之前都不会使用的对象,却不能回收时,这就是内存泄漏。

      解决方法:key弱引用ThreadLocal对象。弱引用会在gc时,将引用的对象回收。这样当给强引用置为null时,ThreadLocal对象就会被回收。

      新问题:虽然弱引用在gc后ThreadLocal对象被回收,但是null和value数据还是封装在Entry数组中,key为null的数据永远不会被访问,造成了内存泄漏。并且如果线程处于线程池中,那么线程不会被销毁,在该线程复用的过程中,很长时间都会携带这些信息,造成严重的内存泄漏问题。

      解决方法:ThreadLocal的set和get方法在遇到key为null时,会自动删除该entry对象,但是如果没有调用这些方法,那么问题还是没有解决。必须在使用完threadLocal后,调用remove删除entry对象。

      

      内存泄漏总结:

        1. threadLocal对象不使用了,但是就是gc不掉,这是因为线程没结束的原因。

        2.entry数组一直null和value的entry对象,这是应为没有调用remove方法。

      

    ThreadLocal为什么要加static

      优点:

      作为一个具体类的成员变量,如果不加static,那么每次创建这个具体的类的时候都新建一个ThreadLocal<?>对象,

      加了static后,整个类中就只有一份ThreadLocal对象,节省堆空间。ThreadLocal代码形式如下:

    public class SequenceNumber {  
       private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){  
          public Integer initialValue(){  
           return 0;  
        }  
       };  
    } 

      缺点:

      加了static后,还是会产生内存泄漏问题。及(null,value)没有释放掉。解决方法还是remove()。

    ReentrantLock与AQS

      AQS,即队列同步器,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

      同步器的主要使用方法是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁(重入的次数),当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

      AQS通过内置的FIFO同步队列来完成线程获取资源的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

      同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

      共享式获取和独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以读写锁ReentrantReadWriteLock为例,它的读取锁ReadLock是共享式的,可以允许多个程序同时对文件进行读操作;但它的写入锁WriteLock是独占式的,同一时刻只能允许一个线程对文件进行写操作。

      独占锁主要代码如下:

    public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }
    • tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
    • addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
    • acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
    • selfInterrupt:产生一个中断。

    共享锁主要代码如下:

    public final void acquireShared(int arg) {
      if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
    }

      方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。同样,tryAcquireShared()方法也需要自定义同步组件自己实现。在ReentrantReadWriteLock.ReadLock中重写的tryAcquireShared()方法中,通过获取锁的共享计数是否超过限制(MAX_COUNT,65535)来进行判断。

      锁的公平性,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获得锁,也就是说锁获取是顺序的。当然,公平锁机制往往没有非公平的效率高,但也能够减少“饥饿”发生的概率。

      公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。 释放锁不存在公平性和非公平性。

      比较公平锁和非公平锁获取同步状态的tryAcquire()方法(以独占式排他锁ReentrantLock为例,所以是tryAcquire(),如果是共享锁,则是tryAcquireShared()),两者区别在于公平锁在获取同步状态时多了一个限制条件hasQueuedPredecessors(),定义如下。

    public final boolean hasQueuedPredecessors() {
      Node t = tail; //尾节点
      Node h = head; //头节点
      Node s;
      //头节点 != 尾节点
      //同步队列第一个节点不为null
      //当前线程是同步队列第一个节点
      return h != t &&
      ((s = h.next) == null || s.thread != Thread.currentThread());
    }

      该方法主要判断当前线程是否位于CLH同步队列中的第一个,如果是则返回true,否则返回false。这点保证了公平性。 具体代码强烈建议这块去看一下ReentrantReadWriteLock、ReentrantLock类的源码。

      非公平锁:  

    • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
    • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

      可中断锁:  

        ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

           那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?根据它们的实现语义来理解,我认为lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。

      定时锁:

        当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。 

      条件锁:

        Condition类能实现synchronized和wait、notify搭配的功能,另外比后者更灵活,Condition可以实现多路通知功能,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活。而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在这个对象上。线程开始notifyAll时,需要通知所有的WAITING线程,没有选择权,会有相当大的效率问题。条件锁代码如下:

    public class BlockingQueue<T> {
        private Queue<T> mQueue = new LinkedList<>();
        private int mCapacity;
        private Lock mLock = new ReentrantLock();
        private Condition mNotFull = mLock.newCondition();
        private Condition mNotEmpty = mLock.newCondition();
    
        public BlockingQueue(int capacity) {
            this.mCapacity = capacity;
        }
    
        public synchronized void put(T element) throws InterruptedException{
            mLock.lockInterruptibly();
            try {
                while (mQueue.size() == mCapacity){
                    mNotFull.await();
                }
                mQueue.add(element);
                mNotFull.signal();
            }finally {
                mLock.unlock();
            }
        }
    
        public synchronized T take() throws InterruptedException{
            mLock.lockInterruptibly();
            try {
                while (mQueue.size() == 0){
                    mNotEmpty.await();
                }
                T item = mQueue.remove();
                mNotEmpty.signal();
                return item;
            }finally {
                mLock.unlock();
            }
        }
    
    }

      条件锁总结:mNotFull这个Condition进行signal()时,会唤醒注册了mNotFull条件的线程。而不会唤醒没有注册mNotFull的线程。nNotEmpty同理。

      AQS组件总结:

    • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
    • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
    • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

    synchronized

      1. 当一个线程进入某一个对象的一个synchronized的实例方法后,其他线程是否可以进入此对象的其他方法?

      如果该实例的其他方法没有synchronized修饰的话,其他线程是可以进入的。

      另外,需要注意的是,synchronized是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),static synchronized是类锁(该锁针对的是类,无论实例多少个对象,那么线程都是共享该锁),并且对象锁与类锁互不干扰,与对象无关。

    • synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块(注:是所有),注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。
    • static synchronized恰好就是要控制类的所有实例的并发访问,static synchronized是限制多线程中该类的所有实例同时访问jvm中该类所对应的代码块。

    synchronized锁优化

      锁优化 

       锁优化详细

    线程池相关

      线程池

  • 相关阅读:
    201671030123叶虹 实验十四 团队项目评审&课程学习总结
    201671030123叶虹《英文文本统计分析》结对项目报告
    201671030123 叶虹 实验三作业互评与改进报告
    《构建之法》——三个问题
    201671030129 周婷 实验十四 团队项目评审&课程学习总结
    201671030129 周婷 《英文文本统计分析》结对项目报告
    201671030129 词频统计项目报告
    201671030129 周婷 实验三:作业互评与改进
    快速通读《现代软件工程——构建之法》
    201673020127 郁文曦 课程学习总结
  • 原文地址:https://www.cnblogs.com/yulianggo/p/13437649.html
Copyright © 2011-2022 走看看