美文网首页
JAVA多线程

JAVA多线程

作者: irckwk1 | 来源:发表于2018-05-02 20:46 被阅读0次

    线程池ThreadPoolExecutor

    corepoolsize:核心池的大小,默认情况下,在创建了线程池之后,线程池中线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中线程数达到corepoolsize 后,就把任务放在任务缓存队列中。

    Maximumpoolsize:线程池中最多创建多少个线程。Keeplivetime:线程没有任务执行时,最多保存多久的时间会终止,默认情况下,当线程池中线程数>corepoolsize 时,Keeplivetime 才起作用,直到线程数不大于corepoolsize。

    workQueue:阻塞队列,用来存放等待被执行的任务threadFactory:线程工厂,用来创建线程。

    锁有一个专门的名字:对象监视器。当多个线程同时请求某个锁时,则锁会设置几种状态来区分请求的线程;1)connection list:所以所有请求锁的线程将被首先放置到该竞争队列中;2)entry lsit:那些有资格成为候选人的线程被移到entry lsit;3)wait set:那些调用

    wait 方法被阻塞的线程放置到wait set 中

    继承关系

    Executor 接口

    ExecutoServicer 接口

    AbstractExecutoServicer 抽象类

    ThreadPoolExecutor

    线程池的状态

    1.当线程池创建后,初始为running 状态

    2.调用shutdown 方法后,处shutdown 状态,此时不再接受新的任务,等待已有的任务执行完毕

    3.调用shutdownnow 方法后,进入stop 状态,不再接受新的任务,并且会尝试终止正在执行的任务。

    4.当处于shotdown 或stop 状态,并且所有工作线程已经销毁,任务缓存队列已清空,线程池被设为terminated 状态。

    当有任务提交到线程池之后的一些操作:

    1.若当前线程池中线程数

    [if !supportLists]2. [endif]若当前线程池中线程数>=corepoolsize,会尝试将任务添加到任务缓存队列中去,若添加成功,则任务会等待空闲线程将其取出执行,若添加失败,则尝试创建线程去执行这个任务。

    [if !supportLists]3. [endif]若当前线程池中线程数>= Maximumpoolsize,则采取拒绝策略(有4 种,1)abortpolicy 丢弃任务,抛出RejectedExecutionException 2 ) discardpolicy 拒绝执行,不抛异常3 )

    discardoldestpolicy 丢弃任务缓存队列中最老的任务,并且尝试重新提交新的任务4)

    callerrunspolicy 有反馈机制,使任务提交的速度变慢)。

    Java 内存模型和线程

    每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享。

    下面8 个操作都是原子的,不可再分的:

    [if !supportLists]1) [endif]lock:作用于主内存的变量,它把一个变量标识为一个线程独占的状态。

    [if !supportLists]2) [endif]unlock:作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    [if !supportLists]3) [endif]read:作用于主内存变量,他把一个变量的值从主内存传输到线程的工作内存,以便随后的load 操作使用。

    [if !supportLists]4) [endif]load:作用于工作内存的变量,他把read 操作从主内存中得到的变量值放入工作内存的变量副本中。

    [if !supportLists]5) [endif]use:作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。

    [if !supportLists]6) [endif]assign:作用于工作内存的变量,他把一个从执行引擎接收到的值付给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    [if !supportLists]7) [endif]store:作用于工作内存的变量,他把工作内存中一个变量的值传送到主内存中,以便随后write 使用。

    [if !supportLists]8) [endif]write:作用于主内存的变量,他把store 操作从工作内存中得到的变量的值放入主内存的变量中。

    关键字volatile 是轻量级的同步机制。

    Volatile 变量对于all 线程的可见性,指当一条线程修改了这个变量的值,新值对于其他线程来说是可见的、立即得知的。

    Volatile 变量在多线程下不一定安全,因为他只有可见性、有序性,但是没有原子性。

    线程的状态

    在任意一个时间点,一个线程只能有且只有其中的一种状态

    1)新建:创建后尚未启动的线程处于这种状态。

    2)运行:包括了OS 中Running 和Ready 状态,也就是处于次状态的线程可能正在运行,也可能正在等待cpu 为他分配执行时间。

    3)无限期等待:处于这种状态的线程不会被分配cpu 执行时间,要等待其他线程显示唤醒。以下方法会让线程进入无限期等待:1.没有设置timeout 的object.wait()方法2.没有设置timeout 参数的Thread.join()方法3.LockSupport.park();

    4)有限期的等待:处于这种状态的线程也不会被分配cpu 执行时间,不过无需等待被其他线程显示唤醒,而是在一定时间后,他们会由os 自动唤醒1.设置了timeout 的

    object.wait() 方法 2. 设 置 了 timeout  参数的 Thread.join() 3.LockSupport.parkNanos()

    [if !supportLists]2. [endif]LockSupport.parkUnit()

    5)阻塞:线程被阻塞了与“等待状态”的区别是:阻塞状态在等待获取一个排它锁, 这个事件将在另外一个线程放弃这个锁的时候发生。等待状态在等待一段时间或者唤醒动作。

    6)结束:已终止线程的线程状态,线程已经结束执行。

    synchronizedSVS lockL

    1)L 是接口,S 是关键字

    2)S 在发生异常时,会自动释放线程占有的锁,不会发生死锁。L 在发生异常时,若没有主动通过unlock()释放锁,则很有可能造成死锁。所以用lock 时要在finally 中释放锁。

    3)L 可以当等待锁的线程响应中断,而S 不行,使用S 时,等待的线程将会一直等下去,不能响应中断。

    4)通过L 可以知道是否成功获得锁,S 不可以。

    5)L 可以提高多个线程进行读写操作的效率。

    同步:多个线程并发访问共享数据时,保证共享数据在同一个时刻,只被一个(or 一些,使用信号量)线程使用,而互斥是实现同步的一种手段,临界区,互斥量,信号量都是主要的互斥实现方式。互斥是因,同步是果;互斥是方法,同步是目的。

    公平锁:(先申请先得到)多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获取,而非公平锁则不保证这一点。Synchronized  不是公平锁,reetrantlock  默认下也是非公平的,但是在构造函数中,可以设置为公平的。

    互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的需要转入内核态中完成。若物理机器上有一个以上的处理器,能让2 个or2 个以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就释放锁,为了让线程等待,我们只需要让他执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

    自适应自旋锁的自旋时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

    使用线程池。我们只需要运行Executors 类给我们提供的静态方法,就可以创建相应的线程池。

    Public static ExecutorService newSingleThreadExecutor() Public static ExecutorService newFixedThreadPool() Public static ExecutorService newCahedThreadPool()

    newSingleThreadExecutor 返回包含一个单线程的Executor,将多个任务交给这个Executor

    时,这个线程处理完一个任务之后接着处理下一个任务,若该线程出现异常,将会有一个新线程来代替。

    newFixedThreadPool 返回一个包含指定数目线程的线程池,若任务数多于线程数目,则没有执行的任务必须等待,直到有任务完成为止。

    newCahedThreadPool   根据用户的任务数创建相应的线程数来处理,该线程池不会对线程数加以限制,完全依赖于JVM 能创建线程的数量,可能引起内存不足。

    我们只需要将待执行的方法放入run 方法中,将Runnable 接口的实现类交给线程池的

    execute 方法,作为他的一个参数,比如:

    Executor e=Executors.newSingleThreadExecutor(); e.execute(new Runnable(){ //匿名内部类public void run(){

    //需要执行的任务

    }

    });

    多线程安全隐患的原因:当多条语句在操作同一个线程共享语句时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与执行,导致共享数据错误。解决办法:对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其他行程不能执行。

    Synchronized(对象){

    需要被同步的代码

    }

    Sleep 只释放cpu 执行权,不释放锁。

    找多个线程的共享数据及操作共享数据的语句。同步函数使用的锁:this

    静态同步函数使用的锁:函数所在类的class 对象

    第一个被加载的类是所谓的“初始化类”,也就是有main 方法的类,这个类由jvm 本身加载。

    一个抽象类可以没有抽象方法。

    一个接口可以继承任意数量的接口,一个抽象类只能继承一个抽象类。集合里放的是对象。

    Finally 除了try 块调用了System.exit(0),finally 都会执行,有return,也会先执行finally

    中的内容,再执行return。

    乐观锁VS 悲观锁1)悲观锁:就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。这样别人想拿这个数据就会block 直到它拿到锁。传统的关系型数据库就用到了很多这种机制,比如行锁,写锁等,都是在操作之前上锁。2)乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。适用于多读,比如write_condition. 两种锁各有优缺点,不能认为一种比一种好。

    共享锁VS 排他锁:1)共享锁,又称读锁,若事务T 对数据对象A 加了S 锁,则是事务T 可以读A 但不能修改A,其它事务只能再对他加S 锁,而不能加X 锁,直到T 释放A 上的S 锁。这保证了其他事务可以读A,但在事务T 释放S 锁之前,不能对A 做任何操作。2)排他锁,又称写锁,若事务T 对数据对象加X 锁,事务T 可以读A 也可以修改A,其他事务不能对A 加任何锁,直到T 释放A 上的锁。这保证了,其他事务在T 释放A 上的锁之前不能再读取和修改A。

    Executors.newCachedThreadPool:创建一个可缓存线程池,若线程池中线程数量超过处理需要,可灵活回收空闲线程(终止并从缓存中移除那些已有60 秒钟未被使用的线程,因此长时间保持空闲的线程是不会使用任何资源)。对于需要执行很多短期异步任务的程序来说,这个可以提高程序性能,因为长时间保持空闲的这种类型线程池,不会占用任何资源。调用缓存的线程池对象,将重用以前构造的线程(线程可用状态),若没有线程可用,则创建一个新线程添加到池中,缓存线程池将终止并移除60 秒未被使用的线程。

    当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize  时,若还有任务到来,就会采取任务拒绝策略。通常有以下四种策略:1)ThreadPoolExecutor.AbortPolicy-  丢弃任务并抛出RejectedExecutionException  异常。2)ThreadPoolExecutor.DiscardPolicy-丢弃任务,但不抛出异常。3)ThreadPoolExecutor. DiscardOldestPolicy-丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。4)ThreadPoolExecutor.CallerRunsPolicy-重试添加当前任务,他会自动重复调用execute 方法,直到成功。此策略提供简单的反馈机制,能够减缓新任务的提交速度。

    Concurrenthashmap

    采用了二次哈希的方式,第一次哈希将key 映射到对应的segment 中,而第二次哈希则是映射到segment 的不同桶中。

    主要实现类为Concurrenthashmap(整个hash 表)、segment(桶)、HashEntry(每个哈希链中的一个节点)

    HashEntry 中,除了value,其余3 个key、hash、next 都是final 的。这意味着不能从中间或者尾部添加或删除节点,因为这需要改变next 的引用值,所有节点修改只能从头部开始,对于put 操作,可以一律添加到哈希链头部,对于remove 操作,可能需要从中间删一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除节点的下一个节点

    Segment 类中,count 用来统计该段数据的个数,每次修改操作做了结构上的改变,比如增加或删除(修改节点的值不算结构上的改变),都要写count 的值,每次读取操作开始都要读count 的值。

    put 操作也是委托给段的put 方法;remove 操作也是委托给段的remove 方法;get 操作也是委托给段的get 方法。都是在持有段锁的情况下进行,lock 和unlock。

    由于所有修改操作,在进行结构修改时,都会在最后一步写count 变量,通过这种机制保证get 操作,能够得到几乎最新的结构更新。count 是volatile 的,对hash 链进行遍历不需要加锁的原因在于next 指针是final 的。读的时候,若value 不为空,直接返回;若为空则,加锁重读

    Segment 是一种可重入锁

    Segments 数组的长度size 通过Concurrencylevel 计算得出(Concurrencylevel 为14、15、

    16 时,size 都为16。Concurrencylevel 默认为16),Segments 数组长度必须为2n(一个

    Concurrenthashmap 有一个Segments 数组,每个Segment 相当于一个hashtable)。

    Concurrenthashmap 的get 操作时如何做到不加锁的呢?因为get 方法里将需要使用的共享变量都定义为volatile(定义为volatile 的变量,可以在线程之间保证可见性,可以被多线程同时读,但只能被单线程写)。Get 中只需要读,不需要写,所以不用加锁。之所以不会读到过期值,因为java 内存模型的happen-before 原则,对volatile 字段的写入操作先于读操作,即使两个线程同时修改volatile 变量,get 也能拿到最新的值。

    Resize 时,只对Segment 扩容,不对整个Concurrenthashmap 扩容,先判断是否需要扩容,再put,传统hashmap 是先put 再判断。

    Size(),求大小:最安全的做法是统计size()时把all Segment 的put、remove、clean 方法全部锁住,效率低。所以Concurrenthashmap 的做法是:先尝试两次不锁住Segment 的方法来统计各个Segment 大小。若统计的过程中,容器的count 发生了变化,则采用加锁的方式来统计。使用modcount 变量来判断统计过程中,count 是否变化。在put、remove、clean 方法操作元素前modcount 要加1,那么在统计size 前后比较modcount 是否发生变化,从而得知容器大小是否变化。

    对中断操作(调用interrupt)的正确理解是,他并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时候中断自己。比如,wait、sleep、

    join等方法,当他们收到中断请求或开始执行时,发现某个已经被设置好的中断状态,则抛出异常interruptedException。 

    每个线程都有一个boolean中断状态,当中断线程时,这个中断状态将被设为true。

    interrupt方法:中断目标线程。isInterrupted:返回目标线程的中断状态。静态的

    interrupted方法:清除当前线程的中断状态,并返回它之前的值。大多数可中断的阻塞方法会在入口处检查中断状态。 线程可以分为两种:普通线程和守护线程.t.setDaemon(true)将 t 设为守护线程。

    有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限的等待。

    newFixedThreadPool 中线程数固定且线程不超时,newCachedThreadPool 最大线程数

    Integer.Max_value,并且超时设为60 秒。

    AbortPolicy 是默认拒绝策略。

    This.interrpted:检查当前线程中断状态,且将中断状态标志清除;this.isInterrupted:检查this 的中断状态,且不改变线程的状态标识。在Thread 对象上调用isInterrupted 方法可以检查任何线程的中断状态,但是线程一旦被中断,isInterrupted 方法变会返回true,而一旦sleep().wait 方法抛出异常,他将清除中断标志,下一次调用isInterrupted 返回false。

    interrpted 是静态方法,不能在特定的线程中使用,只能报告它的线程的中断状态,若当前线程被中断,返回 true,但它会自动重置中断状态为 false,第二次调用时总是返回 false。

    Hashmap 的resize 在多线程的情况下可能产生条件竞争。因为如果两个线程都发现

    hashmap 需要进行resize 了,他们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为移动到新的位置时,hashmap 并不会将元素放在链表的尾部,而是放在头部在,这是为了避免尾部遍历。(否则,针对key 的hashcode 相同的entry 每次添加还要定位到尾节点)。如果条件竞争发生了,可能出现环形列表。之后,当我们调用

    get(key)操作时就可能发生死循环。

    Concurrenthashmap:初始化时除了initialCapacity,loadfactor 参数,还有一个重要的参数concurrency level,它决定了segment 数组的长度,默认为16(长度需要为2 的n 次方,与采用的hash 算法有关)。每次get/put 操作都会通过hash 算法定位到一个segment,then 再通过hash 算法定位到某个具体的entry。

    Get 操作时不需要加锁的。因为get 方法里将要使用的共享变量都定义为volatile。定义为volatile 的变量,能够在线程之间保存可见性,能够被多个线程同时读,并且保证不会读到过期的值。但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值,像直接set 值就可以,而i++这种就非线程安全。)

    Put 必须加锁。

    ReenTrantLock 1)可重入,单线程可以重复进入,但要重复退出。synchronized 的也是可重入锁。2)可中断,可响应中断。3)可限时:超时不能获得锁,就返回false,不会永久等待构成死锁。4)公平锁:线程先来,先得到锁,可在构造函数中设置。

    Condition 的await()与single()和object 的wait()和notify()类似。

    对于锁来说,它是互斥的排它的,意思就是只要我获得了锁,没人再能获得,而对于

    somaphore 来说,它允许多个线程同时进入临界区,可认为它是一个共享锁,但是共享的额度是有限的,额度用完了,其他没有拿到额度的线程还是要阻塞在临界区外,当额度为1 时,相当于lock。

    ReadWriteLock:读-读不互斥,读-写互斥,写-写互斥,并发性提高。

    CountDownLatch:倒数计时器,一种典型的场景就是火箭发射,在火箭发射前,往往还要进行各项设备仪器的检查,只能等所有检查完成后,引擎才能点火,这种场景就非常适合使用CountDownLatch,它可以使点火线程使得所有检查线程全部完工后,再执行。

    cyclicBarrier:模拟高并发。Hashmap 的resize 在多线程的情况下可能产生条件竞争。因为如果两个线程都发现

    hashmap 需要进行resize 了,他们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为移动到新的位置时,hashmap 并不会将元素放在链表的尾部,而是放在头部在,这是为了避免尾部遍历。(否则,针对key 的hashcode 相同的entry 每次添加还要定位到尾节点)。如果条件竞争发生了,可能出现环形列表。之后,当我们调用

    get(key)操作时就可能发生死循环。

    Concurrenthashmap:初始化时除了initialCapacity,loadfactor 参数,还有一个重要的参数concurrency level,它决定了segment 数组的长度,默认为16(长度需要为2 的n 次方,与采用的hash 算法有关)。每次get/put 操作都会通过hash 算法定位到一个segment,then 再通过hash 算法定位到某个具体的entry。

    Get 操作时不需要加锁的。因为get 方法里将要使用的共享变量都定义为volatile。定义为volatile 的变量,能够在线程之间保存可见性,能够被多个线程同时读,并且保证不会读到过期的值。但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值,像直接set 值就可以,而i++这种就非线程安全。)

    Put 必须加锁。

    ReenTrantLock 1)可重入,单线程可以重复进入,但要重复退出。synchronized 的也是可重入锁。2)可中断,可响应中断。3)可限时:超时不能获得锁,就返回false,不会永久等待构成死锁。4)公平锁:线程先来,先得到锁,可在构造函数中设置。

    Condition 的await()与single()和object 的wait()和notify()类似。

    对于锁来说,它是互斥的排它的,意思就是只要我获得了锁,没人再能获得,而对于

    somaphore 来说,它允许多个线程同时进入临界区,可认为它是一个共享锁,但是共享的额度是有限的,额度用完了,其他没有拿到额度的线程还是要阻塞在临界区外,当额度为1 时,相当于lock。

    ReadWriteLock:读-读不互斥,读-写互斥,写-写互斥,并发性提高。

    CountDownLatch:倒数计时器,一种典型的场景就是火箭发射,在火箭发射前,往往还要进行各项设备仪器的检查,只能等所有检查完成后,引擎才能点火,这种场景就非常适合使用CountDownLatch,它可以使点火线程使得所有检查线程全部完工后,再执行。

    cyclicBarrier:模拟高并发。

    同步阻塞,用户空间的应用程序执行一个系统调用,这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或者发生错误)。

    同步非阻塞,设备以非阻塞形式打开,这意味着io 操作不会立刻完成,需要应用程序调用多次来等待完成。

    同步和异步1)同步:发出一个调用时,在没有得到结果前,该调用就不返回,一旦返回就有结果。2)异步:调用在发出之后就直接返回,所以没有返回结果,换句话说,当一个异步调用发生后,调用者不会立即得到结果,而是在调用发生后,被调用者通过状态通知来通知调用者,或者通过回调函数来处理这个调用。

    阻塞和非阻塞1)阻塞:调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。2)非阻塞:不能立刻得到结果之前,该调用不会阻塞当前线程。

    BIO:同步并阻塞,一个连接一个线程,适用于链接数量小且固定的架构。

    NIO:同步非阻塞:一个请求一个线程,客户端发送的链接请求都会注册到多路复用器上,多路复用器轮训到链接有io 请求时才启动一个线程进行处理,适用于链接比较多,比较短。

    AIO:异步非阻塞,一个有效请求一个线程,适用于链接数目多且长。

    悲观锁大多数情况下,依靠数据库的锁机制。乐观锁大多基于数据版本,记录机制实现。数据版本。为数据增加一个版本标识,比如增加一个version 字段。读数据时,将版本号一块儿读出,之后更新时,版本号加1,将提交数据的版本号与数据库表对应记录的当前版本号进行对比。若提交的大于数据库里面的,则可以更新,否则认为是过期数据。将乐观锁策略在存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据表直接对外公开。

    解决超卖的方法1)乐观锁2)队列,建立一条先进先出的队列,每个请求加入到队列中,然后异步获取队列数据进行处理,把多线程变为单线程,处理完一个就从队列中移出一个,因为高并发,可能一下将内存撑爆,然后系统又陷入异常状态。或者设计一个极大的队列,但是系统处理速度和涌入速度根本没办法比,平均响应时间小。

    锁隔离级别实现原理:1)读未提交,事务在读数据时并不加锁,事务在写数据时加行级共享锁。2)读已提交,事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完,立即释放该行级共享锁;事务在更新某数据的瞬间,必须先加行级排他锁,直到事务结束才释放。3)可重复读,事务在读取某数据瞬间(开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放;事务在更新某数据瞬间(发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。4)序列化,事务在读取数据时,必须先对其加表级共享锁,直到事务结束才释放;事务在更新数据时,必须先对其加表级排他锁,直到事务结束才释放。

    CountDownLatch 是一种灵活的闭锁实现,可以使一个或多个线程等待一组事件发生,

    事件全发生后阻塞的才能继续执行。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示要等待的事件发生。CountDown 方法递减计数器,表示有一个事件已经发生,而

    await   方法等待计数器变为零,表示所有需要等待的事情都发生。当计数器为零时,await

    上等待的线程可继续执行。

    CountDownLatch 构造函数传入的值就是计数器的初始值,且只可被设置一次。

    Semaphore:用来控制同时访问某个特定资源的操作数量。用acquire 获取一个许可,若没有则等待。用release 释放一个许可。单个信号量Semaphore 可实现互斥。

    CountDownLatch 计数只能用1 次。一个或多个线程等待其他线程完成后再执行;

    线程池中线程任务就是不断检测任务队列是否有任务并不断执行队列中的任务。

    一、Runnable 的run 方法没有返回值,并且不可以抛出异常;callable 的call 方法有返回值,并且可以抛出异常,且返回值可以被future 接收。

    Callable c=new Callable(){ Public Integer call() throws Exception{

    Return new Random().nextInt(100);

    }}

    Future 是一个接口。Futuretask 是他的一个实现类,可通过futuretask 的get 方法得到返回值。

    Futuretask future =new Futuretask< Integer >( callable); New Thread(future).start();

    二、使用ExecutorService 的submit 方法执行callable;

    ExecutorService threadpoll=Executors.new SingleThreadExecutor(); Futuretask future= threadpoll.submit(new callable(){

    Public Integer call() throws Exception{ Return new Random().nextInt(100);

    }

    })

    Future 对象表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用get 方法来获取结果,若没有执行完,get 方法可能会阻塞,直到线程执行完。取消由cancel 方法执行。isDone 确定任务是正常执行还是被取消。一旦计算完成,就不能再取消了。

    乐观锁适用于写较少的情况,即冲突真的很少发生,这样可以省去了锁的开销,加大了系统的吞吐量。

    若只是让某个类实现serializable 接口,而没有其他处理的话,就是使用默认序列化机制,即在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其他对象也进行序列化。同样的,这些其他对象引用的另外对象也将被序列化。

    若一个成员变量被 transient 修饰后,不可被序列化。要想被序列化,则要在类中加上两个方法:1.WriteObject(ObjectOutputStream out) out.defaultWriteObject(); out.writeInt(age);

    和 2.ReadObject(OnjectInputStream in) in.defaultReadObject(); age=in.ReadInt();都是

    private 的,通过反射调用。

    无状态的对象一定是线程安全的。无状态指:不包含任何域,也不包含任何对其他类中域的引用。我理解的状态就是域。(i++不是原子的)。

    发布一个对象:使对象能在当前作用域之外的代码中使用。某个不该发布的对象被发布称为逸出。

    如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。

    同步封装类是由Collections.synchronizedXxx 等工厂方法创建的,这些类实现安全的方式是,将他们的状态封装起来,并且每个公有方法进行同步,使得每次只有一个线程能访问容器的状态。

    同步容器类都是线程安全的,但是在某些情况下可能需要额外的客户端加锁来保护复合操作。比如:“若没有,则添加”。

    如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出问题。有三种方法可以修复这个问题:1)不在线程之间共享该变量。2)变量改为不可变。

    3)使用同步。

    竟态条件:由于不恰当的执行顺序而不正确的结果。本质——基于一种可能失效的观察结果来作出判断。先检查后执行:首先观察到某个条件为真(例如某个文件x 不存在),然后根据这个观察结果采取相应的动作(创建文件x),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在此之间创建了文件x),从而导致各种问题。

    要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

    可重入锁:若某个线程试图获取一个已经由他自己持有的锁,那么这个请求会成功。实现方式:为每一个锁关联一个获取计数器和一个所有者线程。当计数器为零时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,将计数器置为1.若同一个线程再次获取这个锁,计数器递增,而当前线程退出同步代码块时,计数器会相应递减,为0 时,会释放锁。

    锁保证原子性和可见性,volatile 只有可见性。

    当且仅当满足以下所有条件,才使用volatile 变量:1)写操作不依赖当前值。2)该变量不会与其他变量一起纳入不变性条件。3)访问变量时不需要加锁。

    线程封闭:仅在单线程内访问数据,比如threadlocal。

    Threadlocal——当使用 Threadlocal 维护变量时,Threadlocal 为每个使用该变量的线程提供独立的变量副本,所以每个线程可以独立改变自己的变量副本,而不影响其他线程的副本。

    从线程的角度看,目标变量就是线程本地变量。

    在Threadlocal 类中有一个map,用于存储每一个线程的变量副本,map 中key 为

    Threadlocal 对象,value 为该线程的变量副本。

    线程隔离的秘密就在ThreadlocalMap 类中。ThreadlocalMap 类是Threadlocal 类的静态内部类。它实现了键值对的设置和获取。每个线程对应一个ThreadlocalMap,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。

    Threadlocald 的set 方法

    Public void set(T value){

    Thread T=Thread.concurrentThread(); ThreadLocalMap map=getMap(t); If(map!=null)

    Map.set(this.value);

    Else

    }

    createMap(t,value);

    当前线程的ThreadlocalMap 是在第一次调用set 方法时创建,并设置上对应的值。每个线程的数据还是在自己线程内部,只是用Threadlocal 引用。ThreadlocalMap(key,value).key 为当前Threadlocal 对象,value 为变量值。一个线程的ThreadlocalMap 中有很多变量,通过

    Threadlocal 对象判断选哪个变量。

    wait() 抛出interceptedException

    并发包——1)Executor、ExecutorSerice、AbstractExecutorSerice、ThreadPoolExecutor。

    2)copyOnWriteArrayList、copyOnWriteSet。3)BlockingQueue4)CycleBarrier、CountDownLatch、

    Semophore5)Future、callable6)lock

    NIO:nio 是new io,主要用到的是块,所以nio 效率比io 高。Java api 中有2 套nio:1)针对标准输入输出nio;2)网络编程nio。Io 以流的方式处理数据;nio 以块的方式处理数据。面向流的io 一次处理一个字节,一个输入流产生一个字节,一个输出流消费一个字节。面向块的io,每一个操作都在一步中产生或消费一个数据块。

    channel 是对原io 中流的模拟,任何来源和目的数据都必须通过一个channel 对象。一个Buffer 是一个容器对象,发给channel 的所有对象都必须先放到Buffer 中,同样的,从

    channel 中读取的任何数据都要读到Buffer。

    在nio 中,数据是放入buffer 对象的。在io 中,数据是直接写入或读到stream 对象。应用程序不能直接对channel 进行读写操作,而必须通过buffer 来进行。

    使用buffer 读写数据,一般经过以下四步:1)写入数据到buffer;2)调用flip 方法;3)从buffer 中读取数据;4)调用clear 方法和compact 方法。

    当向buffer 写入数据时,buffer 会记录下写了多少数据,一旦要读取数据,通过调用flip 方法将buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到buffer 中的所有数据。一旦读完所有的数据,就需要清空缓冲区,让他可以再次被写入。有两种方式能清空缓冲区:1)clear 清空整个缓冲区2)compact 只会清除已经读过的数据,未读的数据被移到缓冲区的起始处,新写的数据将放到缓冲区未读数据之后。

    Buffer 是父类,它的子类有Bytebuffer、shortbuffe、intbuffer、longbuffer、floatbuffer、

    doublebuffer、charbuffer。

    buffer 是一个对象,它包含要写入或读取的数据;channel 是一个对象,可以通过他读取和写入数据。

    并发编程中的三个概念:原子性、可见性、有序性。

    cpu 为了提高程序运行顺序,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行结果一致。

    cpu 在进行重排序时,会考虑指令之间的依赖性,若一个指令instruction 2 必须用到instruction

    1 的结果,则cpu 保证1 在2 之前执行.

    指令重排序不影响单个线程,但是影响线程并发执行的正确性。

    线程对变量的所有操作都必须在工作内存中进行,而不能直接对主内存进行操作,且每个线程不能访问其他线程的工作内存。

    X=10; X=y; X++;

    X=x+1;只有第一个是原子的,后3 个是非原子的。

    volatile 禁止指令重排序有两层意思:1)当程序执行到volatile 变量的读操作或者写操作时,在其前面操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没执行。2)在进行指令优化时,不能将在对volatile 变量访问的语句放在其后面执行,也不能把volatile 变量后面的语句放到其前面执行。

    x=0;//语句 1 x,y 为非 volatile 变量,flag 是 volatile 的;

    y=2;//语句2

    flag=true;//语句3

    x=4;//语句4

    y=-1;//语句5

    由于flag 为volatile 的,那么在进行指令重排序时,不会将语句3 放在语句1、2 前,也不会将语句3 放到语句4、5 后,但是1、2 的顺序和4、5 的顺序不可保证.并且volatile 关键字能保证执行到语句3 时,语句1 和2 已经执行完毕,且语句1 和语句2 的执行结果对语句3、4、5 是可见的。

    Volatile 原理:观察加入volatile 关键字和没有加入volatile 时所产生的汇编代码,发现加入volatile 时,会多出一个lock 前缀指令,lock 前缀指令相当于内存屏障,内存屏障提供三个功能:1)它确保指令重排序时不会把其后面的指令排到内存屏障之前,也不会把前面的放在内存屏障之后,即在执行到内存屏障这句指令时,在他前面的操作已经全部完成。2)它会强制将对缓存的修改操作立即写入主存。3)若是写操作,它会导致其他cpu 中对应的缓存行无效。

    使用场景:1)对变量的写操作不依赖当前值。2)该变量没有包含在具有其他变量的不变式中。比如:1 标记状态、2)双重检查。

    JVM 如何知道一个对象要分配多大的内存

    当类被加载如内存后,jvm 就知道。JVM 可以通过普通java 对象的元数据信息确定java

    对象的大小。

    线性窥孔优化:1)既可以是中间代码,又可以是目标级代码2)每次处理的只是一组相邻指令,相当于将一组相邻指令暴露在一个优化窗口中(正如窥孔的含义)。3)对优化对象进行线性扫描4)优化后产生的结果可能会给后面的代码提供进一步优化的机会5)窥孔优化程序通常很小,只需很少的额内存,执行速度很快。

    类需要同时满足下面3 个条件才是“无用的类”:

    1.该类的all 实例都已经被回收

    2.加载改类的classloader 已经被回收

    3.该对象的java.lang.class 对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

    永久代的垃圾回收主要回收废弃常量和无用的类。

    对象所需的内存大小在类加载完成就可以确定,为对象分配内存1)指针碰撞(内存是绝对规则的,用过的放一边,没用过的放一边)2)空闲列表

    在虚拟机中,对象在内存中的存储的布局可以分为3 个区域:对象头、实例数据、对齐

    填充。对象头由2 部分组成:标记字段(对象自身运行时的数据)和类型指针(对象指向它的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类是实例)。对象的大小必须是8 字节的整数倍,而对象头已经是8 字节的整数倍(1 倍or 2 倍),所以当实例数据没有对齐时,就要通过对齐填充来补全。

    对象的访问方式

    1)句柄访问,堆中有一个句柄池,reference 中存放的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

    2)直接指针访问,reference 放的就是对象的地址

    假如堆内存最大为2G,现在发现电脑内存占用了3G,原因可能是直接内存。

    like”%aaa%” 不会使用索引,like” aaa%可以使用索引。

    被动引用:1)通过子类引用父类的静态字段,不会导致子类的初始化,即父类静态代码块执行,子类静态代码块不执行。2)通过数组定义来引用类,不会触发此类的初始化,eg,SuperClass [] ss=new SuperClass[10]。3)常量在编译阶段会存入调用的类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

    synchronized 是重量级的同步机制。

    all 的可重入代码都是线程安全的,但是并非all 线程安全的代码都是可重入的。

    锁消除是指虚拟机的编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

    并发编程中的三个概念:原子性、可见性、有序性。

    cpu 为了提高程序运行顺序,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行结果一致。

    cpu 在进行重排序时,会考虑指令之间的依赖性,若一个指令instruction 2 必须用到instruction

    1 的结果,则cpu 保证1 在2 之前执行.

    指令重排序不影响单个线程,但是影响线程并发执行的正确性。

    线程对变量的所有操作都必须在工作内存中进行,而不能直接对主内存进行操作,且每个线程不能访问其他线程的工作内存。

    X=10; X=y; X++;

    X=x+1;只有第一个是原子的,后3 个是非原子的。

    volatile 禁止指令重排序有两层意思:1)当程序执行到volatile 变量的读操作或者写操作时,在其前面操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没执行。2)在进行指令优化时,不能将在对volatile 变量访问的语句放在其后面执行,也不能把volatile 变量后面的语句放到其前面执行。

    x=0;//语句 1 x,y 为非 volatile 变量,flag 是 volatile 的;

    y=2;//语句2

    flag=true;//语句3

    x=4;//语句4

    y=-1;//语句5

    由于flag 为volatile 的,那么在进行指令重排序时,不会将语句3 放在语句1、2 前,也不会将语句3 放到语句4、5 后,但是1、2 的顺序和4、5 的顺序不可保证.并且volatile 关键字能保证执行到语句3 时,语句1 和2 已经执行完毕,且语句1 和语句2 的执行结果对语句3、4、5 是可见的。

    Volatile 原理:观察加入volatile 关键字和没有加入volatile 时所产生的汇编代码,发现加入volatile 时,会多出一个lock 前缀指令,lock 前缀指令相当于内存屏障,内存屏障提供三个功能:1)它确保指令重排序时不会把其后面的指令排到内存屏障之前,也不会把前面的放在内存屏障之后,即在执行到内存屏障这句指令时,在他前面的操作已经全部完成。2)它会强制将对缓存的修改操作立即写入主存。3)若是写操作,它会导致其他cpu 中对应的缓存行无效。

    使用场景:1)对变量的写操作不依赖当前值。2)该变量没有包含在具有其他变量的不变式中。比如:1 标记状态、2)双重检查。

    win32 环境中,线程有3 种线程模式,单线程(一个线程完成all 工作,一个人搬房子),单元线程(all 线程都在主应用程序内存中各自的子段范围内运行,此模式允许多个代码实例同时单独立运行,找朋友搬房子,每个朋友在单独的房间工作,并且不能帮助在其他房间工作的人),自由线程(多个线程可以同时调用相同的方法和组件,与单元线程不同,自由线程不会被限制在独立的内存空间,找朋友搬房子,all 朋友可以随时在任何一个房间工作)

    同步:一个进程在执行某个请求时,若该请求需要一段时间才能返回消息,那么这个进程将会一直等下去,直到收到返回信息才继续执行下去。异步:进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态,当有消息返回时,系统会通知进程进行处理,这样可以提高效率。

    一个流程图称为可归约的等价于流程图中去除回边外,其余的边构成一个无环路流图。耦合程度从高到低内容耦合>标记耦合>数据耦合

    PAD 图:问题分析图,描述软件的详细设计,只能描述结构化程序运行使用的几种基本结果。

    守护线程(deamon thread),必须声明在start 方法之前。

    Thread 的yield():暂停当前正在执行的线程,并执行其他线程。

    Java 中的线程由一个虚拟处理机、cpu 执行的代码以及代码操作的数据三部分组成。

    相关文章

      网友评论

          本文标题:JAVA多线程

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