前言:
当应用启动后,有一条主线程贯穿应用的整个生命周期,主线程承载应用的所有事务处理,必须保障主线程一直处于工作状态,及时响应各种请求,所以一些耗时阻塞型的操作就不适合在主线程中进行,从而使用子线程来处理这类型请求,换句话说,主线程充当调度员的角色,将请求分配给子线程处理,需要的话在子线程完成请求后再次通知主线程,由主线程回复该请求,这保证请求被处理,也不会耽误新请求的接收。
优点
多线程的应用,提高了应用处理能力,增加性能,更加充分利用处理器资源。多线程的使用是并行还是并发?都有可能,同一进程的多个线程有可能被分配到同一个 CPU 内核中执行,也有可能被分配到多个 CPU 中处理,分配是操作系统行为,非人为可控。
那么多线程的使用中,有些时候出现不同线程对同一数据的处理,这引出本文的中心 – – 数据安全,保证数据安全有同步(synchronized),使用锁(Lock),基本数据类型可以使用 java.util.concurrent.atomic 中的原子性类型保证数据同步。
同步(synchronized)
同步保证数据的原子性和可见性,有三种使用,同步方法,同步静态方法,同步代码块(对象),同步锁为重量级锁,但在 Java SE 1.6 优化后,在一些情况下减低了性能消耗,让锁不再那么重。同步引入四种锁状态,分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁在使用过程中升级是单向的,只能升级不能降级,对于 synchronized
在 Java 1.6 的变化,可以参见 Java SE1.6中的Synchronized 进行了解。
- 同步方法,持有锁是当前对象实例,在该方法执行时先获取锁,执行完后释放
class Sync{
public synchronized void syncMethod() {
System.out.println("This is synchronized method.");
}
}
- 同步静态方法,持有锁是该函数所属字节码文件对象(xx.class)
class Sync{
public static synchronized void syncStatic() {
System.out.println("This is static synchronized.");
}
}
- 同步代码块,持有锁是任意对象,当该对象被持有后,其他线程的对该对象操作只能等待该对象被释放后才能进行
class Sync{
public void method(){
synchronized (object) {//任何对象均可
System.out.println("This is synchronized Object.");
}
}
}
在非频繁操作的处理,使用 synchronized
可以快速写出保证数据安全的代码,内部完成了锁的获取以及锁的释放,在非争用激烈场景,不需要中断锁、锁等候、锁投票时,同步是一个不错的选择。应用场景,单例使用同步块懒加载示例:
class Lazy{
private static Lazy instance = null;
public static Lazy getInstance() {
if (instance == null) {
synchronized (Lazy.class) {
if (instance == null) {
instance = new Lazy();
}
}
}
return instance;
}
}
synchronized
无法中断一个正在获取锁的线程,也不能通过投票获取锁,而且要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行。另一种锁框架,弥补了同步的缺点,提供一种更灵活的锁方式Lock
,与同步不同的是,在每次使用之前需要手动调用 lock()
,在结束后调用 unlock()
来释放锁,锁的操作主动权交给开发者,相较同步,Lock
更加灵活,除了锁的获取、释放,还有类似锁投票、定时锁等候和可中断锁等,还提供在激烈争用下的更加性能。
Lock 锁
- 实现类
ReentrantLock
,创建锁有两种机制,公平锁(锁的获取按申请的时间顺序)、非公平锁(与申请锁时间无关,由 JVM 分配,默认使用不公平策略)
ReentrantLock lock = new ReentrantLock();
public void method(){
lock.lock();
//...
finally{
lock.unlock();
}
}
- 另外两个是实现类
ReadLock
,WriteLock
.(ReadWriteLock
的内部类)
ReadWriteLock 读写锁
实现类 ReentrantReadWriteLock
,内部构造读锁 ReadLock
和写锁 WriteLock
读读共享,读写互斥,写写互斥
StampedLock 1.8 引入
认为读不应该阻塞写,读时发现数据被改变,重读数据或者抛出异常。读操作有乐观读和悲观读,乐观读认为读操作和写操作同时发生的概率很低,每次检查是否有写锁;悲观读每次将读锁转化为写锁,判断转换是否成功,否则继续上一转换过程直到成功。
- 乐观读
private StampedLock stampedLock = new StampedLock();
private void readOptimism() {
long stamp = stampedLock.tryOptimisticRead();//获取读锁
if (!stampedLock.validate(stamp)) {//检查是否有写锁,true 有,false 无
stamp = stampedLock.readLock();
try {
//read...
} finally {
stampedLock.unlockRead(stamp);
}
}
}
- 悲观读
private StampedLock stampedLock = new StampedLock();
private void readPessimistic() {
long stamp = stampedLock.readLock();//获取读锁
try {
while (true) {
long result = stampedLock.tryConvertToWriteLock(stamp);//转换写锁
if (0 != result) {//success
stamp = result;//更新 stamp
//read...
break;
} else {//fail
stampedLock.unlockRead(stamp);//释放写锁
stamp = stampedLock.writeLock();//直接拿写锁,重试
}
}
} finally {
stampedLock.unlock(stamp);
}
}
关键字 volatile
可以理解为轻量级 synchronized
,保证共享变量在各线程中的可见性,但不保证原子性。
每次线程访问该变量,将主内存中的值复制到工作内存中,每次修改及时将数据更新到主内存。可能存在的问题,线程 A 、线程 B 同时取出主内存中的变量复制到各自的工作内存,线程 A (假设先执行)对该变量进行修改,此时主内存中该变量被刷新,而线程 B 在执行时,使用的是工作内存中的(缓存)变量,并非最新的变量,所以 volatile
不适合对变量要求严格的使用场景,它仅保证变量的可见性,不能保证变量的原子性。