Java线程新特征——Java并发库

一、线程池

  Sun在Java5中,对Java线程的类库做了大量的扩展,其中线程池就是Java5的新特征之一,除了线程池之外,还有很多多线程相关的内容,为多线程的编程带来了极大便利。为了编写高效稳定可靠的多线程程序,线程部分的新增内容显得尤为重要。
    有关Java5线程新特征的内容全部在java.util.concurrent下面,里面包含数目众多的接口和类,熟悉这部分API特征是一项艰难的学习过程。当然新特征对做多线程程序没有必须的关系,在java5之前通用可以写出很优秀的多线程程序。只是代价不一样而已。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
    在Java5之前,要实现一个线程池是相当有难度的,现在Java5为我们做好了一切,我们只需要按照提供的API来使用,即可享受线程池带来的极大便利。
Java5的线程池分好多种:固定尺寸的线程池可变尺寸连接池
 
在使用线程池之前,必须知道如何去创建一个线程池,在Java5中,需要了解的是java.util.concurrent.Executors类的API,这个类提供大量创建连接池的静态方法,是必须掌握的。

a、固定大小的线程池

import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; /** 
* Java线程:线程池- 
* 
* */ 
public class Test { public static void main(String[] args) { //创建一个可重用固定线程数的线程池 ExecutorService pool = Executors.newFixedThreadPool(2); //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口 Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); Thread t4 = new MyThread(); Thread t5 = new MyThread(); //将线程放入池中进行执行 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); //关闭线程池 
                pool.shutdown(); } 
} class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在执行..."); } 
}

运行结果:

pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...

 如果将线程池的大小改为4,则运行结果如下:

pool-1-thread-2正在执行...
pool-1-thread-3正在执行...
pool-1-thread-3正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...

 

b、单任务线程池

在上例的基础上改一行创建pool对象的代码为:

 //创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。 
ExecutorService pool = Executors.newSingleThreadExecutor(); 

则,运行结果为:

pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
对于以上两种连接池,大小都是固定的,当要加入的池的线程(或者任务)超过池最大尺寸时候,则入此线程池需要排队等待。
一旦池中有线程完毕,则排队等待的某个线程会入池执行。

 c、可变尺寸的线程池

 与上面的类似,只是改动下pool的创建方式:

  //创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。 ExecutorService pool = Executors.newCachedThreadPool(); 

运行结果如下:

pool-1-thread-1正在执行...
pool-1-thread-5正在执行...
pool-1-thread-4正在执行...
pool-1-thread-3正在执行...
pool-1-thread-2正在执行...

d、延迟连接池

package concurrent;
import java.util.concurrent.Executors; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/** 
* Java线程:线程池- 
* 
* 
*/ 
public class Test { public static void main(String[] args) { //创建一个线程池,它可那排在给定延迟后运行命令或者定期地执行ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口 Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); Thread t4 = new MyThread(); Thread t5 = new MyThread(); //将线程放入池中进行执行 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); //使用延迟执行风格的方法pool.schedule(t4, 5, TimeUnit.SECONDS);pool.schedule(t5, 10, TimeUnit.SECONDS);//关闭线程池 
                pool.shutdown(); } 
} class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在执行..."); } 
}

e、单任务连接线程池

在d的代码基础上,做改动

 //创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

运行时,会发现,t4延迟5s后得到执行,t5延迟10s后得到执行。运行结果如下:

pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...

f、自定义线程池

package concurrent;
import java.util.concurrent.ArrayBlockingQueue; 
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.ThreadPoolExecutor; 
import java.util.concurrent.TimeUnit; /** 
* Java线程:线程池-自定义线程池 
* 
* 
*/ 
public class Test { public static void main(String[] args) { //创建等待队列 BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20); //创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2,TimeUnit.MILLISECONDS,bqueue); //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口 Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); Thread t4 = new MyThread(); Thread t5 = new MyThread(); Thread t6 = new MyThread(); Thread t7 = new MyThread(); //将线程放入池中进行执行 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); pool.execute(t6); pool.execute(t7); //关闭线程池 
                pool.shutdown(); } 
} class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + "正在执行..."); try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } } 
}

运行结构如下:
创建自定义线程池的构造方法很多,本例中参数的含义如下:

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue)
用给定的初始参数和默认的线程工厂及处理程序创建新的 ThreadPoolExecutor。使用 Executors 工厂方法之一比使用此通用构造方法方便得多。 
参数:
corePoolSize - 池中所保存的线程数,包括空闲线程。 
maximumPoolSize - 池中允许的最大线程数。 
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 
unit - keepAliveTime 参数的时间单位。 
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。 
抛出:
IllegalArgumentException - 如果 corePoolSize 或 keepAliveTime 小于零,或者 maximumPoolSize 小于或等于零,或者 corePoolSize 大于 maximumPoolSize。 
NullPointerException - 如果 workQueue 为 null
自定义连接池稍微麻烦些,不过通过创建的ThreadPoolExecutor线程池对象,可以获取到当前线程池的尺寸、正在执行任务的线程数、工作队列等等。

二、有返回值的线程

在Java5之前,线程是没有返回值的,常常为了“有”返回值,破费周折,而且代码很不好写。或者干脆绕过这道坎,走别的路了。现在Java终于有可返回值的线程了。
 可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须实现Runnable接口。
 执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。

 下面是一个简单的例子:

 

package MultiThread;
import java.util.concurrent.*; /** 
* Java线程:有返回值的线程 
* 
* 
*/ 
public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(2); //创建两个有返回值的任务 Callable<String> c1 = new MyCallable("A"); Callable<String> c2 = new MyCallable("B"); //执行任务并获取Future对象 Future<String> f1 = pool.submit(c1); Future<String> f2 = pool.submit(c2); //从Future对象上获取任务的返回值,并输出到控制台 System.out.println(">>>"+f1.get().toString()); System.out.println(">>>"+f2.get().toString()); //关闭线程池 
                pool.shutdown(); } 
} class MyCallable implements Callable<String>{ private String oid; MyCallable(String oid) { this.oid = oid; } @Override public String call() throws Exception { return oid+"任务返回的内容"; } 
}

运行结果:

>>>A任务返回的内容
>>>B任务返回的内容

比较简单,要深入了解还需要看Callable和Future接口的API啊。

三、并发库的锁

在Java5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,用来控制对竞争资源并发访问的控制,这些内容主要集中在java.util.concurrent.locks 包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。

接口摘要
ConditionConditionObject 监视器方法(waitnotifynotifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。
LockLock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
ReadWriteLockReadWriteLock 维护了一对相关的,一个用于只读操作,另一个用于写入操作。

 

 

 

 

 有关锁的介绍,API文档解说很多,看得很烦,还是看个例子再看文档比较容易理解

 a、普通锁

package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReentrantLock; /** 
* Java线程:锁 
* 
* 
*/ 
public class Test { public static void main(String[] args) { //创建并发访问的账户 MyCount myCount = new MyCount("95599200901215522", 10000); //创建一个锁对象 Lock lock = new ReentrantLock(); //创建一个线程池 ExecutorService pool = Executors.newCachedThreadPool(); //创建一些并发访问用户,一个信用卡,存的存,取的取,好热闹啊 UserThread ut1 = new UserThread("取款线程1", myCount, -4000, lock); UserThread ut2 = new UserThread("存款线程1", myCount, 6000, lock); UserThread ut3 = new UserThread("取款线程2", myCount, -8000, lock); UserThread ut4 = new UserThread("存款线程2", myCount, 800, lock); //在线程池中执行各个用户的操作 
                pool.execute(ut1); pool.execute(ut2); pool.execute(ut3); pool.execute(ut4); //关闭线程池 
                pool.shutdown(); } 
} /** 
* 信用卡的用户 线程
* 多个用户线程操作该信用卡
*/ 
class UserThread implements Runnable { private String threadName;                //用户线程 private MyCount myCount;        //所要操作的账户 private int iocash;                 //操作的金额,当然有正负之分了 private Lock myLock;                //执行操作所需的锁对象 
UserThread(String name, MyCount myCount, int iocash, Lock myLock) { this.threadName = name; this.myCount = myCount; this.iocash = iocash; this.myLock = myLock; } public void run() { //获取锁 
                myLock.lock(); //执行现金业务 System.out.println(threadName + "正在操作" + myCount + "账户,操作金额为" + iocash + ",当前金额为" + myCount.getCash()); myCount.setCash(myCount.getCash() + iocash); System.out.println("\t操作成功,操作金额为" + iocash + ",当前金额为" + myCount.getCash()); //释放锁,否则别的线程没有机会执行了 
                myLock.unlock(); } 
} /** 
* 信用卡账户,可随意透支 
*/ 
class MyCount { private String oid;         //账号 private int cash;             //账户余额 
MyCount(String oid, int cash) { this.oid = oid; this.cash = cash; } public String getOid() { return oid; } public void setOid(String oid) { this.oid = oid; } public int getCash() { return cash; } public void setCash(int cash) { this.cash = cash; } @Override public String toString() { return "MyCount{" + "oid='" + oid + '\'' + ", cash=" + cash + '}'; } 
}

运行结果:

取款线程1正在操作MyCount{oid='95599200901215522', cash=10000}账户,操作金额为-4000,当前金额为10000操作成功,操作金额为-4000,当前金额为6000
存款线程1正在操作MyCount{oid='95599200901215522', cash=6000}账户,操作金额为6000,当前金额为6000操作成功,操作金额为6000,当前金额为12000
存款线程2正在操作MyCount{oid='95599200901215522', cash=12000}账户,操作金额为800,当前金额为12000操作成功,操作金额为800,当前金额为12800
取款线程2正在操作MyCount{oid='95599200901215522', cash=12800}账户,操作金额为-8000,当前金额为12800操作成功,操作金额为-8000,当前金额为4800
从上面的输出可以看到,利用锁对象太方便了,比直接在某个不知情的对象上用锁清晰多了。但一定要注意的是,在获取了锁对象后,用完后应该尽快释放锁,以便别的等待该锁的线程有机会去执行。

b、读写锁

 在a中提到了Lock接口以及对象,使用它可以很优雅的控制了竞争资源的安全访问,但是这种锁不区分读写,称这种锁为普通锁。为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,在一定程度上提高了程序的执行效率。
Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,详细的API可以查看JavaAPI文档。
下面这个例子是在文例子的基础上,将普通锁改为读写锁,并添加账户余额查询的功能,代码如下:
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock; 
import java.util.concurrent.locks.ReentrantReadWriteLock;/** 
* Java线程:锁 
* 
* 
*/ 
public class Test { public static void main(String[] args) { //创建并发访问的账户 MyCount myCount = new MyCount("95599200901215522", 10000); //创建一个锁对象 ReadWriteLock lock = new ReentrantReadWriteLock(); //创建一个线程池 ExecutorService pool = Executors.newCachedThreadPool(); //创建一些并发访问用户线程,一个信用卡,存的存,取的取,好热闹啊 UserThread ut1 = new UserThread("取款线程1", myCount, -4000, lock,false); UserThread ut2 = new UserThread("存款线程1", myCount, 6000, lock,false); UserThread ut3 = new UserThread("取款线程2", myCount, -8000, lock,false); UserThread ut4 = new UserThread("存款线程2", myCount, 800, lock,false); UserThread ut5 = new UserThread("查询", myCount, 0, lock,true);//在线程池中执行各个用户的操作 
                pool.execute(ut1); pool.execute(ut2); pool.execute(ut3); pool.execute(ut4); pool.execute(ut5);//关闭线程池 
                pool.shutdown(); } 
} /** 
* 信用卡的用户 线程
* 多个用户线程操作该信用卡
*/ 
class UserThread implements Runnable { private String threadName;                //用户线程 private MyCount myCount;        //所要操作的账户 private int iocash;                 //操作的金额,当然有正负之分了 private ReadWriteLock myLock;                //执行操作所需的锁对象 private boolean ischeck;      //是否查询
UserThread(String name, MyCount myCount, int iocash, ReadWriteLock myLock,boolean ischeck) { this.threadName = name; this.myCount = myCount; this.iocash = iocash; this.myLock = myLock; this.ischeck=ischeck;} public void run() {if(ischeck){//获取读锁 
                myLock.readLock().lock(); //执行查询System.out.println("读:"+threadName + "正在查询" + myCount + "账户,,当前金额为" + myCount.getCash()); //释放获取到的读锁
                myLock.readLock().unlock();}else{//获取写锁
                myLock.writeLock().lock();myCount.setCash(myCount.getCash() + iocash); System.out.println("写:"+threadName+"操作成功,操作金额为" + iocash + ",当前金额为" + myCount.getCash()); //释放锁获取到的写锁
                myLock.writeLock().unlock(); }} 
} /** 
* 信用卡账户,可随意透支 
*/ 
class MyCount { private String oid;         //账号 private int cash;             //账户余额 
MyCount(String oid, int cash) { this.oid = oid; this.cash = cash; } public String getOid() { return oid; } public void setOid(String oid) { this.oid = oid; } public int getCash() { return cash; } public void setCash(int cash) { this.cash = cash; } @Override public String toString() { return "MyCount{" + "oid='" + oid + '\'' + ", cash=" + cash + '}'; } 
}

 运行结果:

写:取款线程1操作成功,操作金额为-4000,当前金额为6000
写:取款线程2操作成功,操作金额为-8000,当前金额为-2000
写:存款线程1操作成功,操作金额为6000,当前金额为4000
读:查询正在查询MyCount{oid='95599200901215522', cash=4000}账户,,当前金额为4000
写:存款线程2操作成功,操作金额为800,当前金额为4800

在实际开发中,最好在能用读写锁的情况下使用读写锁,而不要用普通锁,以求更好的性能。

 四、信号量

Java的信号量实际上是一个功能完毕的计数器,对控制一定资源的消费与回收有着很重要的意义,信号量常常用于多线程的代码中,并能监控有多少数目的线程等待获取资源,并且通过信号量可以得知可用资源的数目等等,这里总是在强调“数目”二字,但不能指出来有哪些在等待,哪些资源可用。
 因此,本人认为,这个信号量类如果能返回数目,还能知道哪些对象在等待,哪些资源可使用,就非常完美了,仅仅拿到这些概括性的数字,对精确控制意义不是很大。目前还没想到更好的用法。

下面是一个简单的例子:

package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; /** 
* Java线程:信号量 
* 
* 
*/ 
public class Test { public static void main(String[] args) { MyPool myPool = new MyPool(20); //创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); MyThread t1 = new MyThread("任务A", myPool, 3); MyThread t2 = new MyThread("任务B", myPool, 12); MyThread t3 = new MyThread("任务C", myPool, 7); //在线程池中执行任务 
                threadPool.execute(t1); threadPool.execute(t2); threadPool.execute(t3); //关闭池 
                threadPool.shutdown(); } 
} /** 
* 一个池 
*/ 
class MyPool { private Semaphore sp;     //池相关的信号量 /** * 池的大小,这个大小会传递给信号量 * * @param size 池的大小 */ MyPool(int size) { this.sp = new Semaphore(size); } public Semaphore getSp() { return sp; } public void setSp(Semaphore sp) { this.sp = sp; } 
} class MyThread extends Thread { private String threadName;            //线程的名称 private MyPool pool;                        //自定义池 private int x;                                    //申请信号量的大小 
MyThread(String threadName, MyPool pool, int x) { this.threadName = threadName; this.pool = pool; this.x = x; } public void run() { try { //从此信号量获取给定数目的许可 
                        pool.getSp().acquire(x); //todo:也许这里可以做更复杂的业务 System.out.println(threadName + "成功获取了" + x + "个许可!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放给定数目的许可,将其返回到信号量。 
                        pool.getSp().release(x); System.out.println(threadName + "释放了" + x + "个许可!"); } } 
}

运行结果:

任务A成功获取了3个许可!
任务B成功获取了12个许可!
任务B释放了12个许可!
任务C成功获取了7个许可!
任务A释放了3个许可!
任务C释放了7个许可!

从结果可以看出,信号量仅仅是对池资源进行监控,但不保证线程的安全,因此,在使用时候,应该自己控制线程的安全访问池资源。

五、阻塞队列

    阻塞队列是Java5线程新特征中的内容,Java定义了阻塞队列的接口java.util.concurrent.BlockingQueue,阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。
有了这样的功能,就为多线程的排队等候的模型实现开辟了便捷通道,非常有用。
java.util.concurrent.BlockingQueue继承了java.util.Queue接口,可以参看API文档。
下面给出一个简单应用的例子:
package MultiThread;
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.ArrayBlockingQueue; /** 
* Java线程:并发库-阻塞队列 
* 
* 
*/ 
public class Test { public static void main(String[] args) throws InterruptedException { BlockingQueue<Integer> bqueue = new ArrayBlockingQueue<Integer>(20); for (int i = 0; i < 30; i++) { //将指定元素添加到此队列中,如果没有可用空间,将一直等待(如果有必要)。 
                        bqueue.put(i); System.out.println("向阻塞队列中添加了元素:" + i); } System.out.println("程序到此运行结束,即将退出----"); } 
}

运行结果:

由于阻塞队列的大小为20个,当超过这个数目,又没有元素出队列的时候,队列将会阻塞。到后来的某一个时刻,程序将阻塞队列中的元素出队列,后面的元素才可以进队列。

六、阻塞栈

对于阻塞栈,与阻塞队列相似。不同点在于栈是“后入先出”的结构,每次操作的是栈顶,而队列是“先进先出”的结构,每次操作的是队列头。
这里要特别说明一点的是,阻塞栈是Java6的新特征。、
Java为阻塞栈定义了接口:java.util.concurrent.BlockingDeque,其实现类也比较多,具体可以查看JavaAPI文档。
下面看一个简单例子:
package MultiThread;import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
class HelloWorldThread {/** * Java线程:并发库-阻塞栈* * */ public static void main(String[] args) {BlockingDeque<Integer> bstack=new LinkedBlockingDeque<Integer>(20) ;for(int i=0;i<30;i++){//将指定元素添加到阻塞栈中,如果没有可用空间,将一直等待(如果有必要)。 
          bstack.push(i);System.out.println("向阻塞栈中添加了元素:" + i); if(bstack.size()==20){System.out.println("队列满,弹出"+bstack.pop()); System.out.println("队列满,弹出"+bstack.pop()); System.out.println("队列满,弹出"+bstack.pop()); }}System.out.println("程序到此运行结束,即将退出----"); }}

程序的运行结果和阻塞队列的运行结果一样,程序并没结束,二是阻塞住了,原因是栈已经满了,后面追加元素的操作都被阻塞了。

七、条件变量

    条件变量是Java5线程中很重要的一个概念,顾名思义,条件变量就是表示条件的一种变量。但是必须说明,这里的条件是没有实际含义的,仅仅是个标记而已,并且条件的含义往往通过代码来赋予其含义。这里的条件和普通意义上的条件表达式有着天壤之别。条件变量都实现了java.util.concurrent.locks.Condition接口,条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的,这样,条件就和一个锁对象绑定起来了。因此,Java中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。
    条件变量的出现是为了更精细控制线程等待与唤醒,在Java5之前,线程的等待与唤醒依靠的是Object对象的wait()和notify()/notifyAll()方法,这样的处理不够精细。而在Java5中,一个锁可以有多个条件,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待。当调用signalAll()方法,又可以唤醒该条件下的等待的线程。有关Condition接口的API可以具体参考JavaAPI文档。
条件变量比较抽象,原因是他不是自然语言中的条件概念,而是程序控制的一种手段。
      下面以一个银行存取款的模拟程序为例来揭盖Java多线程条件变量的神秘面纱:
有一个账户,多个用户(线程)在同时操作这个账户,有的存款有的取款,存款随便存,取款有限制,不能透支,任何试图透支的操作都将等待里面有足够存款才执行操作。
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.locks.Condition; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReentrantLock; /** 
* Java线程:条件变量 
* 
* @author leizhimin 2009-11-5 10:57:29 
*/ 
public class Test { public static void main(String[] args) { //创建并发访问的账户 MyCount myCount = new MyCount("95599200901215522", 10000); //创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(2); Thread t1 = new SaveThread("张三", myCount, 2000); Thread t2 = new SaveThread("李四", myCount, 3600); Thread t3 = new DrawThread("王五", myCount, 2700); Thread t4 = new SaveThread("老张", myCount, 600); Thread t5 = new DrawThread("老牛", myCount, 1300); Thread t6 = new DrawThread("胖子", myCount, 800); //执行各个线程 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); pool.execute(t6); //关闭线程池 
                pool.shutdown(); } 
} /** 
* 存款线程类 
*/ 
class SaveThread extends Thread { private String name;                //操作人 private MyCount myCount;        //账户 private int x;                            //存款金额 
SaveThread(String name, MyCount myCount, int x) { this.name = name; this.myCount = myCount; this.x = x; } public void run() { myCount.saving(x, name); } 
} /** 
* 取款线程类 
*/ 
class DrawThread extends Thread { private String name;                //操作人 private MyCount myCount;        //账户 private int x;                            //存款金额 
DrawThread(String name, MyCount myCount, int x) { this.name = name; this.myCount = myCount; this.x = x; } public void run() { myCount.drawing(x, name); } 
} /** 
* 普通银行账户,不可透支 
*/ 
class MyCount { private String oid;                         //账号 private int cash;                             //账户余额 private Lock lock = new ReentrantLock();                //账户锁 private Condition _save = lock.newCondition();    //存款条件 private Condition _draw = lock.newCondition();    //取款条件 
MyCount(String oid, int cash) { this.oid = oid; this.cash = cash; } /** * 存款 * * @param x    操作金额 * @param name 操作人 */ public void saving(int x, String name) { lock.lock();                        //获取锁 if (x > 0) { cash += x;                    //存款 System.out.println(name + "存款" + x + ",当前余额为" + cash); } _draw.signalAll();            //唤醒所有取款等待线程lock.unlock();                    //释放锁 
        } /** * 取款 * * @param x    操作金额 * @param name 操作人 */ public void drawing(int x, String name) { lock.lock();                                 //获取锁 try { if (cash - x < 0) { _draw.await();             //阻塞取款操作 } else { cash -= x;                     //取款 System.out.println(name + "取款" + x + ",当前余额为" + cash); } _save.signalAll();             //唤醒所有存款操作线程 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock();                     //释放锁 
                } } 
}

当然,除了使用并发库来实现存取款操作,我们也可以使用synchronized的方法、synchronized的代码块来实现。对比并发库、synchronized方法、synchronized代码块,第一种最灵活,第二种代码最简单,第三种容易犯错。

八、原子量

     所谓的原子量即操作变量的操作是“原子的”,该操作不可再分,因此是线程安全的。为何要使用原子变量呢,原因是多个线程对单个变量操作也会引起一些问题。在Java5之前,可以通过volatile、synchronized关键字来解决并发访问的安全问题,但这样太麻烦。Java5之后,专门提供了用来进行单变量多线程并发安全访问的工具包java.util.concurrent.atomic,其中的类也很简单。
下面给出一个反面例子(切勿模仿):
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.atomic.AtomicLong; /** 
* Java线程:新特征-原子量 
* 
*
*/ 
public class Test { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); Runnable t1 = new MyRunnable("张三", 2000); Runnable t2 = new MyRunnable("李四", 3600); Runnable t3 = new MyRunnable("王五", 2700); Runnable t4 = new MyRunnable("老张", 600); Runnable t5 = new MyRunnable("老牛", 1300); Runnable t6 = new MyRunnable("胖子", 800); //执行各个线程 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); pool.execute(t6); //关闭线程池 
                pool.shutdown(); } 
} class MyRunnable implements Runnable { private static AtomicLong aLong = new AtomicLong(10000);        //原子量,每个线程都可以自由操作 private String name;                //操作人 private int x;                            //操作数额 
MyRunnable(String name, int x) { this.name = name; this.x = x; } public void run() { System.out.println(name + "执行了" + x + ",当前余额:" + aLong.addAndGet(x)); } 
}

运行结果一:

李四执行了3600,当前余额:13600
张三执行了2000,当前余额:15600
老张执行了600,当前余额:18900
老牛执行了1300,当前余额:20200
胖子执行了800,当前余额:21000
王五执行了2700,当前余额:18300

运行结果二:

张三执行了2000,当前余额:12000
王五执行了2700,当前余额:14700
老张执行了600,当前余额:15300
老牛执行了1300,当前余额:16600
胖子执行了800,当前余额:17400
李四执行了3600,当前余额:21000

运行结果三:

张三执行了2000,当前余额:12000 
王五执行了2700,当前余额:18300 
老张执行了600,当前余额:18900 
老牛执行了1300,当前余额:20200 
胖子执行了800,当前余额:21000 
李四执行了3600,当前余额:15600 
从运行结果可以看出,虽然使用了原子量,但是程序并发访问还是有问题,那究竟问题出在哪里了?
这里要注意的一点是,原子量虽然可以保证单个变量在某一个操作过程的安全,但无法保证你整个代码块,或者整个程序的安全性。因此,通常还应该使用锁等同步机制来控制整个程序的安全性。
下面是对上述代码的修正:
package MultiThread;
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.atomic.AtomicLong; 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/** 
* Java线程:并发库-原子量 
* 
*
*/ 
public class Test { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); Lock lock=new ReentrantLock();Runnable t1 = new MyRunnable("张三", 2000,lock); Runnable t2 = new MyRunnable("李四", 3600,lock); Runnable t3 = new MyRunnable("王五", 2700,lock); Runnable t4 = new MyRunnable("老张", 600,lock); Runnable t5 = new MyRunnable("老牛", 1300,lock); Runnable t6 = new MyRunnable("胖子", 800,lock); //执行各个线程 
                pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); pool.execute(t6); //关闭线程池 
                pool.shutdown(); } 
} class MyRunnable implements Runnable { private static AtomicLong aLong = new AtomicLong(10000);        //原子量,每个线程都可以自由操作 private String name;                //操作人 private int x;      //操作数额 private Lock lock;MyRunnable(String name, int x,Lock lock) { this.name = name; this.x = x; this.lock=lock;} public void run() { lock.lock();System.out.println(name + "执行了" + x + ",当前余额:" + aLong.addAndGet(x)); lock.unlock();} 
}

运行结果:

张三执行了2000,当前余额:12000
李四执行了3600,当前余额:15600
王五执行了2700,当前余额:18300
老张执行了600,当前余额:18900
老牛执行了1300,当前余额:20200
胖子执行了800,当前余额:21000
这里使用了一个对象锁,来控制对并发代码的访问。不管运行多少次,执行次序如何,最终余额均为21000,这个结果是正确的。
有关原子量的用法很简单,关键是对原子量的认识,原子仅仅是保证变量操作的原子性,但整个程序还需要考虑线程安全的。

九、障碍器

     Java5中,添加了障碍器类,为了适应一种新的设计需求,比如一个大型的任务,常常需要分配好多子任务去执行,只有当所有子任务都执行完成时候,才能执行主任务,这时候,就可以选择障碍器了。障碍器是多线程并发控制的一种手段,用法很简单。

下面给个例子:

package MultiThread;
import java.util.concurrent.BrokenBarrierException; 
import java.util.concurrent.CyclicBarrier; /** 
* Java线程:新特征-障碍器 
* 
*  
*/ 
public class Test { public static void main(String[] args) { //创建障碍器,并设置MainTask为所有定数量的线程都达到障碍点时候所要执行的任务(Runnable) CyclicBarrier cb = new CyclicBarrier(7, new MainTask()); new SubTask("A", cb).start(); new SubTask("B", cb).start(); new SubTask("C", cb).start(); new SubTask("D", cb).start(); new SubTask("E", cb).start(); new SubTask("F", cb).start(); new SubTask("G", cb).start(); } 
} /** 
* 主任务 
*/ 
class MainTask implements Runnable { public void run() { System.out.println(">>>>主任务执行了!<<<<"); } 
} /** 
* 子任务 
*/ 
class SubTask extends Thread { private String name; private CyclicBarrier cb; SubTask(String name, CyclicBarrier cb) { this.name = name; this.cb = cb; } public void run() { System.out.println("[子任务" + name + "]开始执行了!"); for (int i = 0; i < 999999; i++) ;    //模拟耗时的任务 System.out.println("[子任务" + name + "]开始执行完成了,并通知障碍器已经完成!"); try { //通知障碍器已经完成,让出锁(并使得,跳跃的障碍数目-1)
                        cb.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } 
}

运行结果:

[子任务B]开始执行了!
[子任务E]开始执行了!
[子任务C]开始执行了!
[子任务D]开始执行了!
[子任务A]开始执行了!
[子任务E]开始执行完成了,并通知障碍器已经完成!
[子任务B]开始执行完成了,并通知障碍器已经完成!
[子任务A]开始执行完成了,并通知障碍器已经完成!
[子任务C]开始执行完成了,并通知障碍器已经完成!
[子任务D]开始执行完成了,并通知障碍器已经完成!
[子任务F]开始执行了!
[子任务F]开始执行完成了,并通知障碍器已经完成!
[子任务G]开始执行了!
[子任务G]开始执行完成了,并通知障碍器已经完成!
>>>>主任务执行了!<<<<

从执行结果可以看出,所有子任务完成的时候,主任务执行了,达到了控制的目标

总结:

Java线程是Java语言中一个非常重要的部分,Java5之前,多线程的语言支持还是比较弱的,内容也较少,写一个复杂的多线程程序是相当有挑战性的。
在Java5以后,Java对多线程做了很多扩展,扩展部分称之为并发包。这部分内容大大增强了Java多线程编程的能力,通过使用Java5线程新特征的API,可以很容易的做出复杂的多线程程序。与其他语言相比,已经是相当强悍了。

转载于:https://www.cnblogs.com/shudonghe/p/3318494.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/377850.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第一篇博文

刚刚申请博客&#xff0c;开通了&#xff0c;很高兴。但是由于这几天考试比较多&#xff0c;等考完之后&#xff0c;再开始正式写博客&#xff0c;与诸君共进步&#xff01; 2012/1/1 18:20 转载于:https://www.cnblogs.com/zhenglichina/archive/2012/01/01/2309561.html

leetcode 40. 组合总和 II 思考分析

题目 给定一个数组 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 思考以及代码 如果我们直接套用39题的思路&#xff0c;那么就会出现重复的组合。 重复组合的…

java vector_Java Vector size()方法与示例

java vector矢量类size()方法 (Vector Class size() method) size() method is available in java.util package. size()方法在java.util包中可用。 size() method is used to return the size (i.e. the number of the element exists) of this Vector. size()方法用于返回此V…

二、线性回归

一、回归 可以拿正态分布为例&#xff0c;比如身高&#xff0c;若平均身高为1.78m&#xff0c;绝大多数人都是1.78m左右&#xff0c;超过2m的很少&#xff0c;低于1m的也不多。 很多事情都会回归到一定的区间之内&#xff0c;即回归到平均值。 机器学习没有完美解&#xff0c…

【转】HMM学习最佳范例五:前向算法1 .

五、前向算法&#xff08;Forward Algorithm&#xff09; 计算观察序列的概率&#xff08;Finding the probability of an observed sequence&#xff09; 1.穷举搜索&#xff08; Exhaustive search for solution&#xff09;  给定隐马尔科夫模型&#xff0c;也就是在模型参…

vs 字体

看代码看得眼疼不能不说是程序员的恶梦&#xff0c;那么&#xff0c;选择适当的字体也算是对自己的救赎吧。周末闲得无聊&#xff0c;在网上乱逛&#xff0c;搜索了一些资料整理一下给大家分享&#xff0c;仅作记录而已&#xff0c;参考使用&#xff1a; 1.一个编程人员痛苦的选…

leetcode 349. 两个数组的交集 思考分析

题目 给定两个数组&#xff0c;编写一个函数来计算它们的交集。 1、暴力双for循环 class Solution { public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {vector<int> result;vector<int> res;if(nums1.siz…

random.next_Java Random next()方法与示例

random.next随机类的next()方法 (Random Class next() method) next() method is available in java.util package. next()方法在java.util包中可用。 next() method is used to return the pseudo-random number in bits. next()方法用于返回以位为单位的伪随机数。 next() me…

VS2008下QT开发环境搭建

http://blog.csdn.net/sunnyboycao/article/details/6364444 转载于:https://www.cnblogs.com/bjfuyumu/p/3321180.html

三、梯度下降法求解最优θ值

一、梯度下降法(GD&#xff0c;Gradient Descent) Ⅰ、得到目标函数J(θ)&#xff0c;求解使得J(θ)最小时的θ值 当然&#xff0c;这里只是取了俩特征而已&#xff0c;实际上会有m个特征维度 通过最小二乘法求目标函数最小值 令偏导为0即可求解出最小的θ值&#xff0c;即…

Delphi中Messagedlg用法

if MessageDlg(Welcome to my Delphi application. Exit now?, mtConfirmation, [mbYes, mbNo], 0) mrYes then begin Close; end;MessageDlg用法 对话框类型&#xff1a;mtwarning——含有感叹号的警告对话框mterror——含有红色叉符号的错误对话框mtinformation——含有蓝…

leetcode 131. 分割回文串 思考分析

题目 给定一个字符串 s&#xff0c;将 s 分割成一些子串&#xff0c;使每个子串都是回文串。 返回 s 所有可能的分割方案。 思考 问题可以分为两个子问题&#xff1a;1、判断回文串2、分割数组 判断回文串 bool isPalindrome_string(string s,int startindex,int endinde…

android淡入淡出动画_在Android中淡入动画示例

android淡入淡出动画1) XML File: activity_main 1)XML文件&#xff1a;activity_main <?xml version"1.0" encoding"utf-8"?><android.support.constraint.ConstraintLayout xmlns:android"http://schemas.android.com/apk/res/android&…

[慢查优化]联表查询注意谁是驱动表 你搞不清楚谁join谁更好时请放手让mysql自行判定...

写在前面的话&#xff1a; 不要求每个人一定理解 联表查询(join/left join/inner join等)时的mysql运算过程&#xff1b; 不要求每个人一定知道线上&#xff08;现在或未来&#xff09;哪张表数据量大&#xff0c;哪张表数据量小&#xff1b; 但把mysql客户端&#xff08;如SQL…

四、梯度下降归一化操作

一、归一化 Ⅰ什么是归一化&#xff1f; 答&#xff1a;其实就是把数据归一到0-1之间&#xff0c;也就是缩放。 常用的归一化操作是最大最小值归一化&#xff0c;公式如下&#xff1a; 例如&#xff1a;1&#xff0c;3&#xff0c;5&#xff0c;7&#xff0c;9&#xff0c;10…

[转帖][强烈推荐]网页表格(Table/GridView)标题栏和列冻结(跨浏览器兼容)

GridView的标题栏、列冻结效果(跨浏览器版) 本文来源&#xff1a;http://blog.darkthread.net/blogs/darkthreadtw/archive/2009/02/18/supertable-plugin-for-jquery.aspx 稍早发表了GridView 的标题列冻结效果&#xff0c;足以满足工作上的需求&#xff0c;不过存在两个缺点:…

psu是什么电脑配件_PSU的完整形式是什么?

psu是什么电脑配件PSU&#xff1a;电源部门/公共部门事业 (PSU: Power Supply Unit / Public Sector Undertaking) 1)PSU&#xff1a;电源设备 (1) PSU: Power Supply Unit) PSU is an abbreviation of the "Power Supply Unit". PSU是“电源设备”的缩写 。 It is a…

【C++grammar】断言与表达式常量

目录1、常量表达式和constexpr关键字2、断言与C11的静态断言1.1. assert : C语言的宏(Macro)&#xff0c;运行时检测。1.2. assert()依赖于NDEBUG 宏1.3. assert 帮助调试解决逻辑bug &#xff08;部分替代“断点/单步调试”&#xff09;2.1static_assert (C11的静态断言 )2.2.…

一些又用的国内著名期刊

记&#xff1a; 电子学报、电子与信息学报、图像图形学报、自动化学报、计算机学报、软件学报、计算机研究与发展。转载于:https://www.cnblogs.com/nanyangzp/p/3322244.html

一、Arduino UNO R3将数据上传至云平台

一、准备工作 ①ESP12E Shield ②Arduino UNO R3开发板 ③把ESP12E Shield安装到Arduino UNO R3开发板上 ④登录物联网平台注册个账号&#xff0c;到时候需要使用。 ⑤记录下来你的Uid和key到时候会用到 ⑥创建个设备&#xff0c;用于测试 ⑦beyondyanyu为设备名&…