Java并发编程---学习笔记
1. 多线程
Java并发编程是采用多线程进行编程。多线程的引入可以提高程序运行的效率。
1.1 线程
创建一个线程一般有三种方法:
1.1.1 Runnable
(1) 创建一个实现了Runnable接口的一个实例, 重写run()方法
(2) 用创建的实例构造一个线程
(3) 调用start()方法启动线程,线程会执行RunnableTask中的run()方法
线程执行完run()方法后,线程就结束了。
public class RunnableThread {
public static class RunnableTask implements Runnable {
@Override
public void run() {
// do something
}
}
public static void main(String[] args) {
RunnableTask r = new RunnableTask();
Thread thread = new Thread(r);
thread.start();
}
}
1.1.2 Callable
区别于Runnable,Callable是带返回值的。
(1) 创建一个实现了Callable接口的一个实例c,重写call()方法
(2) 用FutureTask包装创建的实例
(3) 用FutureTask实例初始化线程
(4) 调用start()方法启动线程,线程会执行CallableTask的call()方法
(5) 调用FutureTask的get方法可以获取call()方法的返回值
public classCallableThread {
publicstaticclassCallableTaskimplements Callable<Integer> {
@Override
public Integer call()throws Exception {
// doSomething
return new Integer(1);
}
}
publicstaticvoidmain(String[] args)throws InterruptedException, ExecutionException {
CallableTask c = new CallableTask();
FutureTask<Integer> fTask = newFutureTask<Integer>(c);
Thread thread = new Thread(fTask);
thread.start();
System.out.println(fTask.get());
}
}
1.2.3 Thread
(1) 继承Thread重写run()方法。
(2) 调用start()方法启动线程,线程会执行run()方法
public class TThread {
public static class ThreadTask extends Thread{
@Override
public void run() {
// do something
}
}
public static void main(String[] args) {
ThreadTask tt = new ThreadTask();
tt.start();
}
}
1.2 线程池
作用:减少线程的创建和销毁的开销。
通过Executors.newFixedThreadPool设置线程池中线程的个数。程序线程数不是越多越好,也不是越少越好。受cpu个数和应用程序的影响。
Java中最优线程个数的计算方法:
public classThreadPool {
publicstaticvoidmain(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
Thread t1 = new Thread(new RunnableTask());
Thread t2 = new Thread(new RunnableTask());
Thread t3 = new Thread(new RunnableTask());
pool.equals(t1);
pool.equals(t2);
pool.equals(t3);
pool.shutdown();
}
}
1.3 ThreadGroup
ThreadGroup优点在于可以遍历所有的线程,知道哪些线程是否运行完毕。调用enumerate方法将线程组所有活跃的子线程复制到指定的数组中,从而获取所有活动线程。
public classTThreadGroup {
publicstaticclassThreadInstanceextends Thread{
ThreadInstance(ThreadGroup tg, Stringname){
super(tg,name);
}
public void run() {
while(true){
}
}
}
public static void main(String[] args)throws InterruptedException {
ThreadGroup tg = new ThreadGroup("group");
ThreadInstance t1 = new ThreadInstance(tg,"t1");
ThreadInstance t2 = new ThreadInstance(tg,"t2");
ThreadInstance t3 = new ThreadInstance(tg,"t3");
t1.start();
t2.start();
t3.start();
Thread threads[] = newThread[tg.activeCount()];
tg.enumerate(threads);
for (Thread t : threads)
System.out.println(t.getName());
}
}
2. 线程安全
并发环境下,会导致线程安全问题。导致错误或者失效数据问题,也可能导致死锁等活跃性问题。可以通过以下的方式保证在并发环境下线程是安全的。
2.1 同步和锁
同步和加锁的方式是为了解决由于多线程资源共享导致的数据不一致性的问题。
需要同步的原因(包含但不仅限于):
(1) 编译器中生成指令的顺序可能与源代码中的顺序不同。
(2) 编译器可能会把变量保存在寄存器中而不是直接存储在内存中
(3) 缓存可能改变写入变量提交到内存中的次序
需要锁的原因(包含但不仅限于):
(1) 多个线程交叉执行,同时修改某个一个相同的对象或者变量。
没有采取同步和加锁的多线程的例子:
public classWrongMultiThreadExample {
privatestaticbooleanready;
privatestaticint value;
privatestaticclassReaderThreadextends Thread {
@SuppressWarnings("static-access")
public void run() {
while (ready) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(value);
}
}
}
publicstaticvoidmain(String[] args)throws InterruptedException {
new ReaderThread().start();
value = 10;
ready = true;
}
}
运行的顺序可能是:
第一种和第二种顺序是因为主线程和子线程之间的交叉执行。
第三种执行的顺序是因为编译代码和源代码之间的顺序不一致,先执行ready=true, 然后执行子线程的run(), 最后执行value=10。
多线程对共享资源进行读写操作,在没有采取加锁和同步的情况下,不能保证代码的执行顺序和正确性。
2.1.1 synchronized
synchronized是java的内置锁,java中每个对象和每个类都对应一把锁,是一种互斥锁,也就是说最多只有一个线程能持有这个锁。synchronized可以保证可见性和原子性。
synchronized用法(包括修饰方法和代码块)都是对对象加锁或者类加锁。
(1) synchronized方法
用synchronized关键字来声明方法。
public synchronized void doSomething();对当前实例加锁。
public synchronized static void doSomething();对当前类加锁。
例如:java中的Vector, synchronized修饰方法是对当前实例加锁,等价于synchronized(this)。最多只有一个线程持有该锁,所以当有一个线程获取实例的锁时,其他线程必须等待。当执行完该方法后,自动释放对象的锁。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
(2) synchronized代码块
用synchronized关键字来声明代码块。
synchronized(lock) {
}
synchronized修饰代码块的时候,一次只有一个线程可以获取lock锁,可以获取任何对象的锁。
2.1.2 volatile
volatile变量是java提供的一种同步的机制。由于多线程环境下,VM的优化等使得不同线程对同一个变量所见状态不一致,volatile修饰后,VM等不会对该变量进行优化,所见即所得。所以volatile能保证变量的可见性
public volatile boolean yesornot;
volatile只能保证可见性,并不能保证独占性。所以volatile变量并不能保证一次只有一个线程操作。
2.1.3 Lock和ReenTrantLock
从Lock接口中可以看出lock是一种可轮询定时及中断的锁。要显示的调用unlock才能释放锁。
public interfaceLock {
public void lock();
public void lockInterruptibly();
public ConditionnewCondition();
public boolean tryLock();
public boolean tryLock(long arg0, TimeUnit arg1);
public void unlock();
}
用法:
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
Lock采用定是与轮询的方式可以避免死锁的发生。例如在指定的时间内得不到需要的锁,则可以平缓的失败,以错误状态结束操作;或者定义好最大的超时时间等,间隔一定的时间获取锁,如果获取失败且没有超过最大的获取时间则继续获取,否则失败。
Java 6使用了改进算法来管理内置锁,所以内置所和ReentrantLock之间的性能差异相近。所以java中建议如果只是简单的加锁,使用内置锁。仅当内置锁不能满足需求时,考虑用ReentrantLock锁。
2.1.4 读-写锁
ReenTrantLock和synchronized都是一种互斥锁,即每次最多只有一个线程能持有锁。这是一种保守的加锁策略。避免写写冲突、读写冲突时,也避免了读读冲突,如果应用场景中大部分的操作是读操作,性能就显得不乐观。
ReenWriteLock暴露了两个lock。
public interfaceReenWriteLock {
LockreadLock();
LockwriteLock();
}
每次只允许一个写操作,允许多个读操作。
用法:
public classReadWriteMap<K, V> {
private Map<K, V>map;
private Locklock =new ReentrantLock();
public ReadWriteMap(Map<K,V> map) {
this.map = map;
}
public V get(K key) {
lock.lock();
try {
returnmap.get(key);
}finally{
lock.unlock();
}
}
public void put(K key, V value) {
lock.lock();
try {
map.put(key, value);
}finally{
lock.unlock();
}
}
}
2.1.5 Condition
Condition是一种条件锁,只有满足一定的条件时,线程才可以执行,否则一直阻塞。
接口:
public interfaceCondition {
void await()throws InterruptedException;
boolean await(long time, TimeUnit unit)throws InterruptedException;
long awaitNanos(long nanosTimeout) ;
void awaitUninterruptibly();
booleanawaitUntil(Date deadline)throws InterruptedException;
voidsignal();
voidsignalAll();
}
例如Java中ArrayBlockingQueue的实现。
用ReentrantLock生成了两个条件锁notEmpty和notFull,用来说明队列是否为空,是否已满。所以Condition实际上是绑定在lock上。
private final ReentrantLock lock;
/** Condition for waiting takes */
privatefinalConditionnotEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity,boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = (E[])newObject[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ArrayBlockingQueue的take方法,当队列为空的时候,线程一直阻塞,直到线程不为空的时候,线程执行extract方法,取出一个元素,并通知所有等待notFull条件的线程可以开始执行了。Put方法也类似,当线程满的时候,线程一直阻塞,直到线程不满的时候,线程insert一个元素,并通知所有等待notEmpty条件的线程可以执行了。
public E take() throws InterruptedException {
finalReentrantLock lock =this.lock;
lock.lockInterruptibly();
try {
try {
while (count == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); //propagate to non-interrupted thread
throw ie;
}
E x = extract();
return x;
} finally {
lock.unlock();
}
}
privateE extract() {
finalE[] items =this.items;
E x = items[takeIndex];
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
2.1.6 同步工具类
(1) 闭锁
在闭锁到达结束状态之前,没有任何线程可以执行,当闭锁结束后,所有的线程都能通过。
也就是说闭锁是用于线程等待的一个事件。
例如:n个人比赛跑步,要保证n个人是同步听见枪响。
public longrunSample(intn,finalRunnable runSport)throws InterruptedException {
finalCountDownLatch gun =new CountDownLatch(1);
finalCountDownLatch end =new CountDownLatch(n);
for(inti = 0; i < n; i++){
Thread t = new Thread() {
public void run(){
try{
gun.await();//等待开枪
try{
runSport.run();
}finally{
end.countDown();
}
}catch( InterruptedException e){
Thread.currentThread().interrupt();
}
}
};
t.start();
}
longstartTime = System.nanoTime();
gun.countDown();//开枪
end.await();
longendTime = System.nanoTime();
returnendTime - startTime;
}
(2) FutureTask
Java中的线程要获取返回结果,需要实现Callable接口,见1.1.2。
例如:程序启动的时候希望预先加载something. 可以通过FutureTask
的get方法等待加载完毕后,获取返回的信息。
public classpreloader{
privatefinalFutureTask<ProductInfo> future=
newFutureTask<ProductInfo> (newCallable<ProductInfo>(){
publicProductInfo call()throws DataLoadException{
returnloadProductInfo();
}
});
privatefinalThread thread = newThread(future);
publicvoidstart() { thread.start(); }
publicProductInfo get() throwsDataLoadException,InterruptedException{
try{
return future.get();
}catch(ExecutionExceptione){
Throwable cause=e.getCause();
if (causeinstanceof DataLoadException)
throw (DataLoadException) cause;
else
throw launderThrowable(cause);
}
}
}
(3) 信号量
控制同时访问某个特定资源的操作数量。
例如:实现一个有界的HashSet,添加元素时,通过Semaphore的acquire()方法获取信号量;移除元素时,调用Semaphore的release()方法释放信号量。
public classBoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded =false;
try {
wasAdded= set.add(o);
return wasAdded;
}finally{
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved =set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}
(4) 栅栏
栅栏类似于闭锁,阻塞一组线程直到某个事件发生。闭锁用于等待事件,而栅栏用于线程之间相互等待。
例如:规定五个人只要都跑到终点了,大家可以喝啤酒。但是,只要有一个人没到终点,就不能喝,那么先到的人就必须等待其他没到的人。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public classBeer {
public static void main(String[] args) {
final int count = 5;
final CyclicBarrier barrier =new CyclicBarrier(count,new Runnable() {
@Override
public void run() {
System.out.println("drink beer!");
}
});
// they do not have to start at the same time...
for (int i = 0; i < count;i++) {
new Thread(new Worker(i,barrier)).start();
}
}
}
class Worker implements Runnable {
final int id;
final CyclicBarrierbarrier;
public Worker(finalint id,final CyclicBarrier barrier){
this.id = id;
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(this.id +"starts to run !");
Thread.sleep((long) (Math.random()* 10000));
System.out.println(this.id +"arrived !");
this.barrier.await();
}catch(InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e) {
e.printStackTrace();
}
}
}
2.2 锁和同步方法的比较
2.2.1 synchronized和volatile
synchronized尽量不要直接修饰方法,特别是执行时间较长的方法,会导致持有锁的时间过长,导致其他线程阻塞。
当只需要满足可见性时,尽量采用volatile,而不是synchronized。例如只有一个线程写,其他线程都是读操作等。
2.2.2 synchronized和ReenTrantLock
java6后对内置锁synchronized进行了优化。synchronized和ReenTrantLock的性能差异很小。synchronized使用简单,而且java一直致力于对内置锁的改进。所以当synchronized能满足需求的时候,尽可能使用synchronized。当且仅当synchronized不满足需求的时候,采用ReenTrantLock。
2.2.3 synchronized和读-写锁
使用场景多数是读操作的时候,采用读写锁的性能大于synchronized。Synchronized在读读操作的时候,也只允许一个线程执行。所以当应用场景读操作远远大于写操作的时候,采用读写锁由于内置锁。
2.3 ThreadLocal
ThreadLocal是一种线程封闭的技术,除了加锁和同步的方法保证线程的安全性以外,线程封闭也是一种在并发环境下保证线程安全型的方法。ThreadLocal将对象封闭在一个线程中,不同的线程操作的变量都是独立的。
ThreadLocal采用Map<Thread, T>存储了每个线程的值。
用法:
public classUniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal<Integer>uniqueNum =
new ThreadLocal<Integer>(){
@Override protected Integer initialValue() {
returnuniqueId.getAndIncrement();
}
};
publicstaticintgetCurrentThreadId() {
returnuniqueId.get();
}
}
2.4 不变性
另一种避免使用同步和加锁的方式是采用不可变的对象,该对象被创建后就不能被修改。final类或者final成员等。
3 性能
并发能提高效率,同时可能因为锁的原因导致性能下降。写多线程程序时,可以通过以下方法提高性能。
3.1 快进快出
尽可能的缩小锁持有的时间。例如将一些无关的代码移除同步代码块,特别是开销大的无关操作。
3.2 减小锁的粒度
降低线程请求锁的频率,对锁进行分解。
例如:将LockOpt的内置锁进行分解,将代码1修改成代码2。用两个锁替代一个锁。降低了线程请求锁的频率。
代码1:
public classLockOpt {
publicfinalList<String>nodes;
publicfinalList<String>operaters;
publicLockOpt(List<String> nodes, List<String> operaters){
this.nodes = nodes;
this.operaters = operaters;
}
publicsynchronizedvoidaddOperater(String operater){
operaters.add(operater);
}
publicsynchronizedvoidaddNode(String node){
nodes.add(node);
}
}
代码2:
public classLockOpt {
publicfinalList<String>nodes;
publicfinalList<String>operaters;
publicLockOpt(List<String> nodes, List<String> operaters) {
this.nodes = nodes;
this.operaters = operaters;
}
publicvoidaddOperater(Stringoperater){
synchronized (operater) {
operaters.add(operater);
}
}
publicvoidaddNode(String node) {
synchronized (nodes) {
nodes.add(node);
}
}
}
3.3 锁分段
锁分段是在锁分解的基础上进一步扩展。以ConcurrentHashMap为例。
ConcurrentHashMap拥有一个包含多个锁的数组。所以当多个操作不在一个分段锁中操作时,能达到并发性能。
ConcurrentHashMap中分段锁的实现, Segment继承了ReentrantLock, 每个Segment保护一个HashEntry<K,V>[]table,以put操作为例, 执行操作之前会lock,执行完毕后会unlock。所以将ConcurrentHashMap的锁进行了分段处理,各个锁之间的操作互不干扰。
staticfinalclassSegment<K,V>extends ReentrantLockimplements Serializable {
transient volatileHashEntry<K,V>[] table;
V put(K key, int hash, V value,boolean onlyIfAbsent) {
lock();
try {
int c =count;
if (c++ >threshold)// ensurecapacity
rehash();
HashEntry<K,V>[]tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V>first = tab[index];
HashEntry<K,V> e =first;
while (e !=null && (e.hash != hash ||!key.equals(e.key)))
e = e.next;
V oldValue;
if (e !=null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = newHashEntry<K,V>(key,hash, first, value);
count = c;// write-volatile
}
return oldValue;
} finally {
unlock();
}
}
}
以上部分的例子来源于《java并发编程》、 互联网或jdk中。