1 基本概括
2 主要介绍
2.1 线程安全的概念
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
2.2 多线程编程的三个概念
2.2.1 原子性
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
2.2.2 可见性
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
2.2.3 有序性
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。
以下面这段代码为例
boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。
处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。
2.3 解决机制
1、加锁:
a、锁能使其保护的代码以串行的形式来访问,当给一个复合操作加锁后,能使其成为原子操作。一种错误的思想是只要对写数据的方法加锁,其实这是错的,对数据进行操作的所有方法都需加锁,不管是读还是写
b、加锁时需要考虑性能问题,不能总是一味地给整个方法加锁synchronized就了事了,应该将方法中不影响共享状态且执行时间比较长的代码分离出去
c、加锁的含义不仅仅局限于互斥,还包括可见性。为了确保所有线程都能看见最新值,读操作和写操作必须使用同样的锁对象
2、不共享状态:
无状态对象: 无状态对象一定是线程安全的,因为不会影响到其他线程
线程关闭: 仅在单线程环境下使用
3、不可变对象:
可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,也即不能对外提供可以修改final对象的接口
2.4 线程安全的级别
线程安全的级别或者粒度有三种,如下:
(1)线程安全
这种情况下其实没有线程安全问题,比如上面的例子中,每个人都有自己专用的卫生间,所以不会存在竞争问题。
(2)条件安全
条件安全,顾名思义是有条件的,所有人共用几个卫生间,抢到资源的就把门关上,通过门来隔离资源,后面的人就在外面等待直到里面的人出来。
(3)不安全
这种情况下连门都没有,所以并不能很好保证资源安全,所以这种情况***不能让同时让多个人直接使用。
2.5 并发的概念
并发 指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的;
并行 指多个cpu同时处理多个线程任务,cpu可以同时处理不同的任务,异步处理;
并发条件
第一,是否有共享变量 第二,是否多线程环境 第三,是否多个线程更新共享变量 一句话:多个线程操作同一个对象;
2.5 并发的优势和风险
2.6 避免并发
2.6.1 线程封闭
什么是线程封闭?
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。
实现线程封闭有哪些方法?
ad-hoc 线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。
Ad-hoc 线程封闭非常脆弱,应该尽量避免使用。
栈封闭
栈封闭是我们编程当中遇到的最多的线程封闭。
什么是栈封闭呢?
简单的说就是局部变量。
多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
2.6.2 无状态的类
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类。
2.6.3 让类不可变
让状态不可变,两种方式:
1、 加 final 关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上 final 关键字,但是加上 final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。
2.6.4 volatile
并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。
2.6.5 加锁和CAS
我们最常使用的保证线程安全的手段,使用 synchronized 关键字,使用显式锁,使用各种原子变量,修改数据时使用 CAS 机制等等。
2.6.6 安全的发布
类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本。
但是如果类中持有的成员变量是对象的引用,如果这个成员对象不是线程安全的,通过 get 等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
2.6.7 ThreadLocal
ThreadLocal 是实现线程封闭的最好方法。
ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,而 Map 的值就是我们要封闭的对象。每个线程中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的线程封闭。
2.7 实现线程安全的方式
2.7.1 synchronized (自动锁,锁的创建和释放都是自动的)
synchronized(同一个锁){ //可能会发生线程冲突问题 }
锁的释放 是在 synchronized 同步代码执行完毕后自动释放。
同步的前提:
1,必须要有两个或者两个以上的线程 ,如果小于2个线程,则没有用,且还会消耗性能(获取锁,释放锁)
2,必须是多个线程使用同一个锁
弊端 :多个线程需要判断锁,较为消耗资源、抢锁的资源。
2.7.2 lock 手动锁 (手动指定锁的创建和释放)
可以视为synchronized的增强版,提供了更灵活的功能。该接口提供了限时锁等待、锁中断、锁尝试等功能。synchronized实现的同步代码块,它的锁是自动加的,且当执行完同步代码块或者抛出异常后,锁的释放也是自动的。
2.7.3 volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
synchronized、volatile和Lock之间的区别
synochronizd和volatile关键字区别:
1) volatile关键字 解决的是变量在多个线程之间的可见性;而 sychronized关键字 解决的是多个线程之间访问共享资源的同步性。
tip: final关键字也能实现可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把 “this” 的引用传递出去(this引用逃逸是一件很危险的事情,其它线程有可能通过这个引用访问到了”初始化一半”的对象),那在其他线程中就能看见final;
2) volatile 只能用于修饰变量,而 synchronized 可以修饰方法,以及代码块。( volatile 是线程同步的轻量级实现,所以 volatile 性能比 synchronized 要好,并且随着JDK新版本的发布, sychronized关键字 在执行上得到很大的提升,在开发中使用 synchronized关键字 的比率还是比较大);
3)多线程访问 volatile 不会发生阻塞,而 sychronized 会出现阻塞;
4) volatile 能保证变量在多个线程之间的可见性,但不能保证原子性;而 sychronized 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。
线程安全 包含 原子性 和 可见性 两个方面。
对于用 volatile 修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
一句话说明volatile的作用 :实现变量在多个线程之间的可见性。
synchronized和lock区别:
1) Lock 是一个接口,而 synchronized 是Java中的关键字, synchronized 是内置的语言实现;
2) synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
3) Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
4)通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
5) Lock 可以提高多个线程进行读操作的效率(读写锁)。
3 使用线程安全的简单用例
3.1 同步代码块
public class ThreadSafeProblem {
public static void main(String[] args) {
Consumer abc = new Consumer();
// 注意要使用同一个abc变量作为thread的参数,
// 如果你使用了两个Consumer对象,那么就不会共享ticket了,就自然不会出现线程安全问题
new Thread(abc,"窗口1").start();
new Thread(abc,"窗口2").start();
}
}
class Consumer implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
synchronized (Consumer.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售卖第" + (100-ticket+1) + "张票");
ticket--;
}
}
}
}
}
3.2 Lock锁是需要手动去加锁和释放
/*
* 使用ReentrantLock类实现同步
* */class MyReenrantLock implements Runnable{
//向上转型
private Lock lock = new ReentrantLock();
public void run() {
//上锁
lock.lock();
for(int i = 0; i < 5; i++) {
System.out.println("当前线程名: "+ Thread.currentThread().getName()+" ,i = "+i);
}
//释放锁
lock.unlock();
}
}
public class MyLock {
public static void main(String[] args) {
MyReenrantLock myReenrantLock = new MyReenrantLock();
Thread thread1 = new Thread(myReenrantLock);
Thread thread2 = new Thread(myReenrantLock);
Thread thread3 = new Thread(myReenrantLock);
thread1.start();
thread2.start();
thread3.start();
}
}
3.3 volatile关键字
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}