-
线程安全:当多个线程访问某个类,不管运行环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就成这个类为线程安全带的。
-
线程不安全: 多线程并发访问时得不到预期的结果。
-
获取一个java代码的字节码
javac -encoding UTF-8 UnSafeThreadDemo.java #以指定的字节码便以一个原文件 javap -c UnSafeThreadDemo.class #获取class文件的字节码
-
线程不安全产生的额原因:某些操作不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为CPU调度,多线程快速的切换,有可能两个线程都读取了同一个目标值,之后对它进行相关的操作,导致线程安全性问题。
-
原子性操作:一个或多个操作,要么全部执行并且在执行的过程中不会被任何因素打断,要么全部不执行。要么一起成功,要么一起失败。
-
把非原子性操作变为原子性操作:对相关的类添加 synchronized。
-
volatile关键字仅仅保证可见性,并不保证原子性;synchronized关键字,使得操作具有原子性。
-
深入理解synchronized
-
内置锁:每个java对象都可以用作一个实现同步的锁,这些所称为内置锁。线程进入同步代码块或方法的时候会自动获取的该锁,在退出同步代码块或方法时会自动释放该锁。
获得内置锁的唯一方法就是进入这个锁保护的同步代码块或方法。
-
互斥锁:内置锁是一个互斥锁,这就意味着在同一时间内做多只有一个线程能获得该锁,当线程A尝试去获取线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果线程B不释放这个锁,那么A线程将永远等待下去。
-
-
synchronized不能修饰类,但可以修饰方法、静态方法、代码块,修饰代码块的时候需要传入一个对象。
-
synchronized修饰普通方法,锁住的是对象实例
-
synchronized修饰静态方法,锁住的是整个类
-
synchronized修饰代码块,锁住一个传入synchronized的对象
-
volatile关键字:只能修饰变量,保证该对象的可见性,可以禁止指令的重排序
-
A、B两个线程同时操作被volatile修饰的变量,A修改了变量的值后,对B是可见的
-
volatile使用场景:
- 作为线程开关
- 单例,修改对象实例,禁止指令重排
-
单例与线程安全
-
饿汉式–本身线程安全
在类加载的时候,已经进行了实例化,无论之后用到用不到。如果该类比较占内存,之后又没用到,资源就被浪费了。
-
懒汉式–
在需要的时候再实例化,非线程安全
-
-
单例–饿汉
/** * 饿汉的单例模式 -- 线程安全 * 在类加载的时候,就进行了示例化,不管将来会不会被用到 */ public class HungerySingleton { private static HungerySingleton ourInstance = new HungerySingleton(); public static HungerySingleton getInstance() { return ourInstance; } private HungerySingleton() { } }
-
单例–懒汉
/** * 懒汉模式 -- 非线程安全 */ public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ } public static LazySingleton getInstance(){ //如果实例为空,则实例化 if(null == lazySingleton){ try { //模拟操作比较耗时 Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } lazySingleton = new LazySingleton(); } return lazySingleton; } public static void main(String[] args) { for (int i = 0; i < 10; i++){ new Thread(() -> { System.out.println(LazySingleton.getInstance()); }).start(); } } }
-
解决懒汉单例的线程安全问题
/** * 懒汉模式 -- 线程安全 */ public class LazySingleton { //volatile禁止指令充排序 private static volatile LazySingleton lazySingleton = null; private LazySingleton(){ } public static LazySingleton getInstance(){ //如果实例为空,则实例化 if(null == lazySingleton){ try { //模拟操作比较耗时 Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LazySingleton.class){ //双重检测锁 if(null == lazySingleton){ lazySingleton = new LazySingleton(); } } } return lazySingleton; } public static void main(String[] args) { for (int i = 0; i < 10; i++){ new Thread(() -> { System.out.println(LazySingleton.getInstance()); }).start(); } } }
-
线程安全问题的产生原因
- 多线程环境
- 多个线程操作同意共享资源
- 对改共享资源进行了非原子性操作
-
如何避免线程安全问题
- 将多线程改为单线程----必要的代码、加锁访问
- 不共享资源 ---- 不共享资源ThreadLocal、不共享、操作无状态化、共享资源不可变
- 将操作修改为原子性操作 ---- 加锁synchronized、使用jdk自带的原子性操作类、JUC提供的并发工具
-
锁的分类
- 自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源耗时短,频繁切换上下文很不值得,jvm实现,当线程没有获得锁的时候,不悲挂起,转而执行空循环,循环几次之后,如果还是没有获得锁,则会被挂起。
- 阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或时间),才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。
- 重入锁:支持线程再次进入的锁,跟我们有房间钥匙,可以多次进入房间类似。synchronized是重入锁。
- 读写锁: 两把锁,读锁和写锁,写写互斥,读写互斥,读读共享。
- 互斥锁:同意时间最多只能有一个线程获得锁,synchronized是互斥锁。
- 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿数据的时候就会被阻塞,直到拿到锁。
- 乐观锁: 每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下此间别人有没有去更新这个数据,可以使用版本号等机制。
- 公平锁: 大家都老老实实排队,对大家而言都很公平。
- 费公平锁: 一部分人排着队,但是新来的可能插队。
- 偏向锁: 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放。
- 独占锁: 独占锁模式下,每次只能由一个线程能持有锁。
- 共享锁: 运行多个线程同时获取锁,并发访问,共享资源。
-
锁的使用
import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 锁的使用 */ public class UnSafeThreadDemo { private static int num = 0; private static CountDownLatch countDownLatch = new CountDownLatch(10); private static Lock lock = new ReentrantLock(); //锁对象 /** * 每次调用对num加1 */ public static void inCreate(){ lock.lock(); //获得锁 num++; lock.unlock(); //释放锁 } public static void main(String[] args) { for(int i = 0; i < 10; i++){ new Thread(()->{ for (int j = 0; j < 100; j++){ inCreate(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } countDownLatch.countDown(); }).start(); } while (true){ if(countDownLatch.getCount() == 0){ System.out.println("num: "+num); break; } } } }
-
lock获取锁和释放锁都需要手动控制,lock采用的是乐观锁,所谓的乐观锁就是每次不加锁而是假设每次没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,乐观色实现的机制就是CAS操作。
-
synchronized托管给jvm执行,采用的是CPU悲观锁的机制,每个线程都是独占的,要想获得锁只能阻塞。
-
实现类lock接口的锁:
-
Lock接口中的方法
-
自定义锁
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * 自定义锁,支持方法的重入 */ public class MyLock implements Lock { private boolean holdLock = false; private Thread houlLockThread = null; private int holdLockCount = 0; /** * 在同一个时刻最多只能有一个线程获得锁 */ @Override public synchronized void lock() { if(holdLock && Thread.currentThread() != houlLockThread){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } houlLockThread = Thread.currentThread(); holdLock = true; holdLockCount++; } @Override public synchronized void unlock() { //判断当前线程是否是持有锁的线程,是,重入次数减一,不是,什么都不干 if(Thread.currentThread() == houlLockThread){ holdLockCount--; if(0 == holdLockCount){ notify(); holdLock = false; } } } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public Condition newCondition() { return null; } }
-
AbstractQueuedSynchronizer–为实现依赖于先进先出(FIFO)
等待队列的阻塞锁或相关的同步器(信号量、事件等等)提供一个框架。此类设计的目标是称为依靠单个原子int值来表示状态的大多数同步器的一个有用基础,子类必须定义更改此状态的受保护的方法,并定义那种状态对于此对象意味着被获取或者被释放。假定这些条件之后,此类中的其他方法就可以实现所有的排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步,而只追踪使用getState(),setState(int),和compareAndSetState(int,int);来操作以原子方式更新的int值。
应该将子类定义为非公共内部帮助器类,可以用他们来实现其封闭类的同步属性,类AbstractQueuedSynchronizer没有实现任何同步接口,而是定义了诸如acquireInterruptibly之类的一些方法,在适当的时候可以通过具体的锁和相关的同步器来调用它们,以实现公共的方法。
-
此类支持默认的独占模式和共享模式、或者两者都支持。处于独占模式下,其他线程试图获取该锁将无法取得成功,在共享模式下,多个线程获取某个锁可能(但不一定)会成功,此类并不了解这些不同,除了机械的意识到在当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁,处于不同模式下的等待线程可以共享相同的FIFO队列,通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock中发挥作用,只支持独占模式或只支持共享模式的子类不必定义支持未使用模式的方法。
-
此类通过支持独占模式的子类定义了一个嵌套的 AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作 Condition 实现。isHeldExclusively() 方法将报告同步对于当前线程是否是独占的;使用当前 getState() 值调用 release(int) 方法则可以完全释放此对象;如果给定保存的状态值,那么 acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然取决于其同步器实现的语义。
-
此类为内部队列提供了检查、检测和监视方法,还为 condition 对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer 将这些方法导出到类中。
-
此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject 方法,该方法在反序列化时将此对象恢复到某个已知初始状态。
-
公平锁和非公平锁的区别:
- 公平锁: 只要前面有线程在排队,那么刚进来的线程就老老实实的排队。
- 非公平锁:只要没有线程持有锁,后面来的线程就会持有锁,不管前面的线程排了多久。
-
非公平锁的弊端:
可能导致等待的线程得不到响应的cpu资源,从而引起线程饥饿
-
读写锁ReentrantReadWriteLock
特性:写写互斥,读写互斥,读读共享
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReentrantReadWriteLockDemo { private int i = 0; private int j = 0; private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); Lock readLock = lock.readLock(); Lock writeLock = lock.writeLock(); public void out(){ readLock.lock(); try { System.out.println("name: " + Thread.currentThread().getName() + ", i value: "+ i + ", j value: "+j); } finally { readLock.unlock(); } } public void inCreate(){ writeLock.lock(); try { i++; Thread.sleep(500L); j++; } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } } public static void main(String[] args) { ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo(); for (int i = 0; i < 3; i++) { new Thread(() -> { reentrantReadWriteLockDemo.inCreate(); reentrantReadWriteLockDemo.out(); }).start(); } } }
-
AQS:用单一int值表示读写两种状态
-
int是32位的,将其拆分成两个无符号的short
高位表示读锁 低位表示写锁
0000000000000000 0000000000000000
两种锁的最大重入次数均为65535,也就是2的16次方减一。
读锁:每次都从当前的状态加上65536
0000000000000000 0000000000000000
0000000000000001 0000000000000000
0000000000000001 0000000000000000
获取读锁个数,将state整个无符号右移16位即为读锁的个数。
0000000000000001
写锁:每次都直接加一
0000000000000000 0000000000000000
0000000000000000 0000000000000001
获取写锁的个数(进行与运算):
0000000000000001 0000000000000001
0000000000000000 1111111111111111
0000000000000000 0000000000000001
-
锁降级: 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
-
锁降级的代码实现
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class LockDegrade { public static void main(String[] args) { ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); Lock readLock = reentrantReadWriteLock.readLock(); Lock writeLock = reentrantReadWriteLock.writeLock(); writeLock.lock(); //获取写锁 readLock.lock(); //获取读锁 writeLock.unlock(); //释放写锁 readLock.unlock(); //释放读锁 System.out.println("程序运行结束"); } }
-
注意: 锁降级之后,写锁并不能直接降级成读锁,不会随着读锁的释放而释放,因此需要显式的释放写锁。
-
没有锁升级,也就是在获取读锁之后,无法获取写锁,同时只能有一个线程获取到写锁。
-
锁降级的应用场景: 对数据比较敏感,需要在对数据修改后,获取修改后的值,并进行后续的操作。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 锁降级demo */ public class LockDegradeDemo { private int i = 0; private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static Lock readLock = null; private static Lock writeLock = null; public void doSomething() { writeLock.lock(); try { i++; readLock.lock(); } finally { writeLock.unlock(); } try { Thread.sleep(2000L); //模拟其他复杂的操作 } catch (InterruptedException e) { e.printStackTrace(); } try { if(1 == i){ System.out.println("i == 1"); } else { System.out.println("i is : "+i); } } finally { readLock.unlock(); } } public static void main(String[] args) { LockDegradeDemo lockDegradeDemo = new LockDegradeDemo(); readLock = readWriteLock.readLock(); writeLock = readWriteLock.writeLock(); for (int i = 0; i < 4; i++){ new Thread(() -> { lockDegradeDemo.doSomething(); }).start(); } } }
-
StampedLock(1.8新增)
一般应用都是读多写少,ReentrantReadWriteLock,因读写互斥,故读的时候阻塞写,因而性能上上不去,可能会使写线程饥饿。
-
StampedLock的特点:
所有获取锁的方法都会返回一个邮戳,Stamp为0表示获取失败,其余表示获取成功;
所有释放锁的方法都需要一个邮戳,这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁);
支持锁升级和锁降级;
可以悲观读也可以乐观读;
使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销;
乐观读不阻塞写操作,悲观读阻塞写操作
-
StampedLock的优点:
相对于ReentrantReadWriteLock,吞吐量大幅提升
-
StampedLock的缺点:
api相对复杂,容器出错
内部事项相对于ReentrantReadWriteLock复杂的多
-
StampedLock的原理:
每次获取锁的时候,都会返回一个邮戳(stamp),相当于MySQL里面的version字段
释放锁的时候,根据之前获得的邮戳,去进行释放
-
使用StampedLock注意点
如果使用乐观锁,一定要判断返回的邮戳是否是一开始获取到的,如果不是,就要获取悲观锁读锁,再进行读取。
-
StampedLock使用示例
import java.util.concurrent.locks.StampedLock; public class StampedLockDemo { private double x,y; //锁实例 private final StampedLock s1 = new StampedLock(); //排他锁--写锁 void move(double deltaX, double delteY){ long stamp = s1.writeLock(); try { x += deltaX; y += delteY; } finally { s1.unlock(stamp); } } //乐观读锁 double distanceFromOrigin(){ //尝试获取乐观读锁(1) long stamp = s1.tryOptimisticRead(); //将全部变量拷贝到方法体栈内(2) double currentX = x, currentY = y; //检查在(1)获取到读锁票据后,锁有没有被其他写线程排他性抢占(3) if(!s1.validate(stamp)){ //如果被抢占则获取一个共享读锁(悲观获取)(排他的)(4) stamp = s1.readLock(); try { //将全部变量拷贝到方法体栈内(5) currentX = x; currentY = y; } finally { //释放共享读锁 s1.unlockRead(stamp); } } return Math.sqrt(currentX * currentX +currentY * currentY); } //锁升级 //使用悲观锁获取读锁,被尝试转化为写锁 void moveIfAtOrigin(double newX, double newY){ //这里可以使用乐观锁进行替换(1) long stamp = s1.readLock(); //如果当前点在原点则移动(2) while(x == 0.0 && y == 0.0){ //尝试将获取的读锁升级为写锁(3) long ws = s1.tryConvertToWriteLock(stamp); //升级成功则更新票据,并设置坐标值,然后退出循环(4) if(0L != ws){ stamp = ws; x = newX; y = newY; break; } else { //读锁升级写锁失败则释放读锁,显式获取独占写锁,然后循环重试(5) s1.unlockRead(stamp); stamp = s1.writeLock(); } } } }