Java多线程知识

12 篇文章 0 订阅

1.线程和进程的概念,并发和并行:

 进程是一个正在执行的应用程序,是线程的集合。

 线程是进程中的一条执行路径,各个线程之间相互独立,互不影响。

并发的关键是你有处理多个任务的能力,不一定要同时。  

并行的关键是你有同时处理多个任务的能力。  

 

2.为什么要使用多线程:

提高程序的效率。

 

3.线程的几种状态:

新建(new):新创建了一个线程对象

就绪(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权

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

运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

 

4.用户线程和守护线程:

当最后 一个非守护线程结束时,jvm会正常退出,守护线程是否结束不影响jvm的退出。

守护线程的存在必须依赖于用户线程。当所有的用户线程结束后,守护线程也会退出,jvm也会退出。

 

5.线程同步:

使用synchronized或者使用锁lock

 

5.1 synchronized和lock的区别

区别如下:

  1. 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  7. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

 

 

6.什么是同步方法:

在方法上修饰synchronized称为同步方法。

 

7.同步方法使用什么锁:

非静态同步方法使用this锁

静态同步方法使用使用类.class文件,可以用 getClass方法获取,也可以用当前  类名.class 表示。

 

8.多线程死锁的原因:

同步中嵌套同步容易导致死锁。
假设进入A线程需要 m,n两把锁,进入B线程需要n,m两把锁,当A线程拿到m锁,B线程拿到n锁 ,由于两个线程并未结束不会释放自己的锁,这时A拿不到n锁,B拿不到m锁,线程就会进入死锁。

 

9.什么是ThreadLocal

给线程提供局部变量的类。

定义一个初始值为10的线程局部 变量 :

public static  ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
    
    protected  Integer initialValue(){
        return 10;
    }
};

ThreadLocal原理:ThreadLocal通过map集合  Map.put(“当前线程”,值);

 

10.多线程三大特性:

原子性:一个操作或者多个操作,要么全部成功执行,要么全部都不执行。

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

有序性:程序执行的顺序按照代码的先后顺序执行。(不过一般来说,处理器为了提高程序的运行效率,可能会改变代码的执行顺序(不会影响结果)---这就是重排序 --可以通过volatile来禁止代码的重排序))

 

11.Java内存模型(jmm):

java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个 线程都有自己的本地内存(存放主内存中共享变量的副本),当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

   

当A线程对共享变量进行+1时,选对A线程本地内存中的a加1,然后A线程再将本地内存修改后的值刷新到主内存,再由主内存把值刷新到其他线程的本地内存。

 

12.什么是volatile:

volatile保证了线程之间共享变量的可见性,但不能保证原子性。

volitale的使用场景:一个线程写,多个线程写

多线程中一旦某个线程修改了被volatile修饰的变量,他会保证修改的值立即被更新到主存,当有其他线程需要读取时,可以立即获取修改后的值。

java为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符 的变量则是直接读写主存。

 当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

  而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步

 

13.volatile特性:

保证修饰变量对所有线程的可见性。

禁止指令重排序优化。

 

14.volatile和synchronized的区别:

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住

  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

  • volatile标记的变量不会被编译器优化(重排序);synchronized标记的变量可以被编译器优化

 

15.什么是重排序:

cpu会对代码的执行顺序进行优化,不会对有依赖关系性做重排序,重排序后代码的执行顺序可能会发生改变,但是程序的执行结果不会发生变化。

int a=1;    int b=2;    int c=a+b;           a ,b都是独立的不依赖任何变量,c则依赖a和b ,程序执行后 int a=1和int b=2可能会发生重排序让int b=2选执行。  int c=a+b则不会发生重排序。

 

15.2、死锁、活锁、饥饿?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。

饥饿:我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。

 

16.线程通信:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作

就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺

wait()方法:该方法用来使得当前线程进入等待状态,直到接到通知或者被中断打断为止。在调用wait()方法之前,线程必须要获得该对象的对象级锁;换句话说就是该方法只能在同步方法或者同步块中调用,如果没有持有合适的锁的话,线程将会抛出异常IllegalArgumentException。调用wait()方法之后,当前线程则释放锁。

notify()方法:该方法用来唤醒处于等待状态获取对象锁的其他线程。如果有多个线程则线程规划器任意选出一个线程进行唤醒,使其去竞争获取对象锁,但线程并不会马上就释放该对象锁,wait()所在的线程也不能马上获取该对象锁,要程序退出同步块或者同步方法之后,当前线程才会释放锁,wait()所在的线程才可以获取该对象锁。

于是我们引出了等待唤醒机制:(wait()notify())----必须在synchronized里进行并且锁要相同

 

16.1:wait,notify为什么必须在synchronized里进行:

事情得从一个多线程编程里面臭名昭著的问题"Lost wake-up problem"说起。

这个问题并不是说只在Java语言中会出现,而是会在所有的多线程环境下出现。

假如有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠:

生产者是两个步骤:

  1. count+1;

  2. notify();

消费者也是两个步骤:

  1. 检查count值;

  2. 睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候count等于0,这个时候消费者检查count的值,发现count小于等于0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……。

 

17.wait和sleep的区别:

1,这两个方法来自不同的类分别是Thread和Object
 2,最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
 3,wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在
    任何地方使用
   synchronized(x){
      x.notify()
     //或者wait()
   }
   4,sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
注意:线程调用notify()之后,只有该线程完全从 synchronized代码里面执行完毕后,monitor才会被释放,被唤醒线程才可以真正得到执行权。

 

 

18.Lock的 Condition,以及上锁,释放锁。

Lock lock=new ReentrantLock();//重入锁

lock.lock();      lock.unlock();

Condition condition = lock.newCondition();  //用于通信

 condition.await();  类似wait

 Condition. Signal() 类似notify

 

 

19.阻塞式队列和非阻塞式队列的区别:

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列非阻塞,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,当队列是满时,往队列里添加元素的操作会被阻塞。

 

 

20.线程池的作用:

第一:降低资源消耗。通过重复利用已建的线程降低线建和造成的消耗。
第二:提高响应速度。当任到达,任可以不需要等到线建就能立即行。
第三:提高线程的可管理性线程是稀缺源,如果无限制地建,不会消耗系统资源,
会降低系定性,使用线程池可以一分配、调优控。

 

21.线程池常见四种创建方式:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

 

再看看ThreadPoolExecutor的构造方法了解一下这个类:上面四种创建线程池的方式最后都会调用new ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
}

构造参数比较多,一个一个说下:

    corePoolSize:线程池中的核心线程数;
    maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
    keepAliveTime:线程池中非核心线程闲置超时时长(准确来说应该是没有任务执行时的回收时间,后面会分析);
        一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
        如果设置allowCoreThreadTimeOut(boolean value),则也会作用于核心线程
    TimeUnit:时间单位。可选的单位有分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS) 等;
    workQueue:任务的阻塞队列,缓存将要执行的Runnable任务,由各线程轮询该任务队列获取任务执行。可以选择以下几个阻塞队列。
        ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
        LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
        SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
        PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
    ThreadFactory:线程创建的工厂。可以进行一些属性设置,比如线程名,优先级等等,有默认实现。
    RejectedExecutionHandler:任务拒绝策略(饱和策略),当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
        AbortPolicy:直接抛出异常。
        CallerRunsPolicy:这提供了一个简单的反馈控制机制,可以减慢提交新任务的速度。
        DiscardOldestPolicy:丢弃队列里最老的一个任务,并执行当前任务。
        DiscardPolicy:不处理,丢弃掉。
        当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

 

 

22.线程池原理分析:

当提交一个新任务到线程池时首先线程池判断基本线程池(corePoolSize)是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程;其次线程池判断工作队列(workQueue)是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程;最后线程池判断整个线程池(maximumPoolSize)是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

如果线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过 keepAliveTime,线程也会被终止。
 

饱和策略:

Abort策略:默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。

CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。

Discard策略:新提交的任务被抛弃。

 

23.线程池为什么要选择阻塞式队列作为缓存队列:

因为从缓存队列取出任务交给线程池去执行时,必须等待线程池中某个线程已经执行完任务。这一过程存在等待,只能适合阻塞式队列。
 

 

24.合理配置线程池

cpu密集:该任务需要大量的运算,而没有阻塞,CPU一直全速运行。任务可以少配置线程数,大概和机器的cpu核数相当。

io密集:该任务需要大量的IO,即大量的阻塞。大部分线程都阻塞,故需要多配置线程数,2*cpu核数。

 

 

25.Callable接口:

在Java中,创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。然而,这两种方式的缺点是在线程任务执行结束后,无法获取执行结果。我们一般只能采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。
不过,Java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果。,而Future用来获得结果。

 

26:Future模式:

Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑,但是调用get方法后会阻塞。

Futrure模式:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果,可以通过get方法获取结果,调用get方法之前A线程不会等待,调用get方法后,如果B线程还没执行完毕,这时get方法会被阻塞从而造成A线程等待

 在多线程中经常举的一个例子就是:网络图片的下载,刚开始是通过模糊的图片来代替最后的图片,等下载图片的线程下载完图片后在替换。而在这个过程中可以做一些其他的事情。

ExecutorService executor = Executors.newCachedThreadPool();

Future<Integer> future = executor.submit(new  callAble()); 

Future来实现线程池中的submit提交方式(Callable接口为参数)就是future模式。

 

 

27.重入锁:

重入锁(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。(任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞)

 

28.读写锁:

.ReentrantLock、Synchronized都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

由于读和读之间并不对数据的完整性造成破坏,所以读写锁允许多个线程同时读,但是读写操作、写写操作之间仍然要相互等待和持有锁的。而重入锁在读和读互斥等待浪费了时间。

ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //读写锁

Lock read = rwl.readLock();  //读             read.lock () ------  读操作  -------read.unlock()

static Lock write = rwl.writeLock();  //写     write.lock () ------  写操作  -------write.unlock()

 

 

29.乐观锁:

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

 version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL语句

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};   

CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

 

30:悲观锁:

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

 

31.CAS无锁模式:

CAS:Compare and Swap,即比较再交换。

好处:

无锁,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

缺点:

CAS存在一个很明显的问题,即ABA问题。

问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?

如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

关于CAS中E(期望值的理解):

CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。(在JMM中E代表的是本地内存的值)。

CAS算法的过程:

它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。结合Java内存模型(JMM)理解:

如上图,CAS中的三个参数(V,E,N),其中V表示要更新的变量,代表的是主内存里共享变量的值,E表示预期值,代表的是本地内存变量的值,N表示新值,当V=E是,吧主内存共享变量的值设为N,当V!=E时,刷新主内存的值,重新开始比较V和E的值,就这样循环下去直到成功。

 

31.自旋锁:

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting

好处:

自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

 

 

32.公平锁与非公平锁:

公平锁:线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序

非公平锁:一种获取锁的抢占机制,是随机获取锁的,和公平锁的区别就是先来的不一定先得到锁,导致某些线程可能一直拿不到锁,所以是不公平的

非公平锁的性能因其系统上下文的切换较少,其性能一般要优于公平锁。(公平锁需要维护队列,并且要等待和唤醒队列中的线程)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值