-------android培训、java培训、期待与您交流! ----------
以下内容是学习张老师Java多线程与线程并发库高级应用时所做的笔记,很有用
网络编辑器直接复制Word文档排版有点乱,提供原始文件下载
先看源文件概貌
张孝祥_Java多线程与并发库高级应用
【视频介绍:】
Java线程是一项非常基本和重要的技术,在偏底层和偏技术的Java程序中不可避免地要使用到Java线程技术,特别是android手机程序和游戏开发中,多线程成了必不可少的一项重要技术。但是,很多Java程序员对Java线程技术的了解都仅停留在初级阶段,在项目中一旦涉及到多线程时往往就表现得糟糕至极,所以,软件公司常常使用Java线程技术来考察面试者的基本功和判断其编码水平的高低。
本套视频教程是专门为了帮助那些已经学习和了解过、但掌握得并不是很深入的人们提高java线程技术而讲解的,所以,Java线程初学者学习本视频教程时可能会比较吃力,可能必须耐心学习多遍才能渐入佳境,但是,你一旦掌握了其中的内容,你对Java线程技术的了解将会相当出众!
【视频目录列表:】
01. 传统线程技术回顾
02. 传统定时器技术回顾
03. 传统线程互斥技术
04. 传统线程同步通信技术
05. 线程范围内共享变量的概念与作用
06. ThreadLocal类及应用技巧
07. 多个线程之间共享数据的方式探讨
08. java5原子性操作类的应用
09. java5线程并发库的应用
10. Callable与Future的应用
11. java5的线程锁技术
12. java5读写锁技术的妙用
13. java5条件阻塞Condition的应用
14. java5的Semaphere同步工具
15. java5的CyclicBarrier同步工具
16. java5的CountDownLatch同步工具
17. java5的Exchanger同步工具
18. java5阻塞队列的应用
19. java5同步集合类的应用
20. 空中网挑选实习生的面试题1
21. 空中网挑选实习生的面试题2
22. 空中网挑选实习生的面试题3
23. 源代码与资料
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();
}
注意:
内部类不能访问局部变量,要访问需加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处使用while以增加程序健壮性,因为存在虚假唤醒,有时候并没有被notify就醒了。如果该方法没有同步的话,此处就更要使用while进行判断了,避免进程不同步问题 |
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)
{
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共享数据设置为:-997057737
Thread-1--A模块数据:-997057737
Thread-0共享数据设置为:11858818
Thread-0--A模块数据:11858818
Thread-0--B模块数据:-997057737
Thread-1--B模块数据:-997057737
最好将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;
publicclass ThreadLocalShareDataDemo
{ /**06. ThreadLocal类及应用技巧
* 将线程范围内共享数据进行封装,封装到一个单独的数据类中,提供设置获取方法
* 将该类单例化,提供获取实例对象的方法,获取到的实例对象是已经封装好的当前线程范围内的对象
*/
publicstaticvoid main(String[] args)
{
for (int i=0; i<2; i++)
{
new Thread(
new Runnable()
{
publicvoid 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();
}
}
staticclass A
{ //可以直接使用获取到的线程范围内的对象实例调用相应方法
String name = MyData.getInstance().getName();
intage = MyData.getInstance().getAge();
publicvoid get()
{
System.out.println(Thread.currentThread().getName()+"-- AA name:"+name+"...age:"+age);
}
}
staticclass B
{
//可以直接使用获取到的线程范围内的对象实例调用相应方法
String name = MyData.getInstance().getName();
intage = MyData.getInstance().getAge();
publicvoid get()
{
System.out.println(Thread.currentThread().getName()+"-- BB name:"+name+"...age:"+age);
}
}
staticclass MyData
{
private Stringname;
privateintage;
public String getName()
{
returnname;
}
publicvoid setName(String name)
{
this.name = name;
}
publicint getAge()
{
returnage;
}
publicvoid setAge(int age)
{
this.age = age;
}
//单例
private MyData() {};
//提供获取实例方法
publicstatic 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对象调用外部类中操作共享数据的方法即可。
注意:要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
08. java5原子性操作类的应用
Java5的线程并发库
java.util.concurrent在并发编程中很常用的实用工具类。
|----locks为锁和等待条件提供一个框架的接口和类,
它不同于内置同步和监视器
|----atomic类的小工具包,支持在单个变量上解除锁的线程安全编程。
可以对基本类型、数组中的基本类型、类中的基本类型等进行操作
|----AtomicInteger
构造方法摘要 |
|
AtomicInteger() 创建具有初始值 0 的新 AtomicInteger。 |
|
AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger。 |
|
方法摘要 |
|
int |
addAndGet(int delta) 以原子方式将给定值与当前值相加。 |
boolean |
compareAndSet(int expect, int update) 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。 |
int |
decrementAndGet() 以原子方式将当前值减 1。 |
double |
doubleValue() 以 double 形式返回指定的数值。 |
float |
floatValue() 以 float 形式返回指定的数值。 |
int |
get() 获取当前值。 |
int |
getAndAdd(int delta) 以原子方式将给定值与当前值相加。 |
int |
getAndDecrement() 以原子方式将当前值减 1。 |
int |
getAndIncrement() 以原子方式将当前值加 1。 |
int |
getAndSet(int newValue) 以原子方式设置为给定值,并返回旧值。 |
int |
incrementAndGet() 以原子方式将当前值加 1。 |
int |
intValue() 以 int 形式返回指定的数值。 |
void |
lazySet(int newValue) 最后设置为给定值。 |
long |
longValue() 以 long 形式返回指定的数值。 |
void |
set(int newValue) 设置为给定值。 |
toString() 返回当前值的字符串表示形式。 |
|
boolean |
weakCompareAndSet(int expect, int update) 如果当前值 == 预期值,则以原子方式将该设置为给定的更新值。 |
|----AtomicIntegerArray
构造方法摘要 |
|
|
|
|
|
方法摘要 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
09. java5线程并发库的应用
如果没有线程池,需要在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)。
10. Callable与Future的应用:获取一个线程的运行结果
public interface Callable<V>
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call的方法。 Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable不会返回结果,并且无法抛出经过检查的异常。
只有一个方法Vcall
()
计算结果,如果无法计算结果,则抛出一个Exception异常。
使用方法:
ExecutorService threadPool = Executors.newSingleThreadExccutor();
如果不需要返回结果,就用executor方法 调用submit方法返回一个Future对象
Future<T> future = threadPool.submit(new Callable<T>(){//接收一个Callable接口的实例对象
覆盖Callable接口中的call方法,抛出异常
publicTcall() throws Exception
{
ruturnT
}
});
获取Future接收的结果
future。get();会抛出异常
future.get()没有拿到结果就会一直等待
Future取得的结果类型和Callable返回的结果类型必须一致,通过泛型实现。Callable要通过ExecutorService的submit方法提交,返回的Future对象可以取消任务。
public interface Future<V>
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。
方法摘要 |
|
boolean |
cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。 |
get() 如有必要,等待计算完成,然后获取其结果。 |
|
get(long timeout,TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 |
|
boolean |
isCancelled() 如果在任务正常完成前将其取消,则返回 true。 |
boolean |
isDone() 如果任务已完成,则返回 true。 |
public interface CompletionService<V>
CompletionService用于提交一组Callable任务,其take方法返回一个已完成的Callable任务对应的Future对象。好比同时种几块麦子等待收割,收割时哪块先熟先收哪块。
将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit执行的任务。使用者 take已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService可以用来管理异步 IO,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
通常,CompletionService依赖于一个单独的Executor来实际执行任务,在这种情况下,CompletionService只管理一个内部完成队列。ExecutorCompletionService类提供了此方法的一个实现。
CompletionService方法摘要 |
||
poll() 获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回 null。 |
||
poll(long timeout,TimeUnit unit) 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则将等待指定的时间(如果有必要)。 |
||
submit(Runnable task,V result) 提交要执行的 Runnable任务,并返回一个表示任务完成的 Future,可以提取或轮询此任务。 |
||
take() 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。 |
||
ExecutorCompletionService(Executor executor) |
|
|
ExecutorCompletionService(Executor executor,BlockingQueue<Future<V>> completionQueue) |
|
示例:
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());
}
11. java5的线程锁技术
java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,
接口摘要 |
||
Condition 将 Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。 |
||
Lock 实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。 |
||
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。 |
||
类摘要 |
||
可以由线程以独占方式拥有的同步器。 |
||
以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。 |
||
为实现依赖于先进先出 (FIFO)等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。 |
||
用来创建锁和其他同步类的基本线程阻塞原语。 |
||
一个可重入的互斥锁Lock,它具有与使用 synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。 |
||
支持与 ReentrantLock 类似语义的ReadWriteLock实现。 |
||
Lock比传统线程模型中的synchronized更加面向对象,锁本身也是一个对象,两个线程执行的代码要实现同步互斥效果,就要使用同一个锁对象。锁要上在要操作的资源类的内部方法中,而不是线程代码中。
public interface Lock
所有已知实现类:
ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally或 try-catch加以保护,以确保在必要时释放锁。
方法摘要 |
|
void |
lock() 获取锁。 |
void |
lockInterruptibly() 如果当前线程未被中断,则获取锁。 |
newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。 |
|
boolean |
tryLock() 仅在调用时锁为空闲状态才获取该锁。 |
boolean |
tryLock(long time,TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void |
unlock() 释放锁。 |
Lock与synchronized对比,打印字符串例子
12. java5读写锁技术的妙用
读写锁,分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,由JVM控制。
ReentrantReadWriteLock
构造方法摘要 |
ReentrantReadWriteLock() 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock。 |
ReentrantReadWriteLock(boolean fair) 使用给定的公平策略创建一个新的 ReentrantReadWriteLock。 |
方法摘要 |
||||
protected Thread |
getOwner() 返回当前拥有写入锁的线程,如果没有这样的线程,则返回 null。 |
|||
protected Collection<Thread> |
getQueuedReaderThreads() 返回一个 collection,它包含可能正在等待获取读取锁的线程。 |
|||
protected Collection<Thread> |
getQueuedThreads() 返回一个 collection,它包含可能正在等待获取读取或写入锁的线程。 |
|||
protected Collection<Thread> |
getQueuedWriterThreads() 返回一个 collection,它包含可能正在等待获取写入锁的线程。 |
|||
int |
getQueueLength() 返回等待获取读取或写入锁的线程估计数目。 |
|||
int |
getReadHoldCount() 查询当前线程在此锁上保持的重入读取锁数量。 |
|||
int |
getReadLockCount() 查询为此锁保持的读取锁数量。 |
|||
protected Collection<Thread> |
getWaitingThreads(Condition condition) 返回一个 collection,它包含可能正在等待与写入锁相关的给定条件的那些线程。 |
|||
int |
getWaitQueueLength(Condition condition) 返回正等待与写入锁相关的给定条件的线程估计数目。 |
|||
int |
getWriteHoldCount() 查询当前线程在此锁上保持的重入写入锁数量。 |
|||
boolean |
hasQueuedThread(Thread thread) 查询是否给定线程正在等待获取读取或写入锁。 |
|||
boolean |
hasQueuedThreads() 查询是否所有的线程正在等待获取读取或写入锁。 |
|||
boolean |
hasWaiters(Condition condition) 查询是否有些线程正在等待与写入锁有关的给定条件。 |
|||
boolean |
isFair() 如果此锁将公平性设置为 ture,则返回 true。 |
|||
boolean |
isWriteLocked() 查询是否某个线程保持了写入锁。 |
|||
boolean |
isWriteLockedByCurrentThread() 查询当前线程是否保持了写入锁。 |
|||
readLock() 返回用于读取操作的锁。 |
||||
toString() 返回标识此锁及其锁状态的字符串。 |
||||
writeLock() 返回用于写入操作的锁。 |
三个线程读数据,三个线程写数据示例:
可以同时读,读的时候不能写,不能同时写,写的时候不能读
读的时候上读锁,读完解锁;写的时候上写锁,写完解锁。注意finally解锁
package cn.itheima;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo
{
/**读写所使用
* 三个线程读,三个线程写
*/
public static void main(String[] args)
{
//共享对象
final Source source = new Source();
//创建线程
for (int i=0; i<3; i++)
{
//读
new Thread(new Runnable()
{
public void run()
{
while (true)
source.get();
}
}).start();
//写
new Thread(new Runnable()
{
public void run()
{
while (true)
source.put(new Random().nextInt(999));
}
}).start();
}
}
static class Source
{
//共享数据
private int data = 0;
//要操作同一把锁上的读或写锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
//读方法
public void get()
{
//上读锁
rwl.readLock().lock();
try
{
//获取数据并输出
System.out.println("读——"+Thread.currentThread().getName()+"正在获取数据。。。");
try
{
Thread.sleep(new Random().nextInt(6)*1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("读——"+Thread.currentThread().getName()+"获取到的数据:"+data);
}finally
{
//解锁
rwl.readLock().unlock();
}
}
//写方法
public void put(int data)
{
//上写锁
rwl.writeLock().lock();
try
{
//提示信息
System.out.println("写——"+Thread.currentThread().getName()+"正在改写数据。。。");
try
{
Thread.sleep(new Random().nextInt(6)*1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
this.data = data;
System.out.println("写——"+Thread.currentThread().getName()+"已将数据改写为:"+data);
}finally
{
//解锁
rwl.writeLock().unlock();
}
}
}
}
JDK帮助文档中的示例用法。下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):
class CachedData {
Object data;
volatile boolean cacheValid; 数据有没有标记
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {处理数据
rwl.readLock().lock();先上读锁
if (!cacheValid) {如果数据不存在
// Must release read lock before acquiring write lock
rwl.readLock().unlock();准备写数据,需先解除读锁
rwl.writeLock().lock();上写锁
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {再次检查数据是否存在,防止其他线程已经存入数据
data = ...
cacheValid = true;写好数据,改变标记
}
// Downgrade by acquiring read lock before releasing write lock
准备释放写锁,数据存在了,释放后就要使用数据,恢复产生数据前的读锁状态
rwl.readLock().lock();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);存在直接使用数据
rwl.readLock().unlock();解除读锁
}
}
面试题:设计一个缓存系统
缓存系统:你要取数据,需调用我的public Object getData(String key)方法,我要检查我内部有没有这个数据,如果有就直接返回,如果没有,就从数据库中查找这个数,查到后将这个数据存入我内部的存储器中,下次再有人来要这个数据,我就直接返回这个数不用再到数据库中找了。 你要取数据不要找数据库,来找我。
class CachedSystem
{ 缓存系统的存储器
private Map<String, Object> cache = new HashMap<String, Object>();
取数据方法 可能有多个线程来取数据,没有数据的话又会去数据库查询,需要互斥
public synchronized Object get(String key)
{ 先查询内部存储器中有没有要的值
Object value = cache.get(key);
if (value==null)如果没有,就去数据库中查询,并将查到的结果存入内部存储器中
{
value =“aaaa”;实际代码是查询后的结果 queryDB(key)
cache.put(key, value);
}
return value;
}
}
上面的代码每次只能有一个线程来查询,但只有写的时候才需要互斥,修改如下
来一个读写锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
public Object get(String key)
{
上读锁
rwl.readLock().lock();
先查询内部存储器中有没有要的值
Object value = cache.get(key);
if (value==null)如果没有,就去数据库中查询,并将查到的结果存入内部存储器中
{
释放读锁 上写锁
rwl.readLock().unlock();
rwl.writeLock().lock();
if (value==null)再次进行判断,防止多个写线程堵在这个地方重复写
{
value =“aaaa”;
cache.put(key, value);
}
设置完成释放写锁,恢复读写状态
rwl.readLock().lock();
rwl.writeLock().unlock();
}
释放读锁
rwl.readLock().unlock();
return value; 注意:try finally中unlock
}
13. java5条件阻塞Condition的应用
Condition的功能类似在传统线程技术中的Object.wait()和Object.natify()的功能,传统线程技术实现的互斥只能一个线程单独干,不能说这个线程干完了通知另一个线程来干,Condition就是解决这个问题的,实现线程间的通信。比如CPU让小弟做事,小弟说我先歇着并通知大哥,大哥就开始做事。
public interface Condition
Condition 将 Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock替代了 synchronized方法和语句的使用,Condition替代了 Object监视器方法的使用。
Condition 实例实质上被绑定到一个锁上。要为特定Lock实例获得 Condition 实例,请使用其newCondition()方法。
作为一个示例,假定有一个绑定的缓冲区,它支持 put和 take方法。如果试图在空的缓冲区上执行 take操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set中保存 put线程和 take线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition实例来做到这一点。
class BoundedBuffer {阻塞队列满了不能放,空了不能取
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
使用方法:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
this.wait()àcondition.await()
this.notify()àcondition.signal()
注意:判断条件时用while防止虚假唤醒,等待在那里,唤醒后再进行判断,确认符合要求后再执行任务。
14. java5的Semaphore同步工具
Semaphore可以维护当前访问自身的线程个数,并且提供了同步机制。
semaphore实现的功能类似于厕所里有5个坑,有10个人要上厕所,同时就只能有5个人占用,当5个人中的任何一个让开后,其中在等待的另外5个人中又有一个可以占用了。
java.util.concurrent.Semaphore
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。例如,下面的类使用信号量控制对内容池的访问:
class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
public void putItem(Object x) {
if (markAsUnused(x))
available.release();
}
// Not a particularly efficient data structure; just for demo
protected Object[] items = ... whatever kinds of items being managed
protected boolean[] used = new boolean[MAX_AVAILABLE];
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (!used[i]) {
used[i] = true;
return items[i];
}
}
return null; // not reached
}
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (item == items[i]) {
if (used[i]) {
used[i] = false;
return true;
} else
return false;
}
}
return false;
}
}
获得一项前,每个线程必须从信号量获取许可,从而保证可以使用该项。该线程结束后,将项返回到池中并将许可返回到该信号量,从而允许其他线程获取该项。注意,调用acquire()时无法保持同步锁,因为这会阻止将项返回到池中。信号量封装所需的同步,以限制对池的访问,这同维持该池本身一致性所需的同步是分开的。
构造方法摘要 |
||||
Semaphore(int permits) 创建具有给定的许可数和非公平的公平设置的 Semaphore。 |
||||
Semaphore(int permits, boolean fair) 创建具有给定的许可数和给定的公平设置的 Semaphore。 |
||||
方法摘要 |
||||
void |
||||
void |
acquire(int permits) 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。 |
|||
void |
acquireUninterruptibly() 从此信号量中获取许可,在有可用的许可前将其阻塞。 |
|||
void |
acquireUninterruptibly(int permits) 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。 |
|||
int |
availablePermits() 返回此信号量中当前可用的许可数。 |
|||
int |
drainPermits() 获取并返回立即可用的所有许可。 |
|||
protected Collection<Thread> |
getQueuedThreads() 返回一个 collection,包含可能等待获取的线程。 |
|||
int |
getQueueLength() 返回正在等待获取的线程的估计数目。 |
|||
boolean |
hasQueuedThreads() 查询是否有线程正在等待获取。 |
|||
boolean |
isFair() 如果此信号量的公平设置为 true,则返回 true。 |
|||
protected void |
reducePermits(int reduction) 根据指定的缩减量减小可用许可的数目。 |
|||
void |
release() 释放一个许可,将其返回给信号量。 |
|||
void |
release(int permits) 释放给定数目的许可,将其返回到信号量。 |
|||
toString() 返回标识此信号量的字符串,以及信号量的状态。 |
||||
boolean |
tryAcquire() 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。 |
|||
boolean |
tryAcquire(int permits) 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 |
|||
boolean |
tryAcquire(int permits, long timeout,TimeUnit unit) 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。 |
|||
boolean |
tryAcquire(long timeout,TimeUnit unit) 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。 |
示例:3个坑 10个人
厕所,有多少人都能装,线程数动态变化,来一个人产生一个线程
ExecutorService service = Exccutors.newCachedThreadPool();
final Semaphore sp =new Semaphore(3);厕所中坑的个数 指定只有3个
3个坑,来了5个人,有2个人要等,其中有一个办完事走了,等待的2个哪个先上呢?默认的构造方法不管,谁抢到了谁上。用new Semaphore(3, true)就可以保证先来的先上。
将坑的个数设置为1就可以达到互斥效果,每次只能有一个线程运行
for (int i=0; i<10; i++)来了10个人
{人的任务 抢坑
Runnable runnable = new Runnable()
{
public void run()
{
sp.acquire();抢坑了会抛中断异常
}有人占住坑了,给出提示
SOP(currentThreadName+进入,当前已有(3-sp.availablePermits())个人了)
Thread.sleep(5000)蹲坑办事
办完事打声招呼
SOP(ThreadName即将离开)
释放坑的占有权
sp.release();
SOP(ThreadName已经走了,还有sp.availablePermits()个坑可用)
}
开始任务吧
service.execute(runnable)
}
传统互斥只能内部释放锁this.unlock(),进去this.lock()晕倒了别人就没法进去了;用信号灯可以外部释放,其他线程可以释放再获取sp.release() sp.acquire()。
15. java5的CyclicBarrier同步工具
例如:组织人员(线程)郊游,约定一个时间地点(路障),人员陆续到达地点,等所有人员全部到达,开始到公园各玩各的,再到约定时间去食堂吃饭,等所有人到齐开饭……
java.util.concurrent.CyclicBarrier
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。
CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。
构造方法摘要 |
|
CyclicBarrier(int parties) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier时执行预定义的操作。 |
|
CyclicBarrier(int parties,Runnable barrierAction) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。 |
|
方法摘要 |
|
int |
|
int |
await(long timeout,TimeUnit unit) 在所有参与者都已经在此屏障上调用 await方法之前将一直等待,或者超出了指定的等待时间。 |
int |
getNumberWaiting() 返回当前在屏障处等待的参与者数目。 |
int |
getParties() 返回要求启动此 barrier 的参与者数目。 |
boolean |
isBroken() 查询此屏障是否处于损坏状态。 |
void |
reset() 将屏障重置为其初始状态。 |
例:
ExecutorService service = Executors.newCachedThreadPool();
final CyclicBarrier cb = new CyclicBarrier(3); 约定3个人
for (int i=0; i<3; i++)产生3个人
{ 每个人的任务
Runnable runnable = newRunnable()
{
public void run()
{ 开始出发到目的地
Thread.sleep((long)Math.random()*1000);
SOP(ThreadName即将到达集合点1,
当前已有cb.getNumberWaiting()+1个
+ (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"))
cb.await();到了其他人没来就等
人到齐了再继续进行
Thread.sleep((long)Math.random()*1000);
SOP(ThreadName即将到达集合点2)
cb.await();到了其他人没来就等
}
}
service.execute(runnable);
}
16. java5的CountDownLatch同步工具
好像倒计时计数器,调用CountDownLatch对象的countDown方法就将计数器减1,当到达0时,所有等待者就开始执行。
举例:多个运动员等待裁判命令: 裁判等所有运动员到齐后发布结果
代码示例:
ExecutorService service = Executors.newCachedThreadPool();
裁判发布命令的计数器,计数器为0,运动员就跑
final CountDownLatch cdOrder = new CountDownLatch(1);
运动员跑到终点的计数器,为0裁判宣布结果
final CountDownLatch cdAnswer = new CountDownLatch(3);
产生3个运动员
for (int i=0; i<3; i++)
{ 运动员的任务
Runnable runnable = new Runnable(){
public void run()
{
SOP(ThreadName准备接受命令)
等待发布命令
cdOrder.await(); 计数器为0继续向下执行
SOP(ThreadName已接受命令) order计数器为0了
Thread.sleep(Random);开始跑步
cdAnswer.countDown();跑到终点了,计数器减1
}
};
service.execute(runnable);运动员开始任务
}
Thread.sleep(1000)裁判休息一会再发布命令
SOP(即将发布命令)
cdOrder.countDown();命令计数器置为0,发布命令
SOP(命令已经发布,等待结果)
cdAnswer.await(); 等待所有运动员,计数器为0 所有运动员到位
SOP(宣布结果)
java.util.concurrent.CountDownLatch
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数初始化 CountDownLatch。由于调用了countDown()方法,所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用CyclicBarrier。
CountDownLatch 是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch用作一个简单的开/关锁存器,或入口:在通过调用countDown()的线程打开入口前,所有调用await的线程都一直在入口处等待。用N初始化的 CountDownLatch 可以使一个线程在N个线程完成某项操作之前一直等待,或者使其在某项操作完成 N次之前一直等待。
CountDownLatch 的一个有用特性是,它不要求调用 countDown 方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个await。
构造方法摘要 |
|
CountDownLatch(int count) 构造一个用给定计数初始化的 CountDownLatch。 |
|
方法摘要 |
|
void |
|
boolean |
await(long timeout,TimeUnit unit) 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。 |
void |
countDown() 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。 |
long |
getCount() 返回当前计数。 |
toString() 返回标识此锁存器及其状态的字符串。 |
17. java5的Exchanger同步工具
用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人会一直等待第二个人,直到第二个人拿着数据到来时,才能彼此交换数据。
举例:毒品交易 双方并不是同时到达,有先有后,只有都到达了,瞬间交换数据,各自飞
代码演示:
ExecutorService service = Executors.newCachedThreadPool();
final Exchanger exchanger = new Exchanger();
毒贩:
service.execute(new Runnable()
{ 毒贩做的事
public void run()
{
String(毒品) data1 =毒品
SOP(毒贩正在将data1换出去)
Thread.sleep(Random)换的过程
毒贩到位了,拿着毒品等待毒人接头,接头后就能换到钱了
String data2 = (String)exchanger.exchange(data1);
SOP(毒贩换到了钱:data2)
}
});
毒人:
service.execute(new Runnable()
{ 吸毒人做的事
public void run()
{
String(钱) data1 =钱
SOP(毒人正在将data1换出去)
Thread.sleep(Random)换的过程
吸毒人到位了,拿着钱等待毒贩接头,接头后就能换到毒品了
String data2 = (String)exchanger.exchange(data1);
SOP(毒人换到了毒品:data2)
}
});
java.util.concurrent.Exchanger<V> V -可以交换的对象类型
可以在对中对元素进行配对和交换的线程的同步点。每个线程将条目上的某个方法呈现给exchange方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象。Exchanger可能被视为SynchronousQueue的双向形式。Exchanger可能在应用程序(比如遗传算法和管道设计)中很有用。
用法示例:以下是重点介绍的一个类,该类使用 Exchanger在线程间交换缓冲区,因此,在需要时,填充缓冲区的线程获取一个新腾空的缓冲区,并将填满的缓冲区传递给腾空缓冲区的线程。
class FillAndEmpty {
Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
DataBuffer initialEmptyBuffer = ... a made-up type
DataBuffer initialFullBuffer = ...
class FillingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialEmptyBuffer;
try {
while (currentBuffer != null) {
addToBuffer(currentBuffer);
if (currentBuffer.isFull())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ... }
}
}
class EmptyingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialFullBuffer;
try {
while (currentBuffer != null) {
takeFromBuffer(currentBuffer);
if (currentBuffer.isEmpty())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ...}
}
}
void start() {
new Thread(new FillingLoop()).start();
new Thread(new EmptyingLoop()).start();
}
}
构造方法摘要 |
|
Exchanger() 创建一个新的 Exchanger。 |
|
方法摘要 |
|
exchange(V x) 等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。 |
|
exchange(V x, long timeout, TimeUnit unit) 等待另一个线程到达此交换点(除非当前线程被中断,或者超出了指定的等待时间),然后将给定的对象传送给该线程,同时接收该线程的对象。 |
18. java5阻塞队列的应用
队列包含固定长度的队列和不固定长度的队列,先进先出
固定长度的队列往里放数据,如果放满了还要放,阻塞式队列就会等待,直到有数据取出,空出位置后才继续放;非阻塞式队列不能等待就只能报错了。
讲Condition时提到了阻塞队列的原理,Java中已经实现了阻塞队列ArrayBlockingQueue
BlockingQueue<E> public interface BlockingQueue<E>extendsQueue<E>
支持两个附加操作的Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。
BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
|
抛出异常 |
特殊值 |
阻塞 |
超时 |
插入 |
||||
移除 |
||||
检查 |
不可用 |
不可用 |
BlockingQueue 不接受 null 元素。试图 add、put或 offer一个 null元素时,某些实现会抛出 NullPointerException。null被用作指示 poll操作失败的警戒值。
BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put附加元素。没有任何内部容量约束的 BlockingQueue总是报告 Integer.MAX_VALUE 的剩余容量。
BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持Collection接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
BlockingQueue 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection操作(addAll、containsAll、retainAll和 removeAll)没有必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c中的一些元素后,addAll(c)有可能失败(抛出一个异常)。
java.util.concurrent.ArrayBlockingQueue<E> E -在此 collection中保持的元素类型
extends AbstractQueue<E>implementsBlockingQueue<E>,Serializable
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素。队列的尾部是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness)设置为 true而构造的队列允许按照 FIFO顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
此类及其迭代器实现了Collection和Iterator接口的所有可选方法。
此类是 Java Collections Framework 的成员。
构造方法摘要 |
|
ArrayBlockingQueue(int capacity) 创建一个带有给定的(固定)容量和默认访问策略的 ArrayBlockingQueue。 |
|
ArrayBlockingQueue(int capacity, boolean fair) 创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue。 |
|
ArrayBlockingQueue(int capacity, boolean fair,Collection<? extendsE> c) 创建一个具有给定的(固定)容量和指定访问策略的 ArrayBlockingQueue,它最初包含给定 collection的元素,并以 collection迭代器的遍历顺序添加元素。 |
|
方法摘要 |
|
boolean |
add(E e) 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则抛出 IllegalStateException。 |
void |
clear() 自动移除此队列中的所有元素。 |
boolean |
|
int |
drainTo(Collection<? super E> c) 移除此队列中所有可用的元素,并将它们添加到给定 collection 中。 |
int |
drainTo(Collection<? super E> c, int maxElements) 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。 |
iterator() 返回在此队列中的元素上按适当顺序进行迭代的迭代器。 |
|
boolean |
offer(E e) 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false。 |
boolean |
offer(E e, long timeout, TimeUnit unit) 将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。 |
peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。 |
|
poll() 获取并移除此队列的头,如果此队列为空,则返回 null。 |
|
poll(long timeout,TimeUnit unit) 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。 |
|
void |
|
int |
remainingCapacity() 返回在无阻塞的理想情况下(不存在内存或资源约束)此队列能接受的其他元素数量。 |
boolean |
|
int |
size() 返回此队列中元素的数量。 |
take() 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。 |
|
Object[] |
toArray() 返回一个按适当顺序包含此队列中所有元素的数组。 |
<T> T[] |
toArray(T[] a) 返回一个按适当顺序包含此队列中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 |
toString() 返回此 collection 的字符串表示形式。 |
阻塞队列的实现原理(Condition锁中有提到await signal)
19. java5同步集合类的应用
传统集合实现同步的问题
举了一个例子:Map集合线程不同步导致的问题。
解决办法:使用同步的Map集合 使用集合工具类中的方法将不同步的集合转为同步的Collections.synchronizedMap(new Map())这个方法返回一个同步的集合
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)
{return new SynchronizedMap<K, V>(m);}
SynchronizedMap类相当于一个代理类,通过查看源代码发现:该类中的所有方法都是直接返回:原Map集合方法调用后的结果,只是将返回结果的代码放在了同步代码块中以实现同步,构造是将同步锁默认置为当前对象。
HashSet与HashMap的关系与区别:
HashSet是单列的,HashMap是双列的(键值对)
关系:HashSet内部使用的是HashMap中的键,不考虑值。
查看HashSet的源代码发现其内部就是用HashMap实现的,只是没有使用HashMap的V,只使用了它的K。
JDK1.5中提供了并发 Collection:提供了设计用于多线程上下文中的 Collection实现:
ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList和CopyOnWriteArraySet。当期望许多线程访问一个给定 collection时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList优于同步的 ArrayList。
ConcurrentSkipListMap<K,V>映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法。
ConcurrentSkipListSet<E>一个基于ConcurrentSkipListMap的可缩放并发NavigableSet实现。set的元素可以根据它们的自然顺序进行排序,也可以根据创建 set时所提供的Comparator进行排序,具体取决于使用的构造方法
CopyOnWriteArrayList<E>ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。
这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“
CopyOnWriteArraySet<E>对其所有操作使用内部CopyOnWriteArrayList的Set。因此,它共享以下相同的基本属性:
它最适合于具有以下特征的应用程序:set大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。它是线程安全的。因为通常需要复制整个基础数组,所以可变操作(add、set和 remove 等等)的开销很大。迭代器不支持可变 remove操作。使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
传统集合中存在的其它问题:对集合迭代时,不能对集合中的元素进行修改(添加、删除……),Java5中提供的并发集合就解决了这个问题。
20.空中网挑选实习生的面试题1
现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。原始代码如下:
public class Test1
{
public static void main(String[] args)
{
SOP(begin:+sys.currentTimeMillis()/1000);
//模拟处理16行日志,下面的代码产生16个日志对象,需运行16秒才能打印完
//修改程序代码,开4个线程让这16个日志在4秒钟打完
for (iint i=0; i<16; i++) //这行代码不能改动
{
final String log = “”+(i+1); //这行代码不能改动
{
Test1.parseLog(log);
}
}
}
//parseLog方法内部代码不能改动
public static void parseLog(String log)
{
SOP(log+”:”+(sys.currentTimeMillis()/1000));
try
{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
刚看到题目还想着很简单;直接在Test.parseLog(log)的地方new4个线程,都执行打印任务即可,仔细一看不行,在这里new4个线程的话就是16*4个线程了,所以要将线程在for循环外边创建出来,for内部将产生的日志对象装在一个共享变量里,在线程内部从共享变量中取数据打印。要考虑线程同步互斥问题,这个共享变量要具备同步功能,可以使用ArrayBlockingQueue这种阻塞式队列来存储日志对象。也可以使用普通集合,但拿数据要考虑同步问题,可能会浪费时间。
在for循环外部创建线程,定义共享变量
final BlockingQueue<String> queue = new ArrayBlockingQueue<String>(16);
for (int i=0; i<4; i++) 创建4个线程
{
new Thread(new Runnable()
{
public void run()
{
while (true)
{ 先从集合中取到日志对象,再打印
String log = queue.take(); 要处理异常
parseLog(log);
}
}
}).start();
}
在16次for循环内部将产生的日志对象装入共享变量中
queue.put(log); 要处理异常
这道题只要用到同步集合来共享数据就可以了 List集合的Vector也可以
21.空中网挑选实习生的面试题2
现成程序中的Test类中的代码在不断地产生数据,然后交给TestDo.doSome()方法去处理。就好像生产者不断地产生数据,消费者不断地消费数据。请将程序改造成有10个线程来消费生产者产生的数据,这些消费者都调用TestDo.doSome()方法去进行处理,故每个消费者都需要一秒才能处理完,程序应保证这些消费者线程依次有序地消费数据,只有上一个消费者消费完后,下一个消费者才能消费数据,下一个消费者是谁都可以,但要保证这些消费者线程拿到的数据是有序的。原始代码如下:
public class Test2
{
public static void main(String[] args)
{
SOP(begin+sys.currentTimeMillis()/1000);
for (int i=0; i<10; i++) //这行不能改动
{
String input = i+””; //这行不能改动
String output = TeatDo.doSome(input);
SOP(ThreadName+output);
}
}
}
//不能改动此TestDo类
class TestDo
{
public static String doSome(String input)
{
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
String output = input + “:” + (Sys.currentTimeMillis());
return output;
}
}
看这题和上一题差不多,也是数据共享问题了,弄一个同步集合存起来。
用同样的方法一样解决 new ArrayBlockingQueue()
张老师又讲了另一个同步队列:SynchronousQueue一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行peek,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。
SynchronousQueue是一个特殊队列,即便是空的也不能插入元素,也读不到元素,要往里边插入的时候如果没有读取操作,插入操作就会阻塞,等到有读取操作出现时,插入操作检测到了读取操作,才能把数据插入进去,而读取操作正好可以拿到刚刚插入进去的数据。就好比毒品买卖,我拿着毒品给谁呢,只有买毒品的人来了,才能立马给他,他也拿到了。与Exchanger类似,不过Exchanger是单对单的交换,SynchronousQueue可以多个抢数据,我拿着毒品等人来买,一下来了3个人买,谁抢到了就是谁的;或者我拿3包毒品,3个人同时每人一份。
这道题用synchronousQueue的话会一下子将10个数据全打印出来,因为10次循环一次放一个并没有人来取,所以没有放进去,后来一下10个线程来取数据,就一下放进去拿走了。我测试的时候没有这种情况,都是间隔一秒一秒的。测试后发现,将doSome处理后的结果存进去,就会有间隔,而直接存进去,取数据后再处理的话就是一下一片了。分析后知道:put时没有take,10个数据都在等待存入,如果存入的数据是doSome(input)的话,开始取数据时才开始执行doSome所以就会有间隔了。直接存数据,取出后在doSome就是一下拿到10个数据了。
要解决这个问题,可以使用厕所抢坑的方式解决,使用Semaphore来获取许可,每取一次数据释放一次即可。
final Semaphore x = new Semaphore(1); 一次一个
final SynchronousQueue queue = new SynchronousQueue();
每次取数据前都要先获取许可
x.acquire();
取完后释放许可
x.release();
这种方式与使用Lock方式一样
22.空中网挑选实习生的面试题3
现有程序同时启动了4个线程去调用TestDo.doSome(key, value)方法,由于TestDo.doSome(key, value)方法内的代码是先暂停1秒,然后再输出以秒为单位的当前时间值,所以,会打印出4个相同的时间值,如下所示:
4:4:1258199615
1:1:1258199615
3:3:1258199615
1:2:1258199615
请修改代码,如果有几个线程调用TestDo.doSome(key, value)方法时,传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果,即当有两个线程的key都是“1”时,她们中的一个要比另外其他线程晚1秒输出结果,如下所示:
4:4:1258199615
1:1:1258199615
3:3:1258199615
1:2:1258199616
总之,当每个线程中指定的key相等时,这些相等key的线程应每隔一秒依次输出时间值(要用互斥),如果key不同,则并行执行(相互之间不互斥)。原始代码如下:
//不能改动此Test类
public class Test3 extends Thread
{
private TestDo testDo;
private String key;
private String value;
public Test3(String key, String key2, String value)
{
this.testDo = TestDo.getInstance();
/*常量“1”和“1”是同一个对象,下面这行代码就是要用“1”+“”的
方式产生新的对象,以实现内容没有改变,仍然相等(都还为“1”),
但对象却不再是同一个的效果
*/
this.key = key + key2;
this.value = value;
}
public static void main(String[] args) throws InterruptedException
{
Test3 a = new Test3(“1”, “”, “1”);
Test3 b = new Test3(“1”, “”, “2”);
Test3 c = new Test3(“3”, “”, “3”);
Test3 d = new Test3(“4”, “”, “4”);
SOP(begin+:+sys.currentTimeMillis()/1000);
a.start();
b.start();
c.start();
d.start();
}
public void run()
{
testDo.doSome(key, value);
}
}
class TestDo
{
private TestDo(){}
private static TestDo _instance = new TestDo();
public static TestDo getInstance()
{
return _instance;
}
public void doSome(Object key, String value)
{
//此大括号内的代码是需要局部同步的代码,不能改动!
{
try
{
Thread.sleep(1000);
SOP(key+”:”+value+”:”+sys.currentTimeMillis()/1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
看完这道题第一个想法是在标记位置加上同步代码块,但是锁不好弄了,因为每次都新建了一个key对象来接受实际key,没法获取到实际key对象。
想到了Lock对象,所以建一个Lock对象,判断key的值是否和指定值“1“相同,如果相同就锁上,不同不管,finally里在解锁前进行判断,避免没上锁还要解锁发生问题。
Lock lock = new ReentrantLock();
public void doSome(Object key, String value)
{
if (key.equals("1"))
lock.lock();
//System.out.println("OKOKOOK");
//synchronized ("1")
try
//此大括号内的代码是需要局部同步的代码,不能改动!
{
try
{
Thread.sleep(1000);
System.out.println(key+":"+value+":"+System.currentTimeMillis()/1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}finally
{
if (key.equals("1"))
lock.unlock();
}
}
但上面的方式写死了,如果换2呢还要改代码,现在要不管是什么只要相同都互斥,将这些加进来的key放到一个集合ArrayList中,每次传进来一个key,先把传进来的key作为锁对象,再判断这个对象有没有存在锁集合中,如果没有,就把它存进去,同时就用这个key做锁;如果已经存在了,就是说这个key已经做过锁对象了,就需要将以前做锁的那个对象拿出来,再让它来当锁,与传进来的key对象一样,这样就产生互斥效果了。
需要注意:拿原来的锁对象时要迭代锁集合,因为有多个线程在运行,所以迭代时有可能出现其他线程的key没有做过锁,需要将它加到锁集合中,可是这时候这个线程还在迭代过程中,迭代时不能操作集合中的数据,就会发生异常。要解决这个问题,就需要用到同步集合了。CopyOnWriteArrayList
使用ArrayList时就经常出异常,换CopyOnWriteArrayList后没有异常了
//将所有传过来的key都存起来
//private List<Object> keys = new ArrayList<Object>();
private CopyOnWriteArrayList<Object>keys =new CopyOnWriteArrayList<Object>();
publicvoid doSome(Object key, String value)
{
//先用这个key当锁,用过一次就存到集合中
Object o = key;
//判断这个锁用过没有
if (!keys.contains(o))
{
//如果这个key没有用过,就用它当锁,把它存到锁集合中
keys.add(o);
}
else //锁集合中已经有了这个key
{
//这个key已经当过锁了,就把它拿出来,还用它做锁,就和现在的key互斥了
//因为不知道原来key的位置,所有需要进行遍历
for (Iterator<Object> it =keys.iterator(); it.hasNext();)
{
//当前遍历到的对象
Object oo = it.next();
//如果找到了,就让它做锁
if (oo.equals(o))
{
o = oo;
break;//找到了,不用再循环了
}
}
//o = keys.get(keys.indexOf(o)); //key和o不是同一个对象,拿不到
}
synchronized (o)
//此大括号内的代码是需要局部同步的代码,不能改动!
{
try
{
Thread.sleep(1000);
System.out.println(key+":"+value+":"+System.currentTimeMillis()/1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
a = “1”+””;
b = “1”+””;
a和b是同一个对象,常量相加 equals为真 ==为假
Object o1 = new String("1");
Object o2 = new String("1");
System.out.println(o1==o2);//false
System.out.println(o1.equals(o2));//true
System.out.println(o1); //1
System.out.println(o2); //1
Object o3 = "1"+"";
Object o4 = "1"+"";
System.out.println(o3==o4);//true
System.out.println(o3.equals(o4));//true
Object o5 = "2"+"";
Object o6 = get("2","");
System.out.println(o5==o6);//false
System.out.println(o5.equals(o6));//true
System.out.println(o5+"__"+o6);//2__2
publicstatic Object get(String a, String b)
{
return a+b;
}