java并发基础
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发基础相关的知识,希望对你有一定的参考价值。
一.基本线程机制
并发编程使我们可以将程序分为多个分离的,独立的运行的任务.通过使用多线程机制这些独立的任务可以由执行线程来驱动.一个线程就是进程中的一个单一的顺序控制流.
1.创建线程
1.1 实现Runnable接口并实现run方法.
将Runnable对象转换成工作任务的传统方式是把它交给Thread构造器.
Thread t = new Thread(new Runnable());
t.start();
1.2 继承Thread类并重写run方法.
1.3 Executor
java SE5 中的java.util.concurrent包中的执行器(Executor)用来管理Thread对象,Executor允许管理异步任务的执行.Executor是首选的启动任务的方法.
ExecutorService exec = Executor.newCachedThreadPool();
ExecutorService exec = Executor.newFixedThreadPool(5);//使用有限的线程集处理任务.
ExecutorService exec = Executor.newSingleThreadPool();//就像线程数为1的FixedThreadPool.如果向SingleThreadPool提交多个任务,这些任务将排队执行.
exec.execute(new Runnable());//执行任务
exec.shutDown();//可以防止新的任务提交给Executor.
1.4 从任务中产生返回值
Runnable是执行工作的独立任务,但是它不返回值.如果希望任务可以返回值需要实现Callable接口实现call方法.Callable是一种具有类型参数的泛型.
exec.submit(new Callable());//submit()将产生Future对象,他用Callable返回的结果参数化.Future对象可以不必等处理结果,立即返回对象,在需要使用结果的时候,在使用Future.get()方法获取.也可以在获取处理结果之前使用idDone()判断处理线程是否结束.
1.5 休眠
让线程处于阻塞状态的一种简单方法是调用sleep()方法.在java se5中引入了TimeUnit类,可以指定sleep的时间单元.
在调用sleep时会抛出InterruptedException,异常不能跨线程传播,所以线程中的异常都要在任务内部就处理.
await()方法也可以让线程处于阻塞状态,它和sleep的最主要区别是await会释放锁,但是sleep不会.await通常用在线程之间的相互协调中.
1.6 优先级
java中可以设置线程的优先级.但是线程的优先级并不保证优先级高的线程就一定先执行.优先级高的线程在争夺cup的时间片时有更大的优势.在绝大多数时间里,所有线程都应该以默认的线程优先级运行,试图改变线程优先级是一种错误.
jdk有10个优先级,但是它与多数操作系统映射的不是很好.能移植的优先级有MAX-PRIORITY,MIN-PRIORITY,NROM-PRIORITY
1.7 让步
Thread的静态方法yied()会让当前线程放弃cup的时间片,让所有就绪状态的线程(包括它自己)重新抢夺cup时间片.调用yied方法就相当于是告诉当前线程:你的工作做得差不多了,可以让别的线程工作了..
1.8 后台线程
后台线程也称为守护线程,是指在程序运行的时候在后台提供通用服务的线程,并且这种线程并不属于程序中不可或缺的一部分.当所有非后台线程结束时,程序也就终止了,并会杀死所有后台线程.java中的GC就是一个后台线程.
将一个线程设置为后台线程可以在在这个线程启动时调用t.setDaemon()方法.需要注意的是后台线程派生的所有线程都默认是后台线程,我们可以通过t.isDaemon()来确定线程是否是后台线程.在后台线程中的finally语句块并不能保证执行,因为当所有非后台线程执行完毕之后,JVM会立即关闭所有后台线程,并退出.finally会被jvm的退出而强行打断.
1.9 加入一个线程
如果某个线程t在另一个线程a上调用t.join(),那么这个线程将被挂起,直到t线程结束才恢复,也可以在join方法中带一个超时参数.对join()方法的调用是可以被中断的,需要在调用线程上调用interrupt()方法.
二 共享受限资源
1.共享资源 2.涉及到修改操作 3.多个线程使用
基本上所有的并发模式在解决线程冲突问题时都是采用序列化访问共享资源的方案,也就是在同一时刻只允许一个线程可以访问共享资源.
2.1 加锁机制
synchronized
使用显示的Lock对象
java se5中java.util.concurrent包中有一个Lock互斥机制.lock对象必须被显示的创建,锁定,释放.与synchronied相比Lock更加的灵活.比如在synchronied中出现异常了,只能抛出一个异常,但是在lock对象中,可以使用try{}finally{},在finally中处理失败的清理工作,并释放锁.synchronied关键字不能尝试着获取锁且最终获取失败,或者尝试着获取一段时间,然后放弃它,要实现这些,必须使用concurrent类库.
private ReentrantLock lock = new ReentrantLock();
boolean captured = lock.tryLock(2,TimeUnit.SECINDS);
try{}
finally{
if(captured){
lock.unlock();
}
}
2.2 原子性和易变形
原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在发生“上下文切换”之前执行完毕。依赖原子性来解决并发问题是很棘手很危险的。专家级的程序员可以利用原子性来编写无锁代码,但是这太难了。
原子性可以应用到除long和double之外的所有基本类型上。long和double采用两个32位的内存你来存储和操作的,在并发线程中可能看到只读取到高32或者低32的情况。
volatitle关键字可以确保应用中的可视性。
2.3 终结任务
对于正在运行的任务必须要有一个合理的终结方法,使他们可以正确的终结并处理终结之后的资源释放操作。有时候必须终结处于阻塞状态的任务。
进入阻塞状态的可能的原因:
1.通过认为的调用休眠方法。
2.通过调用wait使线程被挂起.
3.任务在等待io的输入或输出完成.
4.任务在等待某个对象上的锁.
中断的几种方式
1.Thread类包含interrupt()方法,可以设置线程的中断状态.如果一个线程已经被阻塞或者执行一个阻塞操作,那么设置这个中断状态会抛出InterruptException.
2.新的concurrent类库似乎在避免对Thread对象的直接操作,转而尽量通过Executor来执行所有的操作.Executor中提供了shoutdownNow()来中断所有由当前Executor启动的线程.当需要只中断某一个线程时,可以通过Executor的submit()方法来启动这个线程,然后通过cance;l(true),来中断这个线程.
可以中断sleep造成的阻塞,但是不能中断尝试获取synchronied锁或者试图执行io操作的的线程.通常想要中断这类线程可以通过关闭底层资源来实现中断.
在java SE5并发类库中添加了一个特性,即在ReentrantLock上阻塞的任务具备可以被中断的能力,这与synchronied方法或临界区上阻塞的任务完全不同.
在任务循环时,通常以中断状态来检查任务是否被中断,从而推出.通过设置任务的中断状态并不能立即中断程序,只有当任务处于或者试图进入阻塞状态时才会抛出异常.
三 线程之间的协作
线程之间协作来使得多个任务可以一起工作去解决一个问题.
3.1 wait()和notifyAll()
wait()可以使线程挂起,等待某个条件的发生,通常这个条件是当前线程不可控制的,由其他线程改变并唤醒当前线程.wait()方法和sleep()的根本区别是wait方法释放了锁,并且wait()的调用必须是在同步代码块中,因为wait要操作锁.
wait()方法有两种方式,第一种是接受一个毫秒数作为参数.可以通过notify(),notifyAll()唤醒,或者时间到期从wait方法中恢复.第二种是不接受参数,只能通过notify ,notifyAll方法来唤醒.
wait ,notify,notifyAll都是Object的方法.而不是Thread类的一部分.这三个方法也只能在同步方法或者同步块中调用,因为需要操作锁.如果在非同步代码中调用他们,编译可以通过但是运行会抛出IllegalMonitorStateException异常,并且会伴随一些含糊的消息,比如当前线程不是拥有者.
notify notifyAll都是唤醒当前锁上的线程的,notify是唤醒一个,notifyAll是唤醒所有的.
通常在使用notifyAll唤醒所有线程时,所有被唤醒的线程首先要检查一下自己是否是自己关注的条件,如果不是继续wait,这样做可以确保程序的正确执行.因为如果有多个任务在等待同一个锁,当这些任务同时被唤醒时,第一个任务可能修改条件,那么其他的任务就应该继续wait等待条件.也就是使用while来编写wait这部分的代码,确保正确的条件下才执行代码.
notify和notifyAll
notify是notifyAll的一种优化,但是使用notify时,必须保证被唤醒的是恰当的任务.否则就使用nofityAll.
Condition对象
Condition对象是concurrent包中的允许任务挂起的基本类,可以通过await()挂起一个任务,使用signal(),signalAll()唤醒在这个Condition上的任务.与使用notifyAll想不使用signalAll更安全.
3.2 同步队列
使用wait和notifyAll以一种非常低级的方式完成了任务之间的协作问题,即每次都进行握手.在很对情况下可以选择使用更高的抽象级别,使用同步列队来完成协作问题.同步队列在任何时候只允许一个任务插入或者移除一个元素.在concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的实现.有无届队列LInkedBlockingQueue和有届队列ArrayBlockingQueue.当队列为空时可以挂起试图从队列中获取元素的任务,当队列中有元素时又可以唤醒这些任务.阻塞队列可以解决非常多的问题,比使用wait和notifyAll更简单更可靠.
3.3 任务之间通过使用管道进行输入/输出
在java类库中对应的是PipedWriter和PipedReader类,这个模型看起来就像生产者-消费者的变体.管道基本上就是一个阻塞队列.首先创建一个PipedWriter对象,然后建立一个和这个PepedWriter相关联的PipedReader对象,就是在PipedReader的构造器中传入PipedWriter对象.在PepedReader对象调用reade()方法时,如果管道里没有数据就进入阻塞状态.
3.4 死锁
死锁的条件:
1.有互斥条件.
2.至少有一个任务持有一个资源的锁再去获取另一个被别的任务持有的资源.
3.资源不能被任务抢占.
4.循环等待.
解决死锁只需要打破死锁的任意一个条件即可.
四 新类库中的构件
4.1 CountDownLatch
它被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成.
给CountDownLatch对象设置一个初始值,任何在这个对象上调用wait()方法的任务都将阻塞,直至这个计数值达到0.其他任务可以在执行完成时在该对象上调用countDown()方法来减小这个计数值.注意CountDownLath只能触发一次,计数值不能被重置.
4.2 CyclicBarrier
使用于这样的情况:你希望建一组任务,他们并行的执行工作,然后再进行下一步之前等待,直至所有任务都完成。它使得所有任务都将在栅栏处列队,因此可以一致地向前运动。CyclicBarrier有点像CountiDownLatch,但是CyclicBarrier可以多次重用的。
4.3 delayQueue
这是一个无届的BlockingQueue,用于放置实现了Delayed接口的对象。其中的对象只能在其到期时才能从队列中取出。这种队列是有序的,即对头对象的延迟时间最长。如果没有任何延迟到期的对象,那么就不会有任何头元素,并且poll()将返回null(正因为这样,你不能将null放入这种队列中)。
DelayedTask接口中有一个方法getDelay(),它可以用来告知延迟到期还有多长时间,或者延迟在多长时间之前已经到期了。
DelayQueue队列中的任务的执行顺序和创建顺序无关,只与任务的延迟时间有关。
4.4 PriorityBlockingQueue
这是一个优先级队列,它具有可阻塞的读取操作。这个队列是按照优先级顺序从队列中出现任务的,优先级是通过一个优先级数字定义的。
4.6 ScheduledExecutor
通过使用schedule()(运行一次)或者scheduleAtFixedRate()(每隔一段时间重复执行任务)将Runnable对象设置为在将来某个时间执行。
4.6 Semaphore
正常的锁在任何时刻都只允许一个任务可以访问,而计数信号量允许n个任务同时访问这个资源,你可以将信号计数量看做是在向外分发使用资源的许可证,尽管实际上没有使用任何许可证对象。
4.7 Exchanger
Excheger是两个任务之间交换对象的栅栏。当这些任务进入栅栏时,他们各自有一个对象,当他们离开时,他们拥有对方的对象。Exchanger的典型应用场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象,通着Exchanger可以有更多的对象在创建之后立马被消费。
以上是关于java并发基础的主要内容,如果未能解决你的问题,请参考以下文章