参考<<疯狂JAVA讲义>> 第16章节,还可以参考sina博文 "JAVA多线程相关介绍"
多线程概述
个人觉得这方面已经有很多资料介绍了,不过还是觉得 <<疯狂JAVA讲义>>线程概述还是挺透彻,明了的
2种方式创建线程,一种是extends Thread,一种是implements Runnable
这里需要注意的是Runnable对象仅仅是作为Thread对象的target,Runnable的run方法作为仅是线程执行体
线程的生命周期(这里疯狂JAVA讲义在这块讲的很好)
和线程控制有关的方法:
start():新建的线程进入Runnable状态.
run():线程进入Running状态(不要直接在程序中调用线程的run方法,是由系统调用的)。
wait():线程进入等待状态,等待被notify,这是对象方法,而不是线程方法
notify()/notifyAll():唤醒其他线程,这是对象方法,而不是线程方法
yield():线程放弃执行,使其他优先级不低于该线程的线程有机会运行,是静态方法
getPriority()/setPriority():设置线程优先级
sleep():线程睡眠一段时间
join():调用这个方法的主线程,会等待加入的子线程完成。
多线程同步
A:关键字synchronized 来与对象的互斥锁联系
1)synchronized关键字可以修饰代码块,方法,但是不可以修饰构造器,属性等。
2)特别指出的是synchronized锁定的不是方法或代码块,而是对象。当synchronized作为方法的修饰符时,它所取得的对象锁将被转交给方法的调用者;
当synchronized修饰的是对象时,则取得的对象锁将被转交给该引用指向的对象。Synchronized也可以修改类,表示这个类的所有方法都是Synchronized的
3)释放同步监视器(synchronized)的锁定:
当线程执行到synchronized()块结束时,释放对象锁
当在synchronized()块中遇到break, return或抛出exception,则自动释放对象锁
当一个线程调用wait()方法时,它放弃拥有的对象锁并进入blocked 状态
public class SuperTest { public static void main(String[] str) { Counter ct = new Counter(); C1 c1 = new C1(ct, " c1 "); C1 c2 = new C1(ct, " c2 "); //启动2个子线程 c2.start(); c1.start(); try { c2.join(); c1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ct.getCount()); } } class Counter { private int count = 1000; protected int getCount() { return count; } protected void setCount(int count) { this.count = count; } } class C1 extends Thread { private Counter counter; private String sname; public C1(Counter cc, String s1) { this.counter = cc; this.sname = s1; } public Counter getCounter() { return counter; } public void setCounter(Counter counter) { this.counter = counter; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public void run() { for (int j = 0; j < 100; j++) { synchronized (counter){ int i = counter.getCount(); System.out.println(sname + j + " before " + i); //System.out.println("now in :" + sname + " count = " + i); AddCounter(i - 1); System.out.println(sname + j + " after " + counter.getCount()); } } } public void AddCounter(int i) { counter.setCount(i); } }
B:同步锁Lock
public class TestDraw { public static void main(String[] args) { //创建一个账户 Account acct = new Account("1234567" , 1000); //模拟两个线程对同一个账户取钱 new DrawThread("甲" , acct , 800).start(); new DrawThread("乙" , acct , 800).start(); } } public class DrawThread extends Thread { //模拟用户账户 private Account account; //当前取钱线程所希望取的钱数 private double drawAmount; public DrawThread(String name , Account account , double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } //当多条线程修改同一个共享数据时,将涉及到数据安全问题。 public void run() { account.draw(drawAmount); } } public class Account { //定义锁对象 private final ReentrantLock lock = new ReentrantLock(); private String accountNo; private double balance; public Account(){} public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } public double getBalance() { return this.balance; } public void draw(double drawAmount) { lock.lock(); try { //账户余额大于取钱数目 if (balance >= drawAmount) { //吐出钞票 System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } //修改余额 balance -= drawAmount; System.out.println("\t余额为: " + balance); } else { System.out.println(Thread.currentThread().getName() + "取钱失败!余额不足!"); } } finally { lock.unlock(); } } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (obj != null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
线程通信
(1) 线程的协调运行
wait(),notify(),notifyAll().这3个方法是Object类的方法,而不是Thread的方法,而且这3个方法必须由同步监视器(synchronized)对象来调用,2种情况:
1.synchronized修饰同步方法,因为该类默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这3个方法
2.synchronized修饰同步代码块,同步监视器是synchronized括号中的对象,所以必须使用该对象调用这3个方法
wait()方法:
1. wait()方法是Object对象的方法,而不是Thread的方法
2. wait()方法只可能在synchronized块中被调用
3. wait()被调用时,原来的锁对象打开锁,线程进入blocked状态
4. wait()时间到期或被notify()唤醒的线程从wait()后面的代码开始继续执行
5. wait()和sleep()的主要区别是wait()会释放对象锁,而sleep()不会。
notify()和notifyAll():
1.只能在synchronized中被调用
2.notify它会唤起同一个锁对象上的一个等待线程.但如果有几个线程在等待列表中,它无法决定是哪一个线程被唤醒。所以,为了防止不该唤醒的线程被唤醒,应该调用notifyAll,让所有的等待线程都有机会运行。
public class SuperTest { public static void main(String[] str) { Counter ct = new Counter(); C2 c2 = new C2(ct, " c2 "); C1 c1 = new C1(ct, " c1 "); C2 c3 = new C2(ct, " c3 "); C1 c4 = new C1(ct, " c4 "); c3.start(); c4.start(); c2.start(); c1.start(); try { c2.join(); c1.join(); c3.join(); c4.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(ct.getCount()); } } class Counter { private int count = 1000; public int getCount() { return count; } public void setCount(int count) { this.count = count; } public synchronized void addCounter(int j,String sname) { while (this.getCount() > 1007){ try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } int i = this.getCount(); System.out.println(sname + j + " before " + i); //System.out.println("now in :" + sname + " count = " + i); this.setCount(i+1); this.notifyAll(); System.out.println(sname + j + " after " + this.getCount()); } public synchronized void delCounter(int j,String sname) { while (this.getCount() < 990){ try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } int i = this.getCount(); System.out.println(sname + j + " before " + i); //System.out.println("now in :" + sname + " count = " + i); this.setCount(i - 1); this.notifyAll(); System.out.println(sname + j + " after " + this.getCount()); } } class C1 extends Thread { private Counter counter; private String sname; public C1(Counter cc, String s1) { this.counter = cc; this.sname = s1; } public Counter getCounter() { return counter; } public void setCounter(Counter counter) { this.counter = counter; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public void run() { for (int j = 0; j < 100; j++) { /*synchronized (counter){ delCounter(j); } */ counter.delCounter(j,sname); } } } class C2 extends Thread { private Counter counter; private String sname; public C2(Counter cc, String s1) { this.counter = cc; this.sname = s1; } public Counter getCounter() { return counter; } public void setCounter(Counter counter) { this.counter = counter; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public void run() { for (int j = 0; j < 100; j++) { /* synchronized (counter) { addCounter(j); } */ if (j % 19 == 0) { try { sleep(0,100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } counter.addCounter(j,sname); } } }
这里使用的while作为判断语句,而不是if,这就是多线程中的“旋锁spin lock”的概念。这样可以避免用if带来的问题(仔细想想),是这样的,我们在一个线程被唤醒时候要重新检测它的等待条件,一般用旋锁来实现。
我们可以和以前我们做项目用的UDI的线程处理来做对比,以前我们要保证共享数据操作的完整性,实现数据的同步时需要我们自己创建互斥量CSUDIOSMutexCreate,然后自己主动调用CSUDIOSMutexWait和CSUDIOSMutexRelease等待和释放互斥量来实现,而JAVA直接使用synchronized来实现了UDI互斥量类似的功能。
JAVA来用wait,notify,notifyAll来实现线程之间的通信,我们以前一般没有类似的线程通信,有一个比较类似的做法,比如要我们要结束某个线程,一般都是定义一个全局变量,修改其FLAG,然后调用CSUDIOSThreadJoin等待其结束,而在实际的线程函数中一般会用一个while循环不断检测该全局变量的flag,以便可以及时结束线程。
(2) 使用条件变量控制协调
如果程序不使用synchronized来保存同步,而是直接使用Lock对象来保证同步,那么系统中不存在隐式的同步监视器对象,也就不能使用
wait(),notify(),notifyAll()实现同步了,那么我们可以利用条件变量Condition来实现。
public class Account { //显示定义Lock对象 private final Lock lock = new ReentrantLock(); //获得指定Lock对象对应的条件变量 private final Condition cond = lock.newCondition(); private String accountNo; private double balance; //标识账户中是否已经存款的旗标 private boolean flag = false; public Account(){} public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } public double getBalance() { return this.balance; } public void draw(double drawAmount) { //加锁 lock.lock(); try { //如果账户中还没有存入存款,该线程等待 if (!flag) { cond.await(); } else { //执行取钱操作 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); //将标识是否成功存入存款的旗标设为false flag = false; //唤醒该Lock对象对应的其他线程 cond.signalAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } //使用finally块来确保释放锁 finally { lock.unlock(); } } public void deposit(double depositAmount) { lock.lock(); try { //如果账户中已经存入了存款,该线程等待 if(flag) { cond.await(); } else { //执行存款操作 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); //将标识是否成功存入存款的旗标设为true flag = true; //唤醒该Lock对象对应的其他线程 cond.signalAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } //使用finally块来确保释放锁 finally { lock.unlock(); } } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (obj != null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
(3) 使用管道流
管道流的3种存在形式:
PipedInputStream和PipedOutputStream,PipedReader和PipedWriter,Pipe.SinkChannel和Pipe.SourceChannel
通常没有必要使用管道流来控制2个线程间通信,因为2个线程属于同一个进程,它们可以非常方便的共享数据,不必用管流
class ReaderThread extends Thread { private PipedReader pr; //用于包装管道流的BufferReader对象 private BufferedReader br; public ReaderThread(){} public ReaderThread(PipedReader pr) { this.pr = pr; this.br = new BufferedReader(pr); } public void run() { String buf = null; try { //逐行读取管道输入流中的内容 while ((buf = br.readLine()) != null) { System.out.println(buf); } } catch (IOException ex) { ex.printStackTrace(); } //使用finally块来关闭输入流 finally { try { if (br != null) { br.close(); } } catch (IOException ex) { ex.printStackTrace(); } } } } class WriterThread extends Thread { String[] books = new String[] { "Struts2权威指南", "ROR敏捷开发指南", "基于J2EE的Ajax宝典", "轻量级J2EE企业应用指南" }; private PipedWriter pw; public WriterThread(){} public WriterThread(PipedWriter pw) { this.pw = pw; } public void run() { try { //循环100次,向管道输出流中写入100个字符串 for (int i = 0; i < 100 ; i++) { pw.write(books[i % 4] + "\n"); } } catch (IOException ex) { ex.printStackTrace(); } //使用finally块来关闭管道输出流 finally { try { if (pw != null) { pw.close(); } } catch (IOException ex) { ex.printStackTrace(); } } } } public class PipedCommunicationTest { public static void main(String[] args) { PipedWriter pw = null; PipedReader pr = null; try { //分别创建两个独立的管道输出流、输入流 pw = new PipedWriter(); pr = new PipedReader(); //连接管道输出流、出入流 pw.connect(pr); //将连接好的管道流分别传入2个线程, //就可以让两个线程通过管道流进行通信 new WriterThread(pw).start(); new ReaderThread(pr).start(); } catch (IOException ex) { ex.printStackTrace(); } } }
JAVA线程池:(参考疯狂JAVA编程对线程池的介绍)
为什么要用线程池:
1)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务 2)可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,
而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
几个重要的类:
ExecutorService:真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
Executors类里面提供了一些静态工厂,生成一些常用的线程池。
newSingleThreadExecutor:
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool:
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,
当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,
线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:
创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
那我个人感觉就是new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEPALIVE_TIME, TIME_UNIT, workQueue, rejectedExecutionHandler);
提供了更定制化的线程池制造方法。因为newFixedThreadPool方法其实也是return new ThreadPoolExecutor
例子一:
//实现Runnable接口来定义一个简单的 class TestThread implements Runnable { public void run() { for (int i = 0; i < 100 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值为:" + i); } } } public class ThreadPoolTest { public static void main(String[] args) { //创建一个具有固定线程数(6)的线程池 ExecutorService pool = Executors.newFixedThreadPool(6); //向线程池中提交2个线程 pool.submit(new TestThread()); pool.submit(new TestThread()); //关闭线程池 pool.shutdown(); } }
例子二:
public class ThreadPoolTask implements Runnable { // 保存任务所需要的数据 private Object threadPoolTaskData; private static int consumeTaskSleepTime = 2000; ThreadPoolTask(Object tasks) { this.threadPoolTaskData = tasks; } public void run() { // 处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句 System.out.println("start .." + threadPoolTaskData); try { //便于观察,等待一段时间 Thread.sleep(consumeTaskSleepTime); } catch (Exception e) { e.printStackTrace(); } threadPoolTaskData = null; } public Object getTask() { return this.threadPoolTaskData; } } public class ThreadPool { private static int produceTaskSleepTime = 2; private static int consumeTaskSleepTime = 2000; private static int produceTaskMaxNumber = 10; public static void main(String[] args) { // 构造一个线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3), new ThreadPoolExecutor.DiscardOldestPolicy()); for (int i = 1; i <= produceTaskMaxNumber; i++) { try { // 产生一个任务,并将其加入到线程池 String task = "task@ " + i; System.out.println("put " + task); threadPool.execute(new ThreadPoolTask(task)); // 便于观察,等待一段时间 Thread.sleep(produceTaskSleepTime); } catch (Exception e) { e.printStackTrace(); } } } }
多线程的一般规则:
1. 如果2个或以上的线程都修改一个对象,那么把执行修改的方法定义为同步的,如果对象更新影响到只读方法,那么只读方法也要定义成同步的。
2. 如果一个线程必须等待一个对象的状态发生变化,那么它应该在对象内部等待,而不是在外部。它可以调用一个被同步的方法,并让这个方法调用wait()
3. 每当一个方法返回某个对象的锁时,它应该调用notify()/notifyAll()来让等待中的其他线程有机会调用。
4. 有wait()在的地方必须相应的有notify/notifyAll方法,且他们都作用于同一对象
5. 针对wait/notify/notifyAll使用旋锁(spin lock)
6. 优先使用notifyAll,而不是notify
7. 按照固定的顺序获取对象锁,以免死锁
8. 不要对上锁的对象改变他的引用
9. 不要滥用同步机制,避免无谓的同步控制