Java并发编程的艺术(十二)——线程安全

1. 什么是『线程安全』?

如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。

2. 线程安全的几种程度

线程安全性的前提:对『线程安全性』的讨论必须建立在对象内部存在共享变量这一前提,若对象在多条线程间没有共享数据,那这个对象一定是线程安全的!

2.1. 绝对的线程安全

上述线程安全性的定义即为绝对线程安全的情况,即:一个对象在构造完之后,调用者无需任何额外的操作,就可以在多线程环境下随意使用。 绝对的线程安全是一种理想的状态,若要达到这一状态,往往需要付出巨大的代价。 通常并不需要达到绝对的线程安全。

2.2. 相对的线程安全

我们通常所说的『线程安全』即为『相对的线程安全』,JDK中标注为线程安全的类通常就是『相对的线程安全』,如:Vector、HashTable、Collections.synchronizedXXX。 对于相对线程安全的类,使用它们时一般不需要使用额外的保障措施,但对于一些特定的使用场景,仍然需要额外的操作来保证线程安全,如:

// 读线程
Thread t1 = new Thread( new Runnable(){
    public void run(){
        for(int i=0; i<vector.size(); i++){
            System.out.println( vector.get(i) );
        }
    }
}).start();

// 写线程
Thread t2 = new Thread( new Runnable(){
    public void run(){
        for(int i=0; i<vector.size(); i++){
            vector.remove(i);
        }
    }
}).start();

vector是一个线程安全的容器,它所提供的方法均为同步方法,但上述代码仍然会出现线程安全性问题: 若线程1读了一半的元素后暂停,线程2开始执行,并删除了所有的元素,然后线程1继续执行,此时发生角标越界异常! 修改方案:加上额外的同步

// 读线程
Thread t1 = new Thread( new Runnable(){
    public void run(){
        synchronized( vector ){
            for(int i=0; i<vector.size(); i++){
                System.out.println( vector.get(i) );
            }
        }
    }
}).start();

// 写线程
Thread t2 = new Thread( new Runnable(){
    public void run(){
        synchronized( vector ){
            for(int i=0; i<vector.size(); i++){
                vector.remove(i);
            }
        }
    }
}).start();

2.3. 线程对立

线程对立指的是:不论调用者采用何种同步措施,都无法达到线程安全的目的。 如Thread类的suspend、resume方法就是线程对立的方法。 suspend方法会暂停线程,但它不会释放资源,若resume需要请求到该资源才会被运行的话,系统就会进入死锁状态。

3. 实现线程安全的方法

3.1. 互斥同步

同步指的是同一时刻,只有一条线程操作『共享变量』。

实现同步的方式有很多:互斥访问、CAS操作。 互斥会引起阻塞,当一条线程请求一个已经被另一线程使用的锁时,就会进入阻塞态;而进入阻塞态会涉及上下文切换。因此,使用互斥来实现同步的开销是很大的。

互斥同步(阻塞式同步)是一种『悲观锁』,即它认为总是存在多条线程竞争资源的情况,因此它不管当前是不是真的有多条线程在竞争共享资源,它总是先上锁,然后再处理。

Java中有两种实现互斥同步的方式:synchronized和ReentrantLock。

  1. synchronized
    • 编译器会在synchronized同步块的开始和结束位置加上monitorenter和monitorexit指令;
    • 这两个指令需要一个reference类型的参数来指名要锁定和解锁的对象;
    • 若同步块没有明确指定锁对象,那么就使用当前对象或当前类的Class对象;
    • 它是一把可重入的锁,即:当前线程在已经获得锁的情况下,可以再次获取该锁,因此不会出现当前线程把自己锁死的情况;
  2. ReentrantLock 它也是一把可重入的锁,但比synchronized多如下功能:
    • 等待可中断:若一条线程长时间占用锁不释放,那被阻塞的线程可以选择放弃等待,而去做别的事;这对于要处理长时间的同步块时是很有帮助的。
    • 可实现公平锁:synchronized是一种非公平锁,即:被阻塞的线程竞争锁是随机的;而公平锁是根据被阻塞线程先来后到的顺序给予锁。ReentrantLock默认是非公平锁,可以通过构造函数构造公平锁。
    • 可以绑定多个条件:synchronized可使用wait/notify来实现等待/通知机制,但一个synchronized同步块只能使用一次,若要使用多次,就需要嵌套同步块;但ReentrantLock可以通过newCondition创建多个条件。

synchronized和ReentrantLock如何选择? 优先选择synchronized! JDK1.6已经对synchronized做了很多优化,性能与ReentrantLock相差不大。在条件允许的请况下应优先选择synchronized。

3.2. 非阻塞同步

它是一种『乐观锁』,即它总是认为当前没有线程使用共享资源,因此它不管当前的状态,直接操作共享资源,若发现产生了冲突,那么再采取补偿措施(如:CAS的补偿措施就是不断尝试,直到不发生冲突为止),这种方式线程无需进入阻塞态(挂起态),因此称为『非阻塞同步』。

JUC中各种整形原子类的自增、自减等操作就使用了CAS。

CAS操作过程:CAS操作存在3个值:共享变量V、预期的旧值A、新值B,若V与A相同,则将V更新成B,否则就不更新,继续循环比较,直到更新完成为止。

CAS操作可能引发的问题:ABA问题。 若V一开始的值为A,但在准备赋新值的过程中A变成了B,又变成了A,而CAS操作误认为V没有被改过。

无同步方案

『阻塞式同步』和『非阻塞式同步』都是同一时刻只让一条线程处理共享数据,而下面的方案使得多条线程之间不存在共享数据,从而无需同步。

  1. 可重入代码 如果一块代码段只要输入的值一样其结果就一样的话,这段代码就叫『可重入代码』。 这一类代码天生具有线程安全性,线程随意切换结果都一样。
  2. 线程封闭 线程封闭:把所有涉及共享变量操作的任务都放在一个线程中运行。 这样就不存在多条线程同时处理共享变量了,从而达到了线程安全目的。

WEB服务器采用的就是这种方式,它把每个请求封装在一条线程中处理,从而不存在线程安全性问题。

  1. 不可变对象 如果是共享的基本数据类型变量,只要被final修饰,它就是不可变的; 如果是共享的对象,那就要确保它内部的共享成员变量不会被它的行为所改变。 PS:保证对象内部共享变量不会被改变的方法有很多,最简单粗暴的方式就是将所有共享变量用final修饰。

不可变对象一定是线程安全的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java、Spring、技术分享

java线程

  从操作系统的角度,可以简单认为,线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register...

672
来自专栏向治洪

java线程池

一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。...

1896
来自专栏互扯程序

java多线程-概念&创建启动&中断&守护线程&优先级&线程状态

在现代操作在运行一个程序时,会为其创建一个进程。例如启动一个QQ程序,操作系统就会为其创建一个进程。而操作系统中调度的最小单位元是线程,也叫轻量级进程,在一个进...

793
来自专栏小灰灰

Java并发学习之玩转线程池

线程池的使用姿势 基本上实际的项目不可能离开线程池,只是看你有没有注意到罢了 作为以业务需求为驱动,最顺溜的是写if-else的码农我来说,线程池就比较高端了...

1826
来自专栏钟绍威的专栏

模拟Executor策略的实现如何控制执行顺序?怎么限制最大同时开启线程的个数?为什么要有一个线程来将结束的线程移除出执行区?转移线程的时候要判断线程是否为空遍历线程的容器会抛出ConcurrentM

Executor作为现在线程的一个管理工具,就像管理线程的管理器一样,不用像以前一样,通过start来开启线程 Executor将提交线程与执行线程分离开来...

2616
来自专栏平凡文摘

详细分析Java中断机制

864
来自专栏互联网大杂烩

Java锁与并发

保护临界区资源不会被多个线程同时访问时而受到破坏。通过锁,可以让多个线程排队。一个一个地进入临界区访问目标对象,使目标对象的状态总是保持一致。

832
来自专栏Java3y

多线程之死锁就是这么简单

1815
来自专栏好好学java的技术栈

深入线程Thread类的start()方法和run()方法

java的线程是通过java.lang.Thread类来实现的。VM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都...

580
来自专栏mini188

学习笔记: Delphi之线程类TThread

新的公司接手的第一份工作就是一个多线程计算的小系统。也幸亏最近对线程有了一些学习,这次一接手就起到了作用。但是在实际的开发过程中还是发现了许多的问题,比如挂起与...

2118

扫码关注云+社区