线程同步

首先,假设现在有一个容器类,它能够简单地增加元素,删除元素,并在每次增删元素之后显示自身持有了多少个元素、是否为空。为了能够共用,它是单例的。

/**
 * source1
 */
class Container {

    // 象征元素个数的索引对象
    private int index =50;

    private final static Container c = new Container();

    private Container() {
        super();
    }

    public static Container getInstance() {
        return c;
    }

    public void add() throws InterruptedException {
        index ++;
        System.out.print(c + "\t");
    }

    public void sub() throws InterruptedException {
        if (index != 0) {
            index --;
        }
        System.out.print(c + "\t");
        Thread.sleep(500);
    }

    public String toString() {
        if (index != 0) {
            return "Container points " + index;
        } else {
            return "Container is empty";
        }
    }

}

 

现在需求是尽快消耗掉Container中预存的50个元素,

对于这个需求,可以通过反复调用Container.sub()来实现,但是这样做太慢了,每次调用都要等待0.5s,

这个时间可以认为是实际开发中,方法内部其他业务逻辑所需要消耗的时间。

合适的做法可能是启用若干个专门的线程,通过并发来删除元素。

/**
 * source2
 */
class SimpleThread implements Runnable {

    // 持有共有容器
    Container c = Container.getInstance();

    @Override
    public void run() {
        try {
            move();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void move() throws Exception {
        while (true) {
            c.sub();
        }

    }
}

 

试验一下

/**
 * source3
 */
public static void main(String[] args) {
    new Thread(new SimpleThread()).start();
    new Thread(new SimpleThread()).start();
    new Thread(new SimpleThread()).start();
    new Thread(new SimpleThread()).start();
}
Container points 48    Container points 45    Container points 47    
Container points 46    Container points 49    Container points 42    
Container points 41    Container points 40    Container points 42    
Container points 42    Container points 38    Container points 35    
Container points 36    Container points 38    Container points 37    
Container points 32    Container points 32    Container points 31    
Container points 32    Container points 30    Container points 29    
Container points 27    Container points 26    Container points 28    
Container points 25    Container points 23    Container points 20    ……

 

试验运行了一会就被停止了,因为早在一开始,打印值就完全混乱了...

引发混乱的原因是,所有的线程能通过Container.sub(),来访问并改变Container.index的值。

而Container对象只有一个,并被4个线程同时持有,这4个线程又不停地在就绪状态和运行状态间切换,这种切换并不受Java代码控制,很容易发生诸如下面的现象:

某个线程(Thread0)刚执行到index --(假设此时index为50),时间片就消耗完毕了,那么暂时就无法打印此时的index值(49)。

接着时间片被另一个线程(Thread1)获得,而这个线程比较幸运,执行完了index --,还有机会打印index值,而此时打印值已经成了48。

index(49)会在下次Thread0获得时间片时被打印出来,那么打印结果就完全错误了。

不光如此,由于demo中用Thread.sleep(500)来代表实际项目中业务逻辑运行的时间,index--的时间相对0.5来说可以忽略不计,

那么虽然打印值不正确,但是index真正的值基本上是正确地一点点减少。

但是实际项目中,过程不会那么简单,“index--”象征的删除操作需要的时间会被放大,如果在“index--”时线程进入就绪状态,另一个线程就会在现在index值的基础上进行删除操作

这样一来,同一个元素就被两次(或者更多)执行了删除操作,容器内部的发生了元素不同步。

Deolin是这样理解线程不同步的,

  对象的状态在并发过程中发生了改变,但某些线程还在处理改变前的对象,造成了信息不同步。

解决方法是,想办法将“index --”和“println(c)”绑定起来,让“index--”执行完了以后,必须等“println(c)”也执行完,才允许其他线程执行“index--”。

Java中提供了这样的关键字——synchronized

    public void add() throws InterruptedException {
        synchronized (this) {
            index ++;
            Thread.sleep(500);
            System.out.print(c + "\t");
        }
    }

    public void sub() throws InterruptedException {
        synchronized (this) {
            if (index != 0) {
                index --;
            }
            Thread.sleep(500);
            System.out.print(c + "\t");
        }
    }

 

再测试一下

Container points 49    Container points 48    Container points 47    
Container points 46    Container points 45    Container points 44    
Container points 43    Container points 42    Container points 41    
Container points 40    Container points 39    Container points 38    
Container points 37    Container points 36    Container points 35    
Container points 34    Container points 33    Container points 32    
Container points 31    Container points 30    Container points 29    
Container points 28    Container points 27    Container points 26    
Container points 25    Container points 24    Container points 23    
Container points 22    Container points 21    Container points 20    
Container points 19    Container points 18    Container points 17    
Container points 16    Container points 15    Container points 14    
Container points 13    Container points 12    Container points 11    
Container points 10    Container points 9    Container points 8    
Container points 7    Container points 6    Container points 5    
Container points 4    Container points 3    Container points 2    
Container points 1    Container is empty    Container is empty    
Container is empty    Container is empty    ……

结果正确。

 

线程一旦执行到synchronized{}内部,synchronized{}代码块会被“上锁”,线程会“获得锁”,其他线程只能被拦截在synchronized{}以外,

(所有被拦截的线程,会进入一种叫做同步阻塞的状态,被JVM放入锁池(lock pool))

直到获得锁的线程将synchronized{}剩余的代码执行完毕,释放“锁”。下个获得时间片的线程才有机会“获得锁”,进入synchronized{}

为了向安全性(线程同步)让步,Deolin觉得synchronized是一种牺牲效率的“反并发”做法。

锁住越多的代码意味着越安全,代价就是更低效,最极端的情况,锁住了一切,那么就根本没有并发了。

锁住越少的代码意味着越高效,代价就是要编码过程中要考虑更多。

 

synchronized的另一个组成部分是()中对对象的声明。synchronized(this)的对象实际上算是个象征,如果对象中某一个synchronized代码块锁被获得了,

那么访问其他的synchronized代码块也会进入阻塞状态。

 

改变source2,让线程对象有两种运行策略。

/**
 * source2
 */
class SimpleThread implements Runnable {

    // 持有共有容器
    Container c = Container.getInstance();

    // true:调用add();false:调用sub();
    boolean strategy;

    public simpleThread(boolean strategy) {
        this.strategy = strategy;
    }

    @Override
    public void run() {
        try {
            move();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void move() throws Exception {
        while (true) {
            if (strategy) {
                c.add();
            } else {
                c.sub();
            }
        }
    }

}

改变sub()和add()方法,让sub()方法内sleep时间足够长,是suber线程能长时间获得锁。

    public void add() throws InterruptedException {
        synchronized (this) {
            index ++;
            System.out.print(c + "\t");
            Thread.sleep(500);
        }
    }

    public void sub() throws InterruptedException {
        synchronized (this) {
            if (index != 0) {
                index --;
            }
            System.out.print(c + "\t");
            System.out.println("subing");
            Thread.sleep(10000);
        }
    }

测试一下

Container points 49    subing
Container points 48    subing
Container points 47    subing
Container points 46    subing
Container points 45    subing
Container points 46    Container points 47    Container points 48    Container points 49    Container points 50   ……

所有有“subing”的行,其下一行都等待了足足10s才打印出来。

 

当希望用synchronized关键字锁住整个方法时,语法发生了改变,不再需要()部分,默认为this对象。

    public synchronized void add() throws InterruptedException {
        index ++;
        System.out.print(c + "\t");
        Thread.sleep(500);
    }

    public synchronized void sub() throws InterruptedException {
        if (index != 0) {
            index --;
        }
        System.out.print(c + "\t");
        System.out.println("subing");
        Thread.sleep(10000);
    }

 

重入特性

synchronized方法与synchronized(this)是等价的,都是对象锁

JVM会通过一个计数器统计某个对象的上锁次数。

当一个线程进入了synchronized声明的代码之后,计数器变成了1。

此时该线程可以通过代码流程的引导,进入该对象中别的被synchronized声明的代码(它能够这样做,因为他持有了该对象的锁)

一旦进入了新的synchronized代码,计数器会+1,变成2,

直到退出从新的synchronized代码退出或是返回,计数器变回1。

而只有计数器变成0,锁池中的其他线程才有机会进入synchronized代码。

基于这点,获得了对象锁的线程可以自由调用对象的任何方法。

而一个线程必须等对象的每个对象锁全部释放,才有机会逃离lock pool,获得对象锁

那么,关于线程同步,在开发过程中,本质上就是考虑4件事情

哪个线程获得了锁?

线程了获得了哪个对象的锁?

还有哪些线程在等锁?

对象锁释放完全了吗?

 

static synchronized方法

特性与synchronized方法基本一致,唯一不同的是锁的对象

synchronized方法对象是类的某一个实例(this),而static synchronized方法对象是类的所有实例,某种程度上可以认为对象是类的字节吗。

前者获得锁的条件是没有线程获得this所有的锁,后者获得锁的条件是没有线程获得该类型所有对象所有的锁。

 

关于线程同步,除了snychronized关键字,还有Object.wait()和Object.notifyAll()/notify()方法,

Object.wait()和Thread.sleep()很相似。

双方都会让线程本身进入阻塞状态,前者是属于等待阻塞,后者属于其他阻塞。

前者释放锁,后者不释放锁;

wait中的线程需要被其他线程notifyAll才能恢复运行,sleep中的线程需要超时才能恢复运行。

线程只有获得锁,才能调用所在对象的wait()方法,从而释放锁,如果不那么做,线程运行到wait()时会抛出异常。

wait()会让当前线程进入等待池,不同于锁池,等待池中的线程甚至没有像锁池中的线程那样去竞争锁的机会只有被唤醒,才能进入锁池。

 

至于notifyAll()与notify()的区别,则在于notifyAll会将wait在本对象中的线程全部唤醒,而notify只会唤醒一个线程(由JVM决定是哪一个线程)。

 

posted @ 2017-04-08 13:20  Deolin  阅读(208)  评论(0编辑  收藏  举报