java线程知识点汇总

线程状态

java线程知识点汇总_第1张图片

       线程的状态可以分为5种:新建new、可运行runnable、正在运行running、阻塞blocked和死亡dead。

       新建new:当一个线程新建出来,例如 Thread thread = new Thread() 此时,线程状态就是new。

       可运行Runnable:当新建的线程调用start方法,线程状态就变为runnable,此时线程随时等待CPU调度执行,但未执行。

       正在运行running:CPU开始调度执行此线程,需要注意的是,线程running状态只能从runnable转换过来。

       阻塞blocked:当处于正在运行的线程因为一些情况放弃争夺CPU资源,此时就进入了阻塞状态,如果线程需要再次运行必须先转变成runnable状态。

       死亡dead:线程运行完成或出现异常,线程生命周期结束。

Java线程阻塞代价以及原因

       实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程实现。Java在JDK1.2之前采用用户线程实现线程,在之后就采用一对一的内核线程来实现,也就是java的线程都是映射到操作系统的原生线程上的;另一方面java调度线程的方式是抢占式线程调度,相比于协同式调度方式更加稳定,不会因为某个线程占着资源不释放而导致系统崩溃。

       线程阻塞代价:刚刚说到java分配线程的方式是映射到内核线程的,也因此线程阻塞状态的阻塞和唤醒是需要操作系统介入的,需要在用户态和内核态切换,这种线程上下文切换会消耗较大资源,浪费CPU的处理时间,因此尽可能的较少线程上下文切换的消耗(例如自旋锁、轻量级锁)。

用户线程和守护线程

       用户线程是像main或新建的线程这种;JVM启动之后不止main一个用户线程还有一些例如垃圾回收这种守护线程,当所有的用户线程都死亡之后JVM会正常退出。

线程使用的三种方式

  1. 继承Thread类,重写run方法。
  2. 实现Runnable接口,重写run方法(和第一种方式相比,这种方式更符合面向对象的设计)。
  3. 实现Callable接口通过FutureTask来创建Thread线程。具体为:

new Thread(new FutureTask(new OneCallable())); 其中OneCallable是实现Callable接口并重写了call方法的类。

使用startrun方法的区别

       直接调用线程的run方法是在原来现在调用的,跟调用普通方法没什么区别;调用start方法是将新建线程状态变为runnable,线程run方法的调用是在获取到CPU时间片才执行,是在另一个线程中运行的。

Sleepwait(notify/notifyAll)yieldjoin

       Sleep是线程类的静态方法,是让该线程睡眠一段时间,睡眠的线程进入阻塞状态,放弃争夺CPU资源,但是不释放锁,也就是当拥有synchronized的代码块的程序sleep之后虽然暂停执行,但是其他线程还是不能访问。

       Wait一般和notify/notifyAll一起使用,这三个方法都是Object的方法,一般用于多个线程对共享数据的获取,并且只能在synchrnoized中使用,因为waitnotify方法使用的前提是必须先获取一个锁。Wait的作用是使当前线程进入阻塞状态并释放持有的对象锁,线程会进入该对象的等待池中,但不会主动去竞争该对象的锁;notify是随机唤醒一个等待当前对象的锁的线程,notifyAll是唤醒所有等待当前对象的锁的线程。

       Join是让当前的线程进入阻塞状态,等待调用join的线程执行完毕之后再进入runnable状态。

       Yield方法是让running的线程重新进入runnable不会释放锁

线程安全、线程同步、线程互斥、线程通信

       线程安全:是指多线程执行同一份代码每次执行结果都和单线程一样。

       线程同步:对临界区的共享数据,A线程去操作数据,并且需要另一线程B的操作才能继续完成,这种线程之间协作的就是线程同步。

       线程互斥:对临界区的共享数据,两个线程都有修改情况,如果没有加锁或cas等的操作会造成数据混乱异常,这种就是线程互斥。

       线程通信:可以认为是线程同步的扩展,因为wait/notify必须获取了对象锁才能使用,通过wait/notify这种方式实现两个线程的等待唤醒。

线程通信和进程通信的几种方式

       线程通信:共享变量、wait/notifylock/ ConditionCondition通过ReentrantLocknew condition,其await等同于waitsignal等同于notify)。

       进程通信:内存映射、消息队列、socket

线程同步的7种方式

  1. synchronized同步方法。如果是普通方法会锁住这个对象,如果是静态方法锁住的是整个类。
  2. synchronized同步代码块。
  3. volatile修饰变量。
  4. 重入锁ReenreantLock。实现了Lock接口,可重入,但效率低。
  5. ThreadLocal线程级别的变量共享。
  6. 阻塞队列LinkedBlockingQueue。主要通过synchronizedputtake实现。
  7. 原子变量。

RunnableThreadCallableFutureFutureTaskThreadLocal

       Runnable:是一个接口,只有一个run方法,当线程进入running状态,就会执行run方法,可以认为是对运行任务的封装。

       Thread:是一个类,当线程进入running状态,会执行run方法,如果没有重写run方法默认会调用Runnablerun方法。

       Callable:是一个接口,只有一个call方法,与runnable相似,但不同的是有返回值并能抛出异常。

       Future:是一个接口。是对runnablecallable的运行结果的操作,可以判断运行是否完成、是否取消以及获取运行的结果,获取运行结果调用get方法,这种方式获取结果是同步的。

       FutureTask:是一个实现类,实现了FutureRunnable接口。所以既可以作为Runnable去启动一个线程,也可以作为Future去获取线程运行结果。而需要主要的是,Future是接口不能直接操作运行结果,FutureTask可以,也是Future唯一的实现类。

       ThreadLocal:内部是一个map,其作用是将一个变量限定为线程级的变量。

获取线程运行结果

       使用callablecall方法可以有返回值,一般搭配ExectorServicesubmit(callable)方法使用,其返回值是Future,再通过Futureget方法获取线程运行结果。类似的可以将FutureFutureTask代替。

Executor框架

       Executor框架是一个异步执行框架,执行任务的线程相当于消费者,提交任务的线程相当于生产者。主要API包括以下:

  1. Executor:执行器接口,只有一个execute(Runnable)的方法,可以执行一个Runnable的任务,但是该方法没有返回值,使用较少。
  2. ExecutorService:是Executor的子接口,也是ThreadPoolExecutor的父接口,增加了一些方法包括关闭等,最重要的是增加了submit(Callable)的方法,返回值是FutureFutureTask
  3. ScheduledExecutorService:是ExecutorService的子接口,主要增加了一些定时任务的方法。
  4. ThreadPoolExecutorJDK的线程池类。
  5. Executors:是一个工厂类,提供了5个静态方法去创建线程池。

竞态条件、临界区

       竞态条件是指程序的运行结果取决于线程执行的顺序,也就是程序结果对不对看运气。最典型的竞态条件是先检查后执行,也就是前面的检查是一个无效的过期的检查,但是执行了后面的代码。

       临界区是指访问那种一次只能有一个线程执行的资源的代码块。

SynchronizedLockReentrantLockReadWriteLock ReentrantReadWriteLock

       SynchronizedJVM内置的关键字。可以使用在代码块、方法上;非公平锁;可重入锁;不能响应中断;在执行完了锁住的代码块或发生中断会自动释放锁;无法得到是否持有锁、是否有等待线程等信息。

       Lock是一个接口,ReentrantLockLock的实现类,提供了locktrylock等获取锁的方法和unlock释放锁的方法;默认是非公平锁。也可以设置为公平锁;可重入锁;能响应中断;需要主动释放锁,否则很有可能发生死锁现象;能得到锁的一些信息。

       ReadWriteLock是另外一个接口,ReentrantReadWriteLockReadWriteLock的实现类,主要特点是有readLockwriteLock两种锁,也需要主动释放锁。如果一个线程占了读锁,不影响其他线程获取读锁,但若其他线程要获取写锁,需要等待读锁完成;如果一个线程占了写锁,那么其他线程获取读写锁都需要等待该写锁释放。

一些锁的定义

       可重入锁:可重入锁是指当持有锁的线程再次获取这个锁时能立即成功,锁的计数+1,当执行完代码块时计数-1,当计数为0时释放锁;不可重入锁是持有锁的线程再次获取时会造成死锁的情况。

       可中断锁:可中断锁就是线程在等待获取锁的时候可以中断,去处理别的任务。Synchronized不是可中断锁,lock就是可中断锁。

       (非)公平锁:公平锁是指获取锁是有顺序的,例如ReentrantLock可以通过构造方法设置成公平锁,等待时间长的锁优先获取锁;非公平锁就是获取锁跟顺序无关,随机选择一个线程分配锁,例如synchronized就是典型的非公平锁,这种非公平锁有可能导致某个线程一直获取不到锁。

       独享锁\共享锁(互斥锁\读写锁):独享锁是指这个锁同一时间只能被一个线程持有,例如synchronizedReentrantLock;共享锁是这个锁可以被多个线程共同持有,例如ReadWriteLock和其子类ReentrantReadWriteLock,其读锁是共享锁,写锁是独享锁。而互斥锁和读写锁就分别是独享锁和共享锁的具体表现。

       乐观锁\悲观锁:乐观锁是认为读多写少,所以在读的时候不会加锁,在写的时候会先照常执行,当发现执行结果不对时会舍弃本次操作再重试,例如CAS算法;悲观锁是认为读少写多,所以在每次读写都会进行加锁,例如独占锁。

       偏向锁、轻量级锁、重量级锁:这三种其实是锁的三种状态,并且都是真的synchroinzed而言的。偏向锁是指同步代码一直被一个线程访问,那么这个线程会自动获取这个锁,这个锁就是偏向锁;轻量级锁是指之前的偏向锁对应的代码块被另一个线程访问了,那么这个偏向锁会升级为轻量级锁,这时线程获取锁通过自旋的方式获取,减少上下文切换的消耗;重量级锁是指线程去获取轻量级锁的自旋了一段时间还是不能获取到锁,为了降低CPU的消耗,会让该锁升级为重量级锁,线程获取重量级锁的时候时候进入阻塞状态。

CAS算法

       CAS(Compareand Swap,比较并交换)是乐观锁的一种典型算法实现。其核心是对于修改操作,会有旧值、预期值和新值,当去修改内存中的旧值时,会先去判断是否和自己的预期值相等,如果相等说明没有被别的线程修改过,直接替换为新值;如果不相等说明被别的线程修改了,就舍弃本次操作。这种方式能优化锁,提高效率,但是也可能出现ABA(A值被改为B,有改为了ACAS不能发现)的情况。

锁优化的6种方式

  1. (减少锁的持有时间)细化锁。减少加锁的代码块,因为加锁部分的代码越长运行时间越长,别的线程等待时间越长。
  2. (减小锁的粒度)分段锁。例如concurrentHashMap,内部实现是多个segmentreentrantLock子类),通过将这个表分段提升性能。
  3. 粗化锁。对于重量级锁会是等待线程进入阻塞状态,增加线程上下文切换的开销,因此频繁的使用锁还不如使用较粗粒度的锁,虽然单个锁的运行时间长了,但是减少了CPU开销。
  4. 自旋锁。针对等待时间短的锁。
  5. 锁分离。例如读写锁ReadWriteLock,或LinkedBlockingQueue维护头尾两个锁。
  6. 锁消除。JIT会在编译时对不会共享的对象进行锁消除。

Volatile

       在说volatile之前需要明白java的内存模型。Java的内存模型是一个逻辑概念,并不真实存在,跟java内存区域是不一样的概念,如果要对照的话,内存模型中的工作内存对应虚拟机栈,主内存对应java堆中的对象,从更低层次来说,主内存相当于物理机的主存,工作内存相当于寄存器或高速缓存。

java线程知识点汇总_第2张图片

       Java内存模型:每个线程有自己独立的工作内存,其他线程不能访问,共享变量存在于主内存中,工作内存的是共享变量的副本。对于普通变量的值修改来说,需要从主内存中获取一份共享变量副本,然后在工作内存中修改之后再返回到主内存完成修改,线程之间是不可见的。

       volatile修饰的变量有两个特点:

  1. 此变量对于所有线程是可见的。也就是volatile修饰的变量在不同线程中值永远是相等的。但不能说这个变量是线程安全的,例如race++的自增操作,虽然不同线程获取的race的值是相等的,但是自增操作不是原子的,包含多个指令。
  2. 使用volatile的变量禁止指令重排。指令重排是指CPU执行代码编译后的指令顺序并不保证和代码顺序一致,但最终结果一致,是提高效率的一种措施。但是在一些情况下,这种方式会带来错误,例如判断是否读取了配置文件。

死锁、活锁

       死锁:例如A1占用A2资源,并且请求获取B2资源;B1占用了B2资源,并且请求获取A2资源。上述情况还不足以发生死锁,死锁发生的条件必须满足以下四点:

  1. 互斥条件。A1占用A2资源,并且不允许其他人(B1)也占用资源,独占锁。
  2. 请求和保持条件。A1占了资源去等待A2资源,等待的过程一直保持着占用A2的状态。
  3. 不可剥夺条件。A1占了A2资源,除非A1主动释放。
  4. 循环等待条件。一定会有一个A1等待B1B1等待A1类似这样的情况。

       避免死锁主要有对加锁顺序考量;使用定时锁等。

       活锁是由于两个线程不断动态变化,但始终无法满足,活锁对性能损耗更大。

线程池

       threadPoolExecutor:

              corePoolSize:线程池的基本大小。线程池刚创建的时候线程数为0,当有任务提交线程数为corePoolSize.

              maximumPoolSize:线程池最大大小。线程池可同时活动的线程数。

              WorkQueue:

                     (有界队列)ArrayBlockingQueue:基于数组实现的有界阻塞队列。

                     (无界队列)LinkedBlockingQueue:基于链表实现的无界阻塞队列。

                     (直接提交)SynchronousQueue:(无界)不是真正的队列因为它不会维护队列空间大小,而是维护一组线程,put必然伴随着take。提交的任务直接交给corethread处理,如果corethread满了,直接新建一个线程自行处理,一般要求maxthread为无穷。一般用在线程有先后依赖关系。

                     PriorityBlockingQueue:具有优先级的队列。

       newFixedThreadPool: corePoolSizemaximumPoolSize设定为固定值,并且不会超时。采用LinkedBlockingQueue

       newCachedThreadPool: corePoolSize=0maximumPoolSize最大值,超时1分钟。采用SynchronousQueue.

       newSingleThreadExecutor: corePoolSizemaximumPoolSize设定为1,并且不会超时。采用LinkedBlockingQueue

       饱和策略:当有界队列填满之后,饱和策略发挥作用,无界队列没有饱和策略。分为四种。

  1. Abort。默认策略,队列满了之后,新的任务提交会抛出异常,可以捕获异常进行处理。
  2. CallerRun。调节策略,队列满了之后,新的任务不会丢弃也不会抛出异常,而是让调用executer的线程去执行。
  3. Discard。丢弃策略。队列满了之后,新的任务会被丢弃。
  4. DiscardOldest。丢弃最老的任务,也就是队列头的任务。

你可能感兴趣的:(java开发工程师)