(二)

187 阅读16分钟

一.java内存模型

内存模型 这个概念。我们可以理解为:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理计算机可以有不一样的内存模型,JVM 也有自己的内存模型。 JVM 中试图定义一种 Java 内存模型(Java Memory Model, JMM) 来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序 在各种平台下都能达到一致的内存访问效果。

JMM 规定了所有的变量都存储在主内存(Main Memory)中。 每条线程还有自己的工作内存(Working Memory),工作内存中保留了该线程使用到的变量的主内存的副本。工作内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

原子性 原子性即一个操作或者多个操作,要么全部执行(执行的过程不会被任何因素打断),要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰
可见性 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 JMM 是通过 "变量修改后将新值同步回主内存, 变量读取前从主内存刷新变量值" 这种依赖主内存作为传递媒介的方式来实现的。
有序性 有序性规则表现在以下两种场景: 线程内和线程间

  • 线程内 - 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。
  • 线程间 - 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized 关键字修饰)以及 volatile 字段的操作仍维持相对有序。

JMM3.png

内存间交互操作
JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。JVM 实现时必须保证下面介绍的每种操作都是 原子的(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外 )。

  • lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
  • load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。
  • assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。

JMM 还规定了上述 8 种基本操作,需要满足以下规则:

  • read 和 load 必须成对出现;store 和 write 必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须把变化同步到主内存中。
  • 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
  • 个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load 或 assign 操作初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

二.多线程,锁

1.1线程的生命周期

image.png 1.1.1、新建状态(New)
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间。
1.1.2、就绪状态(Runnable)
线程对象创建后,其他线程调用了该对象的start()方法时进入就绪状态。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
1.1.3、运行状态(Running)
就绪状态的线程获取了CPU,执行程序代码。它可以变为阻塞状态、就绪状态和死亡状态
1.1.4、阻塞状态(Blocked)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。 wait()方法, sleep()或join()方法
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM 会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
1.1.5、死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

1.2多线程的使用

1)继承Thread类创建线程类; 2)实现Runnable接口创建线程类; 3)使用Callable和Future创建线程。

线程池

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

  1. corePoolSize:指定了线程池中的线程数量。
  2. maximumPoolSize:指定了线程池中的最大线程数量。
  3. keepAliveTime:当前线程池数量超过corePoolSize 时,多余的空闲线程的存活时间,即多 次时间内会被销毁。
  4. unit:keepAliveTime 的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

submit 返回一个 Future 对象,我们可以调用其 get 方法获取任务执行的结果。代码很简单,就是将 Runnable 包装成 FutureTask 而已。可以看到,最终还是调用 execute 方法

java.util.concurrent.Executors
根据返回的对象类型创建线程池可以分为三类:

创建返回ThreadPoolExecutor对象
Executors#newCachedThreadPool => 创建可缓存的线程池,无限创建 Executors#newSingleThreadExecutor => 创建单线程的线程池 Executors#newFixedThreadPool => 创建固定长度的线程池

创建返回ScheduleThreadPoolExecutor对象
创建返回ForkJoinPool对象

CountDownLatch
作用:是一个线程等待其他的线程完成工作以后在执行,加强版join,AQS来实现的

CountDownLatch latch = new CountDownLatch(3);
latch.countDown();//调用时计数器减一
latch.await();等待计数器归0

await 用来等待,countDown 负责计数器的减一
CyclicBarrier [ˈsaɪklɪk]
让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行,每个线程互相等待其他线程,ReentrantLock实现

public CyclicBarrier(int parties,, Runnable barrierAction)//线程数,回调函数
 public int await() //互相等待

CyclicBarrier中最重要的方法就是await方法,没有参数的版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
`` Semaphore [ˈseməfɔː(r)]

1.3锁

1.3.1 对象的布局

对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
mark word +klass pointer +array length(数组) 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。保证对象大小是8的倍数,便于分配内存

为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。

markword-64.png

对象分代年龄的分配的空间是4bit,而4bit能表示的最大数就是2^4-1 = 15。

1.3.2 锁的四种状态

锁的四种状态:没有锁,偏向锁,轻量级,重量级

偏向锁:不存在被线程争抢,通过线程id占有,当有多个线程争抢时,第一次升级--》轻量级锁,为了在只有一个线程时减少锁的竞争
轻量级锁:自旋锁,cpu操作,原地等待
实现方式:CAS(CompareAndSwap比较并替换)
1.获取当前值A
2.操作
3.重新读取A并比较
4.相等则更新为操作结果
5.不相等,修改A为当前值,重新循环过程
问题:
ABA,--解决方式,加版本号
原子性问题,A在3和4之间有被操作--解决,底层汇编 lock cmpxchg保证原子性

重量级锁:需要经过操作系统调度的锁,当轻量级锁自旋一定次数,升级为重量级 竞争激烈,线程等待时间长时,适用重量级锁

死锁

四个条件

  1. 互斥:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不能抢占:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

处理方法,破坏其中一个条件

1.3.3 synchronized关键字

mp.weixin.qq.com/s/3PBGQBR9D…

synchronized关键字在经过javac编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit两个字节码指令。这两个指令都需要一个reference类型的参数来指明要锁定和解锁的对象,如果代码中指定了对象参数,就以这个对象的引用作为reference,如果没有指定,就根据修饰的方法类型(实例方法或类方法)来决定取代码所在的对象还是取类型所对应的Class对象作为线程要持有的锁

在1.6 之前只是重量级锁。

因为会有线程的阻塞和唤醒,这个操作是借助操作系统的系统调用来实现的,常见的 Linux 下就是利用 pthread 的 mutex 来实现的。而涉及到系统调用就会有上下文的切换,即用户态和内核态的切换,我们知道这种切换的开销还是挺大的。

这里有个概念叫临界区。

我们知道,之所以会有竞争是因为有共享资源的存在,多个线程都想要得到那个共享资源,所以就划分了一个区域,操作共享资源资源的代码就在区域内。

可以理解为想要进入到这个区域就必须持有锁,不然就无法进入,这个区域叫临界区。

-- 当用 Synchronized 修饰代码块时 此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,我习惯按照临界区来理解,enter 就是要进入临界区了,exit 就是要退出临界区了,与之对应的就是获得锁和解锁。 每个对象都有一个 monitor 对象于之关联,执行 monitorenter 指令的线程就是试图去获取 monitor 的所有权,抢到了就是成功获取锁了。 这个 monitor 在 HotSpot 中是 c++ 实现的,叫 ObjectMonitor

--修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。 原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。

然后不论是正常退出还是异常退出,都会进行解锁的操作,所以本质还是一样的。

锁升级过程:

1.当锁对象第一次被线程获取时,jvm将对象头中的锁标志设为'01',偏向模式设为'1',同时CAS把该线程ID记录到Markword,一旦成功,持有偏向锁的线程每次进入锁相关的同步块时,jvm都不进行任何同步操作
2.一旦有另一个线程尝试获取锁,立即结束偏向模式,根据锁对象目前是否处于锁状态,将标志位恢复到未锁定'01',或锁定'00',偏向模式设置为'0'.
如果对象没有被锁定,jvm首先在当前线程的栈帧中建立一个锁记录的空间(Lock Record),用于存储Mark word的拷贝,然后CAS将Mark Word更新为指向Copy的指针,并将锁标志位设为'00'.
如果更新失败了,说明有其他线程在竞争锁,虚拟机会检查对象的Mark word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有对象锁,否则说明被其他线程抢占了.
如果同时出现两条以上线程抢占锁,则膨胀为重量级锁,锁标志变为'10',此时Mark word存储的就是指向重量级锁的指针

1.4 Lock

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    
    private final Sync sync;
    
     //抽象类,继承AQS,核心实现全部在AQS中
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        abstract void lock();
 
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    //非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            //调用lock直接尝试抢锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    //公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }
    }

   //默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

  
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    public void lock() {
        sync.lock();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

   
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public void unlock() {
        sync.release(1);
    }
    ...
    }