Java并发编程

多线程

线程的五种状态

新建状态(NEW)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

就绪状态(RUNNABLE)

当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

运行状态(RUNNING)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态

阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了cpu资源,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu资源转到运行(running)状态。阻塞的情况分三种:

  • 等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法, JVM 会把该线程放入等待队列(waitting queue)中
  • 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中
  • 其他阻塞(sleep/join):运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态

死亡状态(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态

  • 正常结束:run()或 call()方法执行完成,线程正常结束
  • 异常结束:程抛出一个未捕获的 Exception 或 Error
  • 调用stop():直接调用该线程的 stop()方法来结束该线程,该方法通常容易导致死锁,不推荐使用

创建线程的四种方式

继承Thread类

通过集成Thread类并重写run方法实现

    public static void main(String[] args) {
        MyThread myThread = new MyThread("Thread1");
        myThread.start();
    }

    static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程执行");
        }
    }

实现Runnable接口

通过实现Runnable接口并实现run方法创建一个线程

    public static void main(String[] args) {
        // 实现Runnable接口
        new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程执行"), "Thread1").start();
    }

结合lambda表达式一句代码搞定

实现Callable接口(带返回值)

通过实现Callable接口并实现call方法结合FutureTask创建一个线程,通常适用于带有返回值的情况

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 实现Callable接口
        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "线程执行");
            return 1;
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        // 阻塞等待线程执行结果
        Integer result = futureTask.get();
    }

终止线程的两种方式

stop方法(不推荐)

程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程

interrupt方法

    public static void main(String[] args) throws InterruptedException {
        // 实现Runnable接口
        Thread thread = new Thread(() -> {
            int i = 0;
            while (!Thread.interrupted()) {
                System.out.println(Thread.currentThread().getName() + "线程执行");
                i++;
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (i > 5) {
                    // 中断线程跳出循环
                    Thread.currentThread().interrupt();
                }
            }
        }, "Thread1").start();
    }

使用interrupt方法中断线程时如果线程处于阻塞状态的话会抛出异常,因此需要注意异常捕获之后break才能终止线程

线程池带来的好处

线程池是一种基于池化的思想的线程管理工具,通过线程池我们可以:

  1. 对历史创建的线程进行重复利用,从而减低线程频繁创建/销毁带来的资源损耗;
  2. 当任务到达时,我们可以直接从线程池中取出已经初始化好的线程立即执行,提高执行速率;
  3. 线程是服务器中昂贵的资源,毫无节制的去创建线程有资源耗尽的风险,通过线程池可以对线程进行合理的管控。

线程池的工作流程

四种线程池

newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。 调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。 因此,长时间保持空闲的线程池不会使用任何资源

newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在

newSchduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行,可用于定时任务执行

newSingleThreadPool

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程) ,这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去

守护线程

定义:守护线程--也称“服务线程”, 他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开,垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做, 所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源

Java锁

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作
Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换! 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁

Synchronized同步锁

synchronized它可以把任意一个非NULL的对象当作锁。他属于独占式悲观锁,同时属于可重入锁

Synchronized作用范围

  • 作用于方法时,锁住的是对象的实例,只会锁住调用该实例方法的线程
  • 作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程

Synchronized创建同步代码块

Synchronized创建同步代码块属于更细粒度的锁定,只锁定某个方法中的某段代码,其他代码还是可以被执行的

ReentrantLock

ReentantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁可轮询锁请求定时锁等避免多线程死锁的方法

Synchronized和ReentrantLock对比

相同点

  • 都是通过加锁的方式同步阻塞,以解决共享资源同步问题,都是悲观锁、可重入锁

不同点

  • Synchronized是java提供的关键字,是jvm级别的,ReentrantLock是API级别的
  • Synchronized是隐式加锁,ReentrantLock需要显式指定加锁解锁,且加锁解锁需成对出现
  • ReentrantLock可响应中断,即ockInterruptibly()方法,线程在等待获取锁时间过长时,可中断获取锁转而去做其他事,这个Synchronized做不到
  • ReentrantLock可实现公平锁
  • ReentrantLock可实现对阻塞等待的线程精准唤醒,即为ReentrantLock指定不同的Condition,根据情况调用不同的Condition阻塞等待,在唤醒时也可根据Condition对某一组线程唤醒——LinkedBlockingQueue

CAS

CAS即Compare And Swap/Set,是比较并交换的意思,算法过程如下:

CAS包含三个参数(VEN)V表示要更新的变量(内存值),E表示预期值(旧值),N表示新值,当且仅当V=E时,才会将V的值设置成N,如果V和E不同,说明在此期间有其他线程已经对该变量做了修改,则当前线程什么也不做。

CAS就是抱着乐观的态度进行的(乐观锁),它认为读多写少,只有在真正去编辑值的时候才去判断是否有别的线程做了修改,如果有则操作失败,因此整个过程是无锁的,不会导致其他线程等待,而且能够保证线程安全。

锁自旋

所谓自旋,就是当前线程如果通过CAS没有修改成功,则继续通过CAS的方式进行自旋尝试,直到操作完成未知,JDK1.5中的原子包java.util.atomic中的一组原子类,都是通过CAS+自旋保证在不加锁的情况下保证线程安全的。

ABA问题

CAS会导致ABA问题,比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。

部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少 。

AQS

AQS,即Abstract Queue Synchronizer,抽象队列同步器,AQS定义了一套多线程访问共享资源同步器框架,只是一个框架,具体如何控制多线程访问共享变量需要自行实现,许多同步类都依赖于他,如ReentrantLock、Semaphore信号量、CountDownLatch(计数器)

image-20210312214749466

AQS维护了一个volatile int state(代表共享资源),和一个FIFO的线程等待队列(多线程争用资源被阻塞时会进入此队列 )。

AQS定义资源的访问方式总共有两种:

  • Exclusive(独占式)即每次只能有一个线程能够执行,其他线程均被阻塞,如ReentrantLock
  • Share(共享式)可以有多个线程同步执行,如Semaphore、CountDownLatch

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

Java阻塞队列

ArrayBlockingQueue

LinkedBlockingQueue

DelayQueue

JUC框架思维导图

JUC并发包类库

posted @ 2020-10-11 22:55  ThomasYue  阅读(138)  评论(0编辑  收藏  举报