lock
Lock使用方法
Lock lock = new ReentrantLock;
lock.Lock();//加锁
try{
业务代码;
} catch () {
} finally {
lock.unlock();//释放锁
}
ReentrantLock实现源码
FairSync:公平锁,先来后到
NonfairSync:非公平锁,根据CPU的情况,可以插队(默认)
Synchronized 和 Lock的区别
1.Synchronized是内置关键字,而Lock是一个Java类
2.Synchronized无法获取锁的状态,Lock判断是否获取到了锁
3.Synchronized会自动释放锁,而Lock需要手动释放锁(否则死锁)
4.Synchronized会让线程排队,假设有两个线程,如果线程1获得锁则阻塞,而线程2则会一直等待。但Lock就不会一定出现这种现象
5.Synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平(默认非公平,可设置成公平)
生产者和消费者问题
public class ConsumerAndProductor {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 10; i++) {
resource.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
resource.decrement();
}
},"B").start();
}
}
class Resource{
private int num = 0;
//+1,生产
public synchronized void increment() {
if(num != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName() + "->" + num);
this.notifyAll();
}
//-1,消费
public synchronized void decrement() {
if(num == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println(Thread.currentThread().getName() + "->" + num);
this.notifyAll();
}
}
运行结果:
但是当有A,B,C,D四个线程同时运行时,会出现错误!这就是虚假唤醒,原因是用了if进行判断,多个线程同时进入临界区if只会判断一次
解决办法:改为while进行判断
JUC版生产者和消费者问题
使用condition代替wait,notify,notifyAll
代码改进:
public class ConsumerAndProductor {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 6; i++) {
resource.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 6; i++) {
resource.decrement();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 6; i++) {
resource.increment();
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 6; i++) {
resource.decrement();
}
},"D").start();
}
}
class Resource{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1,生产
public void increment(){
lock.lock();
try {
while(num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "->" + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1,消费
public void decrement() {
lock.lock();
try {
while(num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "->" + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
结果:
condition有个更强大的功能:精准唤醒
public class ConsumerAndProductor {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 4; i++) {
resource.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 4; i++) {
resource.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 4; i++) {
resource.printC();
}
},"C").start();
}
}
class Resource{
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void printA() {
lock.lock();
try {
while (num != 1) {
conditionA.await();//A等待
}
System.out.println(Thread.currentThread().getName() + "->" + "A在执行");
//唤醒制定线程
num = 2;
conditionB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num != 2) {
conditionB.await();//A等待
}
System.out.println(Thread.currentThread().getName() + "->" + "B在执行");
//唤醒制定线程
num = 3;
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num != 3) {
conditionC.await();//A等待
}
System.out.println(Thread.currentThread().getName() + "->" + "C在执行");
//唤醒制定线程
num = 1;
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果:
8锁现象
如何判断锁的是谁?是什么锁?
通过以下八种情况的运行结果,分别进行总结
//1.标准情况下(main方法里面延时2秒)
//2.sendSms先延时4秒
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
resource.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.call();
},"B").start();
}
}
class Resource{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
nchronized锁的是方法的调用者,同一个对象调用不同的方法但锁都是一样的,谁先拿到谁先执行,故先执行sendSms()方法
//3.一个普通方法,一个静态方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
resource.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.printHello();
},"B").start();
}
}
class Resource{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public void printHello() {
System.out.println("hello");
}
}
普通方法没有锁,不受Synchronized的影响,故先输出hello
//4.两个对象分别执行两个同步方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();
new Thread(()->{
resource1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource2.call();
},"B").start();
}
}
class Resource{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
两个对象所以有两把锁,sendSms()方法有延时故先输出 打电话
//5.一个对象执行两个静态同步方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
resource.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.call();
},"B").start();
}
}
class Resource{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
//6.两个个对象分别执行两个静态同步方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();
new Thread(()->{
resource1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource2.call();
},"B").start();
}
}
class Resource{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
方法在类加载的时候就有了,锁的是class模板,Resource是惟一的(即使有多个对象)
//6.一个对象执行一个个静态同步方法,一个普通同步方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
resource.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.call();
},"B").start();
}
}
class Resource{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
有两个锁,一个锁锁的是class模板,一个锁的是方法的调用者,所以call()方法不需要等待sendSms()方法
//7.两个对象分别执行一个个静态同步方法,一个普通同步方法,谁先执行?
public class Test {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();
new Thread(()->{
resource1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource2.call();
},"B").start();
}
}
class Resource{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
两个锁,一个锁锁的是class模板,一个锁的是方法的调用者,所以call()方法不需要等待sendSms()方法
并发编程下集合类不安全
List不安全
解决方案:1.使用vector代替List
2.使用工具类中的synchronizedLis(List<T> list)t方法
List<String> list = Collections.synchronizedList(new ArrayList);
3.使用JUC包下的CopyOnWriterArrayList类
List<String> list = new CopyOnWriterArrayLis<>();
CopyOnWriterArrayList底层也是个数组,但这个数组使用了transient和volatile关键字进行了声明;
CopyOnWrite(写入时复制,达到读写分离的效果)是计算机程序设计领域的一种优化策略,多个线程调用List的时候,读取的时候是固定的但写入的时候会发生覆盖,CopyOnWrite会在写入时复制一份数据,等所有写入操作结束后将复制的数据插入到List中来避免覆盖;CopyOnWriterArrayList相对于Vector的优势在于CopyOnWriterArrayList的add()效率更高,这是因为Vector中的add()底层是用Synchronized进行加锁和释放锁,而CopyOnWriterArrayList中的add()是用Lock类进行加锁和解锁
set不安全
解决方案:1.使用工具类中的synchronizedSet(Set<T> s)方法
Set<String> set = Collections.synchronizedSet(new HashSet<>());
2.使用JUC包下的CopyOnWriterArraySet类
Set<String> set = new CopyOnWriteArraySet<>();
其实HashSet底层是是个HashMap,HashSet之所以插入的数据不能重复是因为HashSet插入的是HashMap中的key,其中PRESENT是个常量
Map不安全
工作中不用HashMap
Map<Object, Object> map = new HashMap<>();//默认等价于Map<Object, Object> map = new HashMap<>("16","0.75")
解决方案:使用JUC包下的ConcurrentHashMap类
ConcurrentHashMap,HashMap和HashTable的对比:
1.HashMap不是线程安全:
在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的
2.HashTable是线程安全的:
HashTable和HashMap的实现原理几乎一样,
差别:
1.HashTable不允许key和value为null;
2.HashTable是线程安全的。
HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,见下图:
3.ConcurrentHashMap是线程安全的:
JDK1.7版本:JDK7ConcurrentHashMap源码
容器中有多把锁,每一把锁锁一段数据,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。
这就是ConcurrentHashMap所采用的"分段锁"思想,见下图:
每一个segment都是一个HashEntry<K,V>[] table, table中的每一个元素本质上都是一个HashEntry的单向队列(原理和hashMap一样)。比如table[3]为首节点,table[0]->next为A,之后为B,依次类推。
JDK1.8版本:做了2点修改,见下图:
取消Segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.
在ConcurrentHashMap中通过一个Node<K,V>[]数组来保存添加到map中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个ConcurrentHashMap对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容。
第一次添加元素的时候,默认初始长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取余来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,才会将该节点的链表转换成红黑树。通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新数组中,同一个链表中的元素通过hash值的数组长度位来区分,是放在原来的位置还是放到扩容的长度的相同位置去 。
在扩容完成之后,如果某个节点是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。
取元素的时候,相对来说比较简单,通过计算hash来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断key和key的hash,取出value值。
想要了解更具体的朋友可以移步https://blog.youkuaiyun.com/qq_38826019/article/details/119984944进行观看
Callable
1.有返回值
2.可以抛出异常
3.继承Thread类和Runnable接口实现的线程使用run()方法,继承Callable接口实现的线程使用call()方法
注意:Thread无法直接启动Callable,需要通过中间件来实现启动。如下:
new Thread(new FutureTask<k>(Callable)).start();
实例代码:
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask(myThread);
new Thread(futureTask, "A").start();
Object o = futureTask.get();//获取Callable的返回结果,且可能会产生阻塞(解决:放在最后一步或做异步通信处理)
}
}
class MyThread implements Callable {
@Override
public Object call() throws Exception {
return 1;
}
}
常用辅助类
1.CountDownLatch
//CountDownLatch类————减法计数器
public class test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "走了");
countDownLatch.countDown();//数量-1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零,然后继续向下执行
System.out.println("关门了");
}
}
2.CyclicBarrier
//CyclicBarrier--加法计数器
public class test {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{
System.out.println("成功召唤出神龙");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集第" + temp + "颗龙珠");
try {
cyclicBarrier.await();//等待计数器达到7,才会继续执行
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
3.Semaphore
//Semaphore--信号量
public class test {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);//传入资源的临界值
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire();//获得资源,当到达临界值时其余线程会被阻塞,直到资源被释放出来
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);//模拟现实情况(车走需要时间),等两秒
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//释放资源
}
},String.valueOf(i)).start();
}
}
}
作用:多个共享资源的互斥访问;并发限流;
读写锁
允许多个线程同时去读,但同一时刻只允许一个线程去写。
对写入线程执行的方法:
public void write() {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
try{
业务代码;
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
对于读线程执行的方法:
public void read() {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
try{
业务代码;
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
独占锁–写锁,共享锁–读锁
阻塞队列
应用场景:多线程并发,线程池。
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待(一直等到死) | 超时等待(超过设定时间就退出) |
---|---|---|---|---|
添加 | add(Element e) | offer(Element e) | put(Element e) | offer(Element e, time, 时间单位) |
移除 | remove() | poll() | take() | poll(time, 时间单位) |
检查队首元素 | element() | peek() |
SynchronizedQueue同步队列
没有容量,进去一个元素必须等待取出来之后才能再往里面放一个元素。(put, take)
线程池
池化技术
程序运行的本质可以说是:占用系统的资源。优化资源的使用–>池化技术,线程池,连接池,内存池,对象池等都是池化技术的应用。
池化技术:通俗点说就是事先准备好一定数量的资源,统一放在一块区域进行管理,有“人”需要使用就在里面拿,用完就还回去。
优点:节省资源,提高响应速度,方便管理
线程池 (三大方法,七大参数,四种拒绝策略)
三大方法
ExecutorService pool = Executors.newSingleThreadExecutor();//创建单一线程的线程池,只能存放一个线程
ExecutorService pool = Executors.newFixedThreadPool(5);//创建固定大小的线程池,大小由传入的参数指定,此处是5
ExecutorService pool = Executors.newCachedThreadPool();//创建一个无大小限制的线程池,理论上是21亿左右,但实际数量和CPU有关
七大参数
本质上都是调用了ThreadPoolExecutor。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:核心线程池大小,maximumPoolSize:最大线程池大小,keepAliveTime:超时时间,超过设置的时间便释放,unit:keepAliveTime的单位,workQueue:阻塞队列,threadFactory:线程工厂,handler:拒绝策略
线程来了先满足corePoolSize,其次是workQueue,只有当corePoolSize和workQueue都满了,才会启用(maximumPoolSize - corePoolSize)空间(即下图中红色实心正方形部分)
最大承载量:maximumPoolSize +(workQueue的容量),下图中显示的最大承载量是8
超过最大承载量则根据拒绝策略进行处理
自定义线程池,代码实例:
ExecutorService pool = new ThreadPoolExecutor(2,5,3,TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactor(), new ThreadPoolExecutor.AbortPolicy());
四种拒绝策略
AbortPolicy:超过最大承载量,不处理其余线程,直接抛出异常
CallerRunsPolicy:超过最大承载量,其余线程从哪来的回哪去
DiscardPolicy:超过最大承载量,把剩余任务直接扔掉,不会抛出异常
DiscardOldestPolicy:超过最大承载量,剩余线程会尝试去和最早被执行的线程进行竞争,不抛出异常
Runtime.getRuntime().availableProcessors()
执行此代码可以获取CPU的核数,以便更好的调优,比如:设置线程池的maximumPoolSize
异步回调
Java本身也提供了异步回调的机制:CompletableFuture<T>类,CompletableFuture<T>主要有两种回调方式:runAsync和supplyAsync。
runAsync:没有返回值,构造方法有两种:runAsync(Runnable runnable)和runAsync(Runnable runnable, Executor executor)。
supplyAsync:有返回值,构造方法有两种:supplyAsync(Supplier <U> supplier)和supplyAsync(Supplier <U> supplier, Executor executor)。
CompletableFuture可以通过调用whenComplete方法,成功的时候返回成功的值,错误的时候返回错误的值
创建CompletableFuture代码实例:
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync();
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync();
JMM
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
-
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
-
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
注意JMM存在一个问题–不保证可见性:当有多个线程对统一资源进行操作时,资源的改变对那些线程是不可见的,即在线程自己的工作内存中该资源并未改变!
解决办法:在资源(如变量)前面加上Volatile关键字
Volatile
Volatile是Java虚拟机提供的轻量级的同步机制,有三大特性:1.保证可见性,2.不保证原子性,3.禁止指令重排
1.保证可见性
让主存中的资源对线程是可见的
2.不保证原子性
关于不保证原子性的经典问题:多个线程同时调用num++。num++不是一个原子性操作,底层分为了五步
如何解决原子性的问题?
1、lock
2、Synchronized
3、原子类(直接和操作系统底层挂钩,直接在内存中修改值)
原子类中提供了加一的方法:getAndIncrement(),其底层调用的是CAS方法
3.禁止指令重排
指令重排:计算机会对你写的程序代码进行重新排列顺序
源代码–>编译器优化的重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑:数据之间的依赖性
Volatile禁止指令重排的原因:内存屏障。
内存屏障也是保证可见性的原因
Volatile在单例模式中使用的最多
单例模式
主要讲讲饿汉式,懒汉式中的DCL懒汉式
饿汉式单例模式
public class HungryMan{
private HungryMan(){
}
private final static HungryMan hungryMan = new HungryMan();
public static HungryMan getInstance(){
return hungryMan;
}
}
懒汉式单例模式
public class LazyMan{
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyman = new LazyMan();
}
return lazyMan;
}
}
多线程下普通懒汉式单例模式不安全,可以用DCL懒汉式单例模式(双重检测锁的懒汉式单例模式)
//DCL懒汉式单例模式
public class LazyMan{
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized(LazyMan.class){
if(lazyMan == null){
lazyman = new LazyMan();
}
}
}
return lazyMan;
}
}
但是这样的DCL还是不够安全,因为new LazyMan()并不是原子性的操作!解决办法:
private volatile static LazyMan lazyMan;
但其实这还是不安全,因为有个万恶之源–反射!!!
不过,有“人”可以治反射,那就是枚举!枚举真正用的是有参构造!
CAS
CAS:compareAndSet,比较并交换,是CPU的并发原语。
compareAndSet(int expect, int update),如果期望的值expect拿到了,就更新为update,否则不更新
操作底层,调用了unsafe类,如下图:
CAS也存在以下缺点:
1.耗时,底层是个自旋锁,拿不到期望的值就会一直循环
2.一次性只能保证一个共享变量的原子性
3.存在ABA问题
ABA问题(狸猫换太子)
如上所示,其实左边线程拿到的A已经不是最开始的A,被右边的线程动过了。
解决办法:原子引用
原子引用
使用带时间戳的AtomicStampedReference(value, timeStamp),时间戳变化了则表明value被动过了
可重入锁,自旋锁
可重入锁(递归锁)
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义之一在于防止死锁。
实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。
如果同一个线程再次请求这个锁,计数器将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁(很好理解吧)。
自旋锁
自旋锁是一种锁,它使试图获取它的线程在循环(“自旋”)中简单地等待,同时反复检查该锁是否可用。