代理模式UML类图
代理模式
1. 静态代理
-
-
-
-
- public interface IHello {
- public void sayHello();
- }
-
-
-
-
- public class Hello implements IHello
- {
- public void sayHello(){
- System.out.println("被代理的方法");
- }
- }
-
-
-
-
-
- public class StaticProxy implements IHello {
- IHello hello;
- public StaticProxy(IHello hello) {
- this.hello = hello;
- }
-
-
-
- public void sayHello() {
- System.out.println("在被代理的对象之前执行");
-
- hello.sayHello();
- System.out.println("在被代理的对象之前执行");
- }
- }
-
-
-
-
- public class Test {
- public static void main(String[] args) {
-
-
- IHello hello = new Hello();
-
- StaticProxy sp = new StaticProxy(hello);
-
- sp.sayHello();
- }
- }
/**
* 为被代理的类提供一个接口,是为了提高代理的通用性,凡是实现了该接口的类,都可以被代理
* 这里其实就是运用了java面向对象的多态性
*/
public interface IHello {
public void sayHello();
}
/**
* 被代理的类,最根本的想法就是想用另外一个类来代理这个类,给这个类添加一些额外的东西
* 我们只需要创建另外一个类引用这个类就行了
*/
public class Hello implements IHello
{
public void sayHello(){
System.out.println("被代理的方法");
}
}
/**
* 静态代理类,其实就是(被代理类的)接口的另外一种实现,
* 用来代替原来的被代理类
* @author qiuxy
*/
public class StaticProxy implements IHello {
IHello hello;
public StaticProxy(IHello hello) {
this.hello = hello;
}
/**
* 重新实现了sayHello()方法,这种实现其实就是在被代理类实现该方法中添加一些额外的东西, 以实现代理的作用
*/
public void sayHello() {
System.out.println("在被代理的对象之前执行");
// 被代理的对象执行方法
hello.sayHello();
System.out.println("在被代理的对象之前执行");
}
}
/**
* 测试类
* @author qiuxy
*/
public class Test {
public static void main(String[] args) {
//产生一个被代理对象,只要实现了Ihello接口的对象,都可以成为被代理对象
//这里就是利用接口的好处,但这个也有局限性,就是只限于某种接口
IHello hello = new Hello();
//产生一个代理对象
StaticProxy sp = new StaticProxy(hello);
//执行代理对象的sayHello()方法,这个方法在被代理的方法前后添加了其他代码
sp.sayHello();
}
}
2. 动态代理
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
-
-
-
-
-
- public class MyProxyHandler implements InvocationHandler {
-
- Object delegate;
-
- public MyProxyHandler(Object delegate) {
- this.delegate = delegate;
- }
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println("我在被代理的方法之前执行");
-
- method.invoke(delegate, args);
- System.out.println("我在被代理的方法之后执行");
- return null;
- }
- }
- import java.lang.reflect.Proxy;
-
-
-
- public class Test
- {
- public static void main(String[] args)
- {
- Hello hello = new Hello();
-
- MyProxyHandler mph = new MyProxyHandler(hello);
-
-
- IHello myProxy = (IHello)Proxy.newProxyInstance(hello.getClass().getClassLoader() , hello.getClass().getInterfaces(), mph);
-
- myProxy.sayHello();
- }
-
- }
代理模式
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式一般涉及到的角色有:
抽象角色:声明真实对象和代理对象的共同接口;
代理角色:代理对象角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。
真实角色:代理角色所代表的真实对象,是我们最终要引用的对象。(参见文献1)
以下以《Java与模式》中的示例为例:
抽象角色:
abstract public class Subject
{
abstract public void request();
}
真实角色:实现了Subject的request()方法。
public class RealSubject extends Subject
{
public RealSubject()
{
}
public void request()
{
System.out.println("From real subject.");
}
}
代理角色:
public class ProxySubject extends Subject
{
private RealSubject realSubject; //以真实角色作为代理角色的属性
public ProxySubject()
{
}
public void request() //该方法封装了真实对象的request方法
{
preRequest();
if( realSubject == null )
{
realSubject = new RealSubject();
}
realSubject.request(); //此处执行真实对象的request方法
postRequest();
}
private void preRequest()
{
//something you want to do before requesting
}
private void postRequest()
{
//something you want to do after requesting
}
}
客户端调用:
Subject sub=new ProxySubject();
Sub.request();
由以上代码可以看出,客户实际需要调用的是RealSubject类的request()方法,现在用ProxySubject来代理RealSubject类,同样达到目的,同时还封装了其他方法(preRequest(),postRequest()),可以处理一些其他问题。
另外,如果要按照上述的方法使用代理模式,那么真实角色必须是事先已经存在的,并将其作为代理对象的内部属性。但是实际使用时,一个真实角色必须对应一个代理角色,如果大量使用会导致类的急剧膨胀;此外,如果事先并不知道真实角色,该如何使用代理呢?这个问题可以通过Java的动态代理类来解决。
2.动态代理类
Java动态代理类位于Java.lang.reflect包下,一般主要涉及到以下两个类:
(1). Interface InvocationHandler:该接口中仅定义了一个方法Object:invoke(Object obj,Method method, J2EEjava语言JDK1.4APIjavalangObject.html">Object[] args)。在实际使用时,第一个参数obj一般是指代理类,method是被代理的方法,如上例中的request(),args为该方法的参数数组。这个抽象方法在代理类中动态实现。
(2).Proxy:该类即为动态代理类,作用类似于上例中的ProxySubject,其中主要包含以下内容:
Protected Proxy(InvocationHandler h):构造函数,估计用于给内部的h赋值。
Static Class getProxyClass (ClassLoader loader, Class[] interfaces):获得一个代理类,其中loader是类装载器,interfaces是真实类所拥有的全部接口的数组。
Static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h):返回代理类的一个实例,返回后的代理类可以当作被代理类使用(可使用被代理类的在Subject接口中声明过的方法)。
所谓Dynamic Proxy是这样一种class:它是在运行时生成的class,在生成它时你必须提供一组interface给它,然后该class就宣称它实现了这些interface。你当然可以把该class的实例当作这些interface中的任何一个来用。当然啦,这个Dynamic Proxy其实就是一个Proxy,它不会替你作实质性的工作,在生成它的实例时你必须提供一个handler,由它接管实际的工作。(参见文献3)
在使用动态代理类时,我们必须实现InvocationHandler接口,以第一节中的示例为例:
抽象角色(之前是抽象类,此处应改为接口):
public interface Subject
{
abstract public void request();
}
具体角色RealSubject:同上;
代理角色:
import java.lang.reflect.Method;
import java.lang.reflect.InvocationHandler;
public class DynamicSubject implements InvocationHandler {
private Object sub;
public DynamicSubject() {
}
public DynamicSubject(Object obj) {
sub = obj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before calling " + method);
method.invoke(sub,args);
System.out.println("after calling " + method);
return null;
}
}
该代理类的内部属性为Object类,实际使用时通过该类的构造函数DynamicSubject(Object obj)对其赋值;此外,在该类还实现了invoke方法,该方法中的
method.invoke(sub,args);
其实就是调用被代理对象的将要被执行的方法,方法参数sub是实际的被代理对象,args为执行被代理对象相应操作所需的参数。通过动态代理类,我们可以在调用之前或之后执行一些相关操作。
客户端:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Client
{
static public void main(String[] args) throws Throwable
{
RealSubject rs = new RealSubject(); //在这里指定被代理类
InvocationHandler ds = new DynamicSubject(rs); //初始化代理类
Class cls = rs.getClass();
//以下是分解步骤
/*
Class c = Proxy.getProxyClass(cls.getClassLoader(),cls.getInterfaces()) ;
Constructor ct=c.getConstructor(new Class[]{InvocationHandler.class});
Subject subject =(Subject) ct.newInstance(new Object[]{ds});
*/
//以下是一次性生成
Subject subject = (Subject) Proxy.newProxyInstance(cls.getClassLoader(),
cls.getInterfaces(),ds );
subject.request();
}
通过这种方式,被代理的对象(RealSubject)可以在运行时动态改变,需要控制的接口(Subject接口)可以在运行时改变,控制的方式(DynamicSubject类)也可以动态改变,从而实现了非常灵活的动态代理关系(参见文献2)。
代理和AOP一.起源
有时,我们在写一些功能方法的时候,需要加上特定的功能.比如说在方法调用的前后加上日志的操作,或者是事务的开启与关闭.对于一个方法来说,很简单,只要在需要的地方增加一些代码就OK.但是如果有很多方法都需要增加这种特定的操作呢?
没错,将这些特定的代码抽象出来,并且提供一个接口供调用者使用:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("记录日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 记录日志的操作
System.out.println("记录日志...");
}
}
那么在其他的方法中,就可以使用RecordLog.recordLog()方法了.但你会发现,这仍不是个好的设计,因为在我们的代码里到处充塞着
RecordLog.recordLog()这样的语句:
- public class A
- {
- public void a()
- {
-
- RecordLog.recordLog();
-
-
- }
- }
- public class B
- {
- public void b()
- {
-
- RecordLog.recordLog();
-
-
- }
- }
- ......
public class A
{
public void a()
{
// 1.记录日志
RecordLog.recordLog();
// 2.类A的方法a的操作
}
}
public class B
{
public void b()
{
// 1.记录日志
RecordLog.recordLog();
// 2.类B的方法b的操作
}
}
......
这样虽然会在一定程度减轻代码量,但你会发现,仍有大量的地方有重复的代码出现!这绝对不是优雅的写法!
为了避免这种吃力不讨好的现象发生,“代理”粉墨登场了.
二.传统的代理.静态的代理.面向接口编程
同样为了实现以上的功能,我们在设计的时候做了个小小的改动.
2.1 抽象出来的记录日志的类:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("记录日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 记录日志的操作
System.out.println("记录日志...");
}
}
2.2 设计了一个接口:
- public interface PeopleInfo
- {
- public void getInfo();
- }
public interface PeopleInfo
{
public void getInfo();
}
该接口只提供了待实现的方法.
2.3 实现该接口的类:
- public class PeopleInfoImpl implements PeopleInfo
- {
- private String name;
-
- private int age;
-
-
- public PeopleInfoImpl(String name, int age)
- {
- this.name = name;
- this.age = age;
- }
-
- public void getInfo()
- {
-
- System.out.println("我是" + name + ",今年" + age + "岁了.");
- }
- }
public class PeopleInfoImpl implements PeopleInfo
{
private String name;
private int age;
// 构造函数
public PeopleInfoImpl(String name, int age)
{
this.name = name;
this.age = age;
}
public void getInfo()
{
// 方法的具体实现
System.out.println("我是" + name + ",今年" + age + "岁了.");
}
}
这个类仅仅是实现了PeopleInfo接口而已.平平实实.好了.关键的地方来了.就在下面!
2.4 创建一个代理类:
- public class PeopleInfoProxy implements PeopleInfo
- {
-
- private PeopleInfo peopleInfo;
-
-
- public RecordLogProxy(PeopleInfo peopleInfo)
- {
- this.peopleInfo = peopleInfo;
- }
-
-
- public void record()
- {
-
- RecordLog.recordLog();
-
-
- peopleInfo.getInfo();
- }
- }
public class PeopleInfoProxy implements PeopleInfo
{
// 接口的引用
private PeopleInfo peopleInfo;
// 构造函数 .针对接口编程,而非针对具体类
public RecordLogProxy(PeopleInfo peopleInfo)
{
this.peopleInfo = peopleInfo;
}
// 实现接口中的方法
public void record()
{
// 1.记录日志
RecordLog.recordLog();
// 2.方法的具体实现
peopleInfo.getInfo();
}
}
这个是类是一个代理类,它同样实现了PeopleInfo接口.比较特殊的地方在于这个类中有一个接口的引用private PeopleInfo peopleInfo;通过
这个引用,可以调用实现了该接口的类的实例的方法!
而不管是谁,只要实现了PeopleInfo这个接口,都可以被这个引用所引用.也就是说,这个代理类可以代理任何实现了接口的PeopleInfo的类.具体
如何实现,请看下面:
2.5 Main
- public class Main
- {
- public static void main(String[] args)
- {
-
- PeopleInfoImpl peopleInfoImpl = new PeopleInfoImpl("Rock",24);
-
-
- PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy(PeopleInfoImpl);
-
-
- peopleInfoProxy.getInfo();
- }
- }
public class Main
{
public static void main(String[] args)
{
// new了一个对象
PeopleInfoImpl peopleInfoImpl = new PeopleInfoImpl("Rock",24);
// 代理该对象
PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy(PeopleInfoImpl);
// 调用代理类的方法.输入的是目标类(即被代理类的方法的实现)
peopleInfoProxy.getInfo();
}
}
这样,输出的结果将是:
记录日志...
我是Rock,今年24岁了.
由这个例子可见,这么做了之后不但省略了很多代码,而且不必要知道具体是由哪个类来执行方法.只需实现了特定的接口,代理类就可以打点一切
了.这就是面向接口的威力!HOHO...
三.动态代理.Java的动态机制.
面向接口的编程确实让我们省了不少心,只要实现一个特定的接口,就可以处理很多的相关的类了.
不过,这总是要实现一个“特定”的接口,如果有很多很多这样的接口需要被实现...也是件比较麻烦的事情.
好在,JDK1.3起,就有了动态代理机制,主要有以下两个类和一个接口:
- java.lang.reflect.Proxy
- java.lang.reflect.Method
- java.lang.reflect.InvocationHandler
java.lang.reflect.Proxy
java.lang.reflect.Method
java.lang.reflect.InvocationHandler
所谓动态代理,就是JVM在内存中动态的构造代理类.说的真是玄,还是看看代码吧.
3.1 抽象出来的记录日志的类:
- public class RecordLog
- {
- public static void recordLog()
- {
-
- System.out.println("记录日志...");
- }
- }
public class RecordLog
{
public static void recordLog()
{
// 记录日志的操作
System.out.println("记录日志...");
}
}
3.2 设计了一个接口:
- public interface PeopleInfo
- {
- public void getInfo();
- }
public interface PeopleInfo
{
public void getInfo();
}
该接口只提供了待实现的方法.
3.3 实现该接口的类:
- public class PeopleInfoImpl implements PeopleInfo
- {
- private String name;
-
- private int age;
-
-
- public PeopleInfoImpl(String name, int age)
- {
- this.name = name;
- this.age = age;
- }
-
- public void getInfo()
- {
-
- System.out.println("我是" + name + ",今年" + age + "岁了.");
- }
- }
public class PeopleInfoImpl implements PeopleInfo
{
private String name;
private int age;
// 构造函数
public PeopleInfoImpl(String name, int age)
{
this.name = name;
this.age = age;
}
public void getInfo()
{
// 方法的具体实现
System.out.println("我是" + name + ",今年" + age + "岁了.");
}
}
一直到这里,都和第二节没区别,好嘛,下面就是关键哟.
3.4 创建一个代理类,实现了接口InvocationHandler:
- public class PeopleInfoProxy implements InvocationHandler
- {
-
- private Object target;
-
-
- public Object bind(Object targer)
- {
- this.target = target;
-
-
- return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
- }
-
-
-
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
- {
- Object result = null;
-
-
- RecordLog.recordLog();
-
-
- result = method.invoke(target, args);
-
-
-
-
- return result;
- }
- }
public class PeopleInfoProxy implements InvocationHandler
{
// 定义需要被代理的目标对象
private Object target;
// 将目标对象与代理对象绑定
public Object bind(Object targer)
{
this.target = target;
// 调用Proxy的newProxyInstance方法产生代理类实例
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
// 实现接口InvocationHandler的invoke方法
// 该方法将在目标类的被代理方法被调用之前,自动触发
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Object result = null;
// 1.目标类的被代理方法被调用之前,可以做的操作
RecordLog.recordLog();
// 2.方法的具体实现
result = method.invoke(target, args);
// 3.还可以在方法调用之后加上的操作
// 自己补充
return result;
}
}
关于Proxy, Method, InvocationHandler的具体说明,请参见JDK_API.
只对代码中关键部分做些解释说明:
3.4.1
- Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
表示生成目标类的代理类,传入的参数有目标类的ClassLoader, 目标类的接口列表, 和实现了接口InvocationHandler的代理类.
这样,bind方法就得到了目标类的代理类.
3.4.2
- method.invoke(target, args);
method.invoke(target, args);
目标类的被代理方法在被代用前,会自动调用InvocationHandler接口的invoke方法.
在该方法中,我们可以对目标类的被代理方法进行加强,比如说在其前后加上事务的开启和关闭等等.
这段代码才是真正调用目标类的被代理方法.
就这样,我们不用实现其他任何的接口,理论上就能代理所有类了.调用的方式如下:
3.5 Main:
- public class Main
- {
- public static void main(String[] args)
- {
- PeopleInfo peopleInfo = null;
-
- PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy();
-
-
- Object obj = peopleInfoProxy.bind(new PeopleInfoImpl("Rock", 24));
-
- if(obj instanceof PeopleInfo)
- {
- peopleInfo = (PeopleInfo)obj;
- }
- peopleInfo.getInfo();
- }
- }
public class Main
{
public static void main(String[] args)
{
PeopleInfo peopleInfo = null;
PeopleInfoProxy peopleInfoProxy = new PeopleInfoProxy();
// 传入的参数是目标类实例,生成代理类实例,类型为Object
Object obj = peopleInfoProxy.bind(new PeopleInfoImpl("Rock", 24));
if(obj instanceof PeopleInfo)
{
peopleInfo = (PeopleInfo)obj;
}
peopleInfo.getInfo();
}
}
执行结果和上一节一样.
这就是使用Java动态代理机制的基本概述.而下一节,将要把Dynamic Proxy(动态代理)和AOP联系起来.
四.AOP概述.Spring的AOP.
AOP(Aspect Oriented Programming)面向切面编程.是一种比较新颖的设计思想.是对OOP(Object Orientd Programming)面向对象编程的一种有益的补充.
4.1 OOP和AOP
OOP对业务处理过程中的实体及其属性和行为进行了抽象封装,以获得更加清晰高效果的逻辑划分.研究的是一种“静态的”领域.
AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段.研究的是一种“动态的”领域.
举例说,某个网站(5016?)用户User类又可分为好几种,区长,管理员,斑竹和普通水友.我们把这些会员的特性进行提取进行封装,这是OOP.
而某一天,区长开会了,召集斑竹等级以上的会员参与,这样,普通水友就不能访问相关资源.
我们怎么做到让普通水友访问不了资源,而斑竹等级以上会员可以访问呢.
权限控制.对,权限.当水友们进行操作的时候,我们给他的身份进行权限的判断.
请注意,当且仅需水友门执行了操作的时候,我们才需要进行权限判断,也就是说,这是发生在一个业务处理的过程中的一个片面.
我们对这一个片面进行编程,就是AOP!
我这样,你应该能理解吧.
4.2 AOP的基本术语
4.2.1 切面Aspect
业务处理过程中的一个截面.就像权限检查.
通过切面,可以将不同层面的问题隔离开:浏览帖子和权限检查两者互不相干.
这样一来,也就降低了耦合性,我们可以把注意力集中到各自的领域中.
上两节的例子中,getInfo()和recordLog()就是两个领域的方法,应该处于切面的不同端.哎呀,不知不觉间,我们就用了AOP.呵呵...
4.2.2 连接点JoinPoint
程序运行中的某个阶段点.如某个方法的调用,或者异常的抛出等.
在前面,我们总是在getInfo()的前后加了recordLog()等操作,这个调用getInfo()就是连接点.
4.2.3 处理逻辑Advice
在某个连接点采取的逻辑.
这里的逻辑有三种:
I. Around 在连接点前后插入预处理和后处理过程.
II. Before 在连接点前插入预处理过程.
III.Throw 在连接点抛出异常的时候进行异常处理.
4.2.4 切点PointCut
一系列连接点的集合,它指明处理逻辑Advice将在何在被触发.
4.3 Spring中的AOP
Spring提供内置AOP支持.是基于动态AOP机制的实现.
所谓动态AOP,其实就是动态Proxy模式,在目标对象的方法前后插入相应的代码.(比如说在getInfo()前后插入的recordLog())
Spring AOP中的动态Proxy模式,是基于Java Dynamic Proxy(面向Interface)和CGLib(面向Class)的实现.
为什么要分面向接口和面向类呢.
还记得我们在生成代理类的代码吗:
- Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
这里面的参数不许为空,也就是说:obj.getClass().getInterfaces()必有值,即目标类一定要实现某个接口.
有了这些,JVM在内存中就动态的构造出代理出来.
而没有实现任何接口的类,就必须使用CGLib来动态构造代理类.值得一提的是,CGLib构造的代理类是目标类的一个子类.
4.4 相关工程简解
Spring的相关知识不应该在这里讲,难度系数过大.这里只给个简单例子.供参考.
4.4.1 准备工作
打开Eclipse.新建Java工程,取名为AOP_Proxy.完成.
复制spring-2.0.jar.粘贴到AOP_Proxy下.
右击AOP_Proxy-->属性-->Java构建路径-->库-->添加JAR-->找spring-2.0.jar-->添加确定.
复制commons-logging.jar.粘贴到AOP_Proxy下.
右击AOP_Proxy-->属性-->Java构建路径-->库-->添加JAR-->找commons-logging.jar-->添加确定.
4.4.2 写代码
代码略.配置文件略.
4.4.3 导入工程的步骤
新建工程AOP_Pro
/Files/qileilove/AOP_Proxy.rarxy-->完成-->右击AOP_Proxy-->导入-->常规-->文件系统-->找到项目文件,导入完成.
两个jar包和项目文件(项目文件需要先解压).
摘要: 不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的。JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于ja...
阅读全文
我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。
线程(Thread)是一份独立运行的程序,有自己专用的运行栈。
线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。
线程同步的真实意思和字面意思恰好相反
。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。 因此,关于线程同步,需要牢牢记住的第一点是:
线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。 关于线程同步,需要牢牢记住的第二点是 “
共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
关于线程同步,需要牢牢记住的第三点是,
只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
关于线程同步,需要牢牢记住的第四点是:
多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。 为了加深理解,下面举几个例子。
有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
(1)到市场上去,寻找并购买有潜力的样品。
(2)回到公司,写报告。
这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。
下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。
这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。
下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。
如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。
同步锁 前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。
线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。)
线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里?
当然是加在共享资源上了。反应快的读者一定会抢先回答。
没错,
如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。
但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。
读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。
于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,
同步锁是加在代码段上的。
同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。
现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。
首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,
同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:
访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。
这就是说,
同步锁本身也一定是多个线程之间的共享对象。
Java语言的synchronized关键字
为了加深理解,举几个代码段同步的例子。
不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为
synchronized(同步锁) {
// 访问共享资源,需要同步的代码段
}
这里尤其要注意的就是,同步锁本身一定要是共享的对象。
… f1() {
Object lock1 = new Object(); // 产生一个同步锁
synchronized(lock1){
// 代码段 A
// 访问共享资源 resource1
// 需要同步
}
}
上面这段代码没有任何意义。因为那个同步锁是在函数体内部产生的。每个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不同的同步锁。根本达不到同步的目的。
同步代码一定要写成如下的形式,才有意义。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 需要同步
}
你不一定要把同步锁声明为static或者public,但是你一定要保证相关的同步代码之间,一定要使用同一个同步锁。
讲到这里,你一定会好奇,这个同步锁到底是个什么东西。为什么随便声明一个Object对象,就可以作为同步锁?
在Java里面,同步锁的概念就是这样的。
任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。
一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。
这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 需要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 B
// 访问共享资源 resource1
// 需要同步
}
}
上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。
如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,
进入到该同步锁的就绪(Ready)队列。
每一个同步锁下面都挂了几个线程队列,包括
就绪(Ready)队列,
待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。
注意,
竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。
成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。
因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。
同步粒度 在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。
比如。
… synchronized … f1() {
// f1 代码段
}
这段代码就等价于
… f1() {
synchronized(this){ // 同步锁就是对象本身
// f1 代码段
}
}
同样的原则适用于静态(static)函数
比如。
… static synchronized … f1() {
// f1 代码段
}
这段代码就等价于
…static … f1() {
synchronized(Class.forName(…)){ // 同步锁是类定义本身
// f1 代码段
}
}
但是,
我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。 我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。 比如,下面的代码
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 需要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 B
// 访问共享资源 resource1
// 需要同步
}
}
… f3() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 C
// 访问共享资源 resource2
// 需要同步
}
}
… f4() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 D
// 访问共享资源 resource2
// 需要同步
}
}
上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。
我们仔细分析一下,发现这是没有必要的。
因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为:
public static final Object lock2 = new Object();
… f3() {
synchronized(lock2){ // lock2 是公用同步锁
// 代码段 C
// 访问共享资源 resource2
// 需要同步
}
}
… f4() {
synchronized(lock2){ // lock2 是公用同步锁
// 代码段 D
// 访问共享资源 resource2
// 需要同步
}
}
这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。
信号量 同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。
有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。
信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。
很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。
同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。
信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。
我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。
我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。
这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。
在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。
Object对象的
wait()方法就是等待通知,Object对象的notify()方法就是发出通知。 具体调用方法为
(1)等待某个信号量的通知
public static final Object signal = new Object();
… f1() {
synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁
// 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列
// 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了
// 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面
// 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
// 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。
…
}
}
需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。
(2)发出某个信号量的通知
… f2() {
synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。
// 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。
// 如果某个线程等到了这个通知,那个线程就会转到就绪队列中
// 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
// 嘿嘿,虽然本线程好心通知其他线程,
// 但是,本线程可没有那么高风亮节,放弃到手的同步锁
// 本线程继续执行下面的代码
…
}
}
需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。
以上就是Object的wait()和notify()的基本用法。
实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。
另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。
这些细节问题,并不对大局产生影响。
绿色线程
绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。
操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的
绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。
当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。
难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。
目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。
另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。
ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。
ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。
final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情。
Java中的多线程使用 synchronized关键字实现同步.为了避免线程中使用共享资源的冲突,当线程进入 synchronized的共享对象时,将为共享对象加上锁,阻止其他的线程进入该共享对象.但是,正因为这样,当多线程访问多个共享对象时,如果线程锁定对象的顺序处理不当话就有可能线程间相互等待的情况,即常说的: 死锁现象.
引发死锁的条件:
必须满足以下四种条件
1,互斥条件,每个资源要么已经分配给一个进程,要么就是可用的。
2,占有等待条件,已经得到了某个资源的进程可以再请求新的资源
3,不可抢占条件,已经分配给一个进程的资源不能强制的被抢占,只能被占有他的进程显示的释放
4,环路等待条件,死锁发生时,系统中一定有两个或者两个以上的进程组成一环路,该环路中的每一个
进程都在等待下一个进程占有的资源。
处理死锁的策略:
1,忽略该问题,你忽略它,它也会忽略你
2,测试死锁并恢复,让死锁发生,检测,一旦检测到,恢复
3,仔细对资源进行分配,动态避免死锁
4,通过破坏四个死锁条件之一
方法一对应的时鸵鸟算法,就是出现这种死锁的可能性很低,比如操作系统的fork,可能5年出现一次,
而在这段过程中,因为硬件等其它原因肯定要重新启动机器,放弃fork损失太大,就可以忽略这种死锁
,象鸵鸟一样,把头埋进沙子,当什么都没发生。
方法二:检测并恢复
恢复方法有:
抢占恢复
回退恢复
杀死进程恢复
银行家算法:
如果有4个人(A,B,C,D)去银行贷款,银行有金额10个单位,
A贷款最大为6 ,A已经贷款1
B贷款最大为5 ,B已经贷款1
C贷款最大为4 ,C已经贷款2
D贷款最大为7 ,D已经贷款4
这个时候只有C的请求能通过,因为现在还有可用贷款2,只有C才能完成,然后释放更多,来让其它完成
这个时候如果给其它任何一个单位的贷款,那么所有的人都不能达到需求,完成。
银行家问题时个经典的问题,但是很少能得到实际的利用,因为每个客户自己都不知道自己需要多少资
源,同时,也不知道有多少个客户。因为不停的有用户login ,logout
方法四:破坏条件
1,破坏互斥条件,不让独占出现,
例如不让一个用户独占打印机,如spooling技术,让多个用户同时进入spooling
问题:可能在spooling中产生死锁
2,破坏占有等待条件
检测这个进程需要的所有资源是不是可用,如果可用分配,不可用的话就等待
问题:进程要在开始知道自己需要多少资源,这样可以使用银行家算法完成。
但是资源利用不是最优。
3,破坏不可抢占,这个实现起来最困难
4,破坏闭环
把所有资源编号,按照顺序请求
饥饿:
与死锁很接近的时饥饿
如果一个打印机的使用,是通过某种算法避免死锁,但是每次都是最小文件先打印,这样就可能产生一
种情况,大的文件永远不能打印,饥饿而死。
一般来说,每一种使用线程的语言中都存在线程死锁问题,Java开发中遇到线程死锁问题也是非常普遍。笔者在程序开发中就常常碰到死锁的问题,并经常束手无策。本文分享笔者在JAVA开发中对线程死锁的一些看法。
一. 什么是线程
在谈到线程死锁的时候,我们首先必须了解什么是Java线程。一个程序的进程会包含多个线程,一个线程就是运行在一个进程中的一个逻辑流。多线程允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。
线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信较进程简单。笔者的经验是编写多线程序,必须注意每个线程是否干扰了其他线程的工作。每个进程开始生命周期时都是单一线程,称为“主线程”,在某一时刻主线程会创建一个对等线程。如果主线程停滞则系统就会切换到其对等线程。和一个进程相关的线程此时会组成一个对等线程池,一个线程可以杀死其任意对等线程。
因为每个线程都能读写相同的共享数据。这样就带来了新的麻烦:由于数据共享会带来同步问题,进而会导致死锁的产生。
二. 死锁的机制
由多线程带来的性能改善是以可靠性为代价的,主要是因为有可能产生线程死锁。死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不能正常运行。简单的说就是:线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源。这里举一个通俗的例子:如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。
Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。下面笔者分析死锁的两个过程“上锁”和“锁死” 。
(1) 上锁
许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态,就需要同步机制。因此大多数应用程序要求线程互相通信来同步它们的动作,在 Java 程序中最简单实现同步的方法就是上锁。在 Java 编程中,所有的对象都有锁。线程可以使用 synchronized 关键字来获得锁。在任一时刻对于给定的类的实例,方法或同步的代码块只能被一个线程执行。这是因为代码在执行之前要求获得对象的锁。
为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。给共享变量上锁就使得 Java 线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,就可以知道没有其他线程能够访问该对象。即使在抢占式模型中,其他线程也不能够访问此对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线程开锁。一旦锁被打开,这些睡眠进程就会被唤醒并移到准备就绪队列中。
(2)锁死
如果程序中有几个竞争资源的并发线程,那么保证均衡是很重要的。系统均衡是指每个线程在执行过程中都能充分访问有限的资源,系统中没有饿死和死锁的线程。当多个并发的线程分别试图同时占有两个锁时,会出现加锁冲突的情形。如果一个线程占有了另一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。
在编写多线程代码时,笔者认为死锁是最难处理的问题之一。因为死锁可能在最意想不到的地方发生,所以查找和修正它既费时又费力。例如,常见的例子如下面这段程序。
public int sumArrays(int[] a1, int[] a2){
int value = 0;
int size = a1.length;
if (size == a2.length) {
synchronized(a1) { //1 synchronized(a2) { //2 for (int i=0; i<size; i++)
value += a1[i] + a2[i];
} } } return value;
}
这段代码在求和操作中访问两个数组对象之前锁定了这两个数组对象。它形式简短,编写也适合所要执行的任务;但不幸的是,它有一个潜在的问题。这个问题就是它埋下了死锁的种子。
三. 如何检测死锁的根源
Java并不提供对死锁的检测机制。笔者认为常用分析Java代码问题的最有效的工具仍然是java thread dump。当死锁发生时,JVM通常处于挂起状态,thread dump可以给出静态稳定的信息,查找死锁只需要查找有问题的线程。Java虚拟机死锁发生时,从操作系统上观察,虚拟机的CPU占用率为零,很快会从top或prstat的输出中消失。这时可以收集thread dump,查找"waiting for monitor entry"的thread,如果大量thread都在等待给同一个地址上锁(因为对于Java,一个对象只有一把锁),这说明很可能死锁发生了。
为了确定问题,笔者建议在隔几分钟后再次收集一次thread dump,如果得到的输出相同,仍然是大量thread都在等待给同一个地址上锁,那么肯定是死锁了。如何找到当前持有锁的线程是解决问题的关键。一般方法是搜索thread dump,查找"locked,找到持有锁的线程。如果持有锁的线程还在等待给另一个对象上锁,那么还是按上面的办法顺藤摸瓜,直到找到死锁的根源为止。
另外,在thread dump里还会经常看到这样的线程,它们是等待一个条件而主动放弃锁的线程。有时也需要分析这类线程,尤其是线程等待的条件。
四. 几种常见死锁及对策
解决死锁没有简单的方法,这是因为线程产生死锁都各有各的原因,而且往往具有很高的负载。大多数软件测试产生不了足够多的负载,所以不可能暴露所有的线程错误。在这里中,笔者将讨论开发过程常见的4类典型的死锁和解决对策。
(1)数据库死锁
在数据库中,如果一个连接占用了另一个连接所需的数据库锁,则它可以阻塞另一个连接。如果两个或两个以上的连接相互阻塞,则它们都不能继续执行,这种情况称为数据库死锁。
数据库死锁问题不易处理,通常数据行进行更新时,需要锁定该数据行,执行更新,然后在提交或回滚封闭事务时释放锁。由于数据库平台、配置的隔离级以及查询提示的不同,获取的锁可能是细粒度或粗粒度的,它会阻塞(或不阻塞)其他对同一数据行、表或数据库的查询。基于数据库模式,读写操作会要求遍历或更新多个索引、验证约束、执行触发器等。每个要求都会引入更多锁。此外,其他应用程序还可能正在访问同一数据库模式中的某些对象,并获取不同应用程序所具有的锁。
所有这些因素综合在一起,数据库死锁几乎不可能被消除了。值得庆幸的是,数据库死锁通常是可恢复的:当数据库发现死锁时,它会强制销毁一个连接(通常是使用最少的连接),并回滚其事务。这将释放所有与已经结束的事务相关联的锁,至少允许其他连接中有一个可以获取它们正在被阻塞的锁。
由于数据库具有这种典型的死锁处理行为,所以当出现数据库死锁问题时,数据库常常只能重试整个事务。当数据库连接被销毁时,会抛出可被应用程序捕获的异常,并标识为数据库死锁。如果允许死锁异常传播到初始化该事务的代码层之外,则该代码层可以启动一个新事务并重做先前所有工作。
当出现问题就重试,由于数据库可以自由地获取锁,所以几乎不可能保证两个或两个以上的线程不发生数据库死锁。此方法至少能保证在出现某些数据库死锁情况时,应用程序能正常运行。
(2)资源池耗尽死锁
客户端的增加导致资源池耗尽死锁是由于负载而造成的,即资源池太小,而每个线程需要的资源超过了池中的可用资源。假设连接池最多有10个连接,同时有10个对外部并发调用。这些线程中每一个都需要一个数据库连接用来清空池。现在,每个线程都执行嵌套的调用。则所有线程都不能继续,但又都不放弃自己的第一个数据库连接。这样,10个线程都将被死锁。
研究此类死锁,会发现线程存储中有大量等待获取资源的线程,以及同等数量的空闲且未阻塞的活动数据库连接。当应用程序死锁时,如果可以在运行时检测连接池,就能确认连接池实际上已空。
修复此类死锁的方法包括:增加连接池的大小或者重构代码,以便单个线程不需要同时使用很多数据库连接。或者可以设置内部调用使用不同的连接池,即使外部调用的连接池为空,内部调用也能使用自己的连接池继续。
(3)单线程、多冲突数据库连接死锁
对同一线程执行嵌套的调用有时出现死锁,此情形即使在非高负载系统中通常也会发生。当第一个(外部)连接已获取第二个(内部)连接所需要的数据库锁,则第二个连接将永久阻塞第一个连接,并等待第一个连接被提交或回滚,这就出现了死锁情形。因为数据库没有注意到两个连接之间的关系,所以数据库不会将此情形检测为死锁。这样即使不存在并发,此代码也将导致死锁。此情形有多种具体的变种,可以涉及多个线程和两个以上的数据库连接。
(4)Java虚拟机锁与数据库锁冲突
这种情形发生在数据库锁与Java虚拟机锁并存的时候。在这种情况下,一个线程占有一个数据库锁并尝试获取Java虚拟机锁。同时,另一个线程占有Java虚拟机锁并尝试获取数据库锁。此时,数据库发现一个连接阻塞了另一个连接,但由于无法阻止连接继续,所以不会检测到死锁。Java虚拟机发现同步的锁中有一个线程,并有另一个尝试进入的线程,所以即使Java虚拟机能检测到死锁并对它们进行处理,它还是不会检测到这种情况。
总而言之,JAVA应用程序中的死锁是一个大问题——它能导致整个应用程序慢慢终止,还很难被分离和修复,尤其是当开发人员不熟悉如何分析死锁环境的时候。
五. 死锁的经验法则
笔者在开发中总结以下死锁问题的经验。
(1) 对大多数的Java程序员来说最简单的防止死锁的方法是对竞争的资源引入序号,如果一个线程需要几个资源,那么它必须先得到小序号的资源,再申请大序号的资源。可以在Java代码中增加同步关键字的使用,这样可以减少死锁,但这样做也会影响性能。如果负载过重,数据库内部也有可能发生死锁。
(2)了解数据库锁的发生行为。假定任何数据库访问都有可能陷入数据库死锁状况,但是都能正确进行重试。例如了解如何从应用服务器获取完整的线程转储以及从数据库获取数据库连接列表(包括互相阻塞的连接),知道每个数据库连接与哪个Java线程相关联。了解Java线程和数据库连接之间映射的最简单方法是向连接池访问模式添加日志记录功能。
(3)当进行嵌套的调用时,了解哪些调用使用了与其它调用同样的数据库连接。即使嵌套调用运行在同一个全局事务中,它仍将使用不同的数据库连接,而不会导致嵌套死锁。
(4)确保在峰值并发时有足够大的资源池。
(5)避免执行数据库调用或在占有Java虚拟机锁时,执行其他与Java虚拟机无关的操作。
最重要的是,多线程设计虽然是困难的,但在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。死锁在语言层面上不能解决,就需要一个良好设计来避免死锁。
摘要: 在java中有一类线程,专门在后台提供服务,此类线程无需显式关闭,当程序结束了,它也就结束了,这就是守护线程 daemon thread。如果还有非守护线程的线程在执行,它就不会结束。 守护线程有何用处呢?让我们来看个实践中的例子。 在我们的系统中经常应用各种配置文件...
阅读全文
同时开始5个线程,用各自的文本框显示count,和按钮控制count的自加
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class Ticker extends Thread{
private Button t=new Button("toggle");
private TextField tf=new TextField(10);
//开关控制count的变化
private runFlag=true;
private int count=0;
class Stop implements ActionListener{
@Override
public void actionPerformed(ActionEvent e){
runFlag=!runFlag;
}
}
public Ticker(Container c){
t.addActionListener(new Stop());
//Panel容器
Panel p=new Panel();
p.add(t);
p.add(tf);
c.add(p);
}
@Override
public void run(){
while(true){
try(
Thread.currentThread().sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
if(runFlag)
tf.setText(Integer.toString(++count));
}
}
}
public class Counter extends Applet{
private Button start=new Button("Start");
private boolean started=false;
private int size=0;
private Ticker[] ts;
@Override
public void init(){
start.addActionListener(new Start());
add(start);
ts=new Ticker[size];
for(int i=0;i<size;i++){
ts[i]=new Ticker(Counter.this);
}
}
class Start implements ActionListener{
@Override
public void actionPerformed(ActionEvent e){
if(!started){
started=true;
for(int i=0;i<size;i++){
ts[i].start();
}
}
}
}
public static void main(String[] args){
Counter c=new Counter();
Frame frame=new Frame("程序片");
frame.addWindowListener(
new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e){
System.exit(0);
}
}
);
frame.setSize(300,c.size*50);
frame.add(c,BorderLayout.CENTER);
c.init();
c.start();
frame.setVisible(true);
}
}
/**--注意--**/
以上代码都是在文本编辑器中写的,可能会有些许纰漏
摘要: (1)方法Join是干啥用的? 简单回答,同步,如何同步? 怎么实现的? 下面将逐个回答。 自从接触Java多线程,一直对Join理解不了。JDK是这样说的: join public final void join(long millis)throws Interrupte...
阅读全文
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class TestCut {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
TestCut test = new TestCut();
List<Student> list = test.readFile("D:\\Students.txt", "GBK");
test.printAver(list);
System.out.println("-------------------------------------------");
test.printExcel(list);
System.out.println("---------------------------------------------");
test.printEnglishAvg(list);
}
/**
* 按照平均分降序格式化输出
*
* @param list
*/
public void printAver(List<Student> list) {
if (list != null) {
String top = "序号\t学号\t平均分\t数学\t语文\t英语";
System.out.println(top);
Collections.sort(list, new Comparator<Student>() {
// 根据平均数来进行比较,降序排序
public int compare(Student arg0, Student arg1) {
if (arg1 == null)
return -1;
if (arg0.getAvg() < arg1.getAvg())
return 1;
else if (arg0.getAvg() == arg1.getAvg())
return 0;
else
return -1;
}
});
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
System.out.print((i + 1) + "\t" + student.getS_no() + "\t"
+ student.getAvg() + "\t");
System.out.print(student.getMaths() + "\t"
+ student.getChinese() + "\t");
if (student.getEnglish() != null)
System.out.println(student.getEnglish());
else {
System.out.println();
}
}
} else {
System.out.println("文件内容为空!");
}
}
/**
* 按照优秀率格式化输出
*
* @param list
*/
public void printExcel(List<Student> list) {
if (list != null) {
String top = "序号\t学号\t优秀率\t数学\t语文\t英语";
System.out.println(top);
Collections.sort(list, new Comparator<Student>() {
// 根据优秀率来进行比较,降序排序
public int compare(Student arg0, Student arg1) {
if (arg1 == null)
return -1;
if (arg0.getExcel() < arg1.getExcel())
return 1;
else if (arg0.getExcel() == arg1.getExcel())
return 0;
else
return -1;
}
});
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
DecimalFormat df = new DecimalFormat("#%");
System.out.print((i + 1) + "\t" + student.getS_no() + "\t"
+ df.format(student.getExcel()) + "\t");
System.out.print(student.getMaths() + "\t"
+ student.getChinese() + "\t");
if (student.getEnglish() != null)
System.out.println(student.getEnglish());
else {
System.out.println();
}
}
} else {
System.out.println("文件内容为空!");
}
}
/**
* 求英语平均成绩
* @param list
*/
public void printEnglishAvg(List<Student> list) {
printAvgByCourse(list, 3);
}
/**
* 求课程平均成绩,并输出
*
* @param list
* @param course
* 课程(1:数学,2:语文,3:英语)
*/
private void printAvgByCourse(List<Student> list, int course) {
Integer avg = 0;
switch (course) {
case 1: {
Integer maths = 0;
for (Student student : list) {
maths += student.getMaths();
}
avg = maths / list.size();
System.out.println("数学平均成绩:\t" + avg);
break;
}
case 2: {
Integer chinese = 0;
for (Student student : list) {
chinese += student.getChinese();
}
avg = chinese / list.size();
System.out.println("语文平均成绩:\t" + avg);
break;
}
case 3: {
Integer english = 0;
Integer size = 0;
for (Student student : list) {
if (student.getEnglish() != null) {
english += student.getEnglish();
size++;
}
}
if (size != 0)
avg = english / size;
System.out.println("英语平均成绩:\t" + avg);
break;
}
default: {
System.out.println("不存在此课程");
break;
}
}
}
/**
* 读取文件信息
*
* @param fileName
* 文件路径
* @param charset
* 编码
* @return
* @throws IOException
*/
public List<Student> readFile(String fileName, String charset) {
List<Student> list = new ArrayList<Student>();
FileInputStream fi = null;
BufferedReader in = null;
try {
fi = new FileInputStream(new File(fileName));
in = new BufferedReader(new InputStreamReader(fi, charset));
String result = in.readLine();
while ((result = in.readLine()) != null) {
String[] str = result.split("\t");
Student student = new Student();
for (int i = 0; i < str.length; i++) {
student.setS_no(str[0]);
student.setMaths(Integer.parseInt(str[1]));
student.setChinese(Integer.parseInt(str[2]));
if (str.length > 3) {
student.setEnglish(Integer.parseInt(str[3]));
}
student.setAvg(student.culAvg());
student.setExcel(student.culExcel());
}
list.add(student);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
}
class Student implements Serializable {
private static final long serialVersionUID = -6517638655032546653L;
/**
* 学号
*/
private String s_no;
/**
* 数学
*/
private Integer maths;
/**
* 语文
*/
private Integer chinese;
/**
* 英语
*/
private Integer english;
/**
* 平均分
*/
private Integer avg;
/**
* 优秀率
*/
private float excel;
public Student() {
}
public Student(Integer maths, Integer chinese, Integer english) {
this.maths = maths;
this.chinese = chinese;
this.english = english;
this.avg = culAvg(maths, chinese, english);
this.excel = culExcel(maths, chinese, english);
}
public String getS_no() {
return s_no;
}
public void setS_no(String sNo) {
s_no = sNo;
}
public Integer getMaths() {
return maths;
}
public void setMaths(Integer maths) {
this.maths = maths;
}
public Integer getChinese() {
return chinese;
}
public void setChinese(Integer chinese) {
this.chinese = chinese;
}
public Integer getEnglish() {
return english;
}
public void setEnglish(Integer english) {
this.english = english;
}
public Integer getAvg() {
return avg;
}
public void setAvg(Integer avg) {
this.avg = avg;
}
public float getExcel() {
return excel;
}
public void setExcel(float excel) {
this.excel = excel;
}
public String toString() {
StringBuffer sb = new StringBuffer("[");
sb.append("s_no=" + s_no).append(",maths=").append(maths).append(
",chinese=").append(chinese).append(",english=")
.append(english).append(",avg=").append(avg).append(",excel=")
.append(excel).append("]");
return sb.toString();
}
/**
* 计算平均数
*
* @param maths
* 数学
* @param chinese
* 语文
* @param english
* 英语
* @return
*/
private Integer culAvg(Integer maths, Integer chinese, Integer english) {
// 计算平均分
Integer sum = chinese + maths;
float aver = english == null ? (float) sum / 2
: (float) (sum + english) / 3;
return Math.round(aver);
}
/**
* 计算优秀率
*
* @param maths
* 数学
* @param chinese
* 语文
* @param english
* 英语
* @return
*/
private float culExcel(Integer maths, Integer chinese, Integer english) {
final Integer EXCEL_NUMBER = 85;
Integer total_number = english == null ? 2 : 3;
Integer ex = 0;
if (maths >= EXCEL_NUMBER)
ex++;
if (chinese >= EXCEL_NUMBER)
ex++;
if (english != null && english >= EXCEL_NUMBER)
ex++;
return (float) ex / total_number;
}
/**
* 计算平均数
*
* @return
*/
public Integer culAvg() {
return culAvg(this.maths, this.chinese, this.english);
}
/**
* 计算优秀率
*
* @return
*/
public float culExcel() {
return culExcel(this.maths, this.chinese, this.english);
}
}
package number;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
public class WriteFile {
public static void main(String[] args) {
File file=new File("temp.txt");//txt文档是E:\workspace\study(你自己的工作区目录)下的temp.txt文档
try{
//先写入内容到指定的文档
System.out.println("请输入文件的内容:");
InputStreamReader isr=new InputStreamReader(System.in );
BufferedReader br=new BufferedReader(isr);
String str=br.readLine();
FileWriter fw=new FileWriter(file);
PrintWriter pw=new PrintWriter(fw);
while(!str.equals("")){
pw.println(str);
str=br.readLine();
}
br.close();
pw.close();
System.out.println("写入内容成功!");
//读取文档里面的内容
FileReader fr=new FileReader(file);
BufferedReader br2=new BufferedReader(fr);
String s=br2.readLine();
System.out.println("文档内容为:");
while(s!=null){
System.out.println(s);
s=br2.readLine();
}
br2.close();
}catch(IOException e){
e.printStackTrace();
}
}
}