线程安全性
定义:当多个线程访问某个类的时候,不管运行环境采用何种调度方式或者这些进程如何交替执行,并且在主调代码中不需要采用额外的同步或者是协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
Atomic包
Atomic:CAS、Unsafe.compareAndSwapInt
AtmomicLong、LongAdder
AtomicReference、AtomicReferenceFieldUpdater
AtomicStampReference:CAS的ABA问题
线程不安全的累加案例
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class ThreadTest {
static int count=0;
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
count++;
System.out.println("count="+count);
}
};
service.execute(runnable);
}
}
}
结果显示:
count=3
count=3
count=3
count=4
count=5
count=6
count=7
count=8
count=9
count=10
Atomic类实现线程安全
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadTest {
static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
System.out.println("count="+count.incrementAndGet());
}
};
service.execute(runnable);
}
}
}
结果显示:
count=2
count=1
count=3
count=4
count=5
count=6
count=7
count=8
count=9
count=10
最终的输出结果为,可见这个程序是线程安全的。如果把AtomicInteger换成变量i的话,那最终结果就不确定了。
打开AtomicInteger的源码可以看到:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;
volatile关键字用来保证内存的可见性(但不能保证线程安全性),线程读的时候直接去主内存读,写操作完成的时候立即把数据刷新到主内存当中。
CAS简要
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
从注释就可以看出:当线程写数据的时候,先对内存中要操作的数据保留一份旧值,真正写的时候,比较当前的值是否和旧值相同,如果相同,则进行写操作。如果不同,说明在此期间值已经被修改过,则重新尝试。
compareAndSet使用Unsafe调用native本地方法CAS(CompareAndSet)递增数值。
CAS利用CPU调用底层指令实现。
Atomic包除了可以实现基本数据类型的原子操作,还能实现对象的原子操作。
AtomicReference对象的原子操作类
实现 原子操作
使用场景:
一个线程使用student对象,另一个线程负责定时读表,更新这个对象。那么就可以用AtomicReference。
赋值操作不是线程安全的。若想不用锁来实现,可以用AtomicReference
虽然在大多数的情况下Atomic包下的原子类操作已经够但是在极度竞争的条件下,即高并发严重的情况下,Atomic的性能表现不是非常理想,结合它的原理,在极度竞争的条件下,每个线程都在进行比较等待的过程,这个现象被称为过度自旋。这时程序的性能急剧下降。那么这时候我们应该怎么办?
IntegerAddr类实现线程安全
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
public class ThreadTest {
static LongAdder count=new LongAdder();
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
// synchronized (this) {
count.increment();
// System.out.println("count="+count.intValue());
// }
}
};
service.execute(runnable);
}
TimeUnit.SECONDS.sleep(1);
System.out.println("count="+count.intValue());
}
}
结果:count=10
原理参考我的文章LongAdder原理解析
Synchronized实现线程安全
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
public class ThreadTest {
static int count=0;
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
synchronized (this) {
count++;
System.out.println("count="+count);
}
}
};
service.execute(runnable);
}
System.out.println("count="+count);
}
}
结果:count=10
ReentrantLock锁实现线程安全
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
private static ReentrantLock lock=new ReentrantLock();
static int count=0;
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
lock.lock();
count++;
lock.unlock();
}
};
service.execute(runnable);
}
service.shutdown();
service.awaitTermination(1, TimeUnit.HOURS);
System.out.println("count="+count);
}
}
结果:count=10
对比
ReentrantLock与synchronized简单对比
ReentrantLock是JDK1.5之后引入的,synchronized作为关键字在ReentrantLock引入后进行的大量修改性能不断提升;
1.可重入性
ReentrantLock和synchronized都具有可重入性,写代码synchronized更简单,ReentrantLock需要将lock()和unlock()进行一一对应否则有死锁的风险;
2.锁的实现方式
Synchronized作为Java关键字是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
3.公平性
ReentrantLock提供了公平锁和非公平锁两种API,开发人员完全可以根据应用场景选择锁的公平性;
synchronized是作为Java关键字是依赖于JVM实现,Java团队应该是优先考虑性能问题,因此synchronized是非公平锁。
Atomic:竞争激烈时候可以维持常态,比Lock性能好;只能同步一个值。
线程安全的类:
ArrayList->Vector、Stack
HashMap->HashTable(key,value不能为null)
Collections.synchronizedXXX(List、Set、Map)
线程安全(并发容器 J.U.C)
ArrayList->CopyOnWriteArrayList(适合读多写少的场景、读写分离、读不加锁、写加锁)
HashSet、TreeSet->CopyOnWriteArraySet、ConcurrentSkipListSet
HashMap、TreeMap->ConcurrentHashMap、ConcurrentSkipListMap
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
用法:
CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
package com.lb.api.thread;
import java.util.concurrent.CountDownLatch;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch c=new CountDownLatch(2);
new Thread(()->{
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("a");
c.countDown();
}).start();
new Thread(()->{
System.out.println("b");
c.countDown();
}).start();
c.await();
System.out.println("c");
// Thread.sleep(30000);
}
}
结果输出 a b c
CountDownLatch底层原理
CountDownLatch通过AQS(AbstractQueuedSynchronizer)里面的共享锁来实现的。
ReentrantLock也是使用AQS
AbstractQuenedSynchronizer-AQS(重中之重)
1)使用Node实现FIFO队列、可以用于构建锁或者其他同步装置的基础框架。
2)利用了一个int类型表示状态。
3)使用方法是继承。
4)子类通过继承实现它的方法管理其状态{acquire和release}的方法操纵状态。
5)可以同时实现排它锁和共享锁(独占、共享)
AQS实现的大致思路:
AQS内部维护了一个CLH队列来管理锁,线程会首先尝试获取锁,如果失败,会将当前线程以及等待状态的信息包装成一个Node节点加入到同步队列,接着不断循环尝试获取锁,它的条件是当前节点为head的直接后继,如果失败就会阻塞自己,直到自己被唤醒,当持有锁的线程释放锁的时候会唤醒队列中的后继线程。
AQS同步组件
CountDownLatch(闭锁,通过一计数来保证线程是否需要一直阻塞)
Semaphore(可以控制线程并发数目)
CyclicBarrier
ReentrantLock(重点)
Condition
FutureTask
Semaphore
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。
Semaphore经常用于限制获取某种资源的线程数量。
package com.lb.api.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
final Semaphore sp = new Semaphore(3);//创建Semaphore信号量,初始化许可大小为3
for(int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e2) {
e2.printStackTrace();
}
Runnable runnable = new Runnable(){
public void run(){
try {
sp.acquire();//请求获得许可,如果有可获得的许可则继续往下执行,许可数减1。否则进入阻塞状态
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"进入,当前已有" + (3-sp.availablePermits()) + "个并发");
try {
Thread.sleep((long)(Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"即将离开");
sp.release();//释放许可,许可数加1
//下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
System.out.println("线程" + Thread.currentThread().getName() +
"已离开,当前已有" + (3-sp.availablePermits()) + "个并发");
}
};
service.execute(runnable);
}
}
}
结果打印:
线程pool-1-thread-1进入,当前已有1个并发
线程pool-1-thread-2进入,当前已有2个并发
线程pool-1-thread-3进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-4进入,当前已有3个并发
线程pool-1-thread-2已离开,当前已有3个并发
线程pool-1-thread-3即将离开
线程pool-1-thread-3已离开,当前已有3个并发
线程pool-1-thread-2进入,当前已有3个并发
线程pool-1-thread-1即将离开
线程pool-1-thread-5进入,当前已有3个并发
线程pool-1-thread-1已离开,当前已有3个并发
线程pool-1-thread-4即将离开
线程pool-1-thread-4已离开,当前已有2个并发
线程pool-1-thread-6进入,当前已有3个并发
线程pool-1-thread-5即将离开
线程pool-1-thread-5已离开,当前已有2个并发
线程pool-1-thread-7进入,当前已有3个并发
线程pool-1-thread-2即将离开
线程pool-1-thread-8进入,当前已有3个并发
线程pool-1-thread-2已离开,当前已有3个并发
线程pool-1-thread-7即将离开
线程pool-1-thread-7已离开,当前已有2个并发
线程pool-1-thread-9进入,当前已有3个并发
线程pool-1-thread-6即将离开
线程pool-1-thread-6已离开,当前已有2个并发
线程pool-1-thread-8即将离开
线程pool-1-thread-8已离开,当前已有1个并发
线程pool-1-thread-9即将离开
线程pool-1-thread-9已离开,当前已有0个并发
CyclicBarrier
类似于CountDownLatch,不过可以循环。
CountDownLatch描述的是一个或n个线程需要等待其他线程完成某个操作之后才能往下执行;CyclicBarrier描述的是多个线程之间等待直到所有线程都满足条件后才可执行。
以上所有关于线程安全的知识我只是做到了抛砖引玉的讲解,具体的还要读者自己去好好消化专研。