高新技术Day03——线程并发库
-------android培训、java培训、期待与您交流!----------
内容概述:
1、传统计时器(TraditionalTimer):
Timer.schedule(TimerTask task, long delay, long period)等;
2、ThreadPool(线程池/并发库)
ExecutorService(1、指定数量线程的线程池;2、延迟或定期执行任务的线程池)
3、Synchronized (wait、notify 和 notifyAll)
4、Lock和Condition (lock()与unlock(),await与signal、signalAll)
5、Semaphonre(信号灯,计数信号量,.acquire()和.release()来控制和释放进入同步区域的许可)
6、CyclicBarrier(同步执行线程数,在屏障点等待,直到线程全部集齐继续执行)
7、CountDownLatch(计数锁存器,执行线程任务中countDown()递减count值,当数值归零时(已完成进入等待)线程恢复启动)
8、Exchanger(对元素进行配对和交换的线程同步点)
9、ReadWriterLock(读写锁,互斥,当读取数据为null时,进入写锁区域写入数据)
10、BlockingQueue (类似IO管道流,一线程存储数据,另一线程释放数据)
【1、ArrayBlockingQueue(数组阻塞队列,固定大小、有界队列)、
2、LinkedBlockingQueue(链表阻塞单端队列(先进先出))、
3、LinkedBlockingDeque(链表阻塞双端队列)】
11、SynchronousQueue 同步队列(向队列中存储数据,当前一个数据未被取出时阻塞);
12、同步集合的旧方法: Collections.synchronizedXXX(XXX xxx);
13、线程同步集合ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList、CopyOnWriteArraySet.
———————————————多线程与定时器技术——————————————
一、基础回顾:
1、多线程创建的两种方式: extends Thread 与implements Runnable;
代码示例:
extends Thread: new Thread(){ run() }.start();
implements Runnable: new Thread( new Runnable(){ run()} ).start();
2、两者区别: 【new Runnable()方式: 】
A、将线程运行的代码封装成为一个函数对象与线程的运行方法(Thread线程)分开(符合Java的编程思想);
B、实现Runnable接口的类不需要继承Thread类(Java单继承多实现),也就可以继承其他需要的父类,利于程序扩展等;
3、理解下面的特殊形式:
new Thread(new Runnable(){run() }){run() }.start();
start()线程执行的run()方法:
优先执行Thread中的run()方法,除非没有才会执行Runnable中的run()方法。
———————————————传统计时器技术——————————————
二、传统计时器(定时器/监视器): TraditionalTimer。
(一)Timer:计时器【java.util包】 ;
.schedule(TimerTask task,Date time): 安排在指定的时间时执行指定的任务;
.schedule(TimerTask task,Date firstTime,long p) 【需要new Date对象】
指定时间开始,之后间隔时间重复执行该任务;
.schedule(TimerTask task, long delay, long period)
安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
PS:TimerTask: 计时器任务,创建或继承此类并复写该类中的run()方法;
(二)[高级提升]计时器的循环嵌套:
1、创建一个计时器,计时器任务内部封装方法并重复调用该方法,可实现间隔性执行(可用固定延迟时间重复执行的方法来简单实现);
2、创建两个计时器任务,A任务中调用B任务,B中调用A,则可实现交叉运行两个任务;
3、扩展: 通过设定数值变量,实现不同时间间隔的调用本方法);
备注:
A、计时器对象属于内部类,因此计时器任务调用局部变量需被final修饰。
B、内部类不能创建静态变量,因此要将全局使用的变量定义为静态成员变量。
—————————
PS: Date与Data的区别(功能): 【菜鸟知识点(O(∩_∩)O~)】
Date(日期):java.util.Date日期类;
相关抽象类: Calendar(日历类)与DateFormat(格式化日期类)。
Data: 数据/资料;
—————————
(三)Synchronized: 同步
1、同步代码块: 类似static代码块使用方式,被大括号包含的代码为相同锁对象时,多线程同步执行代码;【锁: 可以是任意对象。】
2、同步函数: 被Synchronized修饰的方法/函数,方法中的代码为同步代码;
【锁为this: 即调用该方法的类,在同步区域中通过wait()与notify()来控制线程操作】
新的多线程同步锁机制: Condition与Lock (详见基础篇:“多线程”章节)。
经验(重要):要用到共同数据(包括同步锁)或共同算法的若干方法应该归到同一个类上,这种设计正好体现了高类聚和程序的健壮性。
其他: A、静态方法中不能(直接)创建(new)内部类的实例对象;
解析: 1、静态类/方法随类加载而加载,无需创建类实例对象;
2、内部类可以直接访问它外部类的成员变量,因此访问非静态内部类需通过外部类对象,也就需要实例化外部类对象;
3、静态内部类则无需外部类对象,也不需要创建内部类的实例对象。
B、内部类分为: 成员内部类、静态嵌套类、方法内部类、匿名内部类。
内部类注意事项:
1、访问成员内部类的唯一途径就是通过外部类的对象;
2、静态嵌套类可以像其他静态成员一样,没有外部类对象时,也能够被访问,但它不能(直接)访问外部类的成员和方法;
3、方法内部类只能在定义该内部类的方法内实例化,不可以在此方法外对其实例化。
方法内部类对象不能使用该内部类所在方法的非final局部变量。
4、建立匿名内部类是重写父类的方法,而不是创建新的方法。
—————————线程范围内的共享变量—————————
三、线程范围内的共享变量:
1、ThreadLocal:创建一个线程本地变量; (java.lang包)
该类提供了线程局部(thread-local) 变量。每个线程都有自己的局部变量,
通常是private static修饰ThreadLocal实例对象。
【同一个线程中的方法只能获取到ThreadLocal中对应线程的局部变量。】
方法:
set(T value):将此线程局部变量的当前线程副本中的值设置为指定值;
get() :返回此线程局部变量的当前线程副本中的值;
remove() :移除此线程局部变量当前线程的值;
2、代码示例: (应用于同一个变量对象被多线程操作,但每个线程操作数据不同)
private static ThreadLocal tl = newThreadLocal();
//………………………………
new Thread(new Runnable(){
public void run() {
int i = (int) (Math.random()*10+1);
tl.set(i);
System.out.println(Thread.currentThread().getName()+":::"+i);
new A().getA();
//new B().getB(); //重复A类getA方法,只是重复验证。
}
}).start();
同类中多线程执行静态内部类方法:
static class A{
public void getA(){
System.out.println("A:"+Thread.currentThread().getName()+"::" +tl.get());
}
}
结果: 如果多线程重复执行run()方法,每个线程输入值与getA()得到的值相同,而不同线程之间是不同的。
【类似创建: Map,Tvalue> : 将每个线程及其线程局部变量存储到Map集合中,根据线程名获取与该线程对应的变量值。】
3、应用:
A、同一个变量对象被多线程操作,但每个线程操作数据不同;
(该变量可以是任意对象类型T: String、基本数据类型对象或者数据类。)
B、应用于单例设计模式(饿汉式): 封装ThreadLocal在类中,不同线程来获取同一个类变量对象,而取走不同的实例对象。
注意: A、每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal 实例是可访问的;
B、在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
疑惑: (未解决的视频问题)
线程什么时候终结,是否有方法可以获得线程终结的反馈,以便清除ThreadLocal等中的内容。
(类似要求虚拟机结束时删除某个方法程序)
【当多个线程操作共享对象和数据时,可以将操作的对象或数据以及操作的方法封装成单独的个体(类中的成员或方法),而操作的多个线程(封装成类或匿名内部类,类的内容)就是通过调用该类方法来实现操作该对象或数据的动作。】
————————————
其他: Runnable类的特殊方法:
void addShutdownHook(Thread hook): 添加某个线程,当虚拟机终止时启动执行;
Interface ThreadDeathRequest : 了解此线程(com.sun.jdi.request包)
addThreadFilter(ThreadReferencethread)
————————————Java线程池/并发库——————————————
四、ThreadPool: 线程池/并发库:
(1)创建线程池方式:
ExecutorService Executors.new............(创建线程池(单个或指定数量))
例: newSingleThreadExecutor()
等(更多详见
java.util.concurrent.Executors类)
解释: 可用于指定一定数量线程的线程池去执行一定数量的Runnable任务,
一个线程当完成某个任务后,会去寻找其他未执行的任务,直到全部完成。
(2)线程池类型:
1、固定线程数量的线程池(完成任务后自动寻找未执行任务,直到全部完成);
2、缓存线程数量的线程池(根据需要执行的任务数量启动对应数量的线程);
3、单一线程池(当该线程死亡时会自动创建新的线程来继续执行任务);
newScheduledThreadPool().scheduleAtFixedRate(...)
:定时器线程池【指定延迟时间后执行、时间间隔重复执行】;
【因为没有像定时器般提供指定日期方法,可通过指定时间减去当前时间获得时间间隔】
ExecutorSevice方法:
.execute(Runnable command) : 创建需要线程池执行的任务;
.shutdown : 当任务全部完成时终止线程池;
.shutdownNow : 终止当前未被执行的任务;
Future .submit(Callable) :指定任务执行,返回执行结果;
【该结果可通过Future对象get()方法获得,是等待执行结束才能获得,如果任务中有延迟也获取结果步骤阻塞,直到获得结果??????】
Future :存储单个返回结果;
CompletionService : 存储全部返回结果;
CopyOnWriteArrayList : 特殊集合(允许迭代同时向集合中添加数据)
队列
BlockingQueue ArrayBlockingQueue 数组(可阻塞)队列
SynchronousQueue 同步队列(向队列中存储数据,当前一个数据未被取出时阻塞)
————————————读写锁————————————————
五、读写锁: ReadWriterLock (实例子类ReentrantReadWriteLock)
.readLock() : 读锁;
.writeLock() : 写锁;
.lock()和.unlock()加锁与释放
使用: 多个读锁不互斥,读锁和写锁互斥,写锁与写锁互斥:
共享数据,将只能有一个线程能写该数据,但可以有多个线程同时读取该数据。
备注: (注意)
1、读写锁的特殊形式:
更新锁(既能读又能写的锁,只能自身添加,即拥有写锁的线程在写入数据过程中启动读锁,实现读写操作并存,这也就将写锁降级为更新锁)
2、缓冲代理(实体对象代理): 多个线程进入读锁读取共享数据,当共享数据为null时,启动写锁(禁止其他进入)写入数据,直到共享数据区存在数据可供读取。
缓冲系统伪代码: (类似如上,不同在于写入数据是通常访问数据库,伪即没有真访问)
其他: 读写锁与“Lock和Condition”
ReadWriteLock:读写锁(多线程操作共同数据,多线程进入读锁区域,当读取数据为null时,进入写锁区域写入数据)
Lock和Condition: 一个Lock对应多个Condition,被Condition锁的睡眠[await]的线程只能被同个Condition锁的唤醒[signal]方法唤醒。
———————————————Semaphore信号灯—————————————
六、Semaphore: 信号灯(计数信号量)
使用: 通过.acquire()和.release()来控制和释放许可,类似Lock锁功能,不同是Semaphore是可以调节许可集数量,即可控制同时进入同步代码区的线程数。
方法: (详见: java.util.concurrent.Semaphore类)
release(); : 释放一个许可,将其返回给信号量;
acquire(); : 从此信号量获取一个许可,在提供许可前一直将线程阻塞,否则线程被中断。【与Condition等不同之处,在于其他线程可调用方法来释放信号量,解决获取信号执行中的线程进入睡眠中断状态】
availablePermits(); :返回此信号量中当前可用的许可数。
————————————————同步辅助类——————————————
七、同步辅助类: CyclicBarrier与CountDownLatch
CyclicBarrier: 它允许一组线程互相等待,直到到达某个公共屏障点。(即多人一起做某件事,但约定在某个时间/事件点集合,当全部都到齐后再继续执行)
构造方法: CyclicBarrier(intparties,Runnable barrierAction)
:同步执行的线程数、在屏障点等待时执行某些操作;
方法:
.await();: 线程进入等待,当同步执行线程都进入等待则全部被唤醒,一同执行后续操作。
CountDownLatch : 计数锁存器;
构造方法: CountDownLatch
(int count); :
指定计数值。
定义: 指定数值的锁存器,通过countDown()方法递减数值,当数值为零时线程恢复启动;
countDown(): 递减锁存器的计数数值
await() : 使当前线程在锁存器倒计数至零前一直等待,除非线程被中断;
await(long timeout,TimeUnitunit) : 同上,终止因素或为超出指定等待时间;
———————————————Exchanger————————————————
八、Exchanger :可对元素进行配对和交换的线程的同步点。
方法: 【需创建实例对象(无参构造方法)】
V exchange(V x):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
【两个交换数据线程进入exchange()方法,会先处于等待直到另一个线程进入,交换彼此数据后,继续进行各自线程后续操作】
——————————————BlockingQueue(队列)—————————————
九、阻塞队列(BlockingQueue):
1、不同功能实例子类:
ArrayBlockingQueue :数组阻塞队列(固定大小、有界队列)
LinkedBlockingQueue :链表阻塞队列(先进先出、单端队列)
LinkedBlockingDeque : (链表阻塞双端队列)
2、使用: 获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。
【简单概括: 多线程执行,一个线程等待读取数据通过add()或put()数据到队列(没有时处于阻塞),另一个线程提取/获取数据,根据获得的数据操作其他行为。】
PS: 插入、移除、检查有不同的处理情况(抛异常、特殊值、阻塞、超时);
【详见java.util.concurrent包中接口BlockingQueue】
3、特殊应用: 用两个具有1个空间的队列来实现同步通知的功能;
因为当BlockingQueue阻塞队列中存满时会阻塞,如果队列元素被取则恢复并继续向下执行,因此通过给两个具有1个空间的队列,存储、交叉取出,就不需要通过同步也能实现同步通知功能。
(向A方法中阻塞队列存储元素,假如队列为满,A方法阻塞;但B方法中执行到位A方法中的阻塞队列取出元素时,A方法恢复正常继续向下执行,两者交替执行,实现同步通知)
4、BlockingQueue与Semaphore的不同:
BlockingQueue是一方存放数据,另一方释放数据;
Semaphore通常则是由同一方设置和释放信号量。
5、匿名构造方法(函数):{ .............. }
【在其他构造函数运行之前运行,创建几个对象就会运行几次】
【不同于静态代码块、..........等】
————————————线程同步集合——————————————
十、线程同步集合:
因为一般Java集合的隐患: 1、线程不安全;2、并发操作异常;
所以提供支持多线程同步操作的集合等。
1、Collections.SynchronizedMap()等 : 老式集合同步创建方法;
2、线程并发库: (详见:java.util.concurrent包)
ConcurrentHashMap : 支持多线程同步操作的HashMap映射;
ConcurrentSkipListMap : 具备比较规则的同步ListMap;
ConcurrentSkipListSet : 同上,类似TreeSet(但具备线程同步)
CopyOnWriteArrayList : 线程安全的ArrayList变体(允许遍历时操作集合元素);
CopyOnWriteArraySet : 同上,原理是: 增删操作元素时在底层数组进行copy操作;
十一、Java集合与遍历时异常隐患: (算回顾与深入解析)
1、next()方法在取出元素的同时,会将迭代角标后移一位;
【因此一次hasNext()的判断,只允许运行一次next(),如果要对迭代元素进行多次操作,应该赋值给创建的变量,对赋值变量进行多次操作;
例如: String s = (String)iterator.next(); 】
2、hasNext()方法:return cursor != size();[迭代角标位是否等于元素个数];
因此在遍历集合时进行增删,会直接修改集合中元素个数;造成结果:
1、在中间删除会提前结束,打印元素遗漏;
2、删除最后位元素则会使得cursor迭代角标位将大于元素个数,也就不会出现false结束循环情况,因此就会进入死循环;
Java解决方案:
Java为了避免该情况,在迭代方法中引入对操作元素次数的判断;在遍历集合时对元素进行增删等操作会修改操作元素次数,那么迭代执行next()等时将判断当前(被修改后)的操作元素次数与开始迭代时的操作元素次数是否相同,不同就会抛出并发操作异常。
总结:
1、除特殊的ListIterator(依靠是列表角标)外,不能在迭代时对遍历的集合进行增删。
2、因此如果要对集合进行遍历增删操作或者多线程共享操作集合(理由同上,增删改变元素个数容易出现迭代角标越界等)时,应该使用线程同步集合:
【 通过Collections.synchronizedSet(Setset)等方法或使用(java.util.concurrent包)CopyOnWriteArraySet等线程并发集合。】
注意(重要小细节):
public String getKey(Stringkey1,String key2){
String key = key1 + key2;
return key; }
类似: String s1 = new String("hehe"+"gege");
String s2 = new String("hehe"+"gege");
sop(s1==s2); 为false;(此为两个不同对象,内存地址值不同)
sop(s1.equals(s2)); 为true;(两者为相同的字符串)
结论:
因此当需要比较不同时出现的两个字符串对象是否相同时可通过集合容器的equals方法;
例如: 多线程互斥synchronized(key){...}时,可将key判断是否在集合中,没有则存入,有则提取集合中对应key值赋值转换,实现对象地址值统一。