Java多线程复习与巩固(三)--线程同步

来源:互联网 发布:mac切换语言快捷键 编辑:程序博客网 时间:2024/06/09 03:49

多线程容易出现的问题

因为一个进程内,多个线程线程共享该进程的资源,而进程之间,资源的获取是互斥的,所以线程间通信比进程间通信更简单。我们可以直接通过共享资源的访问来实现线程间通信,这种通信方式十分有效(速度快),但也容易产生错误,如:线程干扰内存一致性错误

看一下下面这个例子:这个程序有两个线程,一个线程对计数器进行10000次加一操作,一个线程对计数器进行10000次减一操作,两个线程执行完后,计数器值原本应该等于0。但主线程在两个线程执行完后,打印计数器的值几乎很难得到0这个结果。

public class ThreadCommunicate {    static class Counter {        private int c = 0;        public void increment() {            c++;        }        public void decrement() {            c--;        }        public int value() {            return c;        }    }    static class IncrementTask implements Runnable {        public void run() {            for (int i = 0; i < 10000; i++) {                counter.increment();            }        }    }    static class DecrementTask implements Runnable {        public void run() {            for (int i = 0; i < 10000; i++) {                counter.decrement();            }        }    }    // Counter就是两个线程的共享资源    private static Counter counter = new Counter();    public static void main(String[] args) throws InterruptedException {        // 创建两个线程同时对共享资源进行读写        Thread i = new Thread(new IncrementTask());        Thread d = new Thread(new DecrementTask());        i.start();        d.start();        i.join();        d.join();        System.out.println(counter.value());    }}

问题出现的原因

问题就出在c++c--这两个操作上。

了解过汇编的都应该知道,一个c++自增操作会分为以下几步:

  mov         eax,dword ptr [c]  ;根据c的地址从内存取出c的值放到寄存器中  add         eax,1              ;执行加一操作  mov         dword ptr [c],eax  ;把寄存器的值放回c地址所在的内存

另外从Java的反编译代码也可以看出来,c++c--不止一步:

  public void increment();    Code:       0: aload_0       1: dup       2: getfield      #2                  // 获取字段c的值       5: iconst_1      #                   // 获取常量1的值       6: iadd          #                   // 执行加操作       7: putfield      #2                  // 把相加的结果放回字段c      10: return  public void decrement();    Code:       0: aload_0       1: dup       2: getfield      #2                  // Field c:I       5: iconst_1       6: isub       7: putfield      #2                  // Field c:I      10: return

即使是非常简单的操作,在底层处理时也会分解成若干步。

不论是在单处理机CPU还是多处理机CPU种,两个线程执行指令的前后顺序是不确定的,如果出现下面的这种情况:

CPU执行顺序

通过上图可以看出,两个线程同时执行一轮后,c的值结果等于-1。用一张动态图来形象描述一下这种情况:

线程竞争共享资源的坏情况

很显然下面这张图才是我们想要的结果:

线程竞争共享资源的好情况

所以真正能得到等于0的序列只有两种:

可以得出0的两种序列

这么多条指令的排列中只有2条能够得到0这个结果,难怪上面的程序几乎得不到0这个结果。

同步与异步

同步和异步

“同步”和“异步”,在各个领域中都有这两个词的出现。通俗的讲:同步就是在一条线上执行,异步就是分成多条线执行。

很显然对于上面的问题,我们应该要将两个线程并成一条线,让它按次序执行,这就是我们要讲的“线程同步”。先来看一张动态图来初步了解线程同步的基本原理:

线程同步的原理图

使用线程同步解决问题

线程同步的方式有很多,下面我们介绍Java语言中最简单的线程同步的实现——使用synchronized关键字。synchronized关键字有两种使用方法:同步方法、同步代码块

同步方法

修改后的Counter类代码如下:

    static class Counter {        private int c = 0;        public synchronized void increment() {            c++;        }        public synchronized void decrement() {            c--;        }        public int value() {            return c;        }    }

因为increment方法和decrement方法对共享资源进行了修改属于写操作,而value方法没有进行修改仅仅只是读取属于读操作。对于读操作我们不需要加同步锁。

使用synchronized关键字修饰的方法有以下特点:

  • 首先,同一对象上的两个synchronized方法的调用不可能交织。当一个线程正在执行一个对象的synchronized方法时,调用同一个对象的synchronized方法的所有其他线程都会阻塞(挂起),直到第一个线程执行完同步方法。(解决了线程干扰问题)

  • 第二,当一个synchronized方法退出时,会与后续的synchronized方法自动建立happens-before关联,在这一点上synchronizedvolatile关键字功能有些类似:确保CPU寄存器或高速缓存内的数据能够及时写回内存,从而保证了对象状态的改变对所有线程是可见的。(解决了内存一致性问题)

  • 第三,synchronized对实例方法(非static方法)加同步锁,锁住的是实例对象(this)。synchronized对类静态方法(static方法)加同步锁,锁住的是Class实例(Xxx.class)。

    // 锁住的是Xxx.classpublic static synchronized void function() {}// 锁住的是this实例对象public synchronized void function() {}

构造方法不能加同步锁,构造方法加synchronized关键字产生语法错误。因为对构造方法加同步锁没有任何意义,两个线程可以同时创建同一个类的两个实例对象,这没有任何影响。

多线程的时候不要在构造方法中将this引用共享出去,可能会出现异常。比如你在构造方法中将this引用添加到集合中:List.add(this),其他的线程就可以从集合中获取这个对象的引用,但这个对象并没有完成初始化,有的字段可能为null,这时就可能会发生空指针异常(也可能会发生其他运行时异常)。

同步代码块

还有一种方式是使用同步代码块的方式,这种方法与synchronized方法在功能上基本一致。不同之处在于:同步代码块可以通过synchronized (xxx)锁住任意对象,另外这种方法能有效的减小同步锁的粒度,避免了对大范围的代码加锁。代码如下:

    static class Counter {        private int c = 0;        public void increment() {            synchronized (this) {                c++;            }        }        public void decrement() {            synchronized (this) {                c--;            }        }        public int value() {            return c;        }    }

注意:如果incrementdecrement方法synchronized锁住的不是同一个对象,就无法实现这两个方法的线程同步。

事实上只要锁住的是同一个对象就可以实现同步:

    static class Counter {        private Object lock = new Object();        private int c = 0;        public void increment() {            synchronized (lock) {                c++;            }        }        public void decrement() {            synchronized (lock) {                c--;            }        }        public int value() {            return c;        }    }
原创粉丝点击