Java并发总结

1.创建线程三种方式

  • Runnable.Callable接口
  • 使用继承Thread类的方式创建多线程
  • Runnable 和Callable区别
    • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    • Call方法可以抛出异常,run方法不可以。
    • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的
      方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任
      务的执行,还可获取执行结果
  • start 和 run
    • 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
    • JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果

1) start 方法:

用 start 方法来启动线程,真正实现了多线程运行。通过调用 Thread 类的

start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有

运行,一旦得到 cpu 时间片,就开始执行 run()方法,这里方法 run()称为线

程体,它包含了要执行的这个线程的内容,Run 方法运行结束,此线程随即

终止。

2) run():

run()方法只是类的一个普通方法而已,如果直接调用 run 方法,程序

中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序

执行,还是要等待,run 方法体执行完毕后才可继续执行下面的代码,这样

就没有达到写线程的目的。

总结:调用 start 方法方可启动线程,而 run 方法只是 thread 的一个

普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要

并行处理的代码放在 run()方法中,start()方法启动线程将自动调用 run()

方法,这是由 jvm 的内存机制规定的。并且 run()方法必须是 public 访问权

限,返回值类型为 void。

两种方式的比较 :

实际中往往采用实现 Runable 接口,一方面因为 java 只支持单继承,

继承了 Thread 类就无法再继续继承其它类,而且 Runable 接口只有一个

run 方法;另一方面通过结果可以看出实现 Runable 接口才是真正的多线程。

2.线程的状态流转

  • 新建状态:线程对象创建后,即进入了新建状态,Thread t = new MyThread()
  • 就绪状态:当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
  • 运行状态:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中
  • 阻塞状态:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    • 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    • 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态:线程执行完了或者因异常退出了run()方法,该线程结束生命周期

3.死锁

  • 条件:互斥条件、请求保持、不剥夺条件、循环等待条件
  • 避免死锁:
    • 破坏请求与保持条件:一次性申请所有资源
    • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如申请不到,可以主动释放它占有的资源
    • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
    • 锁排序法:规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。
    • 使用显示锁中的ReentrantLock.try(long,TimeUnit)来申请锁

4.shutdown() VS shutdownNow()

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队
    列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并
    停止处理排队的任务并返回正在等待执行的 List。
    • 原理:遍历线程池中的工作线程,逐个调用线程的interrupt方法中断线程,所以无法响应中断的任务可能永远无法终止

5.isTerminated() VS isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

6.sleep() 和 wait()

  • 区别
    • sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
    • wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
    • sleep 方法没有释放锁,而 wait 方法释放了锁 。
    • sleep 通常被用于暂停执行,Wait 通常被用于线程间交互/通信
    • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者notifyAll() 方法
  • 相同
    • 两者都可以暂停线程的执行

7.yield

  • Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。
  • 静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

8.volatile

  • 语义:
    • volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者
      理解为多线程环境下使用volatile修饰的变量的值一定是最新的。
    • jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。
  • 原理:
    • 获取JIT(即时Java编译器,把字节码解释为机器语言发送给处理器)的汇编代码,发现volatile多加了
      lock addl指令,这个操作相当于一个内存屏障,使得lock指令后的指令不能重排序到内存屏障前的位
      置。这也是为什么JDK1.5以后可以使用双锁检测实现单例模式。
    • lock前缀的另一层意义是使得本线程工作内存中的volatile变量值立即写入到主内存中,并且使得其他线
      程共享的该volatile变量无效化,这样其他线程必须重新从主内存中读取变量值。

9.线程阻塞三种情况

  • 等待阻塞(Object.wait->等待队列):running状态的线程执行object.wait方法后,jvm会将线程放入等待序列
  • 同步阻塞(lock->锁池):running状态的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则jvm将该线程放入锁池中
  • 其他阻塞(sleep/join)running状态的线程执行Thread.sleep(long ms)或发出i/o请求时,jvm会将该线程置为阻塞状态,当sleep状态超时,join等待线程终止或超时,或者i/o处理完毕时,线程重新转入可运行状态

10.线程死亡三种方式

  • 正常结束:run()或call()方法执行完成后,线程正常结束
  • 异常结束:线程抛出一个未捕获的exception 或error,导致线程异常结束
  • 调用stop:直接调用线程的stop方法来结束该线程,会导致死锁,一般不推荐

11.守护线程

  • 运行在后台的一种特殊进程。
  • 独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

12.fork/jion

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。
「分而治之」:以上Fork/Join框架的定义,就是分而治之思想的体现啦

「工作窃取算法」:把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的。任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线
程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程
各在一端

13.cas–Compare and swap ,即比较并交换

  • 一条 CPU 同步原语。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
  • CAS 是一种无锁的非阻塞算法的实现。
  • CAS 包含了 3 个操作数:
    • 需要读写的内存值 V
    • 旧的预期值 A
    • 要修改的更新值 B
  • 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。)
  • 缺陷:
    • ABA 问题:AtomicStampedReference可以解决ABA问题,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
    • 循环时间长开销,CAS是有个自旋次数的,就是为了避开这个耗时问题
    • 只能保证一个变量的原子操作。 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
  • 解决:
    • 使用互斥锁来保证原子性;
    • 将多个变量封装成对象,通过AtomicReference来保证原子性

14.synchronized 和 volatile

  • volatile 解决的是内存可见性问题,使得所有对 volatile 变量的读写都直接写入主存,保证了变量的可见性。
  • synchronized 解决的是执行控制的问题,它会阻止其他线程获取当前对象的监控锁,当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证操作的内存可见性,同时也使得这个锁的线程的所有操作都 happens-before 于随后获得这个锁的线程的操作。
  • 区别:
    • volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    • volatile 仅能使用在变量级别;synchronized 则可以使用在变量. 方法.和类级别的
    • volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性
    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
    • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

15.synchronized 和 Lock

  • synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁; 而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

16.synchronized 和 ReentrantLock

  • 两者都是可重入锁
    • 可重入锁:指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,
    • 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
    • synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的
    • ReentrantLock 是 JDK 层面实现的(需要 lock() 和 unlock() 方法配合 try/finally语句块来完成)
  • ReentrantLock 比 synchronized 增加了一些高级功能
    • 相比synchronized,ReentrantLock增加了一些高级功能。
    • ①等待可中断:通过lock.lockInterruptibly()来实现这个机制。正在等待的线程可以选择放弃等待,改为处理其他事情。
    • ②可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
    • ③可实现选择性通知(锁可以绑定多个条件):ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
  • 使用选择
    • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
      synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放

17.synchronized

  • 用法:

    • 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
    • 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
    • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
    • 特别注意:
      • ① 如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁
      • ② 尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能
  • 作用:

    • 原子性:确保线程互斥的访问同步代码;
    • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
    • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。
  • 底层实现原理

    • synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,
      • monitorenter 指令指向同步代码块的开始位置
      • monitorexit 指令则指明同步代码块的结束位置。
    • 当执行 monitorenter 指令时,线程试图获取锁,即获取 monitor的持有权。(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 。其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
    • synchronized 修饰的方法是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED。访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
  • synchronized 锁升级的原理

    • 在锁对象的对象头里面有一个 threadid 字段
    • 第一次访问时,threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
    • 再次进入时会先判断threadid 是否与其线程 id 一致
      • 一致。则可以直接使用此对象
      • 不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
      • 执行一定次数之后,如果还没有正常获取到要使用的对象
      • 把锁从轻量级升级为重量级锁
      • 此过程构成了 synchronized 锁的升级。
    • 锁升级目的:为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗
  • synchronized 非公平锁

    • 当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
      • 先将锁的持有者 owner 属性赋值为 null
      • 唤醒等待链表中的一个线程(假定继承者)。在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。
    • 当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
  • jvm对synchronized 的优化

    • 锁膨胀:膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

      • 偏向锁:减少统一线程获取锁的代价。多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,此时就是偏向锁。
        • 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程ID等于 Mark Word 的ThreadID即可,省去了大量有关锁申请的操作。
      • 轻量级锁:由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
      • 重量级锁:由轻量级锁升级而来,同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
      • 重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
    • 锁消除:虚拟机另外一种锁的优化,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。object锁是私有变量,不存在所得竞争关系。

    • 锁粗化:虚拟机另一种优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

    • 自旋锁与自适应自旋锁:轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,进行自旋锁的优化手段。

      • 自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。

      • 缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

      • 自适应自旋锁:相当于是对自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

      • 为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大

        重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。
        很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。

  • synchronized锁能降级么

    • 可以。触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁
    • 操作:
      • 恢复锁对象的 markword 对象头;
      • 重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用

18.ThreadLocal

  • 线程本地变量:如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

    应用场景:

    • 数据库连接池
    • 会话管理中使用
  • 实现原理

    • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
    • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
    • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • 内存泄露问题

    • ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用 (只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存 )
    • ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。
    • 解决:使用完ThreadLocal后,及时调用remove()方法释放内存空间。

19.ReentrantLock

  • ReetrantLock是一个可重入的独占锁,支持公平锁和非公平锁,可重入。
  • ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)。
  • ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。

20.ReadWriteLock

  • 使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致。但如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
  • 读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能

1.现在有T1,T2,T3三个线程,怎样保证T2在T1执行完成后执行,T3在T2执行完后执行?

在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

join()方法的作用就是让主线程等待子线程执行结束之后再运行主线程。

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

使用场景,线程2依赖于线程1执行的返回结果

public static void main(String[] args) throws Exception{
    Thread t1 =  new Thread(()->{
        try {
            Thread.sleep(500);
            System.out.println("线程1醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<100;i++){
            System.out.println("线程1 i:"+i);

        }
    });
    t1.setName("线程1");
    Thread t2 = new Thread(()->{
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<100;i++){
            System.out.println("线程2 i:"+i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t2.setName("线程2");
    t2.start();
    t1.start();
}

2.在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允 许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap 这样的高性能数据结构和有条件的阻塞。

区别:

  • 1、lock是一个接口,而synchronized是java的一个关键字。
  • 2、synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。

synchronized

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象
public class SynchronizedTest {

    public synchronized void test1(){}    
    
    public void test2(){        
        synchronized (this){}
    }
}

javap工具可以查看生成的class文件信息来分析Synchronized的实现

同步代码块:是使用monitorenter和monitorexit指令实现的

通过在对象头设置标记,达到获取锁和释放锁的目的。

monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。

线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。获取锁,锁的计数器+1,

线程执行到monitorexit指令时,锁计数器-1,计数器为0时,锁被释放。

获取对象失败,当前线程阻塞等待,直到对象锁被另一个线程释放为止。

同步方法(在这看不出来需要看JVM底层实现):依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。

synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

区别:

  • 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
  • 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  • 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
  • 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
  • Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
  • 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
  • synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,
//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();...condition.await();...condition.signal();
condition.signalAll();

使用

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

2种机制的具体区别:
**synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。**独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。其中比较重要的获得锁的一个方法是compareAndSetState。调用的CPU提供的特殊指令。可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

3.在 java 中 wait 和 sleep 方法的不同?

sleep():使线程暂停执行一算时间的方法。
wait():使线程暂停一段时间的方法。例如,当线程进行交互时,如果线程对一个同步对象x发出一个wait()调用请求,那么该线程会暂停执行,被调用对象进入等待状态,直到被唤醒或者等待时间超时。

  • 原理不同:
    • sleep()方法属于Thread类的静态方法,是线程用来控制自身流的,它会使此线程暂停执行一段时间,而把执行时间让给其他的线程,等倒计时时间一到,此线程会自动苏醒。
    • wait()方法属于Object类的方法,用于线程间的通信,会使当前拥有该对象锁的进程处于等待的状态,直到其他线程调用notify()方法(或者notifyAll)时才会苏醒,可以给它制定一个时间,自动“醒”来。
  • 对锁的处理机制不同:
    • sleep()方法的主要作用是让线程暂停一段时间执行,时间一到则自动恢复,不涉及线程间的通信,调用sleep()不会释放锁。
    • wait()方法则不同,当调用wait()方法后,线程会释放掉它所占有的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
  • 使用区域不同:
    • wait()方法的意义比较特殊,因此 他必须放在同步控制方法或者同步语句块中使用,而sleep()方法则可以在任何的地方使用。
    • sleep()方法必须捕获异常,而wait()、notify()、notifyAll()部需要捕获异常。在sleep()过程中,有可能被其他对象调用它的interrupt()方法,产生InnterruptException异常。
      由于sleep()方法不会释放“锁标志”,容易造成死锁的问题发生,因此,在一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法

slepp()方法与yield()方法有什么区别:
1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会。而yield()方法只会给相同的优先级或者更高优先级的线程以运行的机会。
2)线程执行sleep()方法后会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内肯定不会被执行。而yield()方法只是使当前线程重回到可执行状态,所以执行yeild()方法的线程有可能在进入到可执行状态后马上又被执行。
3)sleep()方法声明抛出InterruptException,而yeild()方法没有声明任何异常。
4)sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。

7.什么是原子操作, Java 中的原子操作是什么?

原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间切换到另一个线程。

java.util.concurrent.atomic 包里面提供了一组原子类。基本特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性。即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,但不会阻塞线程(synchronized 会把别的等待的线程挂,或者说只是在硬件级别上阻塞了)。

8.Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什么不同?

volatile是轻量级的synchronized,其在多处理器开发中保证了共享变量的“可见性”。
可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

一个变量被定义成volatile后具备两种特性:

  • 保证此变量对所有线程的可见性,
  • 禁止指令重排序优化

内存模型定义了8种内存间操作

  • lock和unlock
    • 把一个变量标识为一条线程独占状态
    • 把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定
  • read和write
    • 把一个变量值从主内存传输到线程的工作内存,以便load
    • 把store操作从工作内存得到的变量的值,放入主内存的变量中
  • load和store
    • 把read操作从主内存得到的变量值放入工作内存的变量副本中
    • 把工作内存的变量值传送到主内存中,以便write
  • use和assgin
    • 把工作内存变量值传递给执行引擎
    • 将执行引擎值传递给工作内存变量值

volatile是不是并发安全的

不是,volatile变量在各个线程的工作内存,不存在一致性问题,但运算并非原子操作。

Java代码需要转化为汇编指令在CPU上运行。有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,第二行汇编代码中包含有Lock前缀。

Lock前缀的指令在多核处理器下会引发了两件事:

1.将当前处理器缓存行的数据写回到系统内存。
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

原本处理器会将系统内存的数据读到内部缓存后进行操作,但操作完不知道何时会写到内存。所以当多线程并发的时候,其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
而加上了volatile关键字,对变量进行写操作时,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。这样每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

两大原则

1.Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存

2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

sychronized

悲观锁(不管是否产生竞争,都会加锁)、非公平锁(获取锁行为上,不是按时间前后给等待线程分配锁的,锁释放,任何线程都有机会竞争到锁,缺点:产生线程饥饿)、可重入锁(获取锁+1),可重入锁最大的作用是避免死锁。

级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

public synchronized void synMethod(){
//方法体
}

对某一代码块使用 synchronized后跟括号,括号里是变量,一次只有一个线程进入该代码块,此时,线程获得的是成员锁。

public Object synMethod(Object a1){
synchronized(a1){
//一次只能有一个线程进入
}
}

如果synchronized后面括号里是一个对象,此时,线程获得的是对象锁。如果线程进入,则得到当前对象锁,那么其他没有获得锁的线程,在该类所有对象上的任何操作都不能进行。

public classMyThread implements Runnable{
public static void main(Stringargs[]){
Thread t1=newThread(mt,“t1”);
Thread t2=newThread(mt,“t2”);
t1.start();
t2.start();
}
public void run(){
synchronized(this){
System.out.println(Thread.currentThread().getName());
}
}

如果synchronized后面括号里是类,此时线程获得的是对象锁。如果其他线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法。实际上,对于含有静态方法和静态变量的代码块的同步,我们通常选用例4来加锁。

class ArrayWithLockOrder{
 public ArrayWithLockOrder(int[]a){
  synchronized(ArrayWithLockOrder.class){
   //代码逻辑
   }
 } 
}

jvm对java原生锁的优化

① 自旋锁,把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时无需让线程执行阻塞操作,避免了用户态到内核态的切换

②锁消除:JVM对一些代码上要求同步但被检测到不可能存在共享数据竞争的锁进行消除,主要根据逃逸分析。

③锁粗化:增大锁的作用域

synchronized和reentrantLock 实现原理有什么不同

锁的实现原理基本是为了达到一个目的:让所有线程都能看到某种标记

synchronized:对象头

reentrantLock及基于lock接口的实现类:通过用一个volatile修饰的int型变量,并保证每个线程都拥有对该int的可见性和原子修改,本质是基于所谓的AQS框架。

public class RWSample {

    private final Map m = new TreeMap<>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    
    
    public String get(String key){
        r.lock();
        System.out.println("读锁锁定");
        try {
            return m.get(key);
        } finally {
            r.unlock();
        }
    }
    
    public String put(String key, String entry){
        w.lock();
        System.out.println("写锁锁定");
        try {
            return m.put(key, entry);
        }finally {
            w.unlock();
        }
    }

}

AQS:一个用来构建锁和同步器的框架,各种Lock包中的锁,eg:ReentrantLock,ReadWriteLock,Semaphore,CountDownLatch,FutureTask等。

同步状态:volatile int state,0:没有任何线程占有共享资源的锁,可以获得锁。1:有线程目前正在使用共享变量,其他线程必须加入同步的队列等待。

同步队列:Node内部类构成的一个双向链表结构,完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。

Node类:包含线程本身及其状态waitStatus(5种取值,阻塞,等待唤醒等),每个node结点关联其prev结点 和next结点,方便线程释放后快速唤醒下一个在等待的线程,是一个FIFO过程。常量:SHARED(共享模式)EXCLUSIVE(独占模式)

ConditionObject:构建等待队列,Condition调用wait()方法,线程将会加入等待队列,调用signal从等待队列转移到同步队列中进行锁竞争。

AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,就是两个队列互相移动。

从功能角度,ReetrantLock比Synchronized的同步操作更精细,可以实现更多高级功能:

等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。

带超时的获取锁尝试:指定时间范围内没有获取则返回。

可以判断是否有线程在排队等待获取锁

可以响应中断请求:中断异常会被抛出,锁会被释放

可以实现公平锁

释放锁:synchronized在jvm层面上实现,发生异常时,jvm会自动释放锁定,lock通过代码实现,要保证锁一定会被释放,必须将unlock放到finally中。

性能:

竞争不激烈时,synchronized性能要优于ReetrantLock

高竞争时,synchronized性能会下降几十倍,但reetrantlock性能能维持常态。

reentrantLock可重入性

内部自定义了同步器Sync(既实现了AQS,又实现了AOS—提供了一种互斥锁持有的方式),加锁通过cas算法,将线程对象放到一个双向链表中,每次获取锁的时候,看当前维护的那个线程id和当前请求的线程id是否一样,一样就可重入了

synchronized保证原子性和可见性

volatile保证可见性

ThreadLocal和synchronized 都解决多线程并发访问

synchronized用于实现同步机制,利用锁的机制使变量或代码块在某一时刻只能被一个线程访问 -----时间换空间

ThreadLocal为每个线程都提供变量的副本,每个线程在某一时间访问到的不是同一个对象----空间换时间

ThreadLocal

java提供的一种保护线程私有信息的机制,其在整个线程生命周期内有效。方便地再一个线程关联的不同业务模块之间传递信息,例如事务id,cookie等上下文相关信息。

为每个线程维护变量的副本(Map),共享数据的可见范围为同一个线程之内。

使用注意:remove

基于ThreadLocalMap实现,key是个弱引用。废弃的回收依赖于显式的触发,否则就要等待线程结束,回收相应的ThreadLocalMap—OOM的来源。

建议:应用要自己负责remove,并且不要和线程池配合,以为worker线程往往是不会退出的。

JUC并发工具java.util.concurrent及其子包

CountDownLatch,CyclicBarrier,Semaphore等可以实现更丰富的多线程操作的同步结构。

ConcurrentHashMap,有序的ConcurrentSkipListMap或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等,各种线程安全的容器

ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue等,各种并发队列实现

Executor框架,可以创建各种不同类型的线程池,调度任务运行等。

ReadWriteLock 和 StrampedLock

实际场景中不需要大量竞争的写操作,以并发读为主,进一步优化并发操作的粒度,java提供了读写锁

ReadWriteLock 代表一对锁,读锁和写锁,数据量较大,并发读多,并发写少的时候,能凸出优势。有相对大的开销

StrampedLock:提供类似读写锁的同时,支持优化读模式,逻辑:先试着修改,然后通过validate方法确认是否进入写模式,如果没有进入,就成功避免了开销,如果进入,则尝试获取读锁。

public class StampedSample {

    private final StampedLock s1 = new StampedLock();

    void mutate(){
        long stamp = s1.writeLock();
        try{
            write();
        }finally {
            s1.unlockWrite(stamp);
        }
    }
    Data access(){
        long stamp = s1.tryOptimisticRead();
        Data data = read();
        if (!s1.validate(stamp)){
            stamp = s1.readLock();
            try {
                data = read();
            }finally {
                s1.unlockRead(stamp);
            }
        }
        return data;
    }
}

让线程彼此同步,同步器?

同步器:CountDownLatch,CyclicBarrier,Semaphore实现多个线程之间协作的功能

CountDownLatch 倒计数,允许一个或多个线程等待某些操作完成。

场景:

  • 跑步比赛,等所有运动员(线程)到终点(达成目标)才去算排名和颁奖
  • 模拟并发:启动100个线程同时访问某个地址,同时并发,不是一个一个执行

使用:

CountDownLatch 构造方法指明计数数量,被等待线程调用CountDownLatch 将计数器减1,等待线程使用await进行线程等待。

CyclicBarrier 循环栅栏,实现让一组线程等待至某个状态之后再全部执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使用,

场景:用来等待并发线程结束

主要方法:await(),每被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。之后如果再次调用await(),计数变成N-1,新一轮重新开始,CyclicBarrier.await()带有返回值,用来表示当前线程是第几个到达这个Barrier的线程。

Semaphore:java版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。如果值初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是相似的,区别:互斥锁是持有者的,而对于Semaphore这种计数器结构,虽然有类似的功能,但其实不存在真正意义上的持有者,除非进行扩展包装。

CyclicBarrier和CountDownLatch 对比

行为有一定相似度,区别:

CountDownLatch 不可以重置,无法重用,CyclicBarrier可以重用

CountDownLatch 基本组合:CountDown/await,调用await的线程阻塞等待countDown足够的次数

CyclicBarrier基本组合:CyclicBarrier/await,当所有都调用了await 才继续进行任务并自动进行重置。

CountDownLatch 目的:让一个线程等待其他n个线程达到某个条件后,自己再去做某个事情

CyclicBarrier目的让N多线程互相等待直到所有的都达到某个状态,然后这N个线程再继续执行各自后续。

java线程池的实现

线程:被抽象为一个静态内部类worker,基于AQS实现,存放在线程池HashSet成员变量中

成员变量workQueue(BlockingQueue)需要执行的任务存放在成员变量中。

思想:从workQueue中不断取出需要执行的任务,放在workers中进行处理.

线程池核心构造参数

corePoolSize:核心线程数

maximumPoolSize:线程池允许的最大线程数

keepAliveTime:超过核心线程数时闲置线程的存活时间

workQueue:任务执行前保存任务的队列,保存由execute方法提交的额Runnable任务

线程池中线程如何创建?是一开始就随线程池的启动创建好的么?

不是,线程池默认初始化后不启动worker,等有请求时才启动,调用execute()添加一个任务时,线程池做如下的判断:

  • 正在运行的线程数量小于corePoolSize,马上创建线程运行这个任务
  • 正在运行的线程数量大于等于corePoolSize,将这个任务放入队列
  • 如果队列满了,正在运行的线程数量小于maximumPoolSize,还是要创建非核心线程立刻运行这个任务
  • 如果队列满了,正在运行的线程数量大于或等于maximumPoolSize,线程池会抛出异常RejectExecutionException

一个线程完成任务会从队列中取下一个任务来执行,一个线程没有任务执行,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,线程会被停掉,所有线程池的所有任务完成后,最终会收缩到corePoolSize的大小。

默认线程池

SingleThreadExecutor 线程池:只有一个核心线程在工作,相当于单线程串行执行所有任务,如果唯一线程因为异常结束,会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

corePoolSize:1;maximumPoolSize:1;keepAliveTime:0L;workQueue:new LinkedBlockingQueue,器缓冲队列是无界的。

FixedThreadPool线程池:固定大小的线程池,只有核心线程,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。多数针对一些很稳定很固定的正规并发线程,多用于服务器

CachedThreadPool线程池,无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲线程,当任务数增加时,此线程池又可以智能的添加新任务来处理任务。线程池大小完全依赖于操作系统能够创建的最大线程大小,SynchronousQueue是一个缓冲区为1的阻塞队列,缓存性通常用于执行一些生存期很短的异步型任务,在一些面向连接的daemon型SERVER中用得不多,但对于生存期短的异步任务是首选。

ScheduledThreadPool:核心线程池固定,大小无限的线程池,支持定时周期性的执行任务的需求,创建一个周期性执行任务的线程池,如果闲置,非核心线程会在DEFAULT_KEEPALIVEMILLIS时间内回收。

在java线程池中提交线程

  • execute:ExecutorService.execute方法接收一个Runnable示例 用来执行任务
  • submit():ExecutorService.submit方法返回的是Future对象,可以用isDone()来查询Future是否已经完成,当任务完成时,具有一个结果,可以调用get()来获取结果。也可以直接get(),此时get()将阻塞,直至结果准备就绪。

你可能感兴趣的:(#,java并发编程,java,开发语言)