2008年2月22日
#
本文内容
何时该使用读写锁.
读写锁的写法.
理解读写锁和线程互斥的区别。
复习-同步化的概念
当一个方法或代码块被声明成synchronized,要执行此代码必须先取得一个对象实例或this的锁定,这个锁定要在synchronized修饰的方法或代码块执行完后才能释放掉(无论这段代码是怎样返回的,是正常运行还是异常运行)。每个对象只有一个锁定,如果有两个不同的线程试图同时调用同一对象的同步方法,最终只会有一个能运行此方法,另外一个要等待第一个线程释放掉锁定后才能运行此方法。
读写锁应用的场合
我们有时会遇到对同一个内存区域如数组或者链表进行多线程读写的情况,一般来说有以下几种处理方式: 1.不加任何限制,多见于读取写入都很快的情况,但有时也会出现问题. 2.对读写函数都加以同步互斥,这下问题是没了,但效率也下去了,比如说两个读取线程不是非要排队进入不可. 3.使用读写锁,安全和效率都得到了解决,特别合适读线程多于写线程的情况.也就是下面将要展现的模式.
读写锁的意图
读写锁的本意是分别对读写状态进行互斥区分,有互斥时才加锁,否则放行.互斥的情况有: 1.读写互斥. 2.写写互斥. 不互斥的情况是:读读,这种情况不该加以限制. 程序就是要让锁对象知道当前读写状态,再根据情况对读写的线程进行锁定和解锁。
读写线程都要操作的数据类
读写线程都要操作的数据是链表datas。
注意其中try...finally 的写法,它保证了加锁解锁过程是成对调用的
lpublic class DataLib {
private List<String> datas;
private ReadWriteLock lock;
public DataLib() {
datas = new ArrayList<String>();
lock = new ReadWriteLock();
}
// 写入数据,这时不能读取
public void writeData(List<String> newDatas) {
try {
lock.writeLock();
Test.sleep(2);
datas=newDatas;
} finally {
lock.writeUnlock();
}
}
// 读取数据,这时不能写入
public List<String> readData() {
try {
lock.readLock();
Test.sleep(1);
return datas;
} finally {
lock.readUnlock();
}
}
}
读写锁ReadWriteLock类
public class ReadWriteLock{
// 读状态
private boolean isRead;
// 写状态
private boolean isWrite;
public synchronized void readLock(){
// 有写入时读取线程停止
while(isWrite){
try{
System.out.println("有线程在进行写入,读取线程停止,进入等待状态");
wait();
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
System.out.println("设定锁为读取状态");
isRead=true;
}
public synchronized void readUnlock(){
System.out.println("解除读取锁");
isRead=false;
notifyAll();
}
public synchronized void writeLock(){
// 有读取时读取线程停止
while(isRead){
try{
System.out.println("有线程在进行读取,写入线程停止,进入等待状态");
wait();
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
// 有写入时写入线程也一样要停止
while(isWrite){
try{
System.out.println("有线程在进行写入,写入线程停止,进入等待状态");
wait();
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
System.out.println("设定锁为写入状态");
isWrite=true;
}
public synchronized void writeUnlock(){
System.out.println("解除写入锁");
isWrite=false;
notifyAll();
}
}
写线程类Writer -它用于往DataLib类实例中的datas字段写数据
分析其中dataLib字段的用意。
注意并记住其中持续调用及使用随机数的方法。
lpublic class Writer implements Runnable{
private DataLib dataLib;
private static final Random random=new Random();
private String[] mockDatas={"甲","乙","丙","丁","戊","己","庚","辛","壬","癸"};
public Writer(DataLib dataLib,String[] mockDatas){
this.dataLib=dataLib;
this.mockDatas=mockDatas;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
Test.sleep(random.nextInt(3));
int startIndex=random.nextInt(mockDatas.length);
ArrayList<String> newDatas=new ArrayList<String>();
for(int i=startIndex;i<mockDatas.length;i++){
newDatas.add(mockDatas[i]);
}
dataLib.writeData(newDatas);
}
}
}
读线程类Reader -它用于从DataLib类实例中的datas字段读取数据
分析其中dataLib字段的用意。
注意并记住其中持续调用及使用随机数的方法。
public class Reader implements Runnable{
private DataLib dataLib;
private static final Random random=new Random();
public Reader(DataLib dataLib){
this.dataLib=dataLib;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
Test.sleep(random.nextInt(2));
List<String> datas=dataLib.readData();
System.out.print(">>取得数组为:");
for(String data:datas){
System.out.print(data+",");
}
System.out.print("\n");
}
}
}
将代码运行起来
右边的代码创建了两个写线程和三个读线程,它们都是对dataLib实例进行操作的。
五个线程都有一个dataLib字段,都提供了一个带参构造函数以给datas字段赋值,这就保证了五个线程操作的都是一个实例的同一字段,也就是同一片内存。
读写锁就是对这五个线程进行控制的。
当有一个读线程在操作时,其它的写线程无法进行操作,读线程可以正常操作,互不干扰。
当有一个写线程在操作时,其它的读线程无法进行操作。
public class Test{
public static void main(String[] args){
DataLib dataLib=new DataLib();
String[] mockDatas1={"甲","乙","丙","丁","戊","己","庚","辛","壬","癸"};
Writer writer1=new Writer(dataLib,mockDatas1);
String[] mockDatas2={"子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"};
Writer writer2=new Writer(dataLib,mockDatas2);
Reader reader1=new Reader(dataLib);
Reader reader2=new Reader(dataLib);
Reader reader3=new Reader(dataLib);
}
// 用于延时
public static void sleep(int sleepSecond){
try{
Thread.sleep(sleepSecond*1000);
}
catch(Exception ex){
ex.printStackTrace();
}
}
}
小结
当多个线程试图对同一内容进行读写操作时适合使用读写锁。
请理解并记住ReadWriteLock类读写锁的写法.
读写锁相对于线程互斥的优势在于高效,它不会对两个读线程进行盲目的互斥处理,当读线程数量多于写线程尤其如此,当全是写线程时两者等效。
本文内容
何时该使用线程锁.
线程锁的写法.
以线程锁的例子来理解线程的调度。
使用线程锁的场合
程序中经常采用多线程处理,这可以充分利用系统资源,缩短程序响应时间,改善用户体验;如果程序中只使用单线程,那么程序的速度和响应无疑会大打折扣。
但是,程序采用了多线程后,你就必须认真考虑线程调度的问题,如果调度不当,要么造成程序出错,要么造成荒谬的结果。
一个讽刺僵化体制的笑话
前苏联某官员去视察植树造林的情况,现场他看到一个人在远处挖坑,其后不远另一个人在把刚挖出的坑逐个填上,官员很费解于是询问陪同人员,当地管理人员说“负责种树的人今天病了”。
上面这个笑话如果发生在程序中就是线程调度的问题,种树这个任务有三个线程:挖坑线程,种树线程和填坑线程,后面的线程必须等前一个线程完成才能进行,而不是按时间顺序来进行,否则一旦一个线程出错就会出现上面荒谬的结果。
用线程锁来处理两个线程先后执行的情况
在程序中,和种树一样,很多任务也必须以确定的先后秩序执行,对于两个线程必须以先后秩序执行的情况,我们可以用线程锁来处理。
线程锁的大致思想是:如果线程A和线程B会执行实例的两个函数a和b,如果A必须在B之前运行,那么可以在B进入b函数时让B进入wait set,直到A执行完a函数再把B从wait set中激活。这样就保证了B必定在A之后运行,无论在之前它们的时间先后顺序是怎样的。
线程锁的代码
如右,SwingComponentLock的实例就是一个线程锁,lock函数用于锁定线程,当完成状态isCompleted为false时进入的线程会进入SwingComponentLock的实例的wait set,已完成则不会;要激活SwingComponentLock的实例的wait set中等待的线程需要执行unlock函数。
public class SwingComponentLock {
// 是否初始化完毕
boolean isCompleted = false;
/**
* 锁定线程
*/
public synchronized void lock() {
while (!isCompleted) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
}
}
/**
* 解锁线程
*
*/
public synchronized void unlock() {
isCompleted = true;
notifyAll();
}
}
线程锁的使用
public class TreeViewPanel extends BasePanel {
// 表空间和表树
private JTree tree;
// 这个是防树还未初始化好就被刷新用的
private SwingComponentLock treeLock;
protected void setupComponents() {
// 初始化锁
treeLock = new SwingComponentLock();
// 创建根节点
DefaultMutableTreeNode root = new DefaultMutableTreeNode("DB");
tree = new JTree(root);
// 设置布局并装入树
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
add(new JScrollPane(tree));
// 设置树节点的图标
setupTreeNodeIcons();
// 解除对树的锁定
treeLock.unlock();
}
/**
* 刷新树视图
*
* @param schemas
*/
public synchronized void refreshTree(List<SchemaTable> schemas) {
treeLock.lock();
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
root.removeAllChildren();
for (SchemaTable schemaTable : schemas) {
DefaultMutableTreeNode schemaNode = new DefaultMutableTreeNode(
schemaTable.getSchema());
for (String table : schemaTable.getTables()) {
schemaNode.add(new DefaultMutableTreeNode(table));
}
root.add(schemaNode);
}
model.reload();
}
讲解
上页中,setupComponents函数是Swing主线程执行的,而refreshTree函数是另外的线程执行(初始化时程序开辟一个线程执行,其后执行由用户操作决定)。 refreshTree函数必须要等setupComponents函数把tree初始化完毕后才能执行,而tree初始化的时间较长,可能在初始化的过程中执行refreshTree的线程就进入了,这就会造成问题。
程序使用了一个SwingComponentLock来解决这个问题,setupComponents一开始就创建SwingComponentLock的实例treeLock,然后执行refreshTree的线程以来就会进入treeLock的wait set,变成等待状态,不会往下执行,这是不管tree是否初始化完毕都不会出错;而setupComponents执行到底部会激活treeLock的wait set中等待的线程,这时再执行refreshTree剩下的代码就不会有任何问题,因为setupComponents执行完毕tree已经初始化好了。
让线程等待和激活线程的代码都在SwingComponentLock类中,这样的封装对复用很有好处,如果其它复杂组件如table也要依此办理直接创建SwingComponentLock类的实例就可以了。如果把wait和notifyAll写在TreeViewPanel类中就不会这样方便了。
总结
线程锁用于必须以固定顺序执行的多个线程的调度。
线程锁的思想是先锁定后序线程,然后让线序线程完成任务再接触对后序线程的锁定。
线程锁的写法和使用一定要理解记忆下来。
本文内容
本文将从一个现实例子来实际说明线程调度方法wait,notify和notifyAll的使用。
工厂中任务的领受和执行
某工厂执行这样的机制:当生产任务下达到车间时会统一放在一个地方,由工人们来取活。
工人取活如此执行:一个工人手头只能有一个活,如果没做完不能做下一个,如果做完了则可以到公共的地方去取一个;如果没有活可取则闲着直到来活为止。
本文就是讲述怎样使用线程的调度三方法wait,notify和notifyAll来实现这一现实活动的。
任务类Task-它用来实现一个”活”,其中关键的成员是完成需消耗的工时数manHour和已经完成的工时数completed
public class Task implements Comparable {
private String id;
private String name;
// 完成需消耗的工时数
private int manHour;
// 已经完成的工时数
private int completed;
// 优先级
private int priority;
// 接受任务者
private Worker worker;
public Task(String name, int manHour) {
this(name, manHour, 0);
}
public Task(String name, int manHour, int priority) {
id = IdUtil.generateId();
this.name = name;
this.manHour = manHour;
this.priority = priority;
this.completed = 0;
}
// 任务是否完成
public boolean isCompleted() {
return completed >= manHour;
}
// 添加完成度
public void addCompleted(int n) {
completed += n;
if (isCompleted()) {
completed = manHour;
if (worker != null) {
System.out.println("任务"+this+"处理完毕!");
}
}
}
public int compareTo(Object obj) {
Task another = (Task) obj;
return (another.priority) - this.priority;
}
public String toString() {
return "任务名:" + name + " 工人名:" + worker.getName() + " 完成度:" + completed
* 100 / manHour + "%";
}
public String getCompletedRatio() {
return " 完成度:" + completed * 100 / manHour + "%";
}
...getter/setter方法省略..
}
任务库类TaskLibrary
这个类对应现实中的取活的地方,每个活Task放在这个类的成员tasks中,有两个方法来添加单个任务和多个任务,还有一个fetchTask方法来供工人领受任务.
public class TaskLibrary {
private List<Task> tasks;
public TaskLibrary() {
tasks = new LinkedList<Task>();
}
// 添加单个任务
public synchronized void addTask(Task task) {
tasks.add(task);
notifyAll();
}
// 添加多个任务
public synchronized void addTasks(List<Task> moreTasks) {
tasks.addAll(moreTasks);
notifyAll();
}
public int getTaskSize() {
return tasks.size();
}
// 工人领受任务
public synchronized Task fetchTask(Worker worker) {
while (tasks.size() == 0) {
try {
System.out.println("任务告罄");
System.out.println("工人:" + worker.getName() + "进入闲置状态");
wait();
} catch (InterruptedException ex1) {
ex1.printStackTrace();
}
}
Task task = tasks.get(0);
System.out.println("工人:" + worker.getName() + "取得任务:" + task.getName());
tasks.remove(task);
return task;
}
}
工人类Worker
public class Worker implements Runnable {
private String id;
private String name;
private Task currTask;
private TaskLibrary taskLibrary;
// 工作速度
private int speed;
public Worker(String name, int speed, TaskLibrary taskLibrary) {
id = IdUtil.generateId();
this.currTask = null;
this.name = name;
this.speed = speed;
this.taskLibrary = taskLibrary;
doWork();
}
// 开始干活
public void doWork() {
Thread thread = new Thread(this);
thread.start();
}
// 真正干活
public void run() {
while (true) {
if (currTask == null || currTask.isCompleted()) {
currTask = taskLibrary.fetchTask(this);
currTask.setWorker(this);
}
try {
Thread.sleep(1000);
System.out.println("正在处理的任务" + currTask + " 完成度"
+ currTask.getCompletedRatio() + "个.");
currTask.addCompleted(speed);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
。。。
}
运行代码
TaskLibrary taskLibrary=new TaskLibrary();
taskLibrary.addTask(new Task("培训",8));
List<Task> moreTasks=new LinkedList<Task>();
moreTasks.add(new Task("锻造",4));
moreTasks.add(new Task("打磨",5));
moreTasks.add(new Task("车阶梯",6));
moreTasks.add(new Task("热处理",7));
moreTasks.add(new Task("去皮",8));
moreTasks.add(new Task("镗孔",60));
moreTasks.add(new Task("钻孔",10));
moreTasks.add(new Task("拉槽",11));
taskLibrary.addTasks(moreTasks);
Worker worker01=new Worker("王进喜",1,taskLibrary);
Worker worker02=new Worker("时传详",2,taskLibrary);
Worker worker03=new Worker("张秉贵",3,taskLibrary);
Worker worker04=new Worker("徐虎",3,taskLibrary);
taskLibrary.addTask(new Task("铸造",8));
sleep(1);
taskLibrary.addTask(new Task("校验",9));
sleep(2);
taskLibrary.addTask(new Task("内务",10));
sleep(3);
运行情况分析
一开始先初始化任务库,然后进行给任务库中添加任务,初始化工人实例时会把任务库实例的地址传入,工人实例初始化完毕后会调用doWork函数去任务库取任务开始做,这会进入TaskLibrary类的fetchTask函数,这时如果没有则会让工人等待,有则把第一个任务给他,然后周而复始进行这一过程.
运行结果示例
工人:王进喜取得任务:培训 工人:时传详取得任务:锻造 工人:张秉贵取得任务:打磨 工人:徐虎取得任务:车阶梯 正在处理的任务任务名:培训 工人名:王进喜 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:锻造 工人名:时传详 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:打磨 工人名:张秉贵 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:车阶梯 工人名:徐虎 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:培训 工人名:王进喜 完成度:12% 完成度 完成度:12%个. 正在处理的任务任务名:锻造 工人名:时传详 完成度:50% 完成度 完成度:50%个. 任务任务名:锻造 工人名:时传详 完成度:100%处理完毕! 工人:时传详取得任务:热处理 正在处理的任务任务名:打磨 工人名:张秉贵 完成度:60% 完成度 完成度:60%个. 任务任务名:打磨 工人名:张秉贵 完成度:100%处理完毕! 正在处理的任务任务名:车阶梯 工人名:徐虎 完成度:50% 完成度 完成度:50%个. 任务任务名:车阶梯 工人名:徐虎 完成度:100%处理完毕! 工人:徐虎取得任务:去皮 工人:张秉贵取得任务:镗孔 正在处理的任务任务名:培训 工人名:王进喜 完成度:25% 完成度 完成度:25%个. 正在处理的任务任务名:热处理 工人名:时传详 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:去皮 工人名:徐虎 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:镗孔 工人名:张秉贵 完成度:0% 完成度 完成度:0%个. 正在处理的任务任务名:培训 工人名:王进喜 完成度:37% 完成度 完成度:37%个. 正在处理的任务任务名:热处理 工人名:时传详 完成度:28% 完成度 完成度:28%个. 正在处理的任务任务名:去皮 工人名:徐虎 完成度:37% 完成度 完成度:37%个. 正在处理的任务任务名:镗孔 工人名:张秉贵 完成度:5% 完成度 完成度:5%个. 正在处理的任务任务名:培训 工人名:王进喜 完成度:50% 完成度 完成度:50%个. 正在处理的任务任务名:热处理 工人名:时传详 完成度:57% 完成度 完成度:57%个. 正在处理的任务任务名:去皮 工人名:徐虎 完成度:75% 完成度 完成度:75%个. 任务任务名:去皮 工人名:徐虎 完成度:100%处理完毕! 工人:徐虎取得任务:钻孔
本文内容
Java进行线程调度的方法.
如何让线程进入等待.
wait set概念的理解.
如何激活等待状态的线程.
进行线程协调的方法
从上一章《线程的互斥》中我们知道,当方法体或代码块被synchronized方法修饰时,有一个线程在执行这部分代码时,其它线程无法同时执行这部分代码。但如果我们想进行更高效的处理如主动调整线程而不是让线程被动等待和盲目竞争时该怎么处理呢?
在Java中,有三个方法可以对线程进行主动高效的控制,它们是wait,notify和notifyAll。
wait是主动让线程等待的方法,而notify和notifyAll则是激活等待中的线程的方法。他们都是Object类的方法,也就是说任何对象都可以使用这三个方法。
Wait Set-线程的休息室
在学习使用wait,notify和notifyAll这三个方法之前,我们可以先理解一下Wait Set的概念,它是一个在某实例执行wait方法时,停止操作的线程的集合,类似于线程的休息室,每个实例都拥有这样一个休息室。
Wait方法是用来把线程请入这个休息室的,而notify和notifyAll这两个方法是用来将进入休息室休息的线程激活的。
wait Set是一个虚拟的概念,它既不是实例的字段,也不是可以获取在实例上wait中线程的列表的方法.它只是用来帮助我们理解线程的等待和激活的。
Wait方法,将线程放入Wait Set
使用Wait方法时,线程即进入Wait set。如线程在执行代码时遇到这样的语句:xxObj.wait();则目前的线程会暂时停止运行,进入实例xxObj的wait Set.
当线程进入Wait Set时,即释放了对该实例的锁定.也就是说,即使是被synchronized修饰的方法和代码块,当第一个线程进入实例的wait Set等待后,其它线程就可以再进入这部分代码了.
wait()前如果不写某对象表示其前面的对象是this, wait()=this.wait();
notify方法-从wait set中激活一个线程
使用notify方法时,程序会从处于等待的实例的休息室中激活一个线程.代码如下:
xxObj.notify();程序将从xxObj的wait set中挑出一个激活.这个线程即准备退出wait set.当当前线程释放对xxObj的锁定后,这个线程即获取这个锁定,从上次的停止点-执行wait的地方开始运行。
线程必须有调用的实例的锁定,才能执行notify方法.
Wait set中处于等待状态的线程有多个,具体激活那一个依环境和系统而变,事先无法辩明.我们大致可理解为随机挑选了一个.
notifyAll方法-激活wait set中所有等待的线程
当执行notifyAll方法时,实例中所有处于等待状态的线程都会被激活.代码为:
xxObj.notifyAll();执行此代码后xxObj的wait set中所有处于等待中的线程都会被激活.但具体那个线程获取执行wait方法时释放的锁定要靠竞争,最终只能有一个线程获得锁定,其它的线程只能继续回去等待.
notifyAll与notify的差异在于notifyAll激活所有wait set中等待的线程,而notify只激活其中的一个.
该使用notify还是notifyAll
建议:
1) 选择notifyAll比notify稳当安全,如果notify处理得不好,程序会有隐患.
2) 选择notifyAll比notify可靠,是大多数程序的首选.
3) 当你对代码已经很清楚,对线程理解也很透彻时,你可以选择使用notify,发挥其处理速度高的优势.
当前线程必须持有欲调用实例的锁定,才能调用wait,notify和notifyAll这三个方法.
如果代码是xxObj.notifyAll(或wait, notify)(),则这行代码必须处于synchronized(xxObj){…}代码块中.
如果代码是this.notifyAll(或wait, notify)(),则这行代码必须处于synchronized修饰的方法中.
前面说过, notifyAll和notify会激活线程去获得进入wait时释放的锁定,但这个锁定要等刚才执行notifyAll或notify方法的线程释放这个锁定才能获取.
总结
1) wait,notify和notifyAll都是java.lang.Object的方法,它们用来对线程进行调度.
2) obj.wait()是把执行这句的线程放入obj的wait set中.
3) obj.notify()是从wait set中唤醒一个线程,这个线程在当前线程释放对obj的锁定后即获取这个锁定.
4) obj.notifyAll()是唤醒所有在obj的wait set中的线程,这批线程在当前线程释放obj的锁定后去竞争这个锁定,最终只有一个能获得,其它的又重新返回wait set等待下一次激活.
5) 执行这个wait,notify和notifyAll这三个方法前,当前线程(即执行obj.wait(), obj.notify()和obj.notifyAll()代码的线程)必须持有obj的锁定.
本文内容
同步不是改善程序安全性的灵丹妙药。
发生死锁的两种情况和解决方法。
同步不是改善程序安全性的灵丹妙药
从《线程的同步》一节中我们可以知道,synchronized能保证只有一个线程进入同步方法或同步块,但为了安全性盲目给多线程程序加上synchronized关键字并不是问题解决之道,这不但会降低程序的效率;还有可能带来严重的问题-死锁。
死锁发生在两个或以上的线程在等待对象锁被释放,但程序的环境却让lock无法释放时。下面我们将看到两种类型的死锁例子。
某线程不退出同步函数造成的死锁
public class PaintBoard extends Thread{
private boolean flag=true;
public void paint(){
System.out.println("模拟绘画");
}
public synchronized void run(){
while(flag){
try{
paint();
Thread.sleep(1000);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
public synchronized void stopDraw(){
flag=false;
System.out.println("禁止绘画");
}
public static void main(String[] args){
PaintBoard paintBoard=new PaintBoard();
paintBoard.start();
new StopThread(paintBoard);
}
}
public class StopThread implements Runnable{
private PaintBoard paintBoard;
public StopThread(PaintBoard paintBoard){
this.paintBoard=paintBoard;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
System.out.println("试图停止绘画过程");
paintBoard.stopDraw();
System.out.println("停止绘画过程完成");
}
}
}
问题的发生和解决
刚才的死锁原因是run()函数中有一个无限循环,一个线程进入后会在其中往复操作,这使它永远不会放弃对this的锁定,结果导致其它线程无法获得this的锁定而进入stopDraw函数。
我们把修饰run函数的synchronized取消就能解决问题。 run函数中不会改变任何量,这种函数是不该加上synchronized的。
两个线程争抢资源造成的死锁.
public class Desk{
private Object fork=new Object();
private Object knife=new Object();
public void eatForLeft(){
synchronized(fork){
System.out.println("左撇子拿起叉");
sleep(1);
synchronized(knife){
System.out.println("左撇子拿起刀");
System.out.println("左撇子开始吃饭");
}
}
}
public void eatForRight(){
synchronized(knife){
System.out.println("右撇子拿起刀");
sleep(1);
synchronized(fork){
System.out.println("右撇子拿起叉");
System.out.println("右撇子开始吃饭");
}
}
}
private void sleep(int second){
try{
Thread.sleep(second*1000);
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
Desk desk=new Desk();
new LeftHand(desk);
new RightHand(desk);
}
}
public class LeftHand implements Runnable{
private Desk desk;
public LeftHand(Desk desk){
this.desk=desk;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
desk.eatForLeft();
}
}
}
public class RightHand implements Runnable{
private Desk desk;
public RightHand(Desk desk){
this.desk=desk;
Thread th=new Thread(this);
th.start();
}
public void run(){
while(true){
desk.eatForRight();
}
}
}
问题的发生和解决
这部分程序中于两个线程都要获得两个对象的锁定才能执行实质性操作,但运行起来却发现其它线程持有了自己需要的另一个锁定,于是停在Wait Set中等待对方释放这个锁定,结果造成了死锁。
解决这个问题的方法是保证锁对象的持有顺序,如果两个加上了同步的函数都是先刀后叉的形式则不会发生问题。
小结
同步不是改善程序安全性的灵丹妙药,盲目同步也会导致严重的问题-死锁.
某线程持续不退出同步函数会造成死锁.解决方法是去掉或更换不正确的同步。
两个线程都等待对方释放自己需要的资源也会造成死锁.这种情况的解决方法是确保同步锁对象的持有顺序。
多线程操作同一实例的问题
在多线程环境中,经常有两个以上线程操作同一实例的问题,无论是并行Parallel环境还是并发Concurrent环境,都有发生有多个线程修改同一变量的问题,如果这个变量是成员变量,多线程将会给程序带来破坏性的影响。请见以下代码。
资源库类
public class ResourceLib {
private long count1;
private long count2;
public ResourceLib(int count) {
this.count1 = count;
this.count2 = count;
}
/**
* 取回资源
* 加上synchronized才是线程安全
*
* @param count
*/
public void fetch(int count) {
count1 += count;
mockLongTimeProcess();
count2 += count;
checkTwoCount(count);
}
/**
* 送出资源
* 加上synchronized才是线程安全
*
* @param count
* @return
*/
public void send(int count) {
count1 -= count;
mockLongTimeProcess();
count2 -= count;
checkTwoCount(count);
}
/**
* 模拟一个耗时过程
*
*/
private void mockLongTimeProcess(){
try{
Thread.sleep(1000);
}
catch(Exception ex){
ex.printStackTrace();
}
}
private void checkTwoCount(int borrowCount) {
if (count1 != count2) {
System.out.println(count1 + "!= " + count2);
System.exit(0);
} else {
System.out.println(count1 + "==" + count2);
}
if (Math.abs(count1) > 10000000 || Math.abs(count2) > 10000000) {
count1 = 0;
count2 = 0;
}
}
public static void main(String[] args) {
ResourceLib lib = new ResourceLib(10000);
for (int i = 1; i < 20; i++) {
new Supplier(String.valueOf(i), i, lib);
}
for (int i = 1; i < 10; i++) {
new Comsumer(String.valueOf(i), i, lib);
}
}
}
取资源和给资源的两个线程
public class Comsumer implements Runnable{
private ResourceLib resourceLib;
private int count;
public Comsumer(String name,int count,ResourceLib resourceLib){
this.count=count;
this.resourceLib=resourceLib;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
resourceLib.send(count);
}
}
}
public class Supplier implements Runnable{
private ResourceLib resourceLib;
private int count;
public Supplier(String name,int count,ResourceLib resourceLib){
this.count=count;
this.resourceLib=resourceLib;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
resourceLib.fetch(count);
}
}
}
运行结果
在main函数中,程序启动了多个消费者线程和生产者线程,消费者线程在不断减少count1和count2;生产者线程在不断增加count1和count2,在单线程环境中,程序绝不会出现count1和count2不相等的情况,而多线程环境中,可能有一个线程在检查count1和count2时,其中一个已经被另一个线程所修改。
因此导致了两个值不相等的情况发生。
运行结果之一
10145!= 10001
10145!= 10003
10145!= 10006
10145!= 10010
10145!= 10015
10145!= 10021
10145!= 10028
10145!= 10036
10145!= 10045
10145!= 10055
10145!= 10066
另一个经典多线程实例:银行取款
package com.sitinspring.unsafebank;
public class Bank{
private int count;
public Bank(int count){
this.count=count;
}
public void withdraw(int money){
if(count>money){
mockLongTimeProcess();// 模拟耗时过程
count-=money;
System.out.println("提走"+money+" 现有"+count);
}
else{
System.out.println(" 现有数量"+count+"小于"+money+" 不能提取");
}
checkCount();
}
public void checkCount(){
if(count<0){
System.out.println(count + "< 0 ");
System.exit(0);
}
}
/**
* 模拟一个耗时过程
*
*/
private void mockLongTimeProcess(){
try{
Thread.sleep(1000);
}
catch(Exception ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
Bank bank=new Bank(1000);
for(int i=1;i<10;i++){
new Customer(i*i*i,bank);
}
}
}
客户类及讲述
public class Customer implements Runnable{
private Bank bank;
private int count;
public Customer(int count,Bank bank){
this.count=count;
this.bank=bank;
Thread thread=new Thread(this);
thread.start();
}
public void run(){
while(true){
bank.withdraw(count);
}
}
}
在单线程环境中,提款时银行的总数绝不会是负数,但在多线程环境中,有可能在一个线程A符合条件在进行耗时运算和网络数据传递时,另一个线程B已经把钱提走,总数已经发生变化,结果A线程再提款时总钱数已经减小了,因此致使银行总钱数小于零。
解决方法:在对成员变量进行修改的函数前加上synchronized关键字
synchronized方法又被成为”同步“方法。当一个方法加上关键字synchronized声明之后,就可以让一个线程操作这个方法。“让一个线程操作”并不是说只能让某一个特定的线程操作而已,而是指一次只能让一个线程执行,也就是说,在一个线程没有退出同步方法前,其它线程绝无可能进入这个同步方法和其它并列的同步方法,只能在外面排队等候。
一个实例的synchronized方法只能允许1次一个线程执行。但是非synchronized方法就没有这个限制,它可以供2个以上的线程执行。
修改后的线程安全的Bank类
public class Bank{
private int count;
public Bank(int count){
this.count=count;
}
public synchronized void withdraw(int money){
if(count>money){
mockLongTimeProcess();// 模拟耗时过程
count-=money;
System.out.println("提走"+money+" 现有"+count);
}
else{
System.out.println(" 现有数量"+count+"小于"+money+" 不能提取");
}
checkCount();
}
public void checkCount(){
if(count<0){
System.out.println(count + "< 0 ");
System.exit(0);
}
}
。。。、// 部分代码省略
}

修改后的线程安全的ResourceLib类
public class ResourceLib {
private long count1;
private long count2;
public synchronized void fetch(int count) {
count1 += count;
mockLongTimeProcess();
count2 += count;
checkTwoCount(count);
}
public synchronized void send(int count) {
count1 -= count;
mockLongTimeProcess();
count2 -= count;
checkTwoCount(count);
}
public void checkTwoCount(int borrowCount) {
if (count1 != count2) {
System.out.println(count1 + "!= " + count2);
System.exit(0);
} else {
System.out.println(count1 + "==" + count2);
}
if (Math.abs(count1) > 10000000 || Math.abs(count2) > 10000000) {
count1 = 0;
count2 = 0;
}
}
}
注:部分代码省略

执行之后
在一个执行synchronized方法的线程执行结束后,锁定即被释放, 其它不得其门而入的线程开始争抢锁定,一定会有一个线程获取锁定,没有抢到的线程只好再继续等候.
注意: 非静态的synchronized方法锁定的对象是实例,静态的synchronized方法锁定的对象是类对象。
同步块
以下同步方法可用右边的同步块代替:
public synchronized void fun(){
………
}
与左边同步方法对等的同步块:
public void fun(){
synchronized(this){
………
}
}
同步块和同步方法的比较
1)同步方法锁定的类的实例或类对象,同步块则可以换成任意实例,灵活性更高。
2)有时需要多个锁定而不是一个,如函数A和函数B需要锁定1,函数B和函数C需要锁定2,这时如果使用同步方法无疑会锁定A和C,造成程序效率的降低。这时最应该使用同步块。
什么时候该加同步synchronized
如果一个函数或代码块有可能被多个线程进入,而这个函数或代码块又修改了类的成员变量,则这个这个函数或代码块就应该加上同步synchronized。
如果一个函数或代码有可能被多个线程进入,而这个函数或代码块只是读取类的成员变量,则这个这个函数或代码块就不该加上同步synchronized。
单线程程序
一般来说,在没有线程的帮助下,程序在一个时间段只能执行一段代码,其它代码段只有在等待它完成后才能执行。该程序的处理流程从头到尾只有一条线,这样的程序我们称之为单线程程序(Single Thread Program)
典型的单线程程序:
public class SingleThreadProgram{
public static void main(String[] args){
for(int i=0;i<1000;i++){
System.out.print("SingleThreadProgram");
}
}
}
多线程程序
当程序由一个以上的线程所构成时,称此程序为多线程程序(Multithread Program),java从设计伊始就把程序的多线程能力列入了考虑范围。
典型的多线程程序有:
1)GUI应用程序,我们目前做的Swing桌面程序就属于此类。
2)较花费时间的I/O处理,一般来说,文件和网络的输入/输出处理比较花费时间,如果在这段无法进行其它处理,则程序性能会大打折扣,遇到这种情况首先要想到用多线程解决问题.
3)多连接网络处理。
并发(Concurrent)与并行(Parallel)
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent).
当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

多线程在并发和并行环境中的不同作用
在并发环境时,多线程不可能真正充分利用CPU,节约运行时间,它只是以”挂起->执行->挂起”的方式以很小的时间片分别运行各个线程,给用户以每个线程都在运行的错觉.在这种环境中,多线程程序真正改善的是系统的响应性能和程序的友好性.
在并行环境中, 一个时刻允许多个线程运行,这时多线程程序才真正充分利用了多CPU的处理能力, 节省了整体的运行时间.在这种环境中,多线程程序能体现出它的四大优势:充分利用CPU,节省时间,改善响应和增加程序的友好性.
PS:在多核时代来临后,开发多线程程序的能力更是每个程序员都该具备的.
创建多线程程序
创建多线程程序我们通常有两种方法:
1)让类继承java.lang.Thread,这种方法优势在于调用稍微方便,一般用于后台批处理程序的场合,但劣势是类无法再继承别的类。
2)让类实现接口java.lang.Runnable,这种方法调用时需要借助Thread的帮助,稍显麻烦,但优势在于对类继承体系没有影响,这是使用线程时最常用的方法。
两种方法的线程执行部分都在run()函数中,它们的效率没有差别。
多线程程序创建和启动示例
创建线程
// 继承Thread类
public class Thread1 extends Thread{
public void run(){
while(true){
System.out.println("<Thread1 extends Thread>");
}
}
}
// 实现Runnable接口
public class Thread2 implements Runnable{
public void run(){
while(true){
System.out.println("<Thread2 implements Runnable>");
}
}
}
启动线程
public class Main{
public static void main(String[] args){
// 启动线程1,Thread1直接继承自java.lang.Thread类
Thread1 th1=new Thread1();
th1.start();
// 启动线程2,thread2实现自java.lang.Runnable接口
Thread2 thread2=new Thread2();
Thread th2=new Thread(thread2);
th2.start();
while(true){
System.out.println("<Main Thread>");
}
}
}
概念解析Start和Run
public void run()
这个函数容纳线程启动后执行的代码块,线程启动起来,run函数中的代码会得到执行.
Thead.start()
这是启动一个线程的方法,调用了这个方法后,线程才会得到执行.
取得线程执行的结果
通过观察run函数的签名public void run()我们可以发现,它既没有输入参数,也没有返回值,那如何取得线程的返回值呢?一般来说我们有三种办法:
1)让线程修改公有变量,如某类的静态公有字段.这种方式古老而危险,最好不要采用.
2)轮询线程执行结果,线程执行的结果放在线程类的一个字段中,外界不断通过轮询去查看执行结果.这种方式会浪费很多时间,结果也不可靠,不建议采用.
3)回调方式,把调用方的指针通过线程类的构造函数传入线程类的一个字段中,当线程执行完取得结果后再通过这个字段反向调用调用方的函数.这是取得线程执行结果的最佳解决方案.
下面请看回调方式的实现.
Boss类
这个类用于启动Secretary线程去查找文件, findFile()是启动线程并查找的函数, giveBossResult(String file,String reult)是供Secretary类回调的函数.
public class Boss{
private String name;
public Boss(String name){
this.name=name;
}
public void giveBossResult(String file,String reult){
if(reult!=null){
System.out.println("文件"+file+"序列号等于:"+reult);
}
else{
System.out.println("无法找到文件"+file);
}
}
public void findFile(){
Map<String,String> files=new Hashtable<String,String>();
files.put("001", "员工花名册");
files.put("002", "企业收支");
files.put("003", "客户花名录");
files.put("004", "对手状况分析");
files.put("005", "当月收支");
files.put("006", "市场份额分析");
files.put("007", "大连酒店一览");
files.put("008", "娱乐场所名录");
files.put("009", "关系单位联系名录");
Secretary andy=new Secretary("Andy",this,"员工花名册",files);
Thread th1=new Thread(andy);
th1.start();
Secretary cindy=new Secretary("cindy",this,"上市情况分析",files);
Thread th2=new Thread(cindy);
th2.start();
}
public static void main(String[] args){
Boss boss=new Boss("Bill");
boss.findFile();
}
}
Secretary类
这个类是进行多线程查找文件的类,查找的结果通过回调方法告知Boss实例.
Boss实例,查找的文件名,查找的集合都通过Secretary类的构造函数传进来.
public class Secretary implements Runnable{
private String name;
private Boss boss;
private String file;
private Map<String,String> files;
public Secretary(String name,Boss boss,String file,Map<String,String> files){
this.name=name;
this.boss=boss;
this.file=file;
this.files=files;
}
public void run(){
for(Map.Entry<String,String> entry:files.entrySet()){
if(entry.getValue().equals(file)){
boss.giveBossResult(file,entry.getKey());
return;
}
}
boss.giveBossResult(file,null);
}
}
摘要: 一.Prop类(用来读取属性文件,单例)
package com.sitinspring.standardWeblogicJms;
import java.io.FileInputStream;
import java.util.Hashtable;
import java.util.Properties;
import ja...
阅读全文