## 线程相关的模型
1)启动一个os的用户线程,然后实际的任何操作都直接对应该用户线程,这就是1:1,这样做之后,调度就由os负责,jvm就不管了,hotspot等主流jvm基本上都是这种做法
2)启动一个虚拟线程,然后执行的时候,交给os上的一个用户线程去执行,这样做的好处就是,jvm可以自己实现调度,而且可以控制虚拟线程的大小,这就是n:m或者1:m,看具体的实现,而如果将线程虚拟化之后,调度就可以由jvm来实现了,做成1:m还是n:m完全看jvm调度的实现,这就是协程,go其实是这种做法,早期的solaris上的hotspot也是这种做法,后来改了,改成选择1的做法,这样在不同os上hotspot的实现就统一了
### volatile类型修饰符
- 工具需要自行下载安装
通过 hsdis 可以查看 Java 编译后的机器指令
> HSDIS是一个Sun官方推荐的HotSpot虚拟机编译代码的反汇编插件,包含在HotSpot虚拟机的源码之中,但没有提供编译后的程序。在Project Kenai的网站也可以下载到单独的源码使用 jitwatch 工具,可以帮助分析该日志
> HotSpot JIT编译器的日志分析器和可视化工具
volatile用于修改属性,保证多线程访问属性时的可见性、顺序性,但是并不保证原子性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)--原理
- 禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++这种操作不能保证原子性
volatile 变量的内存可见性是基于内存屏障Memory Barrier实现。内存屏障,又称内存栅栏,是一个CPU指令。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
#### volatile的应用场景
只有在状态真正独立于程序内其他内容时才能使用 volatile
典型案例:双重检查
忽略volatile关键字程序也可以很好的运行,只不过代码的稳定性不是100%,说不定在未来的某个时刻隐藏的 bug 就出来了
public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}
推荐饿汉模式加载优雅写法 Initialization on Demand Holder(IODH)---实际上实现了延迟的效果
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
枚举类型的写法
public enum Singleton{
INSTNCE;
public void pp(){}
}
## 两个重要的实现模型AQS和CAS
### AQS模型
AbstractQueuedSynchronizer抽象队列同步器:AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
抽象队列同步器中包括两个部分:临时资源和一个FIFO的CLH阻塞队列.
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
内部使用AQS的例子:以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;锁的具体实现,依赖于同步器实现锁机制
内部类:
abstract static class Sync extends AbstractQueuedSynchronizer 同步器实际上就是AQS的一个子实现
static final class NonfairSync extends Sync非公平同步器的实现类
static final class FairSync extends Sync 公平同步器的实现类
### CAS模型
Java5中引入了AutomicInteger、AutomicLong、AutomicReference等特殊的原子性变量类,它们提供的如compareAndSet、incrementAndSet和getAndIncrement等方法都使用了CAS操作。都是由硬件指令来保证的原子方法。
CAS即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置V、预期原值A和新值B。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。
比较和交换(Compare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。
原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成
CAS 的特性:
- 通过调用JNI的代码实现
- 非阻塞算法
- 非独占锁
CAS 存在的问题:
- ABA
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
#### 典型案例:原子量
所谓的原子量即操作变量的操作是原子的,该操作不可再分,因此是线程安全的。
为何要使用原子变量呢,原因是多个线程对单个变量操作也会引起一些问题。在Java5之前,可以通过volatile、synchronized关键字来解决并发访问的安全问题,但这样太麻烦。
Java5后专门提供了用来进行单变量多线程并发安全访问的工具java.util.concurrent.atomic,其中的类也很简单。例如AtomicLong aLong=new AtomicLong(10000); //原子量,每个线程都可以自由操作
具体实现:
Java中的Unsafe类提供了类似C++手动管理内存的能力。Unsafe类对普通程序员来说是
危险的,一般应用开发者不会用到这个类。getIntVolatile方法用于在对象指定偏移地
址处volatile读取一个int。putIntVolatile方法用于在对象指定偏移地址处volatile写入一个int。
原子量实现的计数器
//自定义计数器
public class AtomicCounter {
private static AtomicCounter instance = new AtomicCounter();
private AtomicCounter() {
}
public static AtomicCounter getIntance() {
return instance;
}
// 存储的数据,同时利用CAS提供线程安全的特性
private AtomicInteger counter = new AtomicInteger();
public int getCounter() {
return counter.get();
}
public int increase() {
// 内部使用死循环for(;;)调用compareAndSet(current, next)
return counter.incrementAndGet();
// return counter.getAndIncrement();
}
public int increase(int i) {
// 内部使用死循环for(;;)调用compareAndSet(current, next)
return counter.addAndGet(i);
// return counter.getAndAdd(i);
}
public int decrease() {
// 内部使用死循环调用compareAndSet(current, next)
return counter.decrementAndGet();
// return counter.getAndDecrement();
}
public int decrease(int i) {
// 内部使用死循环调用compareAndSet(current, next)
return counter.addAndGet(-i);
// return counter.getAndAdd(-i);
}
public static void main(String[] args) throws Exception {
final AtomicCounter ac = AtomicCounter.getIntance();
// 可缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
Set
for (int i = 0; i < 1000; i++) {
service.execute(new Runnable() {
public void run() {
int rr=ac.increase();
set.add(rr);
System.out.println(Thread.currentThread() + "::" + rr);
}
});
}
Thread.currentThread().sleep(2000);
System.out.println(set.size());
service.shutdown();
}
}
#### CAS模型的问题
1、ABA问题
> 解决方法:JAVA中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。
2、CAS应用场景隐含竞争是短暂的,否则不断的自旋尝试会过度消耗CPU
> 解决方法加入超时设置
3、CAS只能保证一个共享变量的原子操作,解决方法是使用锁或者合并多个变量
> AtomicReference提供了以无锁方式访问共享资源的能力
总结:
AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference这些原子类型,它们无一例外都采用了基于volatile 关键字 +CAS 算法无锁的操作方式来确保共享数据在多线程操作下的线程安全性。
- volatile关键字保证了线程间的可见性,当某线程操作了被volatile关键字修饰的变量,其他线程可以立即看到该共享变量的变化。
- CAS算法,即对比交换算法,是由UNSAFE提供的,实质上是通过操作CPU指令来得到保证的。CAS算法提供了一种快速失败的方式,当某线程修改已经被改变的数据时会快速失败。
- 当CAS算法对共享数据操作失败时,因为有自旋算法的加持,对共享数据的更新终究会得到计算。总之,原子类型用自旋+CAS的无锁操作保证了共享变量的线程安全性和原子性。