Java多线程与并发库高级应用 01. 传统线程技术回顾 传统是相对于JDK1.5而言的 传统线程技术与JDK1.5的线程并发库 线程就是程序的一条执行线索/线路。 创建线程的两种传统方式 1. 创建Thread的子类,覆盖其中的run方法,运行这个子类的start方法即可开启线程 Thread thread = new Thread() { @Override public void run() { while (true) { 获取当前线程对象 获取线程名字 Thread.currentThread() threadObj.getName() 让线程暂停,休眠,此方法会抛出中断异常InterruptedException Thread.sleep(毫秒值); } } }; thread.start(); 2. 创建Thread时传递一个实现Runnable接口的对象实例 Thread thread = new Thread(new Runnable() { public void run() {} }); thread.start(); 问题:下边的线程运行的是Thread子类中的方法还是实现Runnable接口类的方法 new Thread( b、传递实现Runnable接口的对象 new Runnable() { public void run() {} } ){ a、覆盖Thread子类run方法 public void run(){} }.start(); 分析:new Thread(Runnable.run()){run()}.start(); 子类run方法实际就是覆盖父类中的run方法,如果覆盖了就用子类的run方法,不会再找Runnable中的run方法了,所以运行的是子类中的run方法 总结: 由Thread类中的run方法源代码中看出,两种传统创建线程的方式都是在调用Thread对象的run方法,如果Thread对象的run方法没有被覆盖,并且像上边的问题那样为Thread对象传递了一个Runnable对象,就会调用Runnable对象的run方法。 多线程并不一定会提高程序的运行效率。举例:一个人同时在三张桌子做馒头 多线程下载:并不是自己电脑快了,而是抢到更多服务器资源。例:服务器为一个客户分配一个20K的线程下载,你用多个线程,服务器以为是多个用户就分配了多个20K的资源给你。 02. 传统定时器技术回顾 传统定时器的创建:直接使用定时器类Timer a、过多长时间后炸 new Timer().schedule(TimerTask定时任务, Date time定的时间); b、过多长时间后炸,以后每隔多少时间再炸 new Timer().schedule(TimerTask定时任务, Long延迟(第一次执行)时间, Long间隔时间); TimerTask与Runnable类似,有一个run方法 Timer是定时器对象,到时间后会触发炸弹(TimerTask)对象 示例: new Timer().schedule( new TimerTask()定时执行的任务 { public void run() { SOP(“bombing”); } 显示计时信息 while (true) { SOP(new Date().getSeconds()); Thread.sleep(1000); } }, 10 定好的延迟时间,10秒以后执行任务 ); 问题:2秒后炸,爆炸后每隔3秒再炸一次 定时器2秒后炸,炸弹里还有定时器(每3秒炸一次) class MyTimerTask extends TimerTask 这就是准备用的子母弹 { public void run() { 本身就是一颗炸弹 SOP(bombing); 内部子弹 new Timer().schedule( new MyTimerTask(), 2000 ); } } 放置子母弹,2秒后引爆 new Timer().schedule(new MyTimerTask(), 2000); 问题延伸: 上面的问题延伸,母弹炸过后,子弹每隔3秒炸一次,再每隔8秒炸一次 1、在MyTimerTask内部定义一个静态变量记录炸弹号,在run方法内将炸弹号加1,每次产生新炸弹,号码就会加1,根据炸弹号判断是3秒炸还是8秒炸。 注意:内部类中不能声明静态变量 定义一个静态变量private static count = 0; 在run方法内部:count=(count+1)%2; 将定时器的时间设置为:2000+2000*count 2、用两个炸弹来完成,A炸弹炸完后启动定时器安装B炸弹,B炸弹炸完后也启动一个定时器安装A炸弹。 定时器还可以设置具体时间,如某年某月某日某时……可以设置周一到周五做某事,自己设置的话需要换算日期时间,可以使用开源工具quartz来完成。 03. 传统线程互斥技术 线程安全问题例子:银行转账 同一个账户一边进行出账操作(自己交学费),另一边进行入账操作(别人给自己付款),线程不同步带来的安全问题 示例:逐个字符的方式打印字符串
class Outputer
{ public void output(String name) { int len = name.length(); for (int i=0; i<len; i++) SOP(name.charAt(i));逐个字符打印 SOP();换行 } } public void test() { Outputer outputer = new Outputer(); new Thread( new Runnable() { public void run() { Thread.sleep(100); outputer.output(“zhangxiaoxiang”); } }).start(); new Thread( new Runnable() { public void run() { Thread.sleep(100); outputer.output(“lihuoming”); } }).start(); } 要实现互斥,在这个位置必须使用同一个对象 使用name就达不到同步效果 使用output对象即可达到同步效果 要避免下边产生的问题,左边方法体中的代码要实现原子性 有一个线程正在使用这个方法的代码,别的线程就不能再使用。 就和厕所里的坑一样,已经有人在用了,别人就不能再去用了。 Java中某段代码要实现排他性,就将这段代码用synchronized关键字保护起来。 同步锁可以用任意对象,相当于门锁 synchronized(name) { for (int i=0; i<len; i++) SOP(name.charAt(i));逐个字符打印 SOP();换行 } 这样的话,有一个线程进入保护区域后,没出来的话,别的线程就不能进入保护区域。 注意: 内部类不能访问局部变量,要访问需加final 静态方法中不能创建内部类的实例对象 打印结果发现的问题:线程不同步所致,两个线程都在使用同一个对象 互斥方法: a、同步代码块 synchronized (lock){} b、同步方法 方法返回值前加synchronized 同步方法上边用的锁就是this对象 静态同步方法使用的锁是该方法所在的class文件对象 使用synchronized关键字实现互斥,要保证同步的地方使用的是同一个锁对象 public synchronized void output(String name) { int len = name.length(); 这里就不要再加同步了,加上极易出现死锁 for (int i=0; i<len; i++) SOP(name.charAt(i));逐个字符打印 SOP();换行 } 04. 传统线程同步通信技术 面试题,子线程10次与主线程100次来回循环执行50次 下面是我刚看完面试题就暂停视频自己试着写的代码,还可以,结果完成要求了 在单次循环结束后让这个刚结束循环的线程休眠,保证另一个线程可以抢到执行权。 public class ThreadInterViewTest { /** * 刚看到面试题没看答案之前试写 * 子线程循环10次,回主线程循环100次, * 再到子线程循环10次,再回主线程循环100次 * 如此循环50次 */ public static void main(String[] args) { int num = 0; while (num++<50) { new Thread(new Runnable() { @Override public void run() { circle("子线程运行", 10); } }).start(); try { //加这句是保证上边的子线程先运行,刚开始没加,主线程就先开了 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } circle("主线程", 100); } } public static synchronized void circle(String name, int count) { for (int i=1; i<=count; i++) { System.out.println(name+"::"+i); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } 1、将子线程和主线程中要同步的方法进行封装,加上同步关键字实现同步 2、两个线程间隔运行,添加一个标记变量进行比较以实现相互通信,加色的部分 wait notify notifyAll wait会抛出异常 class Business { private boolean bShouleSub = true; public synchronized void sub() {
if (bShouleSub)
{ for (int i=1; i<11; i++) SOP(sub+i); bShouldSub = false; this.notify(); } else this.wait(); } public synchronized void main() { if (!bShouldSub) //此处使用while以增加程序健壮性,因为存在虚假唤醒,有时候并没有被notify就醒了。如果该方法没有同步的话,此处就更要使用while进行判断了,避免进程不同步问题 { for (int i=1; i<101; i++) SOP(main+i); bShouldSub = true; this.notify(); } else this.wait(); } }
经验:要用到共同数据(包括同步锁)或相同算法的多个方法要封装在一个类中 锁是上在代表要操作的资源类的内部方法中的,而不是上在线程代码中的。这样写出来的类就是天然同步的,只要使用的是同一个new出来的对象,那么这个对象就具有同步互斥特性 判断唤醒等待标记时使用while增加程序健壮性,防止伪唤醒 05. 线程范围内共享变量的概念与作用 线程范围内共享数据图解: 代码演示: class ThreadScopeShareData { 三个模块共享数据,主线程模块和AB模块 private static int data = 0; 准备共享的数据 存放各个线程对应的数据 private Map<Thread, Integer> threadData = new HashMap<Thread, Integer>(); public static void main(String[] args) { 创建两个线程 for (int i=0; i<2; i++) { new Thread( new Runnable() { public void run() {现在当前线程中修改一下数据,给出修改信息 data = new Random().nextInt(); SOP(Thread.currentThread().getName()+将数据改为+data); 将线程信息和对应数据存储起来 threadData.put(Thread.currentThread(), data); 使用两个不同的模块操作这个数据,看结果
new A().get();
new B().get(); } } ).start(); } } static class A { public void get() { data = threadData.get(Thread.currentThread()); SOP(A+Thread.currentThread().getName()+拿到的数据+data); } } static class B { public void get() { data = threadData.get(Thread.currentThread()); SOP(B+Thread.currentThread().getName()+拿到的数据+data); } } } 结果并没与实现线程间的数据同步,两个线程使用的是同一个线程的数据。要解决这个问题,可以将每个线程用到的数据与对应的线程号存放到一个map集合中,使用数据时从这个集合中根据线程号获取对应线程的数据。代码实现:上面红色部分 程序中存在的问题:获取的数据与设置的数据不同步 Thread-1共享数据设置为:-679705777777 Thread-1--A 模块数据:-679705777777 Thread-0共享数据设置为:11858818 Thread-0--A 模块数据:11858818 Thread-0--B 模块数据:-679705777777 Thread-1--B 模块数据:-679705777777 最好将Runnable中设置数据的方法也写在对应的模块中,与获取数据模块互斥,以保证数据同步 06. ThreadLocal类及应用技巧 多个模块在同一个线程中运行时要共享同一份数据,实现线程范围内的数据共享可以用上一节中所用的方法。 JDK1.5提供了ThreadLocal类来方便实现线程范围内的数据共享,它的作用就相当于上一节中的Map。 每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map集合中增加一条记录,key就是各自的线程,value就是各自的set方法传进去的值。 在线程结束时可以调用ThreadLocal.clear()方法用来更快释放内存,也可以不调用,因为线程结束后也可以自动释放相关的ThreadLocal变量。 一个ThreadLocal对象只能记录一个线程内部的一个共享变量,需要记录多个共享数据,可以创建多个ThreadLocal对象,或者将这些数据进行封装,将封装后的数据对象存入ThreadLocal对象中。 将数据对象封装成单例,同时提供线程范围内的共享数据的设置和获取方法,提供已经封装好了的线程范围内的对象实例,使用时只需获取实例对象即可实现数据的线程范围内的共享,因为该对象已经是当前线程范围内的对象了。下边给出张老师的优雅代码: package cn.itheima; import java.util.Random; public class ThreadLocalShareDataDemo { /**06. ThreadLocal类及应用技巧 * 将线程范围内共享数据进行封装,封装到一个单独的数据类中,提供设置获取方法 * 将该类单例化,提供获取实例对象的方法,获取到的实例对象是已经封装好的当前线程范围内的对象 */ public static void main(String[] args) { for (int i=0; i<2; i++) { new Thread( new Runnable() { public void run() { int data = new Random().nextInt(889); System.out.println(Thread.currentThread().getName()+"产生数据:"+data); MyData myData = MyData.getInstance(); myData.setAge(data); myData.setName("Name:"+data); new A().get(); new B().get(); } }).start(); } } static class A { //可以直接使用获取到的线程范围内的对象实例调用相应方法 String name = MyData.getInstance().getName(); int age = MyData.getInstance().getAge(); public void get() { System.out.println(Thread.currentThread().getName()+"-- AA name:"+name+"...age:"+age); } } static class B { //可以直接使用获取到的线程范围内的对象实例调用相应方法 String name = MyData.getInstance().getName(); int age = MyData.getInstance().getAge(); public void get() { System.out.println(Thread.currentThread().getName()+"-- BB name:"+name+"...age:"+age); } } static class MyData { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //单例 private MyData() {}; //提供获取实例方法 public static MyData getInstance() { //从当前线程范围内数据集中获取实例对象 MyData instance = threadLocal.get(); if (instance==null) { instance = new MyData(); threadLocal.set(instance); } return instance; } //将实例对象存入当前线程范围内数据集中 static ThreadLocal<MyData> threadLocal = new ThreadLocal<MyData>(); } } 07. 多个线程之间共享数据的方式探讨 例子:卖票:多个窗口同时卖这100张票,票就需要多个线程共享 a、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个对象中有共享数据。 卖票就可以这样做,每个窗口都在做卖票任务,卖的票都是同一个数据。 b、如果每个线程执行的代码不同,就需要使用不同的Runnable对象,有两种方式实现 Runnable对象之间的数据共享: a)将共享数据单独封装到一个对象中,同时在对象中提供操作这些共享数据的方法,可以方便实现对共享数据各项操作的互斥和通信。 b)将各个Runnable对象作为某个类的内部类,共享数据作为外部类的成员变量,对共享数据的操作方法也在外部类中提供,以便实现互斥和通信,内部类的Runnable对象调用外部类中操作共享数据的方法即可。 注意:要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。 08Java5线程并发库的应用 如果没有线程池,需要在run方法中不停判断,还有没有任务需要执行 线程池的通俗比喻:接待客户,为每个客户都安排一个工作人员,接待完成后该工作人员就废掉。服务器每收到一个客户请求就为其分配一个线程提供服务,服务结束后销毁线程,不断创建、销毁线程,影响性能。 线程池:先创建多个线程放在线程池中,当有任务需要执行时,从线程池中找一个空闲线程执行任务,任务完成后,并不销毁线程,而是返回线程池,等待新的任务安排。 线程池编程中,任务是提交给整个线程池的,并不是提交给某个具体的线程,而是由线程池从中挑选一个空闲线程来运行任务。一个线程同时只能执行一个任务,可以同时向一个线程池提交多个任务。 线程池创建方法: a、创建一个拥有固定线程数的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(3); b、创建一个缓存线程池 线程池中的线程数根据任务多少自动增删 动态变化 ExecutorService threadPool = Executors.newCacheThreadPool(); c、创建一个只有一个线程的线程池 与单线程一样 但好处是保证池子里有一个线程, 当线程意外死亡,会自动产生一个替补线程,始终有一个线程存活 ExecutorService threadPool = Executors.newSingleThreadExector(); 往线程池中添加任务 threadPool.executor(Runnable) 关闭线程池: threadPool.shutdown() 线程全部空闲,没有任务就关闭线程池 threadPool.shutdownNow() 不管任务有没有做完,都关掉 用线程池启动定时器: a、创建调度线程池,提交任务 延迟指定时间后执行任务 Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,时间单位); b、创建调度线程池,提交任务, 延迟指定时间执行任务后,间隔指定时间循环执行 Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间, 间隔时间,时间单位); 所有的 schedule 方法都接受相对 延迟和周期作为参数,而不是绝对的时间或日期。将以 Date 所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的 Date 运行,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS) 。 Callable与Future的应用:获取一个线程的运行结果 public interface Callable<V> 返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。 Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。 只有一个方法 V call() 计算结果,如果无法计算结果,则抛出一个Exception 异常。使用方法: ExecutorService threadPool = Executors.newSingleThreadExccutor(); 如果不需要返回结果,就用executor方法 调用submit方法返回一个Future对象 Future<T> future = threadPool.submit(new Callable<T>(){//接收一个Callable接口的实例对象 覆盖Callable接口中的call方法,抛出异常 public T call() throws Exception { ruturn T } }); 获取Future接收的结果 future。get();会抛出异常 future.get()没有拿到结果就会一直等待 Future取得的结果类型和Callable返回的结果类型必须一致,通过泛型实现。Callable要通过ExecutorService的submit方法提交,返回的Future对象可以取消任务。 public interface Future<V> Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。 | | cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。 | | get() 如有必要,等待计算完成,然后获取其结果。 | | get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 | | | | |
public interface CompletionService<V> CompletionService用于提交一组Callable任务,其take方法返回一个已完成的Callable任务对应的Future对象。好比同时种几块麦子等待收割,收割时哪块先熟先收哪块。 将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。 | | poll() 获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回 null。 | | poll(long timeout, TimeUnit unit) 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则将等待指定的时间(如果有必要)。 | | | | submit( Runnable task, V result) 提交要执行的 Runnable 任务,并返回一个表示任务完成的 Future,可以提取或轮询此任务。 | | take() 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。 | | | | | |
示例: ExecutorService threadPool = Executors.newFixedThreadPool(10); //创建线程池,传递给coms 用threadPool执行任务,执行的任务返回结果都是整数 CompletionService<Integer> coms = new ExecutorCompletionService<Integer>(threadPool); 提交10个任务 种麦子 for (int i=0; i<10; i++) { final int num = i+1; coms.submit(new Callable<Integer>(){ public Integer call() 覆盖call方法 {匿名内部类使用外部变量要用final修饰 SOP(任务+num); Thread.sleep(new Random().nextInt(6)*1000); return num; } }); } 等待收获 割麦子 for (int i=0; i<10; i++) { take获取第一个Future对象,用get获取结果 SOP(coms.take().get()); } java5的线程锁技术 java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类, | | | | Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。 | | ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。 | | | | | | | 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。 | | | | 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。 | | | | | | |
Lock比传统线程模型中的synchronized更加面向对象,锁本身也是一个对象,两个线程执行的代码要实现同步互斥效果,就要使用同一个锁对象。锁要上在要操作的资源类的内部方法中,而不是线程代码中。 public interface Lock 所有已知实现类: 随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句: Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); } 锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。 Lock与synchronized对比,打印字符串例子
|