Java并发编程学习三:线程同步的关键字以及理解

上篇文章中介绍了Java线程的带来的问题与内存模型中介绍了线程可能会引起的问题以及对应Java的内存模型,顺带介绍了Volatile和Sychronized关键字。今天对Java中涉及到的常见的关键类和关键字进行一个总结。html


Volatile

与锁相比,Volatile提供了一种更加轻量级的同步机制,使用Volatile的变量在多线程中是不会发生上下文切换或者线程调度等操做的。当一个变量定义成为一个Volatile的时候,这个变量具有了两种特性:java

  • 第一是保证了该变量对全部线程的可见性。
  • 第二是禁止指令重排序优化。

Volatile变量不会缓存在工做内存(对应物理寄存器)当中,在线程A中修改了一个共享变量的值,修改后当即从A的工做内存中同步给了主内存更新值,同时其余线程每次使用该共享变量值时,保证从主内存中获取。不过Volatile也有必定的局限性,虽然提供了类似的可见性保证,但不能用于构建原子的复合操做,所以当一个变量依赖其余变量,或者当前变量依赖与旧值时候,就不能使用Volatile变量,由于Volatile不保证代码的原子性。最多见的就是自增操做的问题。编程

因为Java中的运算并不是原子操做,因此在多线程的状况下进行运算同样是不安全的。示例Demo以下:缓存

class ThreadTest {


    private volatile int count = 0;

    public void update() {

        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(() -> {
                for (int k = 0; k < 100; k++) {
                    count++;
                }

            });
            thread.start();
        }
        try {
            Thread.sleep(5000);
            System.out.println(count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

上面代码获取到的值基本上都是小于5000的,由于count++在执行过程当中分三步进行,首先从主存中复制count到工做内存中,工做内存中将count+1,而后在再刷新回主存。因此存在的问题是当一个进行前两步的时候,其余的线程已经刷新最新值回主存了,那么当前线程再刷新回主存的时候形成了值变小的问题。安全

Volatile最多见的场景就是在线程中充当flag变量的标志,如提供一个方法进行终止线程:多线程

class ThreadTest extends Thread {


    private volatile boolean isCancle;

    public void setCancle(boolean isCancle) {
        this.isCancle = isCancle;
    }

    @Override
    public void run() {
        super.run();
        while (!isCancle) {

        }
        System.out.println("over");
    }
}

当调用setCancle(...)的时候可以立马结束while循环,从而打印出over。并发

第二个,使用Volatile可以禁止指令重排序的优化。在Java线程的带来的问题与内存模型(JMM)中咱们解释了指令重排序的概念,那么在Java中能够经过Volatile关键字添加内存屏障,从而实现禁止指令重排序的优化,关于Volatile禁止指令重排序的一个在经典的案例就是DCL中的使用:app

public class DoubleCheckedLocking {            
    private static Instance instance;              

    public static Instance getInstance() {           
        if (instance == null) {                        
            synchronized (DoubleCheckedLocking.class) {  
                if (instance == null)                 
                    instance = new Instance();        
            }                                     
        }                                                
        return instance;                              
    }                                          
}

在DCL没添加Volatile的版本中,在new Instance()该句中会出现问题,因为new Instance()不是一个原子操做,其操做分为以下过程:ide

  1. 为Instance对象初始化内存空间.
  2. 初始化Instance对象.
  3. 将Instance对象赋值给instance引用.

因为重排序的存在,编译器能够将2,3顺序进行重排序优化:性能

  1. 为Instance对象初始化内存空间.
  2. 将Instance对象赋值给instance引用.
  3. 初始化Instance对象.

当线程A再进行new Instance()时候,此时正好执行到第2个步骤,这时候线程B进行判断instance是否为null,发现instance引用不为空,那么就直接返回了,然而线程A还没初始化Instance对象,这就形成了线程B引用了一个未初始化的引用,那么天然会有问题。解决方案就是为instance变量添加volatile关键字,保证禁止指令的重排序,程序就正确了。

关于DCL更详细的内容能够阅读如下这篇文章

最后总结一下Volatile使用的场景:

  • 对变量的写入操做不依赖变量当前值,或者你能保证只有单个线程更新变量的值。
  • 该变量不会与其余状态变量一块儿归入不变性条件。
  • 在访问变量时候不须要加锁。

Synchronized

Java中最多见到的同步机制就是Synchronized关键字了,通常状况下,若是对性能的要求不是那么的苛刻,经过Sychronized关键字基本上可以解决全部的线程同步问题。通常使用Synchronized方式有以下几种:

  • 在静态方法中添加Synchronized
  • 在实例方法中添加Synchronized
  • 对某个对象添加Synchronized
  • 对Class添加Synchronized

在静态方法中添加Synchronized的方式和对Class添加Synchronized的本质上是同样的,都是是持有对应的class的锁,示例以下:

public class Test{
    private static int num=2;

    public static void main(String[] args){
        
    }
    public static synchronized void increaseNum(){
        num++;
        System.out.println("调用increaseNum,当前值为:"+num);
    }

    public void increseNum2(){
        synchronized(Test.class){
            num++;
             System.out.println("调用increseNum2,当前值为:"+num);
        }
    }
}

在实例方法中添加Synchronized本质上是持有了当前对象实例的锁,示例代码以下:

public synchronized void increseNum3(){
            num++;
            System.out.println("调用increseNum3,当前值为:"+num);
        
    }

对某个对象添加Synchronized本质上是对持有了当前对象的锁,示例代码以下:

public  void increseNum4(){
        synchronized (object) {
            num++;
            System.out.println("调用increseNum4,当前值为:"+num);
        }
    }

上面代码中持有了object对象的锁。

Synchronized称之为互斥锁,使用Synchronized可以保证代码段的可见性和原子性,多线程操做中在某一个线程A得到互斥锁的时候,其余线程只能等待而阻塞等待A的执行完毕后再竞争锁资源。除此以外,使用Synchronized时候具有了可重入性,即一个线程获取了互斥锁以后,该线程其余的声明了Synchronized的,若是被调用了,而且是同一个锁的代码段,则是不须要阻塞,可以一并执行的。示例代码以下:

public  void increseNum4(){
        synchronized (object) {
            num++;
            increseNum5();
            System.out.println("调用increseNum4,当前值为:"+num);
        }
    }
    public  void increseNum5(){
        synchronized (object) {
            num++;
            System.out.println("调用increseNum5,当前值为:"+num);
        }
    }

能够看到,在increseNum4()方法中咱们是有了object对象的锁,其内部中调用了increseNum5()方法,因为increseNum5()中持有相同的object对象锁,因此方法能够等同理解为:

public  void increseNum4(){
        synchronized (object) {
            num++;
            increseNum5();
            System.out.println("调用increseNum4,当前值为:"+num);
        }
    }
    public  void increseNum5(){
            num++;
            System.out.println("调用increseNum5,当前值为:"+num);
    }

若是咱们修改increseNum5()中的Synchronized的修饰,改为以下:

public  void increseNum4(){
        synchronized (object) {
            num++;
            increseNum5();
            System.out.println("调用increseNum4,当前值为:"+num);
        }
    }
    public synchronized  void increseNum5(){
            num++;
            System.out.println("调用increseNum5,当前值为:"+num);
    }

那么因为上述两个方法持有不一样的锁,若是increseNum5()不被其余线程使用锁定,那么可以正常执行;反之,increseNum4()方法必须等到increseNum5()的线程执行完毕后释放对应的锁后才可以继续执行代码段。

上篇文章Java并发编程学习二中讲述了底层中JVM针对工做内存与主存的8种交互操做时讲述了一个规则:**一个变量在同一时刻只容许一条线程进行lock操做,但lock操做能够被同一线程重复执行屡次,屡次执行lock后,只有执行相同次数的unlock操做,变量才会被解锁。**lock跟unlock操做咱们没法直接操做,取而代之的是关键字monitorenter和monitorexit,这个也在上篇文章中举例说过了,这里也不过多叙述。

Java中的同步实现跟操做系统中的管程(监视器,monitor)有关,管程是操做系统实现同步的重要基础概念。关于对应的介绍能够看下这个维基百科的[介绍](https://zh.wikipedia.org/wiki/%E7%9B%A3%E8%A6%96%E5%99%A8_(%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96)。关于更加深刻的知识点,能够仔细阅读这篇文章,这里对底层Synchronized实现作个总结:

  • 经过在方法中添加Synchronized关键字实现方法同步的,该方法在常量池结构中会标记上ACC_SYNCHRONIZED用于表示隐式同步,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有monitor, 而后再执行方法,最后在方法完成(不管是正常完成仍是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其余任何线程都没法再得到同一个monitor。若是一个同步方法执行期间抛 出异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放。
  • 经过Synchronized关键字修饰代码块的,在字节码中会添加monitorenter 和 monitorexit 指令保证同步操做,其中其中monitorenter指令指向同步代码块的开始位置,而monitorexit表示同步代码块结束的位置(异常和非异常,因此字节码中一个monitorenter都会对应两个monitorexit)。每一个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,当一个线程A持有了monitor后,会在对应对象的对象头之间记录持有的信息,其余线程要获取时候,则会阻塞。当monitorexit执行后,A解除持有monitor,其余线程则继续竞争锁资源。

Lock和ReentrantLock

在Java5.0以前只有Synchronized和Volatile使用,在5.0以后增长了Lock接口,可以实现Synchronized的全部工做,而且除此以外拥有Synchronized不具备的以下特性:

  • 调用更灵活,须要主动申请/释放锁。
  • 提供中断操做。(Synchronized是不响应中断的)
  • 提供超时检测操做。(Synchronized是不提供的)

总而言之,Lock接口比Synchronized更加灵活的控制空间,当Synchronized不能知足咱们的需求的时候,能够尝试的考虑使用该接口的实现类,最多见的实现类就是ReentrantLock了,下面就以ReentrantLock做为Demo例子学习。这里首先先介绍一下Lock接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()方法的使用跟Synchronized关键字一致,若是当前monitor没有被占用,则得到monitor,其余线程会一直阻塞,直到调用lock()的线程调用unlock()方法,,示例代码以下:

class ThreadTest {
    private static int num = 1;
    private Lock mLock = new ReentrantLock();

    public void increaseNum() {
        try {
            mLock.lock();
            num++;
            System.out.println(timeStamp2Date() + "  调用increaseNum,当前值为:" + num);
            Thread.sleep(4000);
        } catch (Exception e) {

        } finally {
            mLock.unlock();
        }


    }

    public static String timeStamp2Date() {
        String format = "yyyy-MM-dd HH:mm:ss";
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(new Date(System.currentTimeMillis()));
    }


    public void increaseNum2() {
        try {
            mLock.lock();
            num++;
            System.out.println(timeStamp2Date() + "  调用increaseNum2,当前值为:" + num);
        } catch (Exception e) {

        } finally {
            mLock.unlock();
        }
    }
}
//-------------------------------------------------------

fun main(args: Array<String>) {
    val threadTest = ThreadTest()
    val thread1 = Thread(Runnable {
        threadTest.increaseNum()
    })
    val thread2 = Thread(Runnable {
        threadTest.increaseNum2()
    })
    thread1.start()
    thread2.start()
}
//------------------------------------------------
//输出结果
2018-11-19 16:06:10  调用increaseNum,当前值为:2
2018-11-19 16:06:14  调用increseNum2,当前值为:3

increaseNum()中模拟了4秒的耗时操做,能够看到在结果中increaseNum2()确实等待了4秒左右的时间才进行了调用,调用的方式跟Synchronized一模一样,只不过增长了手动释放的代码。

接下来看看tryLock方法:

  • tryLock()方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不到锁时不会一直在那等待
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不过区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。

接下来仍是代码测试,首先测试一下传递无参的:

public void increaseNum() {
        if (mLock.tryLock()) {
            try {
                num++;
                System.out.println(timeStamp2Date() + "  调用increaseNum,当前值为:" + num);
                Thread.sleep(4000);
            } catch (Exception e) {

            } finally {
                mLock.unlock();
            }

        } else {
            System.out.println(timeStamp2Date() + "  increaseNum 获取锁失败");
        }
    }

    public static String timeStamp2Date() {
        String format = "yyyy-MM-dd HH:mm:ss";
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(new Date(System.currentTimeMillis()));
    }


    public void increaseNum2() {
        if (mLock.tryLock()) {
            try {
                num++;
                System.out.println(timeStamp2Date() + "  调用increaseNum2,当前值为:" + num);
            } catch (Exception e) {

            } finally {
                mLock.unlock();
            }
        } else {
            System.out.println(timeStamp2Date() + "  increaseNum2 获取锁失败");
        }
    }
	
	------------------------------------------------
	fun main(args: Array<String>) {
    val threadTest = ThreadTest()
    val thread1 = Thread(Runnable {
        threadTest.increaseNum()
    })
    var thread2 = Thread(Runnable {
        threadTest.increaseNum2()
    })
    thread2.start()
    thread1.start()
    Thread.sleep(5000)
    thread2 = Thread(Runnable {
        threadTest.increaseNum2()
    })
    thread2.start()
}
//输出结果
2018-11-19 16:37:09  increaseNum 获取锁失败
2018-11-19 16:37:09  调用increaseNum2,当前值为:2
2018-11-19 16:37:14  调用increaseNum2,当前值为:3

接着测试有形参的:

public void increaseNum2(int time) {
        try {
            if (mLock.tryLock(time, TimeUnit.SECONDS)) {
                try {
                    num++;
                    System.out.println(timeStamp2Date() + "  调用increaseNum2,当前值为:" + num);
                } catch (Exception e) {

                } finally {
                    mLock.unlock();
                }
            } else {
                System.out.println(timeStamp2Date() + "  increaseNum2 获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
------------------------------------------------------
fun main(args: Array<String>) {
    val threadTest = ThreadTest()
    val thread1 = Thread(Runnable {
        threadTest.increaseNum()
    })
    var thread2 = Thread(Runnable {
        threadTest.increaseNum2(2)
        threadTest.increaseNum2(4)
    })
    thread1.start()
    thread2.start()
}
//输出结果
2018-11-19 16:43:46  调用increaseNum,当前值为:2
2018-11-19 16:43:48  increaseNum2 获取锁失败
2018-11-19 16:43:50  调用increaseNum2,当前值为:3

第一次调用increaseNum2()的时候因为在2秒的时间内increaseNum()尚未释放掉锁,因此获取锁失败;接着第二次调用increaseNum2()的时候,锁已经释放了,因此正常获取到。

除此以外,经过调用 tryLock(long time, TimeUnit unit)方法,可以抛出InterruptedException异常,因此可以正常响应中断操做,即thread.interrupt(),这是Synchronized没法作到的。

与上面方法相同的是lockInterruptibly()也可以正常响应中断操做,方法的描述以下(摘抄来自该篇文章):

  • 请求锁,除非当前线程被中断。
  • 若是没有其余线程持有锁,则当前线程获取到锁,并为锁计数加1,而且当即返回。
  • 若是当前线程已经持有锁,则为锁计数加1,并当即返回。
  • 若是其余线程持有锁,则当前线程将处于不可用状态以达到于线程调度目的,而且休眠直到下面两个事件中的一个发生:
    1. 当前线程获取到锁。
    2. 其余线程中断当前线程。
  • 若是当前线程获取到锁,则将锁计数设置为1。
  • 若是当前线程在方法条目上设置了中断状态或者在请求锁的时候被中断,将抛出中断异常。

关于这个方法的用法和理解就比较复杂了,lockInterruptibly()自己抛出InterruptedException异常,能够类比Thread.sleep()方法,这样就比较好理解了。下面简单给个Demo测试一下:

public void increaseNum3() {
        boolean flag = false;
        try {
            mLock.lockInterruptibly();
            flag = true;
        } catch (InterruptedException e) {
            System.out.println("中断发生");
        } finally {
            if (flag) {
                mLock.unlock();
            }
        }
    }
    public void increaseNum() {
        if (mLock.tryLock()) {
            try {
                num++;
                System.out.println(timeStamp2Date() + "  调用increaseNum,当前值为:" + num);
                Thread.sleep(4000);
            } catch (Exception e) {

            } finally {
                mLock.unlock();
            }

        } else {
            System.out.println(timeStamp2Date() + "  increaseNum 获取锁失败");
        }
    }
----------------------------------------------------------------
fun main(args: Array<String>) {
    val threadTest = ThreadTest()
    val thread2 = Thread(Runnable {
        threadTest.increaseNum()
    })
    thread2.start()
    val thread1 = Thread(Runnable {
        threadTest.increaseNum3()
    })
    thread1.start()
    Thread.sleep(2000)
    thread1.interrupt()
}
//结果
2018-11-19 17:25:39  调用increaseNum,当前值为:2
中断发生

上述代码thread2在increaseNum()方法中获取到了mLock的锁,因此在thread1调用increaseNum3()时候阻塞了,过了两秒后因为在主线程调用了thread1.interrupt(),因此increaseNum3()中抛出了异常,打印出了中断发生的log。这里只是简单验证了一下一种状况,更多种能够自主测试一下。


wait/notify

最后一个就是wait/notify机制了,wai()方法介绍以下:

  • wait()方法的做用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程.
  • wait(long timeout),该方法与wait()方法相似,惟一的区别就是在指定时间内,若是没有notify或notifAll方法的唤醒,也会自动唤醒。

wait方法是一个本地方法,其底层也是经过monitor对象来完成的,因此咱们使用wait/notify机制时候必须跟Synchronized一块儿使用。除了这个,在线程的概念以及使用文章中还说过:

这里须要区分sleep和wait的区别,wait和notify方法跟sychronized关键字一块儿配套使用,wait()方法在进入等待状态的时候,这个时候会让度出cpu资源让其余线程使用,与sleep()不一样的是,这个时候wait()方法是不占有对应的锁的。

在使用wait方法时候,最好使用以下模板:

synchronized (obj) {
     while (<condition does not hold>)
           obj.wait(timeout);
          ... // Perform action appropriate to condition
     }

关于wait/notify的例子,这里就贴一个单生产者-单消费者模型的Demo吧:

private static final int MAX_NUM = 10;
    private static final Object lock = new Object();
    static ArrayList<String> list = new ArrayList<>();
    public static class ProductThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (lock) {
                    while (list.size() > MAX_NUM) {

                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    list.add("h");
                    System.out.println(getName() + ": 生产者生产一个元素");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify();
                }

            }


        }
    }

    public static class ConsumerThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (lock) {
                    while (list.size() == 0) {

                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    list.remove(0);
                    System.out.println(getName() + ": 消费者消费一个元素");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify();
                }

            }

        }
    }

参考资料

相关文章
相关标签/搜索