Java线程相关知识点

一 进程是什么?线程是什么?两者有什么联系和区别?

1.1 进程:进程,直观点来说,保存在硬盘上的程序运行之后,会在内存空间形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级是操作系统。操作系统会 以进程为单位,分配系统资源(CPU时间片,内存等),进程是最小的资源分配单位;

1.2 线程:有时被称为轻量级的进程,是操作系统调度执行的最小单位

1.3 区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  • 并发性:不仅进程之间可以并发,同一个进程的多个线程之间也可并发执行
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问当前进程的资源
  • 系统开销:在创建和撤销进程的时候,系统要为它分配和回收资源,所以导致系统的开销比线程的创建和销毁时要大。但是进程有独立的地址空间,一个进程崩溃了之后,不会影响其他的进程,而线程没有独立的地址空间,一个进程死掉后,所拥有的线程全部都要销毁,所以多进程的程序比多线程的要健壮,但是在进程切换,消耗的资源比较大,性能及效率较差

1.4 联系:

  • 一个进程有多个线程,至少有一个线程,而一个线程只能属于某一个进程
  • 同一个进程中的所有线程,共享该进程中的所有资源

二 线程的几种状态是什么?生命周期是什么样的?

线程有6中状态:新建,运行,堵塞,等待,计时等待,终止

  • 新建(NEW):当使用new创建新线程时,线程处于“新建”状态
  • 运行(可运行/就绪):调用start()方法
  • 阻塞:当线程需要获取某个对象锁资源时,对象锁资源正在被其他线程持有                 
  • 等待:当线程等待其他线程通知调度表可以运行时,简单来说就是当前线程调用了wait()方法,等待被唤醒
  • 计时等待:对一些含有时间参数的等待方法,如sleep(100)等
  • 终止:当run方法运行完,或者出现异常           

 线程的生命周期:      

  • NEW:在高级语言中,在JAVA层面来说,线程被创建了,而在操作系统中的线程其实还没被创建,所以这个时候是不能分配CPU资源执行线程的!所以这个状态就是高级语言独有的,操作系统的线程没有这个状态,我们new了一个线程,那时候它就是这个状态
  • Runnable:这个状态下是可以分配CPU资源执行的
  • Blocked:这个状态下是不能分配CPU资源执行的,只有一种情况会导致堵塞,那就是synchronized,被synchronized修饰的方法或者代码块同一时间只有一个线程执行,而其他竞争锁的线程就从Runnable到了Blocked状态。
  • Waiting:这个状态下也是不能分配CPU执行的,有三种情况会使Runnable到Waiting。调用Object.wait(),等到了notify或者notifyAll就会回到Runnable状态;调用A.join(),那么你的主线程得等A线程执行完毕之后才能执行,这时你的主线程就是等待状态;调用LockSupport.park(),在调用LockSupport.unpark(),就会回到Runnable状态
  • Timed_Waiting:这种状态和Waiting状态就一个时间差的不同,也是不能分配CPU执行的,有5中情况会存在这个状态。Object.wait(time),Thread.join(time),Thread.sleep(time),LockSupport.parkUntil(time),LockSupport.parkNanos(Object blocked,long deadline)
  • Terminated:在我们线程正常运行完run方法或者run方法抛出异常。

三 如何停止中断线程?

3.1 使用共享变量的方式

定义一个变量,注意使用volatile关键字修饰,该变量可以被多个执行相同任务的线程用来作为是否中断线程的信号

3.2使用interrupt方法终止线程

这个方法不会中断一个正在运行的线程,但是可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出阻塞代码

四 线程阻塞有哪些原因?

线程在运行的过程中因为某些原因发生阻塞,阻塞的特点是:放弃CPU资源,暂停运行,直到阻塞原因消除,才会恢复运行,或者是被其他线程中断,也会退出阻塞状态,同时抛出InterruptedException

导致阻塞的原因大致分为三种,分别是一般线程中的阻塞,Socket客户端的阻塞,Socket服务器端阻塞(不研究)

4.1 一般线程阻塞:

A:线程执行了Thread.sleep,当前线程放弃CPU,休眠一段时间,然后在恢复运行

B:线程执行一段同步代码,无法获得相关的同步锁,就会进入阻塞状态,等到获取了同步锁,才能恢复运行

C:线程调用了对象的wait方法,直接进入阻塞状态,等待其他线程的notify/notifyAll方法

D:线程执行某些IO操作,因为等待相关资源进入阻塞状态,比如监听Ststem.in,当没有收到键盘的输入事件,就会进入阻塞状态

4.2 Socket客户端阻塞:

A:请求服务器链接时,调用connect方法,进入阻塞状态,直至链接成功

B:从socket输入流读取数据,在读取足够的数据之前会进入阻塞状态。比如通过BufferedReader.readLine(),在没有读出一行数据之前,数据量就不算足够,会处在阻塞状态。

五 什么是公平锁?非公平锁?区别是什么?

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,按照FIFO,永远是队列中的第一位才能得到锁;

                优点:所有的线程都能获得资源        缺点:吞吐量下降很多,队列里除了第一个线程,其他的都会阻塞,CPU唤醒阻塞线程的开销比较大

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁

                优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

                缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁

六 谈谈你对ReentrantLock的认识

可重入锁,实现了Lock接口,基于AQS构造的同步器

和synchronized一样,都是可重入的,与synchronized相比增加了一些其他特性,主要有三个:等待可中断,可实现公平锁,锁可以绑定多个条件

  • 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,去处理其他事情,可适用于执行时间非常长的同步块
  • 公平锁:多个线程等待一个锁的时候,会按照申请锁的时间依次获得锁;而非公平不需要去等待,在锁释放的时候,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但是可以通过构造去设定适用公平锁。不过一旦使用了公平锁,在性能方面会有所下降,会影响吞吐量
  • 锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可

synchronized的优势:

1 使用简单,不易出错

2 经过JVM底层的锁优化之后,二者的性能相差不大

3 JVM可以在线程和对象的元数据中记录synchronized中锁的相关信息,而如果使用ReentrantLock,JVM很难得知具体哪些锁对象是由特定线程所持有的

七 volatile关键字是如何使用的?原理是什么?为什么不能保证原子性?

7.1 volatile用法:

通常被称为轻量级的synchronized,是java中一个重要的关键字。它是一个变量修饰符,只能修饰变量,无法修饰代码块和方法等

用法比较简单:声明变量的时候,使用volatile,前提是这个变量可能被多线程同时访问。比如:单例双重锁校验

public class Singleton {
    private volatile static Singleton singleton;
    
    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

7.2 volatile原理

为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于多级缓存的引入,就存在缓存数据不一致的问题。

但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。但是就算写回到内存,如果其他处理器缓存的值还是之前的,继续执行其他操作还是会出现问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就出现了一个缓存一致性协议。

缓存一致性协议:每个处理器通过监控总线上的数据来检查自己缓存的值是不是不一致,当处理器发现了自己缓存行对应的内存地址被修改了时,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行操作的时候,会重新从系统内存中将数据读取拷贝到自己的处理器缓存中。

所以,如果一个变量被volatile修饰的话,在每次数据变化后,值都会被刷新到主内存中。其他线程也会将主内存的新值拷贝一个副本到自己的工作内存中,这就保证了并发编程中,数据的安全,并且也体现了可见性

7.3 不能保证原子性

原子性:指一个操作是不可中断的,要全部执行完成,要不就都不执行。

原因:线程是CPU调度的最小单位,CPU中是有时间片的概念,会根据不同的调度算法调度切换线程,当以个线程拿到CPU时间片的时候,就开始执行,在时间片耗尽的时候,线程就会失去CPU的执行权。那么在多线程场景下,由于时间片的在线程间的切换,就会发生原子性问题。

为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。所以,volatile是不能保证原子性的。

在以下两个场景中可以使用volatile来代替synchronized:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
2、变量不需要与其他状态变量共同参与不变约束。
除以上场景外,都需要使用其他方式来保证原子性,如synchronized

八 CAS问题?

非阻塞的实现CAS(Compare-and-Swap) CAS指令需要有3个操作数

  • 内存地址(在java中理解为变量的内存地址,用V表示)
  • 旧的预期值(用A表示)
  • 新值(用B表示)

CAS指令执行时,当且仅当V处的值符合预期旧值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新V处的值,都会返回V的旧值,这就是一个原子操作

CAS的ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了,CAS只关注了比较前后的值是否改变,不知道在此过程中变量的是否被做了其他处理操作,这就是所谓的ABA问题。

ABA问题解决思路:是使用版本号。

在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

九 谈谈如何保证线程数据安全问题的?

9.1 使用final(不可变)

在java中,不可变的对象一定是线程安全的,无论是调用者还是实现者,都不需要做任何线程安全处理,final关键字修饰的类或者数据不可修改,可靠性最高。比如String类,Integer类

9.2 线程封闭

把对象封装到一个线程里,只有一个线程可以看到这个对象,那么这个对象就算不是线程安全的,也不会出现任何的线程安全方面的问题

  • ThreadLocal:它的内部维护了一个map,map的key就是线程名,map的value就是我们要封装的对象,其中ThreadLocal提供了set,get,remove等方法,这些操作都是针对于当前线程的,所以它是线程安全的
  • 堆栈封闭:就是方法中定义局部变量。当多线程访问一个方法的时候,方法中的局部变量都会拷贝一份到线程中的栈中,所以局部变量是不会被多个线程共享的

9.3 同步

  • 悲观锁
  • 乐观锁

5.3.1 悲观锁

同步最常用的方法就是使用锁,是一种强制机制。每个线程在访问数据前先获取锁,并在结束后释放锁;当锁被别的线程持有的时候,当前线程等待,知道锁被其他线程释放并重新可用。

  • synchronized:控制线程同步,在多线程中,不被多个线程同时执行,确保数据的完整性。可重入,修饰(class,obj,代码块).在使用的过程中,要注意synchronized的使用范围,避免随意使用影响程序性能
  • Lock.lock()/Lock.unLock():ReentrantReadWriteLock是Lock的另一种实现方式,允许多个线程同时访问,但是不允许读线程和写线程,写线程和写线程同时访问。提高了并发性

可见性:当多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他线程能够立即看的到修改的值

有序性:程序的执行按照代码的先后顺序执行,在Java内存模型中,为了效率编译器和处理器会对指令进行重新排序,这种排序不会影响单线程的运行结果,但是对多线程会有影响

原子性:一个操作或者多个操作,要么全部执行完,要么全部都不执行。原子性就像数据库里的事务一样。

                要想在多线程下保证原子性,可以通过锁,synchronized。同时JDK中也给我们提供了一些保证值运算的原子操作类,AtomicInteger等

9.4 工具类

  • 同步容器的工具有Vector、HashTable
  • 并发容器ConcurrentHashMap,CopyOnWriteArrayList

总结
1、总体来说线程安全在三个方面体现:
        原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized)。
        可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)。
        有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happensbefore原则)。
2、要知道确保线程安全作用是什么,无非就是想让程序按照我们预期的行为去执行。 3、确保线程安全的方法除了上面
几种方式还有很多的,我们在实战中可以一步步去发现

十 谈谈对notify的理解?

public final native void notify()throws IllegalMonitorStateException

一个本地方法,此方法在同步方法或者同步块中调用,调用前,线程必须持有改对象的锁,如果没有,则会抛出IllegalMonitorStateException

该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。
(1)如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
(2)当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
(3)优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

十一 为什么线程通信的方法wait,notify,notifyAll被定义在Object 中?而sleep方法被定义在Thread类中?

11.1 wait()、notify()、notifyAll()被定义在Object类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有个锁,而线程是可以获得这个对象的。因此当线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了。而wait()方法如果定义在Thread类中的话,那么线程正在等待的是哪个锁就不明确了。这也就是说wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中是因为锁是属于对象的原因。

11.2 sleep为什么定义在Thread类里面? 

对于sleep为什么被定义在Thread中,我们只要从sleep方法的作用来看就知道了,sleep的作用是:让线程在预期的时间内执行,其他时候不要来占用CPU资源。可以理解为sleep是属于线程级别的,它是为了让线程在限定的时间后去执行。而且sleep方法是不会去释放锁的

十二 wait和sleep的区别?

waitsleep
同步只能在同步上下文中调用,否则会抛出异常IllegalMonitorStateException任何地方都可以调用Thread.sleep()
作用对象定义在Object类中,作用于对象本身定义在Thread类中,作用于线程本身(当前线程)
释放锁资源
唤醒条件其他线程调用notify或者notifyAll方法超时或者抛出中断异常
方法属性实例方法静态方法

十三 你觉得Lock和Synchronized的区别是什么?

类别synchronizedLock
存在层次Java关键字一个类
锁的释放

1当前线程执行完同步代码,释放持有的锁 

2 线程执行发生异常,jvm会让线程释放锁

在finally中必须释放锁,否则容易造成死锁
锁的获取线程A持有锁,线程B等待,如果线程A阻塞,线程B一直等待分情况而定
锁状态无法判断可以判断
锁类型可重入,不可中断,非公平可重入,可判断,公平(两者皆可)默认非公平
性能少量同步大量同步

十四 调用run()和start()的区别?

  • 调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
  • 一个线程的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run()方法没有限制。

十五 transient关键字的用法 & 作用 & 原理?

  • 用法: 修饰变量  private transient String content = "xxxx"
  • 作用:你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中

transient使用小结
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

十六 从源码角度讲讲你对Thread类中run方法的理解?

常用写法

public void run() {
    super.run();
    System.out.println(Thread.currentThread().getName());
}

Thread类中的实现

private Runnable target;

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

由源码可见,我们可以自己实现runnable接口,Thread的run方法和Runnable的run方法处于同一线程时,两个函数均会执行。

十七 ThreadLocal了解吗?说说原理?

17.1 什么是ThreadLocal?

ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,在并发模式下是绝对安全的变量。

可以通过 ThreadLocal value = new ThreadLocal(); 使用

在ThreadLocal中有一个重要的属性ThreadLocalMap,,ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个ThreadLocal。

static class ThreadLocalMap {
    /**
    * 键值对实体的存储结构
    */
    static class Entry extends WeakReference<ThreadLocal<?> > {
   
        /* 当前线程关联的 value,这个 value 并没有用弱引用追踪 */
        Object value;
    
        /**
        * 构造键值对
        *
        * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
        * @param v v 作 value
        */
        Entry( ThreadLocal<?> k, Object v ) {
            super(k);
            value = v;
        }
    }

    /* 初始容量,必须为 2 的幂 */
    private static final int INITIAL_CAPACITY = 16;

    /* 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂 */
    private Entry[] table;

    /* ThreadLocalMap 元素数量 */
    private int size = 0;

    /* 扩容的阈值,默认是数组大小的三分之二 */
    private int threshold;
}

ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,「Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值」。

17.2 ThreadLocal 内存泄漏

ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

那么如何避免内存泄漏呢?
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。

/**
* 清理当前 ThreadLocal 对象关联的键值对
*/
public void remove() {
    /* 返回当前线程持有的 map */
    ThreadLocalMap m = getMap( Thread.currentThread() );
    
    if ( m != null ) {
        /* 从 map 中清理当前 ThreadLocal 对象关联的键值对 */
        m.remove( this );
    }
}

remove 方法是先获取到当前线程的 ThreadLocalMap,并且调用了它的 remove 方法,从 map 中清理当前
ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值