java多线程

作者: 与搬砖有关的日子 | 来源:发表于2018-11-22 10:47 被阅读7次

    1.    线程的实现 

    a.    继承Thread类:在java.lang包中定义,继承Thread类必须重写run()方法,然后通过start()方法去启动线程

    如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

    b.  实现Runnable接口:通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写run方法。

    c.    使用Callable实现有返回结果的多线程

    ExecutorService、Callable、Future这个对象都属于Executor框架中的功能类。返回值的任务必须实现Callable接口,无返回值的任务必须实现Runable接口。执行Callable任务后可以获取一个Future对象,在该对象上调用get就可以获取到Callable任务返回的Object,在结合线程池接口ExecutorService就可以实现有返回值结果的多线程了。

    2.   线程的状态

    创建(new)状态:准备好了一个多线程的对象。

    就绪(runnable)状态:调用了start()方法,等待CPU进行调度。

    运行(running)状态:执行run()方法。

    阻塞(blocked)状态:暂时停止执行,可能将资源交给其他线程使用。

    终止(dead)状态:线程销毁

    Time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

    注:sleep和wait区别:

    Sleep是Thread类的方法,wait是Object类中定义的方法;Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁;Thread.sleep和Object.wait都会暂停当前的线程。OS会将执行时间分配给其他线程。区别是调用wait后,调用对象的wait()方法导致当前线程放弃对象的锁,进入对象的等待池,需要别的线程执行notify/notifyAll(notify唤醒一个处于等待状态的线程,当调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;notifyAll唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有的线程,而是让他们竞争,只有获得锁线程才能进入就绪状态)才能重新获得CPU执行时间。

    sleep()方法:方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

    Yield()方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

    join()方法:在很多情况下,主线程创建并启动了线程,如果子线程中要进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。Join不会释放锁。

    setDaemon和isDaemon:

    用来设置线程是否成为守护线程和判断线程是否是守护线程。守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。如果用户线程全部撤离那么守护线程就没啥线程好服务的了,所以虚拟机就退出了。

    停止线程:在Java中有以下3种方法可以终止正在运行的线程:

    使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

    使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。

    使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

    同步与死锁:同步代码块,在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块。同步代码块格式

    synchronized(同步对象){

     需要同步的代码块;

    }

    同步方法

    除了代码块可以同步,方法也是可以同步的

    方法同步格式

    synchronized void 方法名称(){}synchronized修饰方法锁住的是对象的本身,也是this

    3.    线程的sleep()方法和yield()方法有什么区别?

    a.   sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,yield()方法只会给相同优先级或更高优先级的线程以运行的机会。b.线程执行sleep()方法后转入阻塞状态,而执行yield()方法后转入就绪(ready)状态。c.sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。d.sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    4.   volatile关键字

    Java 内存模型可见性、原子性和有序性

    volatile特性:

    保证变量在线程间可见,对volatile变量所有的写操作都能立即反应到其他线程中;禁止指令的重排序优化。

    volatile解决的是多线程间共享变量的可见性问题,并不能保证原子性,比使用synchronized的成本更低,因为它不会引起线程上下文的切换和调度。一个变量如果用volatile修饰了,则java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以马上看到这个更新。程序运行的数据是存储在主存中,读写主存中的数据没有执行CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU告诉缓存为某个CPU独有,只与在该CPU运行的线程有关。这样会导致数据一致性的问题,在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再跟主存打交道,而是直接从高速缓存读写数据,只有当运行结束后才会将数据刷新到主存中。解决缓存一致性方案有两种: a.通过在总线加LOCK锁方式(这样只能有一个CPU运行,其他CPU都得阻塞,效率较低)b.通过缓存一致性协议(确保每个缓存中使用得共享变量得副本是一致得,当某个CPU在写数据时,如果发现操作得变量是共享变量,则会通知其他CPU告知该变量得缓存是无效得,因此其他CPU在读取变量时,发现其无效会从主存加载数据)。Java语言通过volatile和synchronized两个关键字来保证线程之间操作得有序性,volatile指令“禁止指令重排序”,synchronized关键字“一个变量在同一时刻只允许一条线程对其进行lock操作”。当一个变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

    volatile并不能保证i++在多线程情况下正确执行,因为i++并不是原子操作,可以用AtomicInteger修饰,AtomicInteger原理是CAS。

    5.    synchronized的局限性和Lock的优点

    synchronized锁住的是括号里的对象而不是代码。对于非static的synchronized方法锁的就是对象本身也就是this,synchronized(sync.class)实现了全局锁的效果,static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的class对象,所以static synchronized方法也相当于全局锁,相当于锁住了代码段。

    synchronized是Java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,但synchronized粒度有些大,在处理实际问题时存在诸多局限性。   如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块,其他线程只能一直等待直至占有锁的线程释放锁。占有锁的线程释放锁一般是下面这三种情况之一:占有锁的线程执行完了该代码块,然后释放对锁的占有;占有锁的线程执行发生了异常,此时JVM会让线程自动释放锁;占有锁线程进入WAITING状态从而释放锁,例如在线程中调用wait()方法。为什么使用lock呢?a.在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因被阻塞了,但又没有释放锁那么其他线程就只能一直等待,因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnitunit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。b.读写文件时,读跟写操作会发生冲突现象,写跟写操作也会发生冲突现象,但是读跟读操作不会发生冲突现象。但使用snchronized关键字实现同步的话,会导致多个线程进行读操作时,也只有一个可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。Lock可以解决这种情况(解决方案:ReentrantReadWriteLock)通过Lock得知线程有没有成功获取锁(解决方案:ReentrantLock),这个synchronized无法办到。1)synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;2)采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。

    6.   Java中的锁

    公平锁/非公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大。Synchronized是一种非公平锁。

    可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。可重入锁的一个好处是可以一定程度避免死锁。

    如果不是可重入锁,setB可能不会被当前线程执行,造成死锁。

    独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。Java

    ReentrantLock是独享锁。ReadWriteLock其读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,Synchronized是独享锁。

    互斥锁/读写锁:独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中具体实现是ReadWriteLock.

    乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作

    7.  CAS(Compare and Swap)

    CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

    对于竞争资源较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较小(自旋锁当一个线程获得普通锁后,另一个线程试图获取锁,这个这个线程会挂起阻塞,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候,那么这个线程可以不放弃CPU时间片,而在原地忙等,自旋锁是一种非阻塞锁),因此可以换取更高的性能。对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

    8.   三个线程顺序执行:a.join()方法thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用线程A的join()方法。b.使用synchronized

    9.  ThreadLocal:

    ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将线程与变量绑定在一起,为每一个线程维护一个独立的变量副本。Synchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的访问。而ThreadLocal从本质上讲,无非是提供了一个线程级的变量作用域,它是一种线程封闭技术,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为线程级。ThreadLocal主要解决多线程数据因并发产生不一致的问题,为每个线程的并发访问数据提供一个副本,通过访问副本运行业务。

    10.线程的安全性:

    当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同这个类都能表现出正确的行为,那么这个类就是线程安全的。

    11.线程池:

    为每个请求创建一个新线程的开销很大,为每个请求创建线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际用户请求的时间和资源更多。几种创建线程池的方法:

    Executors.newCachedThreadPool():无限线程池,如果线程池超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程;

    Executors.newFixedThreadPool(nThreads):创建固定大小的线程池,如果工作线程数量达到线程池初始化的最大数,则提交的任务存入到池队列中,线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;

    Executors.newSingleThreadExecutor():创建单个线程的线程池,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行,corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;;

    newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

    ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) ;

    这几个核心参数的作用:

    corePoolSize 为线程池的基本大小。在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。

    maximumPoolSize 为线程池最大线程大小。当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

    keepAliveTime 则是线程空闲后的存活时间。当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

    allowCoreThreadTimeout是否允许核心线程空闲退出,默认值为false。

    queueCapacity任务队列容量。

    workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

    ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;

    handler:表示当拒绝处理任务时的策略,有以下四种取值:

    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

    线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。

    线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步

    线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

    12.进程:

    几乎所有的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程。当一个程序运行时,内部可能包括多个顺序执行流,每个顺序执行流就是一个进程。进程是指处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个单位。当程序进入内存运行时即为进程。进程的三个特点:独立性(进程是系统中独立存在的实体,它可以独立拥有资源,每一个进程都有自己独立的地址空间),动态性(进程和程序的区别在于进程是动态的,进程中有时间的概念,进程具有自己的生命周期和各种不同的状态),并发性(多个进程可以在单个处理器上并发执行互不影响)。

    13.线程阻塞的原因:

    处于阻塞状态的线程的共同特征是: 放弃CPU, 暂停运行, 只有等到导致阻塞的原因消除, 才能恢复运行; 或者被其他线程中断, 该线程会退出阻塞状态, 并且抛出 InterruptedException。线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行;线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁才能恢复运行;线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()和notifyAll()方法,才可能将其唤醒;线程执行I/O操作或进行远程通信时,会因为等待资源进入阻塞状态。

    14.死锁:

    死锁是两个甚至多个线程被永久阻塞时的一种运行局面,这种局面的生成伴随着至少两个线程和两个或者多个资源。避免死锁的方式:避免嵌套封锁,如果你已经有了一个资源就要避免封锁另一个资源;只对有请求的进行封锁;避免无限期的等待。

    15.happens-before原则(先行发生原则):

    程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

    锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作

    volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

    传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

    线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

    线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

    线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

    对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

    16.synchronized实现原理

    synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式:

    同步普通方法,锁的是当前对象。

    同步静态方法,锁的是当前 Class 对象。

    同步块,锁的是 () 中的对象。

    实现原理: JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

    具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。

    其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

    而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

    流程图如下:

    17.多线程通信

    等待通知机制:两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯。

    线程 A 作为消费者:

    获取对象的锁。

    进入 while(判断条件),并调用 wait() 方法。

    当条件满足跳出循环执行具体处理逻辑。

    线程 B 作为生产者:

    获取对象锁。

    更改与线程 A 共用的判断条件。

    调用 notify() 方法。

    join()方法:

    volatile共享内存

    synchronized同步:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了 通信。

    相关文章

      网友评论

        本文标题:java多线程

        本文链接:https://www.haomeiwen.com/subject/dxgkqqtx.html