Android开发探索Android开发Android开发经验谈

Java多线程详细介绍

2018-03-30  本文已影响556人  wo883721

线程是程序执行的最小单元,多线程是指程序同一时间可以有多个执行单元运行(这个与你的CPU核心有关)。
在java中开启一个新线程非常简单,创建一个Thread对象,然后调用它的start方法,一个新线程就开启了。

那么执行代码放在那里呢?有两种方式:1. 创建Thread对象时,复写它的run方法,把执行代码放在run方法里。2. 创建Thread对象时,给它传递一个Runnable对象,把执行代码放在Runnable对象的run方法里。

如果多线程操作的是不同资源,线程之间不会相互影响,不会产生任何问题。但是如果多线程操作相同资源(共享变量),就会产生多线程冲突,要知道这些冲突产生的原因,就要先了解java内存模型(简称JMM)。

一. java内存模型(JMM)

1.1 java内存模型(JMM)介绍

java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽样的角度来说:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

  1. 存在两种内存:主内存和线程本地内存,线程开始时,会复制一份共享变量的副本放在本地内存中。
  2. 线程对共享变量操作其实都是操作线程本地内存中的副本变量,当副本变量发生改变时,线程会将它刷新到主内存中(并不一定立即刷新,何时刷新由线程自己控制)。
  3. 当主内存中变量发生改变,就会通知发出信号通知其他线程将该变量的缓存行置为无效状态,因此当其他线程从本地内存读取这个变量时,发现这个变量已经无效了,那么它就会从内存重新读取。

1.2 可见性

从上面的介绍中,我们看出多线程操作共享变量,会产生一个问题,那就是可见性问题: 即一个线程对共享变量修改,对另一个线程来说并不是立即可见的。

   class Data {
    int a = 0;
    int b = 0;
    int x = 0;
    int y = 0;


    // a线程执行
    public void threadA() {
        a = 1;
        x = b;
    }

    // b线程执行
    public void threadB() {
        b = 2; 
        y = a; 
    }
}

如果有两个线程同时分别执行了threadA和threadB方法。可能会出现x==y==0这个情况(当然这个情况比较少的出现)。
因为a和b被赋值后,还没有刷新到主内存中,就执行x = b和y = a的语句,这个时候线程并不知道a和b还已经被修改了,依然是原来的值0。

1.3 有序性

为了提高程序执行性能,Java内存模型允许编译器和处理器对指令进行重排序。重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

class Reorder {
    int x = 0;
    boolean flag = false;

    public void writer() {
        x = 1;
        flag = true;
    }

    public void reader() {
        if (flag) {
            int a = x * x;
            ...
        }

    }
}

例如上例中,我们使用flag变量,标志x变量已经被赋值了。但是这两个语句之间没有数据依赖,所以它们可能会被重排序,即flag = true语句会在x = 1语句之前,那么这么更改会不会产生问题呢?

  1. 在单线程模式下,不会有任何问题,因为writer方法是一个整体,只有等writer方法执行完毕,其他方法才能执行,所以flag = true语句和x = 1语句顺序改变没有任何影响。
  2. 在多线程模式下,就可能会产生问题,因为writer方法还没有执行完毕,reader方法就被另一线程调用了,这个时候如果flag = true语句和x = 1语句顺序改变,就有可能产生flag为true,但是x还没有赋值情况,与程序意图产生不一样,就会产生意想不到的问题。

1.4 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

        x = 1;  // 原子性
        y = x; // 不是原子性
        x = x + 1; // 不是原子性
        x++; // 不是原子性
       System.out.println(x); // 原子性

公式2:有两个原子性操作,读取x的值,赋值给y。公式3:也是三个原子性操作,读取x的值,加1,赋值给x。公式4:和公式3一样。

所以对于原子性操作就两种:1. 将基本数据类型常量赋值给变量。2. 读取基本数据类型的变量值。任何计算操作都不是原子的。

1.5 小结

多线程操作共享变量,会产生上面三个问题,可见性、有序性和原子性。

  1. 可见性: 一个线程改变共享变量,可能并没有立即刷新到主内存,这个时候另一个线程读取共享变量,就是改变之前的值。所以这个共享变量的改变对其他线程并不是可见的。
  2. 有序性: 编译器和处理器会对指令进行重排序,语句的顺序发生改变,这样在多线程的情况下,可能出现奇怪的异常。
  3. 原子性: 只有对基本数据类型的变量的读取和赋值操作是原子性操作。

要解决这三个问题有两种方式:

  1. volatile关键字:它只能解决两个问题可见性和有序性问题,但是如果volatile修饰基本数据类型变量,而且这个变量只做读取和赋值操作,那么也没有原子性问题了。比如说用它来修饰boolean的变量。
  2. 加锁:可以保证同一时间只有同一线程操作共享变量,当前线程操作共享变量时,共享变量不会被别的线程修改,所以可见性、有序性和原子性问题都得到解决。分为synchronized同步锁和JUC框架下的Lock锁。

二. volatile关键字

看过volatile关键字底层实现就知道
我们使用volatile关键字修饰变量,就相当于给这个变量添加加了内存屏障。那么内存屏障的作用是什么呢?

  1. 它会让本地内存共享变量副本无效,即修改了这个共享变量,它会被强制刷新到主内存。读取这个共享变量,会强制从主内存中读取最新值。因此解决了可见性问题。
  2. 禁止指令重排序,即在程序中在volatile变量进行操作时,在其之前的操作肯定已经全部执行了,而且结果已经对后面的操作可见,在其之后的操作肯定还没有执行。因此解决了有序性问题。
    这个的具体解释,大家请看《深入理解Java内存模型》里面关于happens-before规则的讲解。
class VolatileFeaturesExample {

    //使用volatile声明一个基本数据类型变量vl
    volatile long vl = 0L;

    //对于单个volatile基本数据类型变量赋值
    public void set(long l) {
        vl = l;
    }

    //对于单个volatile基本数据类型变量的复合操作
    public void getAndIncrement () {
        vl++;
    }

    //对于单个volatile基本数据类型变量读取
    public long get() {
        return vl;
    }

}

class VolatileFeaturesExample {
    //声明一个基本数据类型变量vl
    long vl = 0L;

    // 相当于加了同步锁
    public synchronized void set(long l) {
      vl = l;
    }

    // 普通方法
    public void getAndIncrement () {
        long temp = get();
        temp += 1L;
        set(temp);
    }

    // 相当于加了同步锁
    public synchronized long get() {
        return vl;
    }

}

如果volatile修饰基本数据类型变量,而且只对这个变量做读取和赋值操作,那么就相当于加了同步锁。

三. synchronized同步锁

synchronized同步锁作用是访问被锁住的资源时,只要获取锁的线程才能操作被锁住的资源,其他线程必须阻塞等待。
所以一个线程来说,可以阻塞等待,可以运行,那么线程到底有哪些状态呢?

3.1 线程状态

在Thread类中,有一个枚举对象State标志着所有的线程状态。

// 标志线程状态的枚举对象
public enum State {
    /**
     * 新建状态。当创建一个线程Thread对象,但是还没有调用它的start方法,就是这个状态。
      */
    NEW,

    /**
     * 运行状态。当前线程正在运行中
      */
    RUNNABLE,

    /**
     * 阻塞状态。
     * 一般是锁资源被另一线程持有,当前线程处于阻塞等待获取锁的状态,
     * 当线程获取了锁,并获取CPU执行权,就会从BLOCKED状态转成RUNNABLE状态。
     *
      */
    BLOCKED,

    /**
     * 等待状态。调用三个方法当前线程会进人这个状态:
     * 1. Object#wait() 方法
     * 2. #join() 方法 (这个方法在Thread对象中,本质上也是调用wait()方法)
     * 3. LockSupport#park() 方法
     * 这三个方法调用时都没有传递时间参数,所以没有超时限制。
     * WAITING状态的线程是处于线程等待池中,只有调用对应的唤醒方法,才能将当前线程从线程等待池中唤醒,
     * 否则线程一直等待。除非发生中断请求,也会将线程唤醒。
     * 唤醒线程的方法有:
     * 1. Object#notify() notifyAll()
     * 2. LockSupport#unpark()
     * 注意join()是线程对象的wait()方法实现的,当线程执行完毕时,会调用自己的notifyAll()方法,
     * 唤醒等待池中所有的线程。
     *
     * 还有要注意的是Object#wait() 方法只能在synchronized代码块中调用,
     * 所以当线程被唤醒时,它并不是处于可运行状态,而是处于BLOCKED状态,
     * 因为只有获取锁的线程,才能执行synchronized代码块中的代码,所以被唤醒的线程要等待锁。
     *
     * 而LockSupport#park()没有这个方面的限制
     *
     */
    WAITING,


    /**
     * 等待超时状态,调用下面五个方法当前线程会进人这个状态:
     * 1. Object#wait(long)
     * 2. #join(long) Thread.join,就是使用wait方法实现的。
     * 3. LockSupport#parkNanos
     * 4. LockSupport#parkUntil
     * 5. Thread#sleep
     *
     * 与WAITING状态相比较,当线程处于线程等待池中,如果没有调用对应的唤醒方法,
     * 但是超出规定时间,那么线程自动会被唤醒。所以就是多出了一种唤醒方式。
     * 注意Thread#sleep 没有对应的唤醒方法。
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    // 终止状态,当线程运行完毕时,就处于这个状态,而且该状态不能再转换成其他状态。
    TERMINATED;
}

线程一共有六种状态:

  1. NEW: 新建状态。当创建一个线程Thread对象,但是还没有调用它的start方法,就是这个状态。
  2. RUNNABLE: 运行状态。当前线程正在运行中。
  3. BLOCKED: 阻塞状态。当前线程正在等待锁资源。
  4. WAITING: 等待状态。当前线程处于线程等待池中,需要被唤醒。
  5. TIMED_WAITING: 等待超时状态。与WAITING状态相比,多了一种超时会被自动唤醒的方法。
  6. TERMINATED: 终止状态,当线程运行完毕时,就处于这个状态,而且该状态不能再转换成其他状态。

注意处于等待状态的线程只有两种方式被唤醒:

  1. 调用对应的唤醒方法。
  2. 调用该线程变量的interrupt()方法,会唤醒该线程,并抛出InterruptedException异常。

3.2 synchronized同步方法或者同步块

synchronized同步方法或者同步块具体是怎样操作的呢?

  1. 相当于有一个大房间,房间门上有一把锁lock,房间里面存放的是所有与这把锁lock关联的同步方法或者同步块。
  2. 当某一个线程要执行这把锁lock的一个同步方法或者同步块时,它就来到房间门前,如果发现锁lock还在,那么它就拿着锁进入房间,并将房间锁上,它可以执行房间中任何一个同步方法或者同步块。
  3. 这时又有另一个线程要执行这把锁lock的一个同步方法或者同步块时,它就来到房间门前,发现锁lock没有了,就只能在门外等待,此时该线程就在synchronized同步阻塞线程池中。
  4. 等到拿到锁lock的线程,同步方法或者同步块代码执行完毕,它就会从房间中退出来,将锁放到门上。
  5. 这时在门外等待的线程就争夺这把锁lock,拿到锁的线程就可以进入房间,其他线程则又要继续等待。

注:synchronized 锁是锁住所有与这个锁关联的同步方法或者同步块。

synchronized的同步锁到底是什么呢?

其实就是java对象,在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

3.3 wait与notify、notifyAll

这三个方法主要用于实现线程之间相互等待的问题。

调用对象lock的wait方法,会让当前线程进行等待,即将当前线程放入对象lock的线程等待池中。调用对象lock的notify方法会从线程等待池中随机唤醒一个线程,notifyAll方法会唤醒所有线程。
注:对象lock的wait与notify、notifyAll方法调用必须放在以对象lock为锁的同步方法或者同步块中,否则会抛出IllegalMonitorStateException异常。

wait与notify、notifyAll具体是怎么操作的呢?

  1. 前面过程与synchronized中介绍的一样,当调用锁lock的wait方法时,该线程(即当前线程)退出房间,归还锁lock,但并不是进入synchronized同步阻塞线程池中,而是进入锁lock的线程等待池中。
  2. 这时另一个线程拿到锁lock进行房间,如果它执行了锁lock的notify方法,那么就会从锁lock的线程等待池中随机唤醒一个线程,将它放入synchronized同步阻塞线程池中(记住只有拿到锁lock的线程才能进行房间)。调用锁lock的notifyAll方法,即唤醒线程等待池所有线程。

使用wait与notify、notifyAll方法时,有两点需要注意:

  1. wait与notify、notifyAll方法这三个方法必须在synchronized同步代码块中执行,否则抛出IllegalMonitorStateException异常。
  2. 所以当我们使用notify、notifyAll方法唤醒等待的线程时,该线程不能立即执行,因为它在synchronized同步代码块中,所以必须获取锁,才能继续执行。

四. 其他重要方法

4.1 join方法

让当前线程等待另一个线程执行完成后,才继续执行。

    public final void join() throws InterruptedException {
        join(0);
    }
    public final synchronized void join(long millis) throws InterruptedException {
        // 获取当前系统毫秒数
        long base = System.currentTimeMillis();
        long now = 0;

        // millis小于0,抛出异常
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            // 通过isAlive判断当前线程是否存活
            while (isAlive()) {
                // wait(0)表示当前线程无限等待
                wait(0);
            }
        } else {
            // 通过isAlive判断当前线程是否存活
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                // 当前线程等待delay毫秒,超过时间,当前线程就被唤醒
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

join方法是Thread中的方法,synchronized方法同步的锁对象就是Thread对象,通过调用Thread对象的wait方法,让当前线程等待

注意:这里是让当前线程等待,即当前调用join方法的线程,而不是Thread对象的线程。那么当前线程什么时候会被唤醒呢?
当Thread对象线程执行完毕,进入死亡状态时,会调用Thread对象的notifyAll方法,来唤醒Thread对象的线程等待池中所有线程。

示例:

      public static void joinTest() {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":  i==="+i);
                }
            }
        }, "t1");
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+": end");
    }

4.2 sleep方法

只是让当前线程等待一定的时间,然后继续执行。

4.3 yield方法

将当前线程状态从运行状态转成可运行状态,如果再获取CPU执行权,还会继续执行。

4.4 interrupt方法

它会中断处于WAITING和TIMED_WAITING状态下的线程,而对其他状态下的线程不起任何作用。

示例:

    public static void interruptTest() {
        // 处于TIMED_WAITING状态下的线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName()+" 开始");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+" 结束");
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+" 产生异常");
                }
            }
        }, "t1");
        thread.start();

        // 处于运行状态下的线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+" 开始");
                int i = 0;
                while(i < Integer.MAX_VALUE - 10) {
                    i = i + 1;
                    for (int j = 0; j < i; j++);
                }
                System.out.println(Thread.currentThread().getName()+" i=="+i);
                System.out.println(Thread.currentThread().getName()+" 结束");
            }
        }, "t2");
        thread1.start();

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" 进行中断");
        thread.interrupt();
        thread1.interrupt();
    }

4.5 isInterrupted方法

返回这个线程的中断标志位。注意当调用线程的interrupt方法后,该线程的isInterrupted的方法就会返回true。如果异常被处理了,又会将该标志位置位false,即isInterrupted的方法返回false。

4.6 线程优先级以及守护线程

在java中线程优先级范围是1~10,默认的优先级是5。
在java中线程分为用户线程和守护线程,isDaemon返回是true,表示它是守护线程。当所有的用户线程执行完毕后,java虚拟机就会退出,不管是否还有守护线程未执行完毕。
当创建一个新线程时,这个新线程的优先级等于创建它线程的优先级,且只有当创建它线程是守护线程时,新线程才是守护线程。
当然也可以通过setPriority方法修改线程的优先级,已经setDaemon方法设置线程是否为守护线程。

五. synchronized同步锁与lock锁

synchronized同步锁与lock独占锁都可以保证并发操作安全问题,即保证同一时间只有获取锁的那个线程才可以运行,其他线程必须等待。

关于lock独占锁请阅读我的AQS详细介绍ReentrantLock详细分析相关文章。

那么它们有什么异同点呢?

5.1 获取锁的方式不同

  1. 对于synchronized同步锁:进入synchronized代码块中的线程,会自动获取锁,而其他线程就只能阻塞等待。
  2. 对于lock锁:想要获取lock锁,必须调用lock的lock系列方法,根据方法不同获取锁的方式也不同。
    // 获取锁,如果获取不到,就一直等待。不响应中断请求
    void lock();

    // 获取锁,如果获取不到,就一直等待。如果在线程等待期间有中断请求就抛出异常
    void lockInterruptibly() throws InterruptedException;

    // 尝试获取锁,立即返回。返回true表示获取成功,返回false表示获取失败
    boolean tryLock();

    // 在规定的unit时间内获取锁,如果时间到了还没有获取到锁,则返回false,表示获取失败
    // 如果在线程等待期间有中断请求就抛出异常
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

5.2 释放锁的方式不同

  1. 当synchronized代码块执行完成,或者抛出异常返回,都会自动释放锁,不需要用户手动释放。
  2. 如果执行完成,用户必须主动调用unlock来释放锁,否则等待锁的线程就会一直阻塞。为了防止发生异常,导致unlock方法没有执行,所以这个方法必须放在finally的代码块中。

5.3 等待锁的线程状态不一样

  1. 等待synchronized同步锁线程的状态是 BLOCKED(阻塞状态)。
  2. 等待lock锁线程的状态是WAITING(等待状态)或TIMED_WAITING(等待超时状态)。

记得我们在线程状态中介绍过,处于WAITING与TIMED_WAITING状态的线程,是可以响应线程中断的。而处于BLOCKED状态的线程则不可以。

如果获取synchronized锁的线程一直不释放锁,那么等待锁的线程只能一直等待,而获取lock锁的线程一直不释放锁,我们可以调用等待锁的线程的interrupt()方法,将这个线程唤醒。

其实Lock锁中线程等待和唤醒主要是通过LockSupport类实现的,关于LockSupport请看JUC锁框架_ LockSupport详细分析这篇文章。

六. 实例讲解

6.1 不加任何同步锁

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;

class Data {
    int num;

    public Data(int num) {
        this.num = num;
    }

    public int getAndDecrement() {
        return num--;
    }
}

class MyRun implements Runnable {

    private Data data;
    // 用来记录所有卖出票的编号
    private List<Integer> list;
    private CountDownLatch latch;

    public MyRun(Data data, List<Integer> list, CountDownLatch latch) {
        this.data = data;
        this.list = list;
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            action();
        }  finally {
            // 释放latch共享锁
            latch.countDown();
        }
    }

    // 进行买票操作,注意这里没有使用data.num>0作为判断条件,直到卖完线程退出。
    // 那么做会导致这两处使用了共享变量data.num,那么做多线程同步时,就要考虑更多条件。
    // 这里只for循环了5次,表示每个线程只卖5张票,并将所有卖出去编号存入list集合中。
    public void action() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int newNum = data.getAndDecrement();

            System.out.println("线程"+Thread.currentThread().getName()+"  num=="+newNum);
            list.add(newNum);
        }
    }
}

public class ThreadTest {


    public static void startThread(Data data, String name, List<Integer> list,CountDownLatch latch) {
        Thread t = new Thread(new MyRun(data, list, latch), name);
        t.start();
    }

    public static void main(String[] args) {
        // 使用CountDownLatch来让主线程等待子线程都执行完毕时,才结束
        CountDownLatch latch = new CountDownLatch(6);

        long start = System.currentTimeMillis();
        // 这里用并发list集合
        List<Integer> list = new CopyOnWriteArrayList();
        Data data = new Data(30);
        startThread(data, "t1", list, latch);
        startThread(data, "t2", list, latch);
        startThread(data, "t3", list, latch);
        startThread(data, "t4", list, latch);
        startThread(data, "t5", list, latch);
        startThread(data, "t6", list, latch);


        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 处理一下list集合,进行排序和翻转
        Collections.sort(list);
        Collections.reverse(list);
        System.out.println(list);

        long time = System.currentTimeMillis() - start;
        // 输出一共花费的时间
        System.out.println("\n主线程结束 time=="+time);
    }
}

输出的结果是

线程t2  num==29
线程t6  num==27
线程t5  num==28
线程t4  num==28
线程t1  num==30
线程t3  num==30
线程t2  num==26
线程t4  num==24
线程t6  num==25
线程t5  num==23
线程t1  num==22
线程t3  num==21
线程t4  num==20
线程t6  num==19
线程t5  num==18
线程t2  num==17
线程t1  num==16
线程t3  num==15
线程t4  num==14
线程t5  num==12
线程t6  num==13
线程t1  num==9
线程t3  num==10
线程t2  num==11
线程t1  num==8
线程t6  num==5
线程t2  num==7
线程t5  num==3
线程t3  num==4
线程t4  num==6
[30, 30, 29, 28, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3]

主线程结束 time==62

从结果中发现问题,出现了重复票,所以30张票没有被卖完。最主要的原因就是Data类的getAndDecrement方法操作不是多线程安全的。

  1. 首先它不能保证原子性,分为三个操作,先读取num的值,然后num自减,在返回自减前的值。
  2. 因为num不是volatile关键字修饰的,它也不能保证可见性和有序性。

所以只要保证getAndDecrement方法多线程安全,那么就可以解决上面出现的问题。那么保证getAndDecrement方法多线程安全呢?最简单的方式就是在getAndDecrement方法前加synchronized关键字。

这是synchronized关键锁就是这个data对象实例,所以保证了多线程调用getAndDecrement方法时,只有一个线程能调用,等待调用完成,其他线程才能调用getAndDecrement方法。
因为同一时间只有一个线程调用getAndDecrement方法,所以它在做num--操作时,不用担心num变量会发生改变。所以原子性、可见性和有序性都可以得到保证。

6.2 使用最小同步锁

class Data {
    int num;

    public Data(int num) {
        this.num = num;
    }
    // 将getAndDecrement方法加了同步锁
    public synchronized int getAndDecrement() {
        return num--;
    }
}

输出结果

线程t1  num==30
线程t2  num==29
线程t6  num==28
线程t4  num==26
线程t3  num==27
线程t5  num==25
线程t6  num==22
线程t2  num==21
线程t3  num==23
线程t1  num==24
线程t4  num==20
线程t5  num==19
线程t2  num==18
线程t3  num==17
线程t5  num==13
线程t4  num==14
线程t6  num==16
线程t1  num==15
线程t2  num==12
线程t4  num==9
线程t1  num==7
线程t5  num==10
线程t3  num==11
线程t6  num==8
线程t4  num==6
线程t2  num==3
线程t1  num==2
线程t3  num==4
线程t5  num==5
线程t6  num==1
[30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

主线程结束 time==61

我们只是将Data的getAndDecrement方法加了同步锁,发现解决了多线程并发问题。主要是因为我们只在一处使用了共享变量num,所以只需要将这处加同步就行了。而且你会发现最后花费的总时间与没加同步锁时几乎一样,那么因为我们同步代码足够小。
相反地,我们加地同步锁不合理,可能也能实现多线程安全,但是耗时就会大大增加。

6.3 不合理地使用同步锁

@Override
    public void run() {
        try {
            synchronized (data){
                action();
            }
        }  finally {
            // 释放latch共享锁
            latch.countDown();
        }
    }

输入结果:

线程t1  num==30
线程t1  num==29
线程t1  num==28
线程t1  num==27
线程t1  num==26
线程t6  num==25
线程t6  num==24
线程t6  num==23
线程t6  num==22
线程t6  num==21
线程t5  num==20
线程t5  num==19
线程t5  num==18
线程t5  num==17
线程t5  num==16
线程t4  num==15
线程t4  num==14
线程t4  num==13
线程t4  num==12
线程t4  num==11
线程t3  num==10
线程t3  num==9
线程t3  num==8
线程t3  num==7
线程t3  num==6
线程t2  num==5
线程t2  num==4
线程t2  num==3
线程t2  num==2
线程t2  num==1
[30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

主线程结束 time==342

在这里我们将整个action方法,放入同步代码块中,也可以解决多线程冲突问题,但是所耗费的时间是在getAndDecrement方法上加同步锁时间的几倍。

所以我们在加同步锁的时候,那些需要同步,就是看那些地方使用了共享变量。比如这里只在getAndDecrement方法中使用了同步变量,所以只要给它加锁就行了。
但是如果在action方法中,使用data.num>0来作为循环条件,那么在加同步锁时,就必须将整个action方法放在同步模块中,因为我们必须保证,在data.num>0判断到getAndDecrement方法调用这些代码都是在同步模块中,不然就会产生多线程冲突问题。

上一篇下一篇

猜你喜欢

热点阅读