Java并发——线程安全、线程同步、线程通信

线程安全

进程间"共享"对象

多个“写”线程同时访问对象。

例:Timer实例的num成员,即add()方法是用的次数。即Timer实例是资源对象。

class TestSync implements Runnable {

    Timer timer = new Timer();

    public void run() {
        timer.add(Thread.currentThread().getName());
    }

}

class Timer {
    private static int num = 0;

    public void add(String name) {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {}
        System.out.println(name + ", 你是第" + num + "个使用timer的线程");
    }

}

public class TestMain {

    public static void main(String[] args) {
        TestSync test = new TestSync();

        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();

    }

}

 

 

说明:

(1) 程序输出显示:

  t1你是第2个使用timer的线程。

  t2你是第2个使用timer的线程。

(2) 程序执行过程分析:

  线程t1,线程t2 均调用timer的add()方法。

a. 某一线程执行num++(num为1),该线程暂停了1ms。

b. 另一线程执行num++(num为2),该线程暂停了1ms。

c. 某一线程暂停结束,输出"是第2个使用timer的线程"。

d. 另一线程暂停结束,输出"是第2个使用timer的线程"。

(3) 导致以上输出结果的原因:

  两个线程对同一个Timer实例的num成员做操作。没有线程之间协调运作。

 

 

线程安全

当多个线程访问某个类时,该类始终都表现正确行为,则称该类是线程安全的。 线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。

无状态对象是线程安全的。

写Java程序的时候,何时需要进行并发控制,关键在于判断这段程序或这个类是否是线程安全的。

由于多线程的执行条件而出现不正确的结果,被称为Race Condition(竞态条件)

 

线程同步

多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。 解决多个线程使用共通资源的方法是:线程操作资源时独占资源,其他线程不能访问资源。使用锁可以保证在某一代码段上只有一条线程访问共用资源。

 

内置锁

Java提供synchronized关键字来支持内在锁。

 

 

synchronized关键字的使用

synchronized关键字可以放在方法的前面、对象的前面、类的前面。synchronized关键字用作锁定当前对象。这种锁又称为"互斥锁"。

使用方法:

1. 同步代码块

synchronized(obj){
    //.....同步代码块
} 

说明:执行同步代码块。JVM会锁定obj对象。即锁定对象obj只允许单个线程操作。

任何时刻,只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。 同步监视器是为了阻止两个线程对同一个共享资源进行并发访问。

Java程序可以使用任何对象作为同步监视器。一般推荐使用可能被并发访问的共享资源充当同步监视器。

 

2. 同步方法

public synchronized void xxx(){
    //.....
}

 

说明:执行同步方法,该方法只允许单个线程执行。同步方法的同步监视器是this,即对象本身,无须显示指定同步监视器。

synchronized关键字修饰的代码块之运行单个线程占用。只有在线程执行完同步代码块后,其他线程才能占用该代码块。同步方法被某线程执行后排斥其他线程。同步方法会使得程序效率性能降低。

 

3. 同步类

把synchronized关键字放在类的前面,这个类中的所有方法都是同步方法。

 

4. 可重入同步

线程可以获得他已经拥有的锁,运行线程多次获得同一个锁,就是可以重入(reentrant)同步。这种情况通常是同步代码直接或者间接的调用也包含了同步代码的方法,并且两个代码集都使用同一个锁。如果没有可重入同步,那么,同步代码就必须采取很多额外的预防措施避免线程阻塞自己。java java.util.concurrent 包中的 ReentrantLock 即为可重入锁。

 

参考:

http://bbs.csdn.net/topics/80052746 该帖子被许多博文转载与引用

http://blog.163.com/hsh8523@126/blog/static/218935592011214114257822/

 

例:改进程序。

class TestSync implements Runnable {

    Timer timer = new Timer();

    public void run() {
        timer.add(Thread.currentThread().getName());
    }

}

class Timer {
    private static int num = 0;

    public void add(String name) {
        synchronized (this) {
            num++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {}
            System.out.println(name + ", 你是第" + num + "个使用timer的线程");
        }
    }

}
/*
或者以下下形式
class Timer {
    private static int num = 0;

    public synchronized void add(String name) {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {}
        System.out.println(name + ", 你是第" + num + "个使用timer的线程");
    }
}
*/


public class TestMain {
    public static void main(String[] args) {
        TestSync test = new TestSync();

        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }

}

 

 

 

 

1.3.3 理解synchronized关键字

由于Java没有类似于PV操作、进程互斥等相关的方法的。需要说明的是,Java的synchronized()方法类似于操作系统概念中的互斥内存块,在Java中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现Java中简单的同步、互斥操作。明白这个原理,就能理解为什么synchronized(this)与synchronized(static XXX)的区别了,synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。

 

思考以下程序:当执行某个线程执行m1()方法,m2()方法还能执行吗?

public class TestSynchronized {
    int b = 100;

    public synchronized void m1() throws Exception {
        b = 1000;
        Thread.sleep(5000);
        System.out.println("b = " + b);
    }

    public void m2() throws Exception {
        System.out.println(b);
    }
}

答案:m2() 方法可以执行。即synchronized方法的作用只是用于单个线程执行,并没有真正锁定方法对应的this对象。其他线程可以访问没有synchronized关键字的方法。

从上例中可以得到结论,类中的非同步方法可能会影响到类中的同步方法。所以若需要保证类对象的线程同步,则需要仔细考虑类方法是否需要添加synchronized关键字修饰。

关于该问题,自己又做了一个试验,仔细研究了下。 请看:http://shijiaqi1066.iteye.com/admin/blogs/1886791

 

 

1.4 死锁

当1号线程执行过程中需要使用(锁定)对象A,同时还需要使用(锁定)另一个对象B。但是2号线程使用(锁定)了对象B,同时还需要使用(锁定)对象A。

这种情况导致 1号线程等待2号线程执行完才能继续执行;2号线程等待1号线程的执行完才能继续执行。即1号线程、2号线程均无法继续执行;其他线程无法所得资源,也无法继续执行。整个程序无法继续执行。即死锁。

说明:出现死锁现象需要多个锁定对象。

 

例:模拟死锁现象。以下程序会出现死锁。

public class TestDeadLock implements Runnable {
    public int flag = 1;

    static Object o1 = new Object(), o2 = new Object();

    public void run() {
        System.out.println("flag=" + flag);
        if (flag == 1) {
            synchronized (o1) { // 线程锁住o1。
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o2) { // 线程锁住o2。
                    System.out.println("1");
                }
            }
        }

        if (flag == 0) {
            synchronized (o2) { // 线程锁住o2。
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o1) { // 线程锁住o1。
                    System.out.println("0");

                }
            }
        }
    }


    public static void main(String[] args) {

        TestDeadLock td1 = new TestDeadLock();
        TestDeadLock td2 = new TestDeadLock();

        td1.flag = 1;
        td2.flag = 0;

        Thread t1 = new Thread(td1); // flag1线程。
        Thread t2 = new Thread(td2); // flag2线程。

        t1.start();
        t2.start();
    }

}

 

说明:

a. flag1线程锁住o1,睡眠;同时,flag2线程锁住o2,睡眠。

b. flag1线程苏醒需要锁住o2,才能执行完毕,但是o2被flag2线程占用;同时,flag2线程苏醒需要锁住o1,才能执行完毕,但是o1被flag1线程占用。

c. 出现flag1线程等待flag2线程,flag2线程等待flag1线程。程序无法再继续执行。

 

哲学家吃饭问题:5个哲学家围绕圆桌吃饭。每个哲学家各左右手各拿一只筷子(即每个一双筷子)。

WIKI :http://zh.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E5%AE%B6%E5%B0%B1%E9%A4%90%E9%97%AE%E9%A2%98

百度百科 :http://baike.baidu.com/view/3446884.htm

 

解决死锁问题

如果不需要写接近底层的程序或很复杂的程序,死锁问题在实际编程中比较难遇上。

如果出现死锁问题:可以放大锁定对象的粒度。即不要一次锁定多个对象。尽量锁定一个对象。

 

 

1.5 监视器与对象锁

Java并发,会导致线程间共享的对象存在线程安全的问题。线程的同步。JVM提供了相关机制。锁机制。

在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁,锁住了一个对象,就是获得对象相关联的监视器。

监视器好比一座建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据,进入这个建筑叫做"进入监视器",进入建筑中的那个特别的房间叫做"获得监视器",占据房间叫做"持有监视器",离开房间叫做"释放监视器",离开建筑叫做"退出监视器"。

而一个锁就像一种任何时候只允许一个线程拥有的特权。一个线程可以允许多次对同一对象上锁。对于每一个对象来说,JVM维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1。当计数器跳到0的时候,锁就被完全释放了。

JVM中的一个线程在它到达监视区域开始处的时候请求一个锁。Java程序中每一个监视区域都和一个对象引用相关联。

   

 

 

1.6 原子操作和复杂操作

原子性:一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。

很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count++)和check-then-act(先检查后操作),这些都是很容易被忽视的。

例如大家所常用的惰性初始化(延迟初始化)模式,以下代码就不是线程安全的:

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

 

这段代码具体问题在于没有认识到if(instance==null)和instance = new ExpensiveObject();是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance==null)后会被中断,另一个线程也去执行if(instance==null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。

check-then-act从表面上看很简单,普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。

在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。

 

1.7 构建线程安全类

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步,这个类的行为仍然是正确的,那么称这个类是线程安全的。我们设计类就是要在有潜在并发问题存在情况下,设计线程安全的类。线程安全的类可以通过以下手段来满足:

  • 不跨线程共享变量。
  • 使状态变量为不可变的。
  • 在任何访问状态变量的时候使用同步。
  • 每个共享的可变变量都需要由唯一一个确定的锁保护。

 

满足线程安全的一些思路

1. 从源头避免并发问题

很多开发者一想到有并发的可能就通过底层技术来解决问题,其实往往可以通过上层的架构设计和业务分析来避免并发场景。比如我们需要用多线程或分布式集群来计算一堆客户的相关统计值,由于客户的统计值是共享数据,因此会有并发潜在可能。但从业务上我们可以分析出客户与客户之间数据是不共享的,因此可以设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。这种规则很容易设计。当你从源头就避免了并发问题的可能,下面的工作就完全可以不用担心线程安全问题。

 

2. 无状态就是线程安全

多线程编程或者分布式编程最忌讳有状态,一有状态就不但限制了其横向扩展能力,也是产生并发问题的起源。当你设计的类是无状态的,那么它永远都是线程安全的。因此在设计阶段需要考虑如何用无状态的类来满足你的业务需求。

 

3. 锁的合理使用

大家都知道可以用锁来解决并发问题,但在具体使用上还有很多讲究,比如:

  • 每个共享的可变变量都需要由一个个确定的锁保护。
  • 一旦使用了锁,就意味着这段代码的执行就丧失了操作系统多道程序的特性,会在一定程度上影响性能。
  • 锁不能解决在分布式环境共享变量的并发问题。

   

4. 线程封闭

当访问共享可变数据时,通常需要使用同步,同步是需要消耗性能的。

避免使用同步的方式就是不共享数据。若将数据都封闭在各自的线程之中,就不需要同步。这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

一般存在3种线程封闭的方法:

  • Ad-hoc线程封闭
  • 栈封闭
  • ThreadLocal

其中ThreadLocal是Java提供线程封闭的规范。

 

1.8 ThreadLocal类

ThreadLocal用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。

每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。

 

ThreadLocal的应用场景:

例:Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。

ThreadLocal提供了get与set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。

 

例:使用ThreadLocal确保线程封闭性。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}

线程首次调用ThreadLocal.get方法时,会请求initialValue 提供一个初始值。概念上,可以将ThreadLocal<T>看作map< Thread,T>,线程终止后,这些值会被垃圾回收。

若需要将一个单线程的应用迁移到多线程环境中,可以将共享的全局变量都转换为ThreadLocal类型,这样可以确保线程安全。前提是全局共享(shared globals)的语义允许这样。如果将应用级的缓存变成一堆线程本地缓冲,它将毫无价值。

一般使用,需要实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。线程本地变量会降低重用性,引入隐晦的类间的耦合。因此应该谨慎地使用。

 

 

线程通信

2.1 对象的wait,notify/notifyAll

Object类对线程的控制

  • void wait()
  • void wait(long timeout)
  • void wait(long timeout, int nanos)

其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

  • void notify() 唤醒在此对象监视器上等待的单个线程。
  • void notifyAll() 唤醒在此对象监视器上等待的所有线程。

 

在http://blog.csdn.net/zyplus/article/details/6672775看到的一些介绍 (语句略有改动)

如果需要在线程间相互唤醒的话就需要借助Object.wait(), Object.nofity() 。

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。

从功能上来说:

obj.wait()方法使得获取对象锁的线程主动释放对象锁,同时休眠线程;直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

obj.notify()方法唤醒因释放obj的锁而休眠的线程(唤醒obj对象上被wait()的线程)。注意:notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。

 

2.1 生产者与消费者问题

例:模拟生产者消费者问题的Java实现

public class ProducerConsumer {

    public static void main(String[] args) {
        SyncStack ss = new SyncStack();

        Producer p = new Producer(ss);
        Consumer c = new Consumer(ss);

        new Thread(p).start();
        new Thread(p).start();
        new Thread(p).start();
        new Thread(c).start();
    }
}


class WoTou {
    int id;

    WoTou(int id) {
        this.id = id;
    }

    public String toString() {
        return "WoTou : " + id;
    }
}


class SyncStack {
    int index = 0;
    WoTou[] arrWT = new WoTou[6];

    public synchronized void push(WoTou wt) {
        while (index == arrWT.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        arrWT[index] = wt;
        index++;
    }

    public synchronized WoTou pop() {
        while (index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        index--;
        return arrWT[index];
    }
}


class Producer implements Runnable {
    SyncStack ss = null;

    Producer(SyncStack ss) {
        this.ss = ss;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            WoTou wt = new WoTou(i);
            ss.push(wt);
            System.out.println("生产了:" + wt);

            try {
                Thread.sleep((int) (Math.random() * 200));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Consumer implements Runnable {
    SyncStack ss = null;

    Consumer(SyncStack ss) {
        this.ss = ss;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            WoTou wt = ss.pop();
            System.out.println("消费了: " + wt);
            try {
                Thread.sleep((int) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

 

理解wait与notify

 

 

object.wait() 方法必须位于synchronized(object) 代码块中。
object.wait() 方法之后,线程会释放object的监视器。线程被添加到object的wait队列中并处于等待状态。

object.notify()方法之前,必须调用synchronized(object) 。notify方法不会让当前线程会释放object的监视器。
object.notify()方法会通知其他wait于object实例上的一个线程,让其重新去争用object的监视器。之前wait的线程若没有获取到object的监视器其synchronized(object) 代码块不会继续执行。

 

 

 

 

 

notify方法用于唤醒对象上的一个正在等待的线程。
notifyAll方法用于唤醒对象上的所有正在等待的线程。

 

posted @ 2013-11-07 11:52  LaplaceDemon  阅读(6095)  评论(0编辑  收藏  举报