上篇文章中介绍了Java线程的带来的问题与内存模型中介绍了线程可能会引起的问题以及对应Java的内存模型,顺带介绍了Volatile和Sychronized关键字。今天对Java中涉及到的常见的关键类和关键字进行一个总结。html
与锁相比,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
因为重排序的存在,编译器能够将2,3顺序进行重排序优化:性能
当线程A再进行new Instance()
时候,此时正好执行到第2个步骤,这时候线程B进行判断instance是否为null,发现instance引用不为空,那么就直接返回了,然而线程A还没初始化Instance对象,这就形成了线程B引用了一个未初始化的引用,那么天然会有问题。解决方案就是为instance变量添加volatile关键字,保证禁止指令的重排序,程序就正确了。
关于DCL更详细的内容能够阅读如下这篇文章。
最后总结一下Volatile使用的场景:
Java中最多见到的同步机制就是Synchronized关键字了,通常状况下,若是对性能的要求不是那么的苛刻,经过Sychronized关键字基本上可以解决全部的线程同步问题。通常使用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实现作个总结:
在Java5.0以前只有Synchronized和Volatile使用,在5.0以后增长了Lock接口,可以实现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方法:
接下来仍是代码测试,首先测试一下传递无参的:
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()
也可以正常响应中断操做,方法的描述以下(摘抄来自该篇文章):
关于这个方法的用法和理解就比较复杂了,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机制了,wai()方法介绍以下:
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(); } } } }