线程不安全的问题分析:在小朋友抢气球的案例中模拟网络延迟来将问题暴露出来;示例代码以下:java
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小红").start(); new Thread(balloon, "小强").start(); new Thread(balloon, "小明").start(); } } // 气球 class Balloon extends Thread { private int num = 50; @Override public void run() { for (int i = 0; i < 50; i++) { if (num > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "抢到了" + (num--) + "号气球") } } } }
在线程中的run
方法上不能使用throws
来声明抛出异常,因此在run
方法中调用有可能出现异常的代码时,只能使用try-catch
将其捕获来处理。算法
缘由是:子类覆盖父类方法时不能抛出新的异常,父类的run
方法都没有抛出异常,子类就更加不能抛出异常了。详情可查看个人另外一篇文章 「JAVA」运行时异常、编译时异常、自定义异常,经过案例实践转译和异常链
)编程
在上述案例中,经过引入Thread.sleep();
来模拟网络延迟,该方法的做用是让当前线程进入睡眠状态10
毫秒,此时其余线程就能够去抢占资源了,方法的参数是睡眠时间,以毫秒为单位。缓存
经过观察运行结果,发现了问题:安全
在运行结果中,小红、小强两个小朋友都抢到了14
号气球,也就是14
号气球被抢到了2
次。咱们来梳理线程的运行过程来看看发生了什么:网络
14
号气球,因为线程调度,小强得到了CPU
时间片,打印出了抢到的气球,而小红则进入睡眠;小强在打印后对num
作了减一操做,此时num
为13
;13
号气球,并对num
作了减一操做,此时num
为12
;14
号气球;此时的num
为12
,减一后结果为11
;num
作判断时可能上一个线程还未对num
减一,故都能经过(num > 0
)的判断;而后再来运行上述代码,得出以下的结果:多线程
运行结果中出现了本不应出现的0
和-1
,由于按照正常逻辑,气球数量到1
以后就不该该被打印和减一了。出现这样的结果是由于出现了如下的执行步骤:并发
1
号气球,因为线程调度,小强获取了cpu
时间片,得以执行,而小明和小红则进入睡眠;小强打印出结果后,对num
减一,此时num
为0
;num
为0
,而后小明将num
打印出来,再对num
减一,此时num
为-1
;num
为-1
,随后小红将num
打印出来,再对num
减一,此时怒木为-2
;num
作判断时可能上一个线程还未对num
减一,故都能经过(num > 0
)的判断;解决方案:ide
在案例中的抢气球实际上是两步操做:先抢到气球,再对气球总数减一;既然是两步操做,在并发中就彻底有可能会被分开执行,且执行顺序没法获得控制;性能
想要解决上述的线程不安全的问题,就必需要将这两步操做做为一个原子操做,保证其同步运行;也就是当一个线程A
进入操做的时候,其余线程只能在操做外等待,只有当线程A
执行完毕,其余线程才能有机会进入操做。
原子操做:不能被分割的操做,必须保证其从一而终彻底执行,要么都执行,要么都不执行。
为解决多线程并发访问同一个资源的安全性问题,Java
提供以下了几种不一样的同步机制:
Lock
锁机制;同步代码块: 为了保证线程可以正常执行原子操做,Java
引入了线程同步机制,其语法以下:
synchronized (同步锁) { // 须要同步操做的代码 ... ... }
上述中同步锁,又称同步监听对象、同步监听器、互斥锁,同步锁是一个抽象概念,能够理解为在对象上标记了一把锁;
Java
中可使用任何对象做为同步监听对象,但在项目开发中,咱们会把当前并发访问的共享资源对象做为同步监听对象,在任什么时候候,最多只能运行一个线程拥有同步锁。
卫生间的使用就是一个很好的例子,一个卫生间在一段时间内只能被一我的使用,当一我的进入卫生间后,卫生间会被上锁,其余只能等待;只有当使用卫生间的人使用完毕,开锁后才能被下一我的使用。
而后就可使用同步代码块来改写抢气球案例,示例代码以下:
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小红").start(); new Thread(balloon, "小强").start(); new Thread(balloon, "小明").start(); } } // 气球 class Balloon implements Runnable { private int num = 500; @Override public void run() { for (int i = 0; i < 500; i++) { synchronized (this) { if (num > 0) { System.out.println(Thread.currentThread().getName() + "抢到了" + num + "号气球"); num--; } } } } }
经过查看运行结果,线程同步的问题已经获得解决。
同步方法: 使用synchronized
修饰的方法称为同步方法,可以保证当一个线程进入该方法的时候,其余线程在方法外等待。好比:
public synchronized void doSomething() { // 方法逻辑 }
PS:方法修饰符不分前后顺序。
使用同步方法来改写抢气球案例,代码以下:
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小红").start(); new Thread(balloon, "小强").start(); new Thread(balloon, "小明").start(); } } // 气球 class Balloon implements Runnable { private int num = 500; @Override public void run() { for (int i = 0; i < 500; i++) { grabBalloon(); } } // 抢气球 private synchronized void grabBalloon() { if (num > 0) { System.out.println(Thread.currentThread().getName() + "抢到了" + num + "号气球"); num--; } } }
注意:不能使用synchronized
修改线程类中的run
方法,由于使用以后,就会出现一个线程执行完了全部功能,多个线程出现串行;本来是多行道,使用synchronized
修改线程类中的run
方法,多行道变成了单行道。
好:synchronized
保证了并发访问时的同步操做,避免了线程的安全性问题。
坏:使用synchronized
的方法、代码块的性能会比不用要低一些。
StringBuilder和StringBuffer
StringBuilder
和StringBuffer
区别就在于StringBuffer
中的方法都使用了synchronized
修饰,StringBuilder
中的方法没有使用synchronized
修饰;这也是StringBuilder
性能比StringBuffer
高的主要缘由。
Vector和ArrayList
二者都有一样的方法,有一样的实现算法,惟一不一样就是Vector
中的方法使用了synchronized
修饰,因此Vector
的性能要比ArrayList
低。
Hashtable和HashMap
二者都有一样的方法,有一样的实现算法,惟一不一样就是Hashtable
中的方法使用了synchronized
修饰,因此Hashtable
的性能要比HashMap
低。
volatile
关键字的做用在于:被volatile
关键字修饰的变量的值,将不会被本地线程缓存,全部对该变量的读写都是直接操做共享内存,从而能够确保多个线程能正确处理该变量。
须要注意的是,volatile
关键字可能会屏蔽虚拟机中的一些必要的优化操做,因此运行效率不是很高,所以,没有特别的须要,不要使用;即使使用,也要避免大量使用。
单例模式--饿汉模式
代码以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { instance = new SlackerDemo(); } return instance; } }
单例模式--懒汉模式
代码以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { instance = new SlackerDemo(); } return instance; } }
懒汉模式存在线程不安全问题,在对instance
对象作判断时因为并发致使出现和抢气球案例同样的问题。为了解决这个问题,使用双重检查加锁机制来解决。
双重检查加锁机制
使用“双重检查加锁”机制实现的程序,既能实现线程安全,有可以使性能不受较大的影响。那么何谓“双重检查加锁”机制?其指的是:
getInstance
方法都须要同步,而是先不一样步,进入方法后,先检查实例是否存在,若是不存在才执行同步代码块,这是**第一重检查;这样,就只须要同步一次,减小了屡次在同步状况判断所浪费的时间。
“双重检查加锁”机制的实现须要volatile
关键字的配合使用,且Java
版本须要在Java 5
及以上,虽然该机制可实现线程安全的单例模式,也要根据实际状况酌情使用,不宜大量推广使用。
使用“双重检查加锁”机制改写后的懒汉模式,代码以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { synchronized (SlackerDemo.class) { if (instance == null) { instance = new SlackerDemo(); } } } return instance; } }
java.util.concurrent.locks
包提供了Lock
接口,Lock
锁机制提供了比synchronized
代码块和synchronized
方法更普遍的锁定操做,并且功能比synchronized
代码块和synchronized
方法更增强大。
官方的提供了参考价值很大的demo
,可以很好的提现Lock
机制的功能:
使用Lock
机制改写的抢气球案例代码以下所示:
import java.util.concurrent.locks.*; public class LockDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小红").start(); new Thread(balloon, "小强").start(); new Thread(balloon, "小明").start(); } } // 气球 class Balloon implements Runnable { private int num = 500; private final Lock lock = new ReentrantLock(); // 建立锁对象 @Override public void run() { for (int i = 0; i < 500; i++) { grabBalloon(); } } // 抢气球 private void grabBalloon() { lock.lock(); // 获取锁对象 if (num > 0) { try { System.out.println(Thread.currentThread().getName() + "抢到了" + num + "号气球"); num--; } catch (Exception e) { } finally { lock.unlock(); // 释放锁 } } } }
案例运行正常。
完结。老夫虽不正经,但老夫一身的才华!关注我,获取更多编程科技知识。