社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
volatile是Java虚拟机提供的轻量级的同步机制,有3个特性,分别是:保证可见性、不保证原子性、禁止指令重排
计算机在执行程序时,为了提高性能,编译器在编译java代码和处理器jvm字节码的时候常常会做指令重排多线程中使用线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,所以需要使用轻量级的同步机制volatile。
多线程单例模式,通过引入DCL (Double Check Lock) 双端检锁机制
就是在进来和出去的时候,进行检测
public class SingletonDemo {
/**
instance = new SingletonDemo();可以分为以下三步进行完成:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址
可能出现指令重排,故要加上volatile
*/
private static volatile SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
// a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
synchronized (SingletonDemo.class) //b
{
//c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if(instance == null) {
// d 此时才开始初始化
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//模拟多线程环境
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
cas——Compare and Swap(比较并交换)是一种系统原语;它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁,所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
缺点:循环时间长,开销大,只能保证一个共享变量的原子操作,会产生ABA问题;
{ABA例子:假设有一个遵循CAS原理的提款机,小慧有100元存款,要使用这个提款机来提款50,由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50}
解决办法:1、使用AtomicReference原子引用(保证修改对象引用时的线程安全性),2、使用AtomicStampedReference时间戳原子引用修改版本号(每次修改原子值,版本号加1)
使用AtomicInteger,调用其api进行增删改查操作,例如:
atomicInteger.compareAndSet(1, 2)
不安全;java.util.ConcurrentModificationException
1、使用new Vectore<>(),
2、Collections.synchronizedList(new ArrayList<>())
3、使用CopyOnWriteArrayList()
hashmap
hashset的add方法实际调用了hashmap的put方法,只不过添加的值维key,而value是一个常量PRESENTpivate static final Object PRESENT = null;)
乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁可以使用volatile+CAS原语实现,带参数版本来避免ABA问题,在读取和替换的时候进行判定版本是否一致
悲观锁可以使用synchronize的以及Lock
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。
可重入锁就是递归锁
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块
ReentrantLock / Synchronized 就是一个典型的可重入锁
最后得到的结果也是一样的,因为里面不管有几把锁,其它他们都是同一把锁,也就是说用同一个钥匙都能够打开
当我们在getLock方法加两把锁,但是只解一把锁会出现程序直接卡死,线程不能出来,也就说明我们申请几把锁,最后需要解除几把锁
当我们只加一把锁,但是用两把锁来解锁的时候,运行程序会直接报错
自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,原来提到的cas,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。
优点:循环比较获取直到成功为止,没有类似于wait的阻塞
缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源
/**
* 手写一个自旋锁
*
* 循环比较获取直到成功为止,没有类似于wait的阻塞
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
* @author: 陌溪
* @create: 2020-03-15-15:46
*/
public class SpinLockDemo {
// 现在的泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "t come in ");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
while(!atomicReference.compareAndSet(null, thread)) {
}
}
/**
* 解锁
*/
public void myUnLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
// 自己用完了后,把atomicReference变成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "t invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 启动t1线程,开始操作
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后,启动t2线程,开始占用这个锁
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 开始释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读
/**
* 读写锁
* 多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
* 但是,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
*
* @author: 陌溪
* @create: 2020-03-15-16:59
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 资源类
*/
class MyCache {
/**
* 缓存中的东西,必须保持可见性,因此使用volatile修饰
*/
private volatile Map<String, Object> map = new HashMap<>();
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 定义写操作
* 满足:原子 + 独占
* @param key
* @param value
*/
public void put(String key, Object value) {
// 创建一个写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 释放
rwLock.writeLock().unlock();
}
}
/**
* 获取
* @param key
*/
public void get(String key) {
// 读锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "t 读取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 读锁释放
rwLock.readLock().unlock();
}
}
/**
* 清空缓存
*/
public void clean() {
map.clear();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 线程操作资源类, 5个线程读
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
四、你认识哪些高并发下的计数器
1、CountDownLatch:减法,减到0
/**
现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
*/
public class CountDownLatchDemo {
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() + "t 上完自习,离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "t 班长最后关门");
}
}
2、CyclicBarrier:和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final Integer tempInt = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "t 收集到 第" + tempInt + "颗龙珠");
try {
// 先到的被阻塞,等全部线程完成后,才能执行方法
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
3、Semaphore:信号量
信号量主要用于两个目的
一个是用于共享资源的互斥使用
另一个用于并发线程数的控制
/**
模拟一个抢车位的场景,假设一共有6个车,3个停车位
那么我们首先需要定义信号量为3,也就是3个停车位
*/
public class SemaphoreDemo {
public static void main(String[] args) {
/**
* 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
*/
Semaphore semaphore = new Semaphore(3, false);
// 模拟6部车
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
// 代表一辆车,已经占用了该车位
semaphore.acquire(); // 抢占
System.out.println(Thread.currentThread().getName() + "t 抢到车位");
// 每个车停3秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "t 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放停车位
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
用于标记回收算法:从GC root进行遍历,把可达对象都标记,剩下那些不可达的进行回收,这种方式需要中断其他线程,并且可能产生内存碎片。
java中可以作为GC Roots的对象有:虚拟机栈中引用的对象、方法区中的类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(natice方法)引用的对象。
当我们出现死锁的时候,首先需要使用jps命令查看运行的程序jps -l
再使用jstack查看堆栈信息
jstack xxxx # 后面参数是 jps输出的该类的pid
通过查看最后一行,我们看到 Found 1 deadlock,即存在一个死锁
1)引用计数法
每个对象有一个计数器,当对象被引用一次则计数器加1,但对象引用失效一次减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
缺点:每次对象复制时均要维护引用计数器,且计数器本身也有一定的消耗;较难处理循环引用。
在双端循环,互相引用的时候,容易报错,目前很少使用这种方式了
2)复制算法
复制算法在年轻代的时候,进行使用,复制时候有交换
分对象会在From和To区域来回复制,如此交换15次(由jvm参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活就存入老年代中。
算法优点:没有产生内存碎片
3)标记清除
先标记后清除,缺点是会产生内存碎片,用于老年代多一些。
4)标记整理
标记清除整理
使用jps和jinfo进行查看
1、jps:查看java的后台进程
2、jinfo:查看正在运行的java程序
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!