坚持学习,总会有一些不同的东西。html
引用一下百度百科的定义——
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会经过同步机制保证各个线程均可以正常且正确的执行,不会出现数据污染等意外状况。
文字定义老是很含糊,举个反例就很清楚了,想起以前总结过单例模式,就从单例模式开始吧。若是不清楚单例模式的新同窗,能够看一下这篇总结:
java中全面的单例模式多种实现方式总结
单例模式中,懒汉式的实现方案以下:java
public class Singleton { private Singleton() { } private static Singleton sSingleton; public static Singleton getInstance() { if (sSingleton == null) { sSingleton = new Singleton(); } return sSingleton; } }
该方法在单线程中运行是没有问题的,可是在多线程中,某些状况下,多个线程同时都判断到 sSingleton == null
,而后又都执行 sSingleton = new Singleton()
,这样就不能保证单例了,咱们说它不是线程安全的。
一种改进方法:编程
public class Singleton { private Singleton() { } private static Singleton sSingleton; public synchronized static Singleton getInstance() { if (sSingleton == null) { sSingleton = new Singleton(); } return sSingleton; } }
上面这种实现,实际上效率很是低,是彻底不推荐使用的。主要是由于加了 sychronized
关键字,意为同步的,也就是内置锁。使用 synchronized
关键字修饰方法, 是对该方法加锁,这样在同一时刻,只有一个线程能进入该方法,这样保证了线程安全,可是也正由于如此,效率变得很低,由于当对象建立以后,再次调用该方法的时候,直接使用对象就能够了,无需再同步了。因而有了下面改进的实现方式—— DCL(双重检查锁):缓存
public class Singleton { private Singleton() { } /** * volatile is since JDK5 */ private static volatile Singleton sSingleton; public static Singleton getInstance() { if (sSingleton == null) { synchronized (Singleton.class) { // 未初始化,则初始instance变量 if (sSingleton == null) { sSingleton = new Singleton(); } } } return sSingleton; } }
sSingleton = new Singleton()
不是一个原子操做。故须加 volatile 关键字修饰,该关键字在 jdk1.5 以后版本才有。下面就来讲说 synchronized
和 volatile
这两个关键字。安全
java 提供了一种一种内置锁,来实现同步代码块,同步代码块包含两个部分:一个做为锁的对象引用,一个由锁保护的代码块。形式以下:多线程
synchronized (lock) { // 由锁保护的代码块 }
每一个 java 对象均可以做为实现同步的锁,java 的内置锁也称为互斥锁。同一时刻只能有一个线程得到该锁,得到该锁的线程才能进入由锁保护的代码块,其它线程只能等待该线程执行完代码块以后,释放该锁后,再去得到该锁。例子:并发
public class SynchronizedDemo1 { private Object lock = new Object(); public static void main(String[] args) { SynchronizedDemo1 demo = new SynchronizedDemo1(); new Thread(() -> { try { demo.test1(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); try { demo.test1(); } catch (InterruptedException e) { e.printStackTrace(); } } public void test1() throws InterruptedException { System.out.println("--- test1 begin - current thread: " + Thread.currentThread().getId()); Thread.sleep(1000); synchronized (lock) { System.out.println("--- test1 synchronized - current thread: " + Thread.currentThread().getId()); Thread.sleep(5000); } System.out.println("--- test1 end - current thread: " + Thread.currentThread().getId()); } }
执行结果:
框架
从结果能够清楚地看到一个线程进入同步代码块以后,另外一个线程阻塞了,须要等到前者释放锁以后,它得到锁了才能进入同步代码块。jvm
上面代码中,咱们建立的 lock
对象做为锁。用 synchronized
修饰方法又是什么充当了锁呢?ide
以关键字 synchronized
修饰的方法就是一种横跨整个方法的同步代码块,其中该同步代码块的锁就是调用该方法的对象。
class A { public synchronized void a(){ System.out.println("hello"); } }
等价于
class A { public void a(){ synchronized(this) { System.out.println("hello"); } } }
静态方法用 类名.方法名
来调用,以关键字 synchronized
修饰的静态方法则以 Class 对象做为锁。
class A { public static synchronized void a(){ System.out.println("hello"); } }
等价于
class A { public static void a(){ synchronized(A.class) { System.out.println("hello"); } } }
写个demo测试一下:
public class A { public static void main(String[] args) { A obj_a = new A(); new Thread() { @Override public void run() { try { obj_a.a(); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); new Thread() { @Override public void run() { try { obj_a.b(); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); new Thread(){ @Override public void run() { try { A.c(); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); try { A.d(); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void a() throws InterruptedException { System.out.println("--- begin a - current Thread " + Thread.currentThread().getId()); Thread.sleep(8000); System.out.println("--- end a - current Thread " + Thread.currentThread().getId()); } public synchronized void b() throws InterruptedException { System.out.println("--- begin b - current Thread " + Thread.currentThread().getId()); Thread.sleep(8000); System.out.println("--- end b - current Thread " + Thread.currentThread().getId()); } public synchronized static void c() throws InterruptedException { System.out.println("--- begin c - current Thread " + Thread.currentThread().getId()); Thread.sleep(5000); System.out.println("--- end c - current Thread " + Thread.currentThread().getId()); } public synchronized static void d() throws InterruptedException { System.out.println("--- begin d - current Thread " + Thread.currentThread().getId()); Thread.sleep(5000); System.out.println("--- end d - current Thread " + Thread.currentThread().getId()); } }
运行结果以下:
能够看到,因为方法 a 和 方法 b 是同一个锁 obj_A
,因此当某个线程执行其中一个方法是,其余线程也不能执行另外一个方法。可是方法 c 是由 A.class
对象锁住的,执行方法 C 的线程与另外两个线程没有互斥关系。
对于某个类的某个特定对象来讲,该类中,全部 synchronized 修饰的非静态方法共享同一个锁,当在对象上调用其任意 synchronized 方法时,此对象都被加锁,此时,其余线程调用该对象上任意的 synchronized 方法只有等到前一个线程方法调用完毕并释放了锁以后才能被调用。
而对于一个类中,全部 synchronized 修饰的静态方法共享同一个锁。
当某个线程请求一个由其余线程持有的锁时,发出请求的线程就会阻塞,然而,内置锁是可重入的,,若是某个线程试图得到一个已经由它本身持有的锁,那么这个请求就会成功。这也意味着获取锁的操做粒度是“线程”,而不是“调用”。当线程请求一个未被持有的锁时,jvm 将记下锁的持有者(即哪一个线程),并获取该锁的计数值为 1 ,当同一个线程再次获取该锁时, jvm 将计数值递增,当线程退出同步代码块时,计数值将递减,当计数值为 0 时,将释放锁。
public class B { public static void main(String[] args) { B obj_B = new B(); obj_B.b(); } private synchronized void a(){ System.out.println("---a"); } private synchronized void b(){ System.out.println("---b"); a(); } }
执行上面这段代码,将输出
---b ---a
假设没有可重入的锁,对于对象 obj_B
来讲,调用 b 方法时,线程将会持有 obj_B
这个锁,在方法 b 中调用方法 a 时,将会一直等待方法 b 释放锁,形成死锁的状况。
《Java 并发编程实战》 中举的可重入锁的例子:
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }
我第一遍看这段代码的时候,在思考,这里父类子类的方法都有synchronized同步,当调用子类LoggingWidget的doSomething()时锁对象确定是当时调用的那个LoggingWidget实例,但是问题是当执行到super.doSomething()时,要调用父类的同步方法,那此时锁对象是谁?是同一个锁进入了 2 次,仍是得到了子类对象和父类对象的 2 个不一样的锁?
下面这段代码能给出结论:
public class Test { public static void main(String[] args) throws InterruptedException { final TestChild t = new TestChild(); new Thread(new Runnable() { @Override public void run() { t.doSomething(); } }).start(); Thread.sleep(100); t.doSomethingElse(); } public synchronized void doSomething() { System.out.println("something sleepy!"); try { Thread.sleep(1000); System.out.println("woke up!"); } catch (InterruptedException e) { e.printStackTrace(); } } private static class TestChild extends Test { public void doSomething() { super.doSomething(); } public synchronized void doSomethingElse() { System.out.println("something else"); } } }
这段代码输出了:
something sleepy! woke up! something else
而不是
something sleepy! something else woke up!
这说明是同一个锁进入了 2 次,即调用子类方法的子类对象。而这也正好符合多态的思想,调用 super.doSomething() 方法时,是子类对象调用父类方法。
原子是世界上的最小单位,具备不可分割性。 好比 a=0
这个操做不可分割,咱们说这是一个原子操做。而 ++i
就不是一个原子操做。它包含了"读取-修改-写入"的操做。
同步代码块,能够视做是一个原子操做。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操做类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。好比:AtomicBoolean
AtomicInteger
AtomicLong
等原子操做类。具体可查阅JDK源码或者参考《ava并发编程的艺术》第7章。
下面说说复合操做与线程安全的问题。
咱们知道,java 集合框架中的 Vector 类是线程安全的,查看该类的源码发现,不少关键方法都用了synchronized
加以修饰。可是实际使用时候,稍有不慎,你会发现,它可能并非线程安全的。好比在某个类中拓展一下 Vector 的方法,往 vector 中添加一个元素时,先判断该元素是否存在,若是不存在才添加,该方法大概像下面这样:
public class CJUtil{ public void putElement(Vector<E> vector, E x){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } }
上面这个代码确定是线程不安全的,可是为何呢?不是说好,Vector 类是线程安全的吗?上网搜了一下,竟然发现关于 Vector 类是否是线程安全的存在争议,而后我看到有人说它不是线程安全的,给出的理由好比像上面这种先判断再添加,或者先判断再删除,是一种复合操做,而后认真地打开 JDK 的源码看了,发现 Vector 类中 contains
方法并无用 synchronized
修饰,而后得出告终论,Vector不是线程安全的...
事实究竟是怎样的呢?咱们假设 Vector 类的 contains
也用 synchronized
关键字加锁同步了,此时有两个线程 tA 和 tB 同时访问这个方法,tA 调用到 contains
方法的时候,tB 阻塞, tA 执行完 contains
方法,返回 false 后,释放了锁,在 tA 执行 add
以前,tB 抢到了锁,执行了 contains
方法,tA 阻塞。对于同一个元素, tb 判断也不包含,后面, tA 和 tB 都向 Vector 添加了这个元素。通过分析,咱们发现,对于上述复合操做线程不安全的缘由,并不是是其中单个操做没有加锁同步形成的。
那如何解决这个问题呢?可能立刻会想到,给 putElement 方法加上 synchronized
同步。
public class CJUtil{ public synchronized void putElement(Vector<E> vector, E x){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } }
这样整个方法视为一个原子操做,只有当 tA 执行完整个方法后,tB 才能进入,也就不存在上面说的问题了。其实,这只是假象。这种在加锁的方法,并不能保证线程安全。咱们能够从两个方面来分析一下:
putElement
方法去操做 vector,可是咱们无法保证其它线程经过其它方法不去操做这个 vector 。putElement
方法,是不许确的,由于这个方法不是静态的,若是在两个线程中,分别用 CJUtil
的两个不一样的实例对象,是能够同时进入到 putElement
方法的。public class CJUtil{ public void putElement(Vector<E> vector, E x){ synchronized(vector){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } } }
重排序一般是编译器或运行时环境为了优化程序性能而采起的对指令进行从新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境。
不要假设指令执行的顺序,由于根本没法预知不一样线程之间的指令会以何种顺序执行。
编译器重排序的典型就是经过调整指令顺序,在不改变程序语义的前提下,尽量的减小寄存器的读取、存储次数,充分复用寄存器的存储值。
int a = 5;①
int b = 10;②
int c = a + 1;③
假设用的同一个寄存器
这三条语句,若是按照顺序一致性,执行顺序为①②③寄存器要被读写三次;但为了下降重复读写的开销,编译器会交换第二和第三的位置,即执行顺序为①③②
可见性是一种复杂的属性,由于可见性中的错误老是会违背咱们的直觉。一般,咱们没法确保执行读操做的线程能适时地看到其余线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操做的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另外一个线程是可见的。也就是一个线程修改的结果。另外一个线程立刻就能看到。好比:用volatile修饰的变量,就会具备可见性。volatile修饰的变量不容许线程内部缓存和重排序,即直接修改内存。因此对其余线程是可见的。可是这里须要注意一个问题,volatile只能让被他修饰内容具备可见性,但不能保证它具备原子性。
下面这段代码:
public class A { private static boolean flag = false; public static void main(String[] args) { new Thread() { @Override public void run() { while (!flag) { } } }.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; } }
看执行结果:
能够看到程序并无像咱们所期待的那样,在一秒以后,退出,而是一直处于循环中。
下面给 flag
加上 volatile
关键修饰:
public class A { private static volatile boolean flag = false; public static void main(String[] args) { new Thread() { @Override public void run() { while (!flag) { } } }.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; } }
再看结果:
结果代表,没有用 volatile
修饰 flag
以前,改变了不具备可见性,一个线程将它的值改变后,另外一个线程却 “不知道”,因此程序没有退出。当把变量声明为 volatile
类型后,编译器与运行时都会注意到这个变量是共享的,所以不会将该变量上的操做与其余内存操做一块儿重排序。volatile
变量不会被缓存在寄存器或者对其余处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每一个线程先从内存拷贝变量到CPU缓存中。若是计算机有多个CPU,每一个线程可能在不一样的CPU上被处理,这意味着每一个线程能够拷贝到不一样的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
volatile 修饰的遍历具备以下特性:
细心的人应该发现了,上面代码中的循环是一个空循环,我试着去掉 volatile
关键字,在循环里面加了一条打印信息,以下:
public class A { private static boolean flag = false; public static void main(String[] args) { new Thread() { @Override public void run() { while (!flag) { System.out.println("---"); } } }.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; } }
结果会是怎样,会一直打印 "---" 吗?看结果:
奇怪了,为何没有使用 volatile
关键字,一秒以后程序也推出了。点击查看 System.out.println(String x)
的源码:
public void println(String x) { synchronized (this) { print(x); newLine(); } }
咱们发现,该方法加锁同步了。
那么问题来了,synchronized 到底干了什么。。
按理说,synchronized 只会保证该同步块中的变量的可见性,发生变化后当即同步到主存,可是,flag 变量并不在同步块中,实际上,JVM对于现代的机器作了最大程度的优化,也就是说,最大程度的保障了线程和主存之间的及时的同步,也就是至关于虚拟机尽量的帮咱们加了个volatile,可是,当CPU被一直占用的时候,同步就会出现不及时,也就出现了后台线程一直不结束的状况。 参考书籍: 《Java 并发编程实战》 《Java 编程思想 第四版》