多线程总览

线程安全的实现方式

线程安全的实现方式 1

一、 什么叫线程安全 1

二、 实现线程安全的方法 2

2.1互斥同步(阻塞,悲观) 2

2.2非阻塞同步(乐观) 2

2.3无同步方案 3

一、什么叫线程安全

定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。

1. 不可变

Final关键字。Final修饰的对象或变量是只读的,不会有写的操作,当然是不可变的,所以一定是安全的。

2. 绝对线程安全

加引号的“绝对”,即使像Vector这种所有的方法都加了synchronized关键字,也不能保证线程的安全性,必须要在方法调用段提供同步手段方可。

3. 相对线程安全

通常意义上的线程安全,它需要保证对这个对象单独的操作是安全的,我们在调用的时候不需要额外的措施,但对于一些特定顺序的调用,就可能需要调用段通过额外的手段来保证调用的正确性。如对vector,一个线程remove,一个线程读,肯定有问题。

4. 线程兼容

指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中安全使用。我们平时所说的绝大部分是这种情况。

5. 线程对立

二、实现线程安全的方法

2.1互斥同步(阻塞,悲观)

互斥同步是最常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。而互斥是实现同步的手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此在这四个字里,互斥是因,同步是果,互斥是方法,同步是目的。

1. synchronized

在Java里面,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

Synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。已进入的线程执行完之前,会阻塞后面其他线程的进入。Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块,状态转换消耗的时间可能比用户代码执行的时间都长。所以synchronized是Java中一个重量级的操作。

2. Lock

J.U.C高级功能,跟synchronized基本用法很相似,都具有一样的线程重入特性,只是代码的写法上有所区别,lock表现为API层面,通过lock(),unlock(),配合try/finally来实现,synchronized表现为原声语法层面。不过Lock提供了一些高级功能,提供一些等待可中断、公平非公平、绑定condition等条件。

性能上来讲,jdk1.6以后的synchronized和lock基本持平。

3. Volatile

谈到volatile,首先要谈内存。在这里不按照分代来讨论JVM的内存的分配,这里把JAVA内存分为主内存和工作内存(如果硬要对应起来,主内存对应堆内存,工作内存对应栈内存)。所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的比阿娘的主内存拷贝。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。

主内存和工作内存间的具体交互,Java内存模型定义了8种操作:

以i++操作为例,初始值假定为i=1;

1. Lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

2.Unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

ü Read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。(i‘ = 0)

ü Load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中。(i'=1)

ü Use:作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将会执行这个操作。(i'++,i'变为2)

ü Assign:作用于工作内存的变量,他把一个从执行引擎接收到的值赋值给工作内存变量。(i'=2)

ü Store:作用于工作内存的变量,他把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。(i'=2,i=1)

ü Write: 作用于主内存的变量。把store操作从工作内存中得到的变量的值放入主内存的变量中。(i=2,i'没有引用了,工作内存等待回收)

知道了这些,我们来看volatile的一些误区:

首先,volatile变量依然是有工作内存的拷贝的(很多地方说没有拷贝,直接读取主内存是错误的),但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写一般。

其次,volatile只是保证了变量的可见性,但不能保证线程安全。在各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新(也就是use之前的read和load省掉了),执行引擎看不到不一致的情况,因此可以认为不存在一致性问题。可见性没问题,但是后续的几部操作是无法保证的,如两个线程同时在use的时候拿到i,执行i++,虽然执行了两次,但最后一个线程刷到主内存的结果肯定不是我们想要的结果。

Volatile的作用:

1. 保证变量可见性

2. 禁止指令重排序

什么时候使用volatile?

1. 只用来读取的时候,如状态位的判断

2. 出现CAS的地方,必用volatile

2.2非阻塞同步(乐观)

1. CAS

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也被称为阻塞同步。另外,它属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定有问题,无论共享数据真的会出现竞争,他都要进行加锁、用户态和心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。

随着硬件指令集的发展,我们有了另一个选择:基于冲突检查的乐观并发策略,通俗地讲就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(最常见的就是不断重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作被称为非阻塞同步。用的最多的就是CAS(compare and swap)。J.U.C中的AtomicInteger等类底层用到的Unsafe就封装了cas方法。

Public final int getAndIncrement() {

   For(;;) {

   Int current =get();

   Int next = current 1;

   If(compareAndSet(current, next) {

    Return  current;

}

}

}

2. JUC集合类

更高级的API就是JUC集合类

2.3无同步方案

保证线程安全,并不是一定要进行同步,两者没有因果关系。同步只是保障共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那它自然无须任何同步措施去保证正确性。

1. 可重入代码

百度一下,可重入代码是一种允许多个进程同时访问的代码。为了使各进程所执行的代码完全相同,故不允许任何进程对其进行修改。程序在运行过程中可以被打断,并由开始处再次执行,并且在合理的范围内(多次重入,而不造成堆栈溢出等其他问题),程序可以在被打断处继续执行,且执行结果不受影响。如:

void test()

{

int i;

i=2;

printf("%d\n",i );

i++;

printf("%d\n",i);

}

无论谁调用它结果都一样,得到

2

3

说白了就是局部变量。

2. 线程本地存储(Thread Local Storage

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

这就是所谓的ThreadLocal。ThreadLocal为每个线程的并发访问的数据提供一个副本,使得每个线程在某一时间范围得到的并不是同一个对象,这样就隔离了多个线程间多数据的共享。 通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步带来的性能消耗,也减少了线程并发控制的复杂度。

概括起来,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问,互不影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值