1.Java内存模型
Java内存模型JMM的内存模型如图所示,其规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作,都必须在工作内存中进行。
三大特性:
- 可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
- 原子性:不可分割,某个线程在做具体某个事务时,中间不可以被加塞或分割,需要整体完整。
- 有序性:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
2.volatitle
volatitle是jvm提供的轻量级的同步机制,保证可见性,但不保证完整性,禁止指令重排,其并不是并发安全的。由于其只保证可见性,在不符合以下两条规则的场景中,仍然要通过加锁来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
可见性保证:volatile保证了修饰的共享变量在转换为汇编语言时,会加上一个lock为前缀的指令,当CPU发现这个指令时,会立刻做以下两件事:
1.将当前内核中线程工作内存中该共享变量刷新到主存
2.通知其他内核里缓存的该共享变量内存地址无效
3.状态切换
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有一种状态:
- 新建(New):创建后尚未启动的线程处于这种状态
- 运行(Runnable):处于此状态的线程有可能正在执行,也有可能正在等待着CPU给它分配时间
- 无限期等待(Waiting):处于这种状态的线程不会被CPU分配时间,它们要等待被其他线程唤醒。可通过以下方法无限期等待:
1.没有设置Timeout参数的Object.wait()
2.没有设置Timeout参数的Thread.join()
3.LockSupport.park()方法 - 限期等待(Timed Waiting):这种状态下也不会被分配CPU时间,不过无须等待被唤醒,一段时间后会由系统自动唤醒。
- 阻塞(Blocked):线程在等待着获取到一个排他锁。
- 结束(Terminated):已终止的线程状态。
4.线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
5.线程安全的实现方法
5.1 互斥同步
同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
-
synchronized
synchronized是JVM实现的,可用于同步代码块,同步一个方法,同步一个类,同步一个静态方法。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步却不是。
Java的线程是映射到操作系统的原生线程智商的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态切换到内核态种,因此状态转换你需要耗费很多的处理器时间,所以synchronized是Java语言的一个重量级操作。 - synchronized的优化
- 自旋锁:互斥同步进入阻塞状态的开销很大。自旋锁让一个线程请求一个共享数据的锁的时候执行忙循环一段时间,如果在这段时间能获得锁,就可以避免进入阻塞状态。它只适用于共享数据锁定状态很短的情况。
- 锁消除:被检测出不可能存在竞争的共享数据的锁进行消除。
- 锁粗化:如果虚拟机探测到一系列连续的操作对同一个对象频繁加锁,就会把加锁的范围扩展到整个操作序列的外部。
- 轻量级锁:相对于传统的重量级锁而言,使用CAS操作来避免重量级锁使用互斥量的开销。先采用CAS操作进行同步,如果CAS失败了再改用互斥量来进行同步。
- 偏向锁:让第一个获取锁对象的进程,在这之后获取该锁就不再需要进行同步操作。当有另外一个锁去尝试获取这个对象时,偏向状态就宣告结束,此时恢复到无锁状态或轻量级锁状态。
-
ReentrantLock
基本语法上和synchronized相似,都是可重入锁,但增加了一些高级功能,比如等待可中断、可实现公平锁、锁可以绑定多个条件。 -
二者的区别
- synchronized基于jvm实现,ReentrantLock基于jdk实现
- ReentranLock是等待可中断的,synchronized不是
- ReentranLock可实现公平锁
- ReentranLock可绑定多个条件。
5.2 非阻塞同步
从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,就会出现问题,无论是否出现竞争,都要进行加锁。而乐观锁是先进行操作,如果没有其他线程竞争共享数据,那么操作就成功了。这种乐观的并发策略不需要把线程挂起。
-
CAS
比较并交换。CAS指令有3个操作数,内存地址V,旧的期望值A,新的值B。只有当V的值等于A,才把V的值更新为B。 -
JUC中的原子类
例如AtomicInteger中就调用了Unsafe类的CAS操作。其自增操作就是基于CAS实现的。 -
ABA
如果一个变量初次读取的时候是A值,后来被改为了B,然后又被改回A,那CAS会误认为它没有改变过,这就会导致ABA问题。JUC提供了一个带有标记的原子引用类AtomicStampedReference来解决这个问题,它可以通过控制变量值的版本来保证CAS的正确性。
6.AQS(AbstractQueuedSynchronizer队列同步器)
AQS是一个用来构建锁和同步器的框架,ReentrantLock,Semaphore,FutureTask等等均是基于AQS。其核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,需要一套线程阻塞等待以及唤醒时锁分配的机制,这个机制是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH是一个虚拟的双向队列(不存在队列实例,仅存在结点间的关联联系)。AQS将每条请求共享资源的线程封装成一个CLH锁队列的一个结点来实现锁的分配。
- AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如可重入锁
Share(共享):多个线程同时执行,如Semaphore/CountDownLatch。
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法。
同步器可重写的方法
- AQS组件
- Semaphone:允许多个线程同时访问资源。
- CountDownLatch:用来控制一个线程等待多个线程。维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
- CyclicBarrier:用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
- ReentrantReadWriteLock:读写锁允许同时对某一资源进行读
7.ThreadLocal
ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。每个线程都有一个ThreadLocalMap,其结构类似HashMap,但ThreadLocalMap中没有链表结构。每一个ThreadLocal对象作为Map中的key,value为代码中放入的值。
其中的key为弱引用,发生GC后,key会被回收,而value为强引用,不会被回收,这个时候就可能导致内存泄漏。ThreadLocalMap的解决方法为再调用set()、get()、remove()方法的时候,会清理掉key为Null的记录。
8.线程池
https://www.jianshu.com/p/3f6eed342491
9.场景
- 死锁
public static Object lock1=new Object();
public static Object lock2=new Object();
public static void main(String[] args) {
Thread thread1=new Thread(()->{
synchronized (lock1){
System.out.println("Thread 1 get Lock 1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
synchronized (lock2){
System.out.println("Thread 1 get Lock 2");
}
}
}
});
Thread thread2=new Thread(()->{
synchronized (lock2){
System.out.println("Thread 2 get Lock 2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
synchronized (lock1){
System.out.println("Thread 2 get Lock 1");
}
}
}
});
thread1.start();
thread2.start();
}
- 两个线程交替打印A和B
public static volatile boolean flag=true;
public static void main(String[] args) {
Thread thread1=new Thread(() -> {
while (true){
if (flag){
System.out.println("A");
flag=!flag;
}
}
});
Thread thread2=new Thread(() -> {
while (true){
if (!flag){
System.out.println("B");
flag=!flag;
}
}
});
thread1.start();
thread2.start();
}
网友评论