java并发之多线程

一、多线程简介

1.1线程简介

一个进程内可以有多个线程,这些线程作为操作系统调度的最小单元,负责执行各种各样的任务,这些线程都拥有各自的计数器、堆栈、局部变量等属性,并且可以访问共享内存


1.2线程创建的几种方式

实现Runnable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。(3)调用线程对象的start()方法来启动该线程。

继承Thread,重写其 run 方法

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。(2)创建Thread子类的实例,即创建了线程对象。(3)调用线程对象的start()方法来启动该线程。

通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值采用实现Runnable、Callable接口的方式创见多线程时,优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。使用继承Thread类的方式创建多线程时优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。

1.3线程的状态

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

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

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

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

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

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


1.4线程的关键方法

1.4.1 sleep()方法

sleep( )是一个静态方法,让当前正在执行的线程休眠(暂停执行),而且在睡眠的过程是不释放资源的,保持着锁。

在睡眠的过程,可以被中断,注意抛出InterruptedException异常;


作用:

1、暂停当前线程一段时间;

2、让出CPU,特别是不想让高优先级的线程让出CPU给低优先级的线程

try {        //单位是毫秒,睡眠1秒

        Thread.sleep(1000);

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

1.4.2 wait()方法

Object.wait()方法:

·让出 CPU,释放对象锁

·在调用前需要先拥有某对象的锁,所以一般在 synchronized 同步块中使用

·使该线程进入该对象监视器的等待队列

1.4.3 Thread.yield()

“Thread.yield()表示暂停当前线程,让出 CPU 给优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程。

·和 sleep() 方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。

· yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

同样也是一个静态方法,暂停当前正在执行的线程,线程由运行中状态进入就绪状态,重新与其他线程一起参与线程的调度。


作用:

线程让步,顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。但是,这种让步只对同优先级或者更高优先级的线程而言,同时,让步具有不确定性,当前线程也会参与调度,即有可能又被重新调度,那么就没有达到让出CPU的效果了。

1.4.4 Thread.join()

Thread.join()表示线程合并,调用线程会进入阻塞状态,需要等待被调用线程结束后才可以执行。

join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

1.5线程锁

1.5.1 lock和synchronized的区别

那么lock和synchronized的区别对比如下:

(1)synchronized在成功完成功能或者抛出异常时,虚拟机会自动释放线程占有的锁;而Lock对象在发生异常时,如果没有主动调用unLock()方法去释放锁,则锁对象会一直持有,因此使用Lock时需要在finally块中释放锁;

(2)lock接口锁可以通过多种方法来尝试获取锁包括立即返回是否成功的tryLock(),以及一直尝试获取的lock()方法和尝试等待指定时间长度获取的方法,相对灵活了许多比synchronized;

(3)通过在读多,写少的高并发情况下,我们用ReentrantReadWriteLock分别获取读锁和写锁来提高系统的性能,因为读锁是共享锁,即可以同时有多个线程读取共享资源,而写锁则保证了对共享资源的修改只能是单线程的


1.5.2 ReentrantLock(重入锁)

效果和synchronized一样,都可以同步执行,lock方法获得锁,unlock方法释放锁

(1)Lock类也可以实现线程同步,而Lock获得锁需要执行lock方法,释放锁需要执行unLock方法

(2)Lock类可以创建Condition对象,Condition对象用来是线程等待和唤醒线程,需要注意的是Condition对象的唤醒的是用同一个Condition执行await方法的线程,所以也就可以实现唤醒指定类的线程

(3)Lock类分公平锁和不公平锁,公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁

(4)Lock类有读锁和写锁,读读共享,写写互斥,读写互斥



1.5.3 ReentrantReadWriteLock读写锁

几个特性:公平选择性/重进入/锁降级

锁降级是指写锁降级成为读锁。如果当前线程持有写锁,然后将其释放再获取读锁的过程不能称为锁降级。锁降级指的在持有写锁的时候再获取读锁,获取到读锁后释放之前写锁的过程称为锁释放。

锁降级在某些情况下是非常必要的,主要是为了保证数据的可见性。如果当前线程不获取读锁而直接释放写锁,假设此时另外一个线程获取了写锁并修改了数据。那么当前线程无法感知该线程的数据更新。

1.5.4 synchronized修饰方法和修饰代码块

(1)对于普通同步方法,锁是当前实例对象。

(2)对于静态同步方法,锁是当前类的Class对象。

(3)对于同步方法块,锁是Synchonized括号里配置的对象。

参考文章:

http://www.jb51.net/article/74566.htm

https://www.cnblogs.com/paddix/p/5367116.html

 

Synchronized原理每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

(1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

(2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

(3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

1.5.5 Volatile关键字详解

java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

(1)volatile保证可见性

(2)volatile不能确保原子性

(3)volatile保证有序性

参考文档:

http://www.importnew.com/24082.html

1.5.6 ThreadLocal详解

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

当一个变量定义为volatile之后,将具备两种特性:

  1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。

  2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile性能:

volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

1.6线程进程之间的通讯

多线程间通信方式:

(1)共享变量

(2)wait/notify机制

(3)Lock/Condition机制

(4)管道

进程与进程间通信

(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关 系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。(6)内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。(7)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

(8)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字


二、线程池

线程池由任务队列和工作线程组成,它可以重用线程来避免线程创建的开销,在任务过多时通过排队避免创建过多线程来减少系统资源消耗和竞争,确保任务有序完成; ThreadPoolExecutor继承自 AbstractExecutorService

实现了ExecutorService接口,ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor 实现了 ExecutorService 和 ScheduledExecutorService 接口

2.1对象创建的步骤

(1)检查对应的类是否已经被加载、解析和初始化

(2)类加载后,为新生对象分配内存

(3)将分配到的内存空间初始为 0

(4)对象进行关键信息的设置,比如对象的哈希码等

(5)执行 init 方法初始化对象


2.2 线程池包括以下四个基本组成部分

 (1)线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;(2)工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; (3)任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;(4)任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

2.3线程池具体的执行方法ThreadPoolExecutor.execute

主要分为三步:

(1) 当前池中线程比核心数少,新建一个线程执行任务

(2) 核心池已满,但任务队列未满,添加到队列中

(3) 核心池已满,队列已满,试着创建一个新线程

2.4四种常见线程池的实现


2.4.1 FixedThreadPool

用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量

这个线程池执行任务的流程如下

(1)它是一种固定大小的线程池;

(2)corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;

(3)keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;

(4)阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;

(5)由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;

(6)由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。


底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue() 无界阻塞队列


通俗:创建可容纳固定数量线程的池子,每个线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)


适用:执行长期的任务,性能好很多



2.4.2 SingleThreadExecutor

用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

它的执行流程如下:

(1)线程池中没有线程时,新建一个线程执行任务

(2)有一个线程以后,将任务加入阻塞队列,不停加加加

(3)唯一的这一个线程不停地去队列里取任务执

底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue() 无界阻塞队列

通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)

适用:一个任务一个任务执行的场景


2.4.3 CachedThreadPool

用于并发执行大量短期的小任务,或者是负载较轻的服务器

它的执行流程如下:

(1)没有核心线程,直接向SynchronousQueue中提交任务

(2)如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个

(3)执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去

底层:返回ThreadPoolExecutor实例,corePoolSize为0;

maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)

通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。

适用:执行很多短期异步的小程序或者负载较轻的服务器(执行时间短,并发量小的)


2.4.4 ScheduledThreadPool

用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

具体执行任务的步骤:

(1)线程从DelayQueue中获取 time 大于等于当前时间的 ScheduledFutureTask

(2)执行完后修改这个task的 time 为下次被执行的时间

(3)然后再把这个task放回队列中

底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,

maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列

通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构

适用:周期性执行任务的场景


2.5线程池两种提交任务

(1)execute():提交不需要返回值的任务

(2)submit():提交需要返回值的任务

2.6 线程池的关闭

shutdown()和showdownNow()两种方法,都是调用了interruptIdleWorkers()方法去遍历线程池中的工作线程,然后去打断它们


2.7 线程池参考文献

http://greemranqq.iteye.com/blog/2041076

http://blog.csdn.net/u011240877/article/details/73440993#什么是线程池

你可能感兴趣的:(java并发之多线程)