进程和线程的区别

  1. 资源分配的角度来看:进程是资源分配的最小单位,线程的切换仅仅保存了较少的寄存器,线程不是资源分配的最小单位
  2. 调度的角度:没有引入线程的时候,进程是调度的最小单位,有了线程之后操作系统调度的就是线程了。
  3. 切换开销来看:线程的切换开销较小,只用保存较少的寄存器,进程切换的开销较大
  4. 并发的角度来看:同一个进程的线程可以并发,不同进程的线程之间也可以并发

Java的线程和操作系统的线程有什么区别

在jdk1.2以前,java的线程是用户级线程,也就是一对多模型,向下操作系统申请一个线程,这个线程来轮询java的操作,在用户层面看上去是有多线程,但这样效率并没有提升

在此之后java的线程可以直接用内核级线程,也就是java创建的线程就是操作系统认可的一个线程。

创建线程的方法

网上都说有四五种方法,但是其实只用一种就是thread.start()方法,但是为了完整性还是说一下这四种方法:

  1. 继承Thread
  2. 实现Runnable接口:run方法不允许有返回值,也不能抛出异常
  3. 实现Callable接口:call可以有返回值,可以抛出异常
  4. 线程池

什么时候实现runnable接口什么时候继承thread:由于java是单继承的,如果有继承别的类就不能继承Thread了,此时就要使用Runnable接口
runnable和callable只是两个接口而已,直接运行对应的run和call是不能成为一个线程的,start是Thread类才有的方法,所以要真正运行起来线程需要new Thread(),这里面传入的是一个Runnable参数,Callable不行,需要用futureTask包装

callable怎么用?

  1. callable和futureTask:由于new Thread要传一个Runnable参数,Callable不是;所以要用futureTask包装
  2. callable和线程池:submit给线程池返回一个future对象

Future和FutureTask

future是异步编程的一个体现,当前线程正在执行,我需要取到程序运行结束的结果,也就是未来的结果。具有取消任务,查看任务执行情况,获取任务执行结果等功能。而futureTask是其的一个实现类,同时实现了future和runnable接口,由于new Thread只能传Runnable接口,futureTask正式为了搭配Callable接口,使用的时候new Thread(new FutureTask(new Callable))

线程池的submit和execute什么区别

  1. submit返回值为Future,可以执行Callable和Runnable任务,但是如果是runnable那future.get返回就是null,也可以抛出异常
  2. execute返回值为空,只能执行Runnable任务

线程的状态

类似原来408学的线程状态,但是在java中这些状态略有不同,相比于底层操作系统,这些状态更加高层次,因为不涉及到io操作阻塞进程:

  • NEW:初始状态,一般是new Thread(),创建出来但是还没有执行start方法
  • RUNNABLE:运行状态,执行了start方法
  • BLOCKED:被synchronized方法阻塞了,没有抢到锁就进入这个状态,等到锁释放就变为RUNNABLE
  • WAITING:一般是线程之间通讯,调用了wait方法变成waiting状态,而等到notify或者notifyAll变为运行态
  • TIME_WAITING:具有具体时间的等待状态
  • TERMINATED:线程终止

BLOCKED和WAITING的区别

  • 触发条件:线程没有获取到监视器锁(synchronized)就BLOCKING,WAITING是获取到了synchronized锁但是锁对象wait了,也就是释放了锁,主要用于线程通讯
  • 唤醒条件:获取到锁了之后会解除BLOCKING,而别的线程对锁对象.notify或者notifyAll之后会唤醒waiting线程,但是要继续执行还是要获取锁

notify和notifyAll的区别

  • notify随机唤醒一个(这个是hotspot的机制,可能别的虚拟机是指定的)
  • notifyAll,当前锁对象下所有的WAITING线程,共同争抢。唤醒的线程在被 notify() 或 notifyAll() 唤醒后,仍然需要重新争抢对象的锁。它会从 WAITING 状态转换到 BLOCKED 状态,等待对象的锁释放。

wait和sleep方法的区别

  • wait是Object的方法,sleep是Thread的方法
  • wait释放锁,sleep不释放锁
  • wait需要notify唤醒而sleep到时间就唤醒了
  • wait主要用于进程同步,sleep延时

为什么wait定义在object里面而sleep定义在Thread里面:
与他们使用的场合有关,wait需要在synchronized方法中运用,而代码块的锁正是object,操作的并不是线程
而sleep的作用是让线程睡眠一段时间,这显然跟锁没什么关系,就定义在线程中

死锁的四个条件,通过什么方法解决

  • 互斥条件:一个资源同时刻只能有一个线程访问
  • 不剥夺条件:一个线程的资源不可以被别的线程剥夺
  • 占有且等待条件:一个线程获取不到更多的资源,原来持有的资源也不会释放
  • 循环等待条件:资源的请求变成了一个环,大家都拿不到对应的资源

破坏这四个条件是死锁预防要做的事情。此外还有死锁避免(银行家算法),死锁检测和恢复(有向图检测方法)。这三个大的方法性能由低到高,对死锁的控制由严格到松。

  • 破坏互斥条件:多增加临界资源的数量(虚拟化)
  • 破坏不剥夺条件:抢占式调度(还有一种是协同式调度,是等到当前线程结束才进行线程调度),得不到资源就释放
  • 破坏占有等待条件:可以先申请,检查当前系统剩余资源是否能够让线程运行,一次性全拿而不是一次拿一点
  • 循环等待条件:编号,类似TCP的同步号

volatile关键字

  1. 在多线程环境中,每个线程都有自己的 CPU 缓存,线程在读写变量时可能会先将变量的值缓存在自己的工作内存中,而不是直接从主内存中读取或写入。使用volatile关键字就告诉程序这个变量是不稳定的,禁用cpu缓存,每次找这个变量都需要去主存取,
  2. 在jvm的执行优化中可能出现指令重排序,也就是123条指令,可能实际变成132,volatile关键字能够在其中加上内存屏障,禁止指令重排序(主要用在双校验单例模式)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Singleton{
    private static volatile Singleton singleton;
    public Singleton(){}
    public Singleton getSingleton(){
    if (singleton == null){
    synchronized (this){
    if (singleton == null){
    singleton = new Singleton();
    }
    }
    }
    return singleton;
    }
    }
  3. 能保证可见性,但不能保证原子性,例如再出现i++这种操作的时候,即使volatile i,还是会有线程安全问题。因为i++其实是三个操作,从内存取出i,对这个副本+1,再把其写回内存。此时两个线程来操作就会少一次+1。(可以加锁synchronized,reentrantlock或者用AtomicInteger的cas操作)

内存屏障:

  • 写 volatile 变量时: 当一个线程写入 volatile 变量时,JVM 会在写操作后插入一个写内存屏障(Store Memory Barrier),确保所有在 volatile 变量写操作之前的内存操作都已经完成,并且这些操作的结果对其他线程可见。 在 Java 内存模型中,写 volatile 变量时会确保该写入操作之前的所有变量修改都被刷新到主内存中,然后将 volatile 变量的值写入主内存。
  • 读 volatile 变量时:当一个线程读取 volatile 变量时,JVM 会在读操作前插入一个读内存屏障(Load Memory Barrier),确保所有在 volatile 变量读操作之后的内存操作都不会被重排序到它之前。 在 Java 内存模型中,读 volatile 变量时,会确保在读取这个 volatile 变量之后,线程能够看到所有其他线程在写入该 volatile 变量之前对其他变量的修改。

synchronized

  1. 可以修饰实例方法,锁住当前对象
  2. 修饰静态方法,锁住当前类。这里就会有个有意思的事情,修饰在静态方法上的synchronized和实例方法上的不互斥,因为静态方法锁住的是当前类,实例方法锁住的是对象
  3. 修饰代码块,这个比较灵活

    注意这里不能修饰构造函数,构造函数本身就是线程安全的

原理
基于jvm的monitor,在代码块进入前加上monitorenter,退出的时候加上monitorexit,也是可重入的,底层是监视器维护了一个state字段,有锁进来就+1,重入多次就+n,退出-1,同时也会有维护一个队列存放未获取锁的对象,等到释放了就会唤醒(BLOCKED)

两个队列entryList和waitList:

  • entryList是进入锁代码块的所有线程,当锁释放了之后其中的一个线程会给monitor上锁,同时+1(队列里的状态为BLOCKED),注意这里是队列里所有线程都进行竞争,所以synchronized是非公平锁
  • waitList是调用了wait方法的线程,等待notify或者notifyAll进行唤醒,唤醒之后还需要重新获得锁,也就是要进入entryList里(状态为WAITING)

锁升级

  • 不加锁:
  • 偏向锁:当第一个线程进入之后会给其加上偏向锁,使得这个资源偏向。
  • 轻度锁:使用cas操作
  • 重度锁:

为什么会有偏向锁这个过程:
对于竞争不激烈但是还有加了synchronized关键字的,如果是单线程就没有必要进行锁和上下文切换,偏向锁的初衷正是为了减小上下文切换的开销。如果一个线程反复进入和退出同步块,而没有其他线程争用这把锁,那么这些加锁解锁的开销是完全多余的。

AQS

抽象队列同步器(AbstractQueuedSynchronizer)提供了一种同步机制,被应用于Reentrantlock,semaphore,cyclicBarrier等排他和共享锁结构中。

在说AQS之前先要讨论一下CHL锁,因为AQS是基于CHL的。CHL锁是一个队列结构,头节点是一个空节点,当线程进入后会对前驱节点的资源释放状态做cas,例如第一个线程进入之后就对表头节点的锁资源cas,获取到了就改变自己的值为false,后续另外一个线程进入之后就会轮询上一个节点的状态,拉成一个队列。

这种结构的好处是资源状态分布在每个节点上,但同时也增大了cpu的开销,因为每一个节点都需要轮询。

AQS正式基于这种思想,首先他把CHL的虚拟队列改造成了显式队列,保存了前驱和后继(为了满足一些对于公平锁和非公平锁的要求,使用了双向链表),全局维护一个state,当获取到锁之后state+1,可以实现锁重入。

reentrantLock

reentrantLock基于aqs,实现可重入,可中断,可以设置公平锁,可以有多个条件设置,可以设置超时时间。