多线程
来源:互联网 发布:js调试遇到错误中断 编辑:程序博客网 时间:2024/06/06 05:09
1. 线程安全概念
(1)当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
(2)synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
(3)当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排对是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)。若多个线程竞争一把锁,就会增加cpu的使用量,数量过大甚至可能导致宕机。
2. 多个线程多个锁
(1)关键字synchronized修饰方法取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以示例代码中那个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。
(2)有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。
3. 线程业务的原子性
(1)在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue方法同时加锁synchronized同步关键字,保证业务(service)的原子性,不然会出现业务错误(也从侧面保证业务的一致性)。
例:若有500w数据量的表,A客户端9:00时发出一个查询某条数据的操作,此时该条数据字段值为100,因数据量较大,查询需要10分钟,而在这期间,9:05时B客户端发出一个更新的操作,更新了这个字段值为200。(由于Oracle更新时,会有一个undo的概念,记录DML语句【insert、update、delete】的备份,以便事务失败时回滚。)此时9:10A客户端查询发现这个值有变更,就会去undo中找,如果能找到就会返回100,如果找不到就会抛出snapshot too old的异常。即由于oracle的一致性读,9:00时候的查询最后返回的结果一定是9:00这一刻的值.。
4. volatile关键字
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
(1) 在java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝。当线程执行时,他在自己的工作内存区中操作这些变量。为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到他自己所在的工作内存区中,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
(2) volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不去线程工作内存区里去读取,从而实现了多个线程间的变量可见。也就是满足线程安全的可见性。
(3) volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码就大量使用volatile,可见netty性能一定是非常不错的。)这里需要注意:一般volatile用于只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。对一个数据进行更改,可配合使用原子性的Integer类atomicInteger。
5. 同步类容器
(1) 同步类容器都是线程安全的,但在某些场景下可能需要加锁来保护复合操作。复合类操作如:迭代(反复访问元素,遍历完容器中所有的元素)、跳转(根据指定的顺序找到当前元素的下一个元素)、以及条件运算。这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的便是ConcurrentModificationException,原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。
(2) 同步类容器:如古老的Vector、HashTable。这些容器的同步功能其实都是有JDK的Collections.synchronized***等工厂方法去创建实现的。其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须要有足够好的性能。
6. 并发类容器
(1)jdk5.0以后提供了多种并发类容器来替代同步类容器从而改善性能。同步类容器的状态都是串行化的。他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量。
(2)并发类容器是专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持。以及使用了CopyOnWriteArrayList代替Voctor,并发的CopyonWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,后者是以阻塞形式的队列,具体实现Queue还有很多,例如ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
6.1.ConcurrentMap
和hashMap一样,都继承了AbstractMap
(1) ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment)。也就是最高支持16个线程的并发修改操作。这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。
(2)ConcurrentSkipListMap(支持并发排序功能)。
6.2.CopyOnWrite
(1) Copy-On-Write简称COW,是一种用于程序设计中的优化策略。
(2) JDK里的COW容器有两种:CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常有用,可以在非常多的并发场景中使用到。
(3) CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。并发的写时底层用重入锁(ReentrantLock)实现同步,适合读多写少的场景。写比较多的场景可用jdk提供的普通容器,加重入锁(ReentrantLock、ReentrantReadWriteLock【读写锁】)来实现。
6.3.Queue
(1)遵循FIFO先进先出原则。
Deque双端队列(从头尾都可以操作),应用少。
(2)在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue
6.4.并发Queue
6.4.1. ConcurrentLinkedQueue
(1) ConcurrentLinkedQueue是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。
(2)ConcurrentLinkedQueue重要方法:
add()和 offer() 都是加入元素的方法(在ConcurrentLinkedQueue中,这俩个方法没有任何区别。BlockingQueue中offer()提供了三个参数,设置阻塞的时间,返回布尔值)
poll()和 peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
6.5.阻塞Queue
(1)取queue中的元素时,若queue中没有元素,该线程会阻塞,直到取到元素。同理,若向queue中添加元素时,若有界queue中,已达到上界,该线程会阻塞,直到其他线程拿走其中元素,可以添加成功为止。
(2)当非常大量的任务同时执行时,此时即使是分布式架构仍显不足,还需要把这些请求缓存到队列中,若数量很大,则要使用有界队列ArrayBlockingQueue,指明缓存数量,不指定让所有的任务都进入队列,可能一下子会使内存撑爆。若任务量在可承受的范围内,可使用无界队列LinkedBlockingQueue,性能优于有界队列。若不需要缓存数据时,可使用SynchronousQueue。
(3)无界队列构造器中指定参数,参数值为初始化空间数量。
6.5.1. ArrayBlockingQueue
ArrayBlockingQueue: 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合非常适合使用。
6.5.2. LinkedBlockingQueue
LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁,从而实现生产者和消费者操作的完全并行运行。他是一个无界队列。
6.5.3. PriorityBlockingQueue
PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,他也是一个无界的队列。
6.5.4. DelayQueue
DelayQueue:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、 任务超时处理、空闲连接的关闭等等。
6.5.5. SynchronousQueue
SynchronousQueue:一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并消费。
7. 生产消费者模型
生产者和消费者也是一个非常经典的多线程模式,我们在实际开发中应用非常广泛的思想理念。在生产-消费模式中:通常由两类线程,即若干个生产者的线程和若干个消费者的线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。
8. Executor
(1) 为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效地进行线程控制。它们都在java.util.concurrent包中,是JDK并发包的核心。其中有一个比较重要的类:Executors,他扮演这线程工厂的角色,我们通过Executors可以创建特定功能的线程池。
8.1.Executors创建线程池方法
8.1.1. newFixedThreadPool()
newFixedThreadPool()方法,该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中等待有空闲的线程去执行。
8.1.2. newSingleThreadExecutor()
newSingleThreadExecutor()方法,创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务列队中。
8.1.3. newCachedThreadPool()
newCachedThreadPool()方法,返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60秒后自动回收。
8.1.4. newScheduledThreadPool()
newScheduledThreadPool()方法,该方法返回一个SchededExecutorService对象,但该线程池可以指定线程的数量。
8.2.自定义线程池
若Executors工厂类无法满足我们的需求,可以自己去创建自定义的线程池,其实Executors工厂类里面的创建线程方法其内部实现均是用了ThreadPoolExecutor这个类,这个类可以自定义线程。构造方法如下:
public ThreadPoolExecutor(int corePoolSize, --- 默认初始线程数
int maximumPoolSize, ---最大线程数
long keepAliveTime, --- 空闲时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, ---指定缓存队列
ThreadFactorythreadFactory,
RejectedExecutionHandler handler ---拒绝策略的方法
) {...}
8.2.1. 在使用有界队列时
若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,若大于corePoolSize,则会将任务加入队列,若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
8.2.2. 无界的任务队列时
LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而有没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
8.2.3. JDK拒绝策略
AbortPolicy:直接抛出异常组织系统正常工作
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,优于已加入队列中的任务,先运行当前被丢弃的任务。
DiscardOldestPolicy:丢弃队列中最老的一个请求,尝试再次提交当前任务。
DiscardPolicy:丢弃无法处理的任务,不给予任何处理。
如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口,常用。
9. JMS
当前,CORBA、DCOM、RMI 等RPC 中间件技术已广泛应用于各个领域。但是面对规模和复杂度都越来越高的分布式系统,这些技术也显示出其局限性:
(1)同步通信:客户发出调用后,必须等待服务对象完成处理并返回结果后才能继续执行;
(2)客户和服务对象的生命周期紧密耦合:客户进程和服务对象进程都必须正常运行;如果由于服务对象崩溃或者网络故障导致客户的请求不可达,客户会接收到异常;
(3)点对点通信:客户的一次调用只发送给某个单独的目标对象。
面向消息的中间件(Message Oriented Middleware,MOM)较好的解决了以上问题。发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列中,在合适的时候再将消息转发给接收者。这种模式下,发送和接收是异步的,发送者无需等待;二者的生命周期未必相同:发送消息的时候接收者不一定运行,接收消息的时候发送者也不一定运行;一对多通信:对于一个消息可以有多个接收者。
JAVA 消息服务(JMS)定义了Java 中访问消息中间件的接口。JMS 只是接口,并没有给予实现,实现JMS 接口的消息中间件称为JMS Provider,已有的 MOM 系统包括Apache的ActiveMQ、以及阿里巴巴的RocketMQ、IBM 的MQSeries、Microsoft 的MSMQ和BEA的MessageQ、RabbitMQ等等..他们基本都遵循JMS规范。
9.1.jms概念
(1)JMS实现JMS 接口的消息中间件
(2)Provider(MessageProvider):生产者
(3)Consumer(MessageConsumer):消费者
(4)PTP:Point to Point,即点对点的消息模型;
(5)Pub/Sub:Publish/Subscribe,即发布/订阅的消息模型;
(6)Queue:队列目标;
(7)Topic:主题目标;
(8)ConnectionFactory:连接工厂,JMS 用它创建连接;
(9)Connection:JMS 客户端到JMS Provider 的连接;
(10)Destination:消息的目的地;
(11)Session:会话,一个发送或接收消息的线程;
(12)ConnectionFactory接口(连接工厂)
用户用来创建到JMS提供者的连接的被管对象。JMS客户通过可移植的接口访问连接,这样当下层的实现改变时,代码不需要进行修改。管理员在JNDI名字空间中配置连接工厂,这样,JMS客户才能够查找到它们。根据消息类型的不同,用户将使用队列连接工厂,或者主题连接工厂。
(13)Connection接口(连接)
连接代表了应用程序和消息服务器之间的通信链路。在获得了连接工厂后,就可以创建一个与JMS提供者的连接。根据不同的连接类型,连接允许用户创建会话,以发送和接收队列和主题到目标。
(14)Destination接口(目标)
目标是一个包装了消息目标标识符的被管对象,消息目标是指消息发布和接收的地点,或者是队列,或者是主题。JMS管理员创建这些对象,然后用户通过JNDI发现它们。和连接工厂一样,管理员可以创建两种类型的目标,点对点模型的队列,以及发布者/订阅者模型的主题。
(15)MessageConsumer接口(消息消费者)
由会话创建的对象,用于接收发送到目标的消息。消费者可以同步地(阻塞模式),或异步(非阻塞)接收队列和主题类型的消息。
(16)MessageProducer接口(消息生产者)
由会话创建的对象,用于发送消息到目标。用户可以创建某个目标的发送者,也可以创建一个通用的发送者,在发送消息时指定目标。
(17)Message接口(消息)
是在消费者和生产者之间传送的对象,也就是说从一个应用程序创送到另一个应用程序。一个消息有三个主要部分:
消息头(必须):包含用于识别和为消息寻找路由的操作设置。
一组消息属性(可选):包含额外的属性,支持其他提供者和用户的兼容。可以创建定制的字段和过滤器(消息选择器)。
一个消息体(可选):允许用户创建五种类型的消息(文本消息,映射消息,字节消息,流消息和对象消息)。
消息接口非常灵活,并提供了许多方式来定制消息的内容。
(18)Session接口(会话)
表示一个单线程的上下文,用于发送和接收消息。由于会话是单线程的,所以消息是连续的,就是说消息是按照发送的顺序一个一个接收的。会话的好处是它支持事务。如果用户选择了事务支持,会话上下文将保存一组消息,直到事务被提交才发送这些消息。在提交事务之前,用户可以使用回滚操作取消这些消息。一个会话允许用户创建消息生产者来发送消息,创建消息消费者来接收消息。
(19)JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。
StreamMessageJava原始值的数据流
MapMessage一套名称-值对
TextMessage一个字符串对象
ObjectMessage一个序列化的 Java对象
BytesMessage一个未解释字节的数据流