美文网首页
并发编程

并发编程

作者: 34sir | 来源:发表于2019-01-29 10:08 被阅读4次

    synchronized和Lock的区别

    https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1

    组合关系

    线程-->进程-->程序
    线程是程序执行流的最小单位 进程是程序进行资源调度的独立单位

    Thread的几个重要方法

    • join 等待线程结束
    • sleep 线程进入等待

    wait 和 notify?
    这两位是Object的方法 两者配合使用 分别标识线程的挂起和恢复
    wait会释放锁 sleep不会释放锁

    线程状态

    线程状态.png
    • 新建状态: 新建线程对象 调用start之前
    • 就绪状态: 调用start方法进入就绪 线程在挂起和睡眠恢复的时候也进入就绪状态
    • 运行状态: 执行run方法
    • 阻塞状态: 线程暂停 比如调用sleep方法
    • 死亡状态: 线程执行完成

    锁类型

    • 可重入锁
      执行对象中所有同步方法不能再次获得锁
    • 可中断锁
      等待获取锁的过程中可中断
    • 公平锁
      按等待时间获取锁 等待越长优先级越高
    • 读写锁
      读取和写入分两部分 读的时候多线程一起读 写的时候同步写

    synchronized与Lock的区别

    类别 synchronized Lock

    类别 synchronized Lock
    存在层次 Java的关键字,在jvm层面上 是一个类
    锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
    锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
    锁状态 无法判断 可以判断
    锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
    性能 少量同步 大量同步

    Lock详解

    public interface Lock {
    
        /**
         * Acquires the lock.
         */
        // 获取锁 如果锁被暂用 一直等待
        void lock();
    
        /**
         * Acquires the lock unless the current thread is
         * {@linkplain Thread#interrupt interrupted}.
         */
        // 用该锁的获取方式 如果线程在获取锁的阶段进入了等待 那么可以中断此线程 先去做别的事情
        void lockInterruptibly() throws InterruptedException;
    
        /**
         * Acquires the lock only if it is free at the time of invocation.
         */
        // 锁被占用 返回false 否则返回true
        boolean tryLock();
    
        /**
         * Acquires the lock if it is free within the given waiting time and the
         * current thread has not been {@linkplain Thread#interrupt interrupted}.
         */
        // 比起tryLock()就是给了一个时间期限,保证等待参数时间
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
        /**
         * Releases the lock.
         */
        // 释放锁
        void unlock();
    
    }
    

    使用

    • lock
    // ReentrantLock是Lock的一种实现
    private Lock lock = new ReentrantLock();
    
        //需要参与同步的方法
        private void method(Thread thread){
            lock.lock();
            try {
                System.out.println("线程名"+thread.getName() + "获得了锁");
            }catch(Exception e){
                e.printStackTrace();
            } finally {
                System.out.println("线程名"+thread.getName() + "释放了锁");
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            LockTest lockTest = new LockTest();
    
            //线程1
            Thread t1 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    lockTest.method(Thread.currentThread());
                }
            }, "t1");
    
            // 线程2
            Thread t2 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    lockTest.method(Thread.currentThread());
                }
            }, "t2");
    
            t1.start();
            t2.start();
        }
    }
    

    执行情况:
    // 线程名t1获得了锁
    // 线程名t1释放了锁
    // 线程名t2获得了锁
    // 线程名t2释放了锁

    • tryLock
    private Lock lock = new ReentrantLock();
    
        //需要参与同步的方法
        private void method(Thread thread){
    /*      lock.lock();
            try {
                System.out.println("线程名"+thread.getName() + "获得了锁");
            }catch(Exception e){
                e.printStackTrace();
            } finally {
                System.out.println("线程名"+thread.getName() + "释放了锁");
                lock.unlock();
            }*/
    
    
            if(lock.tryLock()){
                try {
                    System.out.println("线程名"+thread.getName() + "获得了锁");
                }catch(Exception e){
                    e.printStackTrace();
                } finally {
                    System.out.println("线程名"+thread.getName() + "释放了锁");
                    lock.unlock();
                }
            }else{
                System.out.println("我是"+Thread.currentThread().getName()+"有人占着锁,我就不要啦");
            }
        }
    
        public static void main(String[] args) {
            LockTest lockTest = new LockTest();
    
            //线程1
            Thread t1 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    lockTest.method(Thread.currentThread());
                }
            }, "t1");
    
            Thread t2 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    lockTest.method(Thread.currentThread());
                }
            }, "t2");
    
            t1.start();
            t2.start();
        }
    

    执行结果:
    // 线程名t2获得了锁
    // 我是t1有人占着锁,我就不要啦
    // 线程名t2释放了锁

    平衡锁(公平锁)

     /**
         * Sync object for non-fair locks
         */
        // 平衡锁
        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() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }
    
        /**
         * Sync object for fair locks
         */
        // 非平衡锁
        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();//默认非公平锁
        }
    

    两种锁的底层实现

    • synchronized
      字节指令控制程序
    • Lock
      synchronized是一种悲观锁 吃之前先把自己关起来
      Lock呢底层其实是CAS乐观锁 别人抢吃的 它再去拿

    尽量使用synchronized而非Lock

    开启线程的三种方式

    ① 继承Thread

    public class FirstThreadTest extends Thread{
        int i = 0;
        //重写run方法,run方法的方法体就是现场执行体
        public void run()
        {
            for(;i<100;i++){
            System.out.println(getName()+"  "+i);
            
            }
        }
        public static void main(String[] args)
        {
            for(int i = 0;i< 100;i++)
            {
                System.out.println(Thread.currentThread().getName()+"  : "+i);
                if(i==20)
                {
                    new FirstThreadTest().start();
                    new FirstThreadTest().start();
                }
            }
        }
     
    }
    

    ② 实现Runnable

    public class RunnableThreadTest implements Runnable
    {
     
        private int i;
        public void run()
        {
            for(i = 0;i <100;i++)
            {
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
        }
        public static void main(String[] args)
        {
            for(int i = 0;i < 100;i++)
            {
                System.out.println(Thread.currentThread().getName()+" "+i);
                if(i==20)
                {
                    RunnableThreadTest rtt = new RunnableThreadTest();
                    new Thread(rtt,"新线程1").start();
                    new Thread(rtt,"新线程2").start();
                }
            }
     
        }
     
    }
    

    ③ Callable和Future 创建线程

    public class CallableThreadTest implements Callable<Integer>
    {
     
        public static void main(String[] args)
        {
            CallableThreadTest ctt = new CallableThreadTest();
                    // 包装Callable对象 
            FutureTask<Integer> ft = new FutureTask<>(ctt);
            for(int i = 0;i < 100;i++)
            {
                System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
                if(i==20)
                {
                    // FutureTask对象作为Thread对象的target
                    new Thread(ft,"有返回值的线程").start();
                }
            }
            try
            {
                System.out.println("子线程的返回值:"+ft.get());
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            } catch (ExecutionException e)
            {
                e.printStackTrace();
            }
     
        }
     
            // 实现call方法 作为线程执行体
        @Override
        public Integer call() throws Exception
        {
            int i = 0;
            for(;i<100;i++)
            {
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            return i;
        }
     
    }
    

    三种方式的对比

    • 实现Runnable、Callable接口
      优: 还可以继承其他类 多个线程可以共享同一个target对象 适合多个相同线程来处理同一份资源
      劣: 访问当前线程,则必须使用Thread.currentThread()方法 稍微复杂

    • 继承Thread
      与上述优劣相对应

    有了进程为什么还要线程?

    • 进程一个时间只能干一件事
    • 进程如果阻塞 整个进程就会挂起

    start和run的区别

    • start
      可以启动线程 会自动调用run方法
    • run
      Thread的普通方法 直接调用不会开启新的线程 还是主线程

    控制方法允许访问的并发线程个数

    Semaphore的两个重要方法:

    • semaphore.acquire()
      请求一个信号量 这时候信号量的个数-1 (一旦没有可使用的信号量 再次请求的时候就会阻塞)
    • semaphore.release()
      释放一个信号量 此时信号量个数+1
    // 保证5个线程在执行test方法
    private Semaphore mSemaphore = new Semaphore(5);
        public void run(){
            for(int i=0; i< 100; i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        test();
                    }
                }).start();
            }
        }
     
        private void test(){
            try {
                mSemaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 进来了");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 出去了");
            mSemaphore.release();
        }
    
    

    wait和notify关键字的理解

    • wait
      将当前状态置于休眠状态 直到接收通知或者被中断为止
      调用此方法之前 线程必须获得该对象的对象级别锁 即只能在同步方法或者同步块中调用此方法
    • notify
      只能在同步方法或者同步块中调用此方法 其他线程等待的是当前线程调用的这个方法 而不是当前线程释放的锁 这与notifyAll 有所不同

    小结

    • 线程调用对象的wait方法 线程进入该对象的等待池中 不会参与此对象锁的竞争
    • 线程调用notify或者notifyAll 进入该对象的锁池中 参与锁的竞争
    • 锁池的线程没有竞争到锁会留在锁池中 直到wait会重新进入到等待池中 获得锁的线程继续执行 直到执行完synchronized代码块释放锁

    什么会导致线程阻塞

    • 线程执行了Thread.sleep(intmillsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
    • 线程执行同步代码 但是无法获得同步锁 只能进入阻塞状态 等到获取到同步锁恢复执行
    • 执行了wait 等待 notify
    • 执行某些IO操作 等待相关资源进入阻塞

    结束线程

    • 使用标志位
      不是很靠谱
    public class MyThread implements Runnable{
        // 需要volatile修饰 
        private volatile boolean isCancelled;
        
        public void run(){
            while(!isCancelled){
                //do something
            }
        }
        
        public void cancel(){   isCancelled=true;    }
    }
    
    • 中断
      Thread三个重要方法:
      ① public void interrupt() //线程的中断状态会被设置为true
      ② public boolean isInterrupted()
      ③ public static boolean interrupted(); // 清除中断标志,并返回原状态
    public class InterruptedExample {
    
        public static void main(String[] args) throws Exception {
            InterruptedExample interruptedExample = new InterruptedExample();
            interruptedExample.start();
        }
    
        public void start() {
            MyThread myThread = new MyThread();
            myThread.start();
    
            try {
                Thread.sleep(3000);
                myThread.cancel();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private class MyThread extends Thread{
    
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println("test");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println("interrupt");
                        //抛出InterruptedException后中断标志被清除,如果什么都不做等于吞噬中断.标准做法是再次调用interrupt恢复中断
                        //调用interrupt方法不会真正中断线程 只是发出请求在合适时候结束自己
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("stop");
            }
    
            public void cancel(){
                interrupt();
            }
        }
    }
    

    什么时候都不应该吞掉中断!每个线程都应该有合适的方法响应中断!

    用Java库

    Executor框架提供了Java线程池的能力,ExecutorService扩展了Executor,提供了管理线程生命周期的关键能力
    ExecutorService.submit返回了Future对象来描述一个线程任务,它有一个cancel()方法

    public static void main(String[] args) throws Exception {
            ExecutorService es = Executors.newSingleThreadExecutor();
            Future<?> task = es.submit(new MyThread());
    
            try {
                //限定时间获取结果
                task.get(5, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                //超时触发线程中止
                System.out.println("thread over time");
            } catch (ExecutionException e) {
                throw e;
            } finally {
                boolean mayInterruptIfRunning = true;
                //对任务所在线程发出中断请求  mayInterruptIfRunning标识任务是否能够接收到中断请求
               //为true 任务如果在某个线程中运行 那么这个线程能够被中断
               //为false 如果任务还未启动 就不要运行
                task.cancel(mayInterruptIfRunning); 
            }
        }
    
        private static class MyThread extends Thread {
    
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {   
                    try {
                        System.out.println("count");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println("interrupt");
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("thread stop");
            }
    
            public void cancel() {
                interrupt();
            }
        }
    

    同步方法

    为什么使用同步?
    多个线程同时操作一个可共享的变量时 保证该变量的唯一性准确性

    • 同步方法
      synchronized 修饰方法
      synchronized修饰静态方法 会锁住整个类

    • 同步代码块
      synchronized 修饰代码块

    • volatile
      ① 为域变量提供一个免锁机制
      ② 修饰域等于告诉虚拟机该域有可能被其他线程更新
      ③ 使用该域需要重新计算 不是直接使用寄存器中的值
      ④ 不提供原子操作 不能修饰final类型的变量

    • 重入锁
      ReentrantLock 可重入 互斥 实现了Lock接口的锁
      常用方法:

    • lock() 获取锁

    • unlock() 释放锁

    private int account = 100;
                //需要声明这个锁
                private Lock lock = new ReentrantLock();
                public int getAccount() {
                    return account;
                }
                //这里不再需要synchronized 
                public void save(int money) {
                    lock.lock();
                    try{
                        account += money;
                    }finally{
                        lock.unlock();
                    }
                }
    

    局部变量实现线程同步

    ThreadLocal管理变量 每个使用该变量的线程都将获得该变量的副本 每个线程都可以修改自己的副本 对其他线程没有影响

    ThreadLocal常用方法:

    • get() 返回当前副本中的值
    • initialValue() 返回当前线程的初始值
    • set(T value) 将当前副本中的值设为value
    //使用ThreadLocal类管理共享变量account
                private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
                    @Override
                    protected Integer initialValue(){
                        return 100;
                    }
                };
                public void save(int money){
                    account.set(account.get()+money);
                }
                public int getAccount(){
                    return account.get();
                }
    

    ThreadLocal相比较同步机制 前者是"空间换时间" 后者是"时间换空间"
    ThreadLocal最常见的使用场景:数据库连接、Session管理

    在Android中的体现:Handler通过ThreadLocal获取对应线程的Looper

    Java中的数据一致性

    原子性 可见性 有序性
    Volatile × ×
    Snychronized
    Final ×

    内存模型

    每一个线程都有一个工作线程和主存隔离; 工作内存中存放主存中值的拷贝

    主存-->工作内存

    • 主存read
    • 工作内存load

    工作内存-->主存

    • 工作内存store
    • 主存write

    以上的四种操作都是原子性的

    Java对象的生命在周期

    • 创建
    • 应用
    • 不可见
    • 不可达
    • 收集
    • 终结
    • 对象空间重分配

    怎么判断Java对象无用

    两种算法:

    • 引用计数
      产生闭环的时候 无法回收
    • 根集算法
      遍历引用关系 能够遍历到的叫做引用可达 遍历不到的叫做引用不可达 不可达的对象会被回收

    变量回收之后的内存处理

    三种算法:

    • 标记-清除
    标记状态.png 清除状态.png

    由上图可见清除之后内存状态可能不是连续的
    如果无法给一个大对象分配一个足够大的连续内存空间 那么GC不得不重新做一次内存整理
    因为不是连续的 所以标记和清除的过程都需要遍历识别内存区域 整个过程效率比较低

    • 标记-复制
      标记的过程不变
      内存分为两部分:
      ① 预留区域
      ② 非预留区域

    预留区域 不分配对象 GC的时候把正在使用的对象复制到预留区域 然后把非预留区域以外的 内存全部清除(典型以空间换时间的做法)

    • 标记-整理
      标记过程不变 处于末端的正在使用的对象向前移动占据覆盖那些被标记了的区域 把剩余的对象赶到一起 清除标记的剩余对象
    标记-整理 过程.png
    • 分代混合
      针对对象的生命周期来划分区域 不同区域使用不同算法
      一般分为:
    • 新生代 生命不长 GC的时候大部分对象已经死亡 有足够的空间 可以使用标记-复制
    • 老生代 使用标记-清除 标记-整理算法
    分代混合.png

    深入浅出synchronized

    synchronized保证统一时刻只有一个线程访问临界区 同时保证共享变量的内存可见性

    Java中每个对象都可以作为锁

    • 普通同步方法
      锁是当前实例对象
    • 静态同步方法
      锁是当前类的class对象
    • 同步代码块
      锁是括号中的对象
    public class WaitNotify {
        static boolean flag = true;
        static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread A = new Thread(new Wait(), "wait thread");
            A.start();
            TimeUnit.SECONDS.sleep(2);
            Thread B = new Thread(new Notify(), "notify thread");
            B.start();
        }
    
        static class Wait implements Runnable {
            @Override
            public void run() {
                synchronized (lock) {
                    while (flag) {
                        try {
                            System.out.println(Thread.currentThread() + " flag is true");
                            lock.wait(); //调用对象的wait方法进入等待
                        } catch (InterruptedException e) {
    
                        }
                    }
                    System.out.println(Thread.currentThread() + " flag is false");
                }
            }
        }
    
        static class Notify implements Runnable {
            @Override
            public void run() {
                synchronized (lock) {
                    flag = false;
                    lock.notifyAll(); //调用notifyAll方法  线程A收到通知后从wait继续执行 对flag的修改是对线程A可见的
                    try {
                        TimeUnit.SECONDS.sleep(7);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    ;运行过程需要注意一下几点:

    • 使用wait notify 和notifyAll的时候需要对对象先加锁 调用wait的时候会释放锁
    • 调用wait之后 线程从RUNNING变成WAITING 并将当前线程放置到对象的等待队列中
    • 调用notify或者notifyAll的方法后 等待线程不会,立即从wait返回 需要等当前线程释放锁
    • notify和notifyAll都将等待线程从等待队列移到同步队列中 被移动的线程从WAITING变成BLOCKED
    • 从wait返回的前提是等待线程获得了调用对象的锁

    如何实现线程间的互斥性和可见性?

     0 getstatic #2 <com/example/ckc/test/SynchronizedTest.object>
     3 dup
     4 astore_1
     5 monitorenter  //监视器进入 获取锁
     6 aload_1
     7 monitorexit   //监视器退出 释放锁
     8 goto 16 (+8)
    11 astore_2
    12 aload_1
    13 monitorexit
    14 aload_2
    15 athrow
    16 return
    
    public static synchronized void m();
       descriptor: ()V
       flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
       Code:
         stack=0, locals=0, args_size=0
            0: return
         LineNumberTable:
           line 9: 0
    

    可以看出:

    • 同步代码块使用 monitorentermonitorexit 实现
    • 同步方法依靠修饰符 ACC_SYNCHRONIZED 实现

    无论哪种情况都是对指定对象相关联的monitor的获取 这个过程是互斥的 同一个时刻只能有一个线程能够成功 其他失败的线程会被阻塞 并加入到同步队列中 进入blocked状态

    锁的内部机制

    锁的四种状态:
    '- 无锁

    • 偏向锁
    • 轻量级锁
    • 重量级锁

    两个概念: ① 对象头 ② monitor

    hotspot虚拟机(Java虚拟机)中 对象在内存的分布分为三个部分:① 对象头 ②实例数据 ③ 对齐填充

    • 对象头
      包括两部分信息:
      ① 对象自身的运行时数据 比如:哈希码 GC分代年龄 锁状态标志 线程持有的锁 偏向线程ID 偏向时间戳 这些数据就是“Mark Word”
      ② 类型指针 即对象指向他的元数据的指针 虚假机通过这个指针确定这个对象是哪个类的实例

    Mark Word 被分为两个部分:①lock word ②标志位

    对象.png

    Klass ptr 指向Class字节码在虚拟机内部的对象表示的地址
    Fields 表示连续的对象实例字段

    Mark Word被设计为非固定的数据结构 目的是在极小的控件存储更多的信息

    32位的hotspot虚拟机中:如果对象处于未被锁定的情况下。mark word 的32bit空间中有25bit存储对象的哈希码、4bit存储对象的分代年龄、2bit存储锁的标记位、1bit固定为0
    其他的状态下(轻量级锁、重量级锁、GC标记、可偏向)下对象的存储结构为


    Paste_image.png
    • monitor
      线程私有的数据结构 每一个线程都有一个monitor列表 同时还有一个全局的可用列表
      monitor内部:


      monitor.png

    ① Owner: 初始化时为NULL 标识当前没有线程拥有该monitor 线程拥有该锁后保存线程唯一标识 锁被释放时又设为NULL
    ② EntryQ: 关联一个系统互斥锁 阻塞所有试图锁住monitor失败的线程
    ③ RcThis: blocked或waiting在该monitor上的所有线程的个数
    ④ Nest: 实现重入锁的计数
    ⑤ HashCode: 保存从对象头拷过来的的HashCode值(可能还含有GC age)
    ⑥ Candidate: 用来避免不必要的阻塞或者等待线程唤醒 因为每一次只有一个线程能够拥有锁 如果每次前一个释放锁的线程唤醒所有阻塞或者等待的线程 回引起不必要的上下文切换(从阻塞到就绪到竞争失败再到阻塞)
    只有两种可能值:① 0标识没有需要唤醒的线程 ② 1标识需要唤醒一个继任线程来竞争锁

    monitor的作用是什么?
    Java虚拟机中 线程一旦进入synchronized 修饰的同步块 指定的锁对象将对象头中的LockWord指向monitor的起始地址与之关联 Owner存放该锁的线程的唯一标识 确保一次只能有一个线程执行这部分代码

    偏向锁

    public class SynchronizedTest {
        private static Object lock = new Object();
        public static void main(String[] args) {
            //访问method1时会在对象头(SynchronizedTest.class的对象头)和栈帧的锁记录中存储偏向锁的线程ID
            method1();
            //访问method2时只需要判断对象头的线程ID是否为当前线程 不需要CAS操作进行加解锁
            method2();
        }
        synchronized static void method1() {}
        synchronized static void method2() {}
    }
    

    什么是CAS锁操作?
    对比交换 是一条CPU的原子指令 作用是让CPU比较两个值是否相等 然后原子性的更新某个位子的值
    相对于重量级锁来说 开销较小

    轻量级锁

    利用了CAS操作

    线程可以通过两种方式锁住一个对象

    • 膨胀一个处于无锁状态(状态位001)的对象获取该对象的锁
    • 对象处于膨胀状态(状态位00) 但LockWord指向的monitor的Owner是NULL 可以通过CAS原子指令将Owner设置为自己的标识来获取锁

    获取锁的过程:
    ① 对象处于无锁状态时 (LockWord的值为hashCode等,状态位为001) 线程首先从monitor列表中获取一个空闲的monitor 初始化Nest和Owner值为1和线程标识 通过CAS替换monitor起始位置到LockWord进行膨胀 如果存在其他线程竞争锁而导致CAS失败 则回到monitorender重新开始获取锁的过程
    ② 对象已经膨胀 monitor中的Owner指向当前线程 这是重入锁的情况 将Nest+1 不需要CAS操作 效率高

    • 对象已经膨胀 monitor的Owner为NULL monitor中的Owner为NULL 此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁 竞争失败的线程则进入第四种情况
    • 对象已经膨胀 同时Owner指向别的线程 在调用操作系统的重量级的互斥锁之前自旋一定次数 达到一定的次数时如果还是没有获得锁 则开始进入阻塞状态 将rfThis值原子加1 由于在+1的过程中可能被其他线程破坏对象和monitor之间的联系 所以在加1后需要再进行一次比较确保lock word的值没有被改变 当发现被改变后则要重新进行monitorenter过程 同时再一次观察Owner是否为NULL 如果是则调用CAS参与竞争锁 锁竞争失败则进入到阻塞状态

    释放锁的过程:

    • 检查该对象是否处于膨胀状态并且这个该线程是这个锁的拥有者 如果不是抛出异常
    • 检查Nest 字段是否大于1 如果Nest大于1就减一并继续持有锁 等于1进入步骤3
    • 检查rfThis是否大于0 大于0那么设置Owner为NULL然后唤醒一个正在阻塞或者等待的线程再一次试图获取锁 如果等于0 进入步骤4
    • 将对象的LockWord置换为原来的HashCode等值解除和monitor之间的关联来释放锁 同时将monitor放回线程私有的可用monitor列表

    重量级锁

    其他线程试图获取锁都会被阻塞 持有锁的线程释放掉该锁后会唤醒这些线程

    内存可见性

    • 线程释放锁时 JMM(Java内存模型) 会把该线程对应的本地内存中的共享变量刷新到主存中
    • 线程获取锁时 JMM(Java内存模型) 会把该线程对应的本地内存置为无效 使得被监视器保护的临界区的代码必须从主存中读取共享变量

    锁的可重入

    public class UnReentrant{
        Lock lock = new Lock();
        public void outer(){
            // 此时outer已经获取到锁 不能在inner中重复利用已经获取到的锁资源 这种锁称为不可重入 也叫自选锁      
            lock.lock();
            inner();
            lock.unlock();
        }
        public void inner(){
            lock.lock();
            //do something
            lock.unlock();
        }
    }
    

    可重入意味着什么?
    线程可以进入任何一个它已经拥有的锁所同步着的代码块

    可重入锁

    • synchronized
    • java.util.concurrent.locks.ReentrantLock

    ReentrantLock对比synchronized

    • ReentrantLock具有某些特性:时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票
    • ReentrantLock具有可伸缩性

    死锁的四个必要条件

    • 互斥条件:一个资源每次只能被一个进程使用
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件:进程已经使用的进程 在未使用完之前不能强行剥夺
    • 循环等待条件:若干非线程形成一个头尾相接的循环等待关系

    对象锁和类锁是否会相互影响?

    不会

    线程池

    为什么使用线程池?

    • 创建和销毁线程 会很大的影响处理效率 线程池缓存线程可以避免创建和销毁线程带来的开销
    • 线程并发数量过多 抢占资源容易造成阻塞 线程池可以控制最大并发数 避免阻塞
    • 可以对线程进行一些简单的管理 比如:延时执行 定时循环执行

    ThreadPoolExecutor

    • corePoolSize
      线程池中核心线程的最大数

    核心线程
    创建线程时 如果当前线程数量小于corePoolSize 那么创建的时核心线程
    核心线程会一直存在与线程池中 即使时闲置状态
    如果设置allowCoreThreadTimeOut为true 那么闲置的核心线程会在适当时机销毁

    • maximumPoolSize

    线程总数最大值

    • keepAliveTime

    非核心线程闲置超时时长
    如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

    • TimeUnit unit
      keepAliveTime的单位
    • BlockingQueue<Runnable> workQueue

    该线程池中的任务队列 维护着等待执行的Runnable对象
    所有的核心线程都在干活时 新添加的任务会被添加到这个队列中等待处理 如果队列满了 新建非核心线程处理任务
    常用的类型:
    1⃣️ SynchronousQueue
    接受任务直接交给线程 如果线程都在工作 那么就新建线程 保证不会因为超过线程的最大数量出现无法新建线程的问题
    2⃣️ LinkedBlockingQueue
    如果当前线程数小于核心线程 那么新建核心线程 如果超过则放入队列中 此队列没有最大值限制
    3⃣️ ArrayBlockingQueue
    可以限定队列的长度 如果没有达到corePoolSize的值 就新建核心线程 如果达到了就入队等候 如果队列已满就新建线程 如果队列已满并且超过maximumPoolSize则异常
    4⃣️ DelayQueue
    任务先入队 达到指定的延迟时间 才会执行

    添加任务

    ThreadPoolExecutor.execute(Runnable command)

    ThreadPoolExecutor的策略

    一个任务被添加到线程池时:

    • 线程数量未达到corePoolSize 新建一个核心线程执行任务
    • 线程数量达到corePoolSize 将任务移入队列等候
    • 队列已满 新建线程执行任务
    • 队列已满 总线程数超过最大线程数 抛出异常

    常见的四种线程池

    1⃣️ CachedThreadPool
    可缓存线程池

    • 线程数无限制
    • 复用空闲线程
    • 一定程度减少创建销毁的系统开销

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

    2⃣️ FixedThreadPool

    • 可控制线程最大并发数
    • 超出的线程会在队列中等待
    //nThreads => 最大线程数即maximumPoolSize
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
    

    3⃣️ ScheduledThreadPool

    • 支持定时或者周期性的执行任务
    //nThreads => 最大线程数即maximumPoolSize
    ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
    

    4⃣️ SingleThreadExecutor

    • 有且只有一个工作线程执行任务
    • 所有任务按照一定的顺序执行 队列的出队入队

    ExecutorService singleThreadPool = Executors.newSingleThreadPool();

    对于并发编程的理解

    并发编程的原则技巧

    • 单一指责
      分离并发相关代码和其他代码
    • 限制数据作用域
      两个线程修改共享对象可能会造成干扰 解决方案之一是构建临界区 但是必须限制临界区的数量
    • 使用数据副本
      数据副本是避免共享数据的好办法 复制出来的对象只读 CopyOnWriteArrayList使用了写时复制的方式创建数据副本进行操作来避免 共享数据的并发问题
    • 线程应该尽可能独立
      尽量避免与其他线程共享数据

    关于ConcurrentHashMap

    既然有Collections.synchronizedMap 为什么需要ConcurrentHashMap?
    因为前者会对整个容器对象上锁 意味着需要一直等到前一个线程离开同步代码块时才有机会执行
    但是 修改HashMap时没必要将整个HashMap对象锁住 只需要锁住对应的桶

    ConcurrentHashMap提供了对应的原子操作的方法:

    • putIfAbsent 如果还没有对应的键值对映射 就将其添加到HashMap中
    • remove 如果键存在而且值与当前状态相等(equals 为true) 利用原子方式移除键值对映射
    • replace 替换掉映射中元素的原子操作

    关于CopyOnWriteArrayList

    ArrayList并发下的替代品 通过增加写时复制语义避免并发访问引起的问题 任何底层操作都会在底层创建一个列表的副本 对于不要求严格读写同步的场景很有用 性能较高 (牺牲空间换时间的做法)

    相关文章

      网友评论

          本文标题:并发编程

          本文链接:https://www.haomeiwen.com/subject/mjjgvftx.html