JAVA包

image.png


JAVA线程实现方式

  • 继承 Thread 类
  • 实现 Runnable 接口
  • ExecutorService、 Callable、 Future 有返回值线程

    有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    | //创建一个线程池ExecutorService pool = Executors.newFixedThreadPool(taskSize);// 创建多个有返回值的任务List list = new ArrayList();
    for (int i = 0; i < taskSize; i++) {
    Callable c = new MyCallable(i + “ “);// 执行任务并获取 Future 对象
    Future f = pool.submit(c);list.add(f);
    }// 关闭线程池pool.shutdown();// 获取所有并发任务的运行结果for (Future f : list) {
    // 从 Future 对象上获取任务的返回值,并输出到控制台
    System.out.println(“res: “ + f.get().toString());
    } | | —- |

  • 基于线程池的方式

    | // 创建线程池ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {
    threadPool.execute(new Runnable() { // 提交多个线程任务,并执行@Override
    public void run() {
    System.out.println(Thread.currentThread().getName() + “ is running ..”);
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    }
    } | | —- |


4种线程池

  • newCachedThreadPool

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

  • newFixedThreadPool

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

  • newScheduledThreadPool

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

    | ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
    scheduledThreadPool.schedule(newRunnable(){
    @Override
    public void run() {
    System.out.println(“延迟三秒”);
    }
    }, 3, TimeUnit.SECONDS);
    scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
    @Override
    public void run() {
    System.out.println(“延迟 1 秒后每三秒执行一次”);
    }
    },1,3,TimeUnit.SECONDS) | | —- |

  • newSingleThreadExecutor

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


线程生命周期

  • 新建(New)

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

  • 就绪(Runnable)

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

  • 运行(Running)

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

  • 阻塞(Blocked)

    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。
    直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(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)

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

image.png
补充:
sleep 与 wait 区别
1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
3. 在调用 sleep()方法的过程中, 线程不会释放对象锁。
4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

start 与 run 区别
1. start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。


JAVA锁

  • 乐观锁

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

  • 悲观锁

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

  • 自旋锁

    自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

  • Synchronized 同步锁

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

    • Synchronized 作用范围
      1. 作用于方法时,锁住的是对象的实例(this);
        2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
        3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。 它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
  • ReentrantLock

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

补充:
ReentrantLock 与 synchronized

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同, ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。
  • AtomicInteger

    首先说明,此处 AtomicInteger ,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、 AtomicInteger、 AtomicLong、 AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference将一个对象的所有操作转化成原子操作。
    我们知道, 在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是 ReentantLock 的好几倍。

  • 可重入锁(递归锁)

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

  • 公平锁

    加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

  • 非公平锁

    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
    1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
    2. Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁

  • ReadWriteLock 读写锁

    为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
    Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现
    ReentrantReadWriteLock。

  • 独占锁

    独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性

  • 共享锁

    共享锁则允许多个线程同时获取锁,并发访问 共享资源,如: ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

  • 重量级锁(Mutex Lock)

    这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”

  • 轻量级锁

    锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。 轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

  • 偏向锁

    偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。 偏向锁则是在只有一个线程执行同步块时进一步提高性能

  • 分段锁

    分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践

补充: 锁优化
减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能
锁粗化
如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
锁消除
锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。


线程基本方法

线程相关的基本方法有 wait, notify, notifyAll, sleep, join, yield 等.

  • 线程等待(wait)

    会释放对象的锁

  • 线程睡眠(sleep)

    sleep 不会释放当前占有的锁

  • 线程让步(yield)

    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片, 但这又不是绝对的,有的操作系统对线程优先级并不敏感。

  • 线程中断(interrupt)

  • Join 等待其他线程终止

    很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

System.out.println(Thread.currentThread().getName() + “线程运行开始!”);Thread6 thread1 = new Thread6();
thread1.setName(“线程 B”);
thread1.join();System.out.println(“这时 thread1 执行完毕之后才能执行主线程”);
  • 线程唤醒(notify)
  • 其他方法

    1.sleep():强迫一个线程睡眠N毫秒。

    1. isAlive(): 判断一个线程是否存活。
      3. join(): 等待线程终止。
      4. activeCount(): 程序中活跃的线程数。
      5. enumerate(): 枚举程序中的线程。
      6. currentThread(): 得到当前线程。
      7. isDaemon(): 一个线程是否为守护线程。
      8. setDaemon(): 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
      9. setName(): 为线程设置一个名称。
      10. wait(): 强迫一个线程等待。
    2. notify(): 通知一个线程继续运行。
      12. setPriority(): 设置一个线程的优先级。
      13. getPriority()::获得一个线程的优先级

线程上下文切换

CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务, 任务的状态保存及再加载, 这段过程就叫做上下文切换。
image.png

上下文 是指某一时间点 CPU 寄存器和程序计数器的内容

上下文切换的活动:
1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。


线程池

  • ThreadPoolExecutor 的构造方法如下 | public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTim
    TimeUnit unit, BlockingQueue workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    Executors.defaultThreadFactory(), defaultHandler);
    } | | —- |
  1. corePoolSize:指定了线程池中的线程数量。
    2. maximumPoolSize:指定了线程池中的最大线程数量。
    3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,
    次时间内会被销毁。
    4. unit: keepAliveTime 的单位。
    5. workQueue:任务队列,被提交但尚未被执行的任务。
    6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
    7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务
  • JDK 内置的拒绝策略
  1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
    2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
    3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
    4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
  • Java 线程池工作过程
  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
    2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
    3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

volatile 关键字的作用(变量可见性、禁止重排序)
volatile 变量具备两种特性, volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是 volatile 的, JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。

特性:

  • 变量可见性
  • 禁止重排序
  • 比sychronized更轻量级的同步锁

ThreadLocal 作用(线程本地存储)

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储, ThreadLocal 的作用是提供线程内的局部变量, 这种变量在线程的生命周期内起作用, 减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。 最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、 Session 管理等。

Java 中用到的线程调度
抢占式调度:
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

协同式调度:
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
JVM 的线程调度实现(抢占式调度)
java 使用的线程调使用抢占式调度, Java 中线程会按优先级分配 CPU 时间片运行, 且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。


什么是 CAS(比较并交换-乐观锁机制-锁自旋)

CAS(Compare And Swap/Set)比较并交换, CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。 V 表示要更新的变量(内存值), E 表示预期值(旧的), N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后, CAS 返回当前 V 的真实值

  • 原子包 java.util.concurrent.atomic(锁自旋) | public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;
    public final int get() {
    return value;
    }
    public final int getAndIncrement() {for (;;) { //CAS 自旋,一直尝试,直达成功
    int current = get();
    int next = current + 1;
    if (compareAndSet(current, next))
    return current;
    }}
    public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    } | | —- |
  • ABA 问题

CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。

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