CountDownLatch,是一种减法计数器。
CountDownLatch主要有两个方法:
await()
会阻塞线程,等待计时器归零。countDown()
会令计数器减1。例如,创建6个线程,需要等待这6个线程执行完再在主线程中输出“main End”。代码如下:
package com.wunian.juc.help;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch 减法计数器
* 创建六个线程,等待这六个线程跑完再执行主线程的End
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//CountDownLatch默认的计数器初始值设置为6
CountDownLatch countDownLatch =new CountDownLatch(6);
for(int i=1;i<=6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Start");
countDownLatch.countDown();//计数器-1
},String.valueOf(i)).start();
}
countDownLatch.await();//只要计数器没有归零,这里就会一直阻塞
//等待上面六个线程跑完再执行主线程的End
System.out.println(Thread.currentThread().getName()+" End");
}
}
CyclicBarrier,作用和CountDownLatch相反,是一种加法计数器。主要是通过判断线程数来控制线程的阻塞。
例如,模拟集齐7颗龙珠召唤神龙。代码如下:
package com.wunian.juc.help;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 加法计数器
* 集齐七颗龙珠召唤神龙
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
//CyclicBarrier 篱栅 7是最多放入的线程数
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for(int i=1;i<=7;i++){
final int temp=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了第"+temp+"颗龙珠");
//线程阻塞 1 2 3 4 5 6 7
try {
cyclicBarrier.await();//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore,信号量,主要用于两种情况:
主要方法
acquire()
release()
例如,模拟停车场有3个位置,现在有6辆车要进来的场景,代码如下:
package com.wunian.juc.help;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Semaphore信号量
* 主要用在两种地方:多线程共享资源互斥!并发线程的控制!
* 模拟场景:停车场有3个位置,现在有6辆车要进来
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//模拟3个车位
Semaphore semaphore=new Semaphore(3);
//模拟6辆车
for(int i=1;i<=6;i++){
new Thread(()->{
try {
semaphore.acquire();//得到车位,如果信号量为0,就会一直等待
System.out.println(Thread.currentThread().getName()+"抢到了车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//释放车位,唤醒等待的车
}
},String.valueOf(i)).start();
}
}
}
JMM,即Java内存模型(Java Memory Mode),是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。
线程的八大操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
JMM对这八种指令的使用,制定了如下规则:
volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。
volatile的特性
volatile保证可见性的论证,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.TimeUnit;
/**
* volatile可见性论证
*/
public class VolatileDemo {
//private static int num=0;
private volatile static int num=0;
public static void main(String[] args) throws InterruptedException {//有3个线程 main、gc、我们定义的线程
new Thread(()->{
while(num==0){
//没加volatile时,这个对象不可见,执行过程中会一直卡住
}
}).start();
TimeUnit.SECONDS.sleep(1);
num=1;//没加volatile时,虽然main线程修改了这个值,但上面的线程并不知道
System.out.println(num);
}
}
volatile不保证原子性(不可分割)的论证,代码如下:
package com.wunian.juc.jmm;
/**
*volatile不保证原子性(不可分割)的论证
*/
public class VolatileDemo2 {
private volatile static int num = 0;
// synchronized
public static void add(){
num++;
}
public static void main(String[] args) {
// 期望 num 最终是 2 万
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add();
}
},String.valueOf(i)).start();
}
// 判断活着的线程
while (Thread.activeCount()>2){ // mian gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
从运行结果可以知道,最终输出的结果并不总是20000,进入class文件目录,打开DOS窗口,输入命令javap -c VolatileDemo2.class
可以查看该类的Java字节码。
显然,num++并不是一个原子性操作。
如果不使用synchronized和lock,应该如何解决int类型变量自增的原子性问题呢?
这个时候就要使用AtomicInteger了。
AtomicInteger
AtomicInteger类提供了int类型变量运算的原子性操作,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile不保证原子性验证
* AtomicInteger是原子性的
* 保证变量原子性的方法:
* 1.在方法中加synchronized关键字
* 2.使用AtomicInteger
*/
public class AtomicIntegerDemo {
//private volatile static int num=0;//int 不是原子性的
/*public synchronized static void add(){
num++;//num++不是原子性的操作
}*/
private volatile static AtomicInteger num=new AtomicInteger();
public static void add(){
num.getAndIncrement();//等价于num++,是原子性操作
//Java不能直接操作内存! native c++=>操作内存
//Unsafe类:后门,可以用它来直接操作内存!
}
public static void main(String[] args){
//期望num的最终结果是20000,当使用volatile时,num的实际结果并不准确
for(int i=1;i<=20;i++){
new Thread(()->{
for(int j=1;j<=1000;j++){
add();
}
},String.valueOf(i)).start();
}
//判断活着的线程
while(Thread.activeCount()>2){//除了main、 gc还有其他线程运行时,先运行其他线程
Thread.yield();//线程让步
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
禁止指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。
Java代码的执行过程:
源代码->编译器(优化重排)->指令并行重排-> 内存系统的重排-> 最终执行的结果
虽然单线程一定是安全的,但是也无法避免指令重排。
处理器在进行重排的时候会考虑指令之间的依赖性。
尝试理解多线程下的指令重排问题,如下所示:
int x,y,a,b = 0;
线程1 线程2
x = a; y = b;
b = 1; a = 2;
理想的结果: x=0 y = 0
指令重排:
线程1 线程2
b = 1; a = 2;
x = a; y = b;
重排后的结果: x=2 y = 1
指令重排小结
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例。
单例模式一般分为两种:饿汉式和懒汉式。
饿汉式,代码如下:
package com.wunian.juc.single;
/**
* 单例模式:饿汉式
* 单例思想:构造器私有
*/
public class Hungry {
//浪费空间 不是我们需要的
private byte[] data=new byte[10*1024*1024];
private Hungry(){}
private final static Hungry HUNGRY=new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式,基础版,代码如下:
package com.wunian.juc.single;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + " start");
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null){
lazy = new Lazy();
}
return lazyMan;
}
public static void main(String[] args) {
// 多线程下单例失效
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
DCL(双重校验锁)懒汉式,代码如下:
package com.wunian.juc.single;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + " start");
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null){
synchronized (Lazy.class){
if (lazy == null){
lazy = new Lazy(); // 请你谈谈这个操作!它不是原子性的
// java创建一个对象
// 1、分配内存空间
// 2、执行构造方法,创建对象
// 3、将对象指向空间
// A 先执行13,这个时候对象还没有完成初始化!
// B 发现对象为空,B线程拿到的对象就不是完成的
}
}
}
return lazy;
}
public static void main(String[] args) {
// 多线程下单例失效
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
单例之所以安全,是因为构造器私有的。但是构造器私有也不安全,使用反射就可以绕过构造器直接创建对象,代码如下:
package com.wunian.juc.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
/**
* 单例模式:DCL(Double Check Lock 双重校验锁)懒汉式
*/
public class Lazy {
private static boolean protectedCode=false;//标记参数,防止被反编译破坏
private Lazy(){
synchronized (Lazy.class){
if(protectedCode==false){
protectedCode=true;
}else{
//病毒代码、文件无限扩容
throw new RuntimeException("不要试图破坏我的单例模式");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance(){
//双重校验锁
if(lazy==null){
synchronized (Lazy.class){
if(lazy==null) {
lazy=new Lazy();//创建对象不是原子性的,还是存在不安全
//java创建一个对象
//1.分配内存空间
//2.执行构造方法,创建对象
//3.将对象指向空间
//如果A先执行1 3,这个时候对象还没完成初始化!B发现对象为空,B线程拿到的对象就不是完成的
}
}
}
return lazy;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//反射安全吗?,官方推荐我们单例真的是DCL懒汉式吗?
//Lazy lazy1=Lazy.getInstance();
//得到无参构造器
// Constructor declaredConstructors = Lazy.class.getDeclaredConstructor(null);
// Lazy lazy2=declaredConstructors.newInstance();//创建对象
// Lazy lazy3=declaredConstructors.newInstance();//创建对象
// //hashcode不一样,所以还是不安全 反射根本不需要通过构造器
// //System.out.println(lazy1.hashCode());
// System.out.println(lazy2.hashCode());
// System.out.println(lazy3.hashCode());
//如何破坏在反编译过程中的保护参数
Constructor declaredConstructors = Lazy.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
Lazy lazy4=declaredConstructors.newInstance();//创建对象
//获取参数对象,必须是在知道参数名称的情况下
Field protectedCode=Lazy.class.getDeclaredField("protectedCode");
//重新将参数值设置为false
protectedCode.setAccessible(true);
protectedCode.set(lazy4,false);
Lazy lazy5=declaredConstructors.newInstance();//创建对象
System.out.println(lazy4.hashCode());
System.out.println(lazy5.hashCode());
}
}
这个时候就要使用枚举类了。枚举是一个类,实现了枚举的接口,反射无法破坏枚举。代码如下:
package com.wunian.juc.single;
import com.sun.org.apache.bcel.internal.generic.INSTANCEOF;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 单例模式:使用枚举类
* 枚举是一个类,实现了枚举的接口
* 反射 无法破坏枚举
*/
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
//至少在做一个普通的jvm的时候,jdk源码没有被修改的时候,枚举就是安全的
//可以通过修改 jdk/jre/lib/rt.jar中java.lang.reflect.Constructor.class来破坏枚举(见Constructor类)
class Demo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过jad.exe反编译可知,SingleEnum类只有一个有参构造器
//Constructor declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
Constructor declaredConstructor=SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
// throw new IllegalArgumentException("Cannot reflectively create enum objects");
SingleEnum singleEnum1=declaredConstructor.newInstance();
SingleEnum singleEnum2=declaredConstructor.newInstance();
System.out.println(singleEnum1.hashCode());
System.out.println(singleEnum2.hashCode());
//这里面没有无参构造!JVM才是王道
//java.lang.NoSuchMethodException: com.wunian.juc.single.SingleEnum.()
}
}
运行代码会发现,报了一个异常:Cannot reflectively create enum objects
,因此无法通过反射破坏枚举。但是如果修改jdk源码,枚举也可能变得不安全,但至少一般情况下枚举还是安全的。
CAS,CompareAndSet,比较并交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
示例代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS:比较并交换
*/
public class CompareAndSetDemo {
public static void main(String[] args) {
//AtomicInteger默认为0
AtomicInteger atomicInteger=new AtomicInteger(5);
//compareAndSet CAS 比较并交换
//如果这个值是期望的值,则更新为指定的值,交换成功返回true,否则返回false
System.out.println(atomicInteger.compareAndSet(5,20));
System.out.println(atomicInteger.get());//输出当前的值
System.out.println(atomicInteger.compareAndSet(20, 5));
}
}
分析AtomicInteger类的getAndIncrement方法
getAndIncrement方法实现了int++的原子性操作,它底层是如何实现的呢?来看看它的源码:
// unsafe可以直接操作内存
public final int getAndIncrement() {
// this 调用的对象
// valueOffset 当前这个对象的值的内存地址偏移值
// 1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; // ?
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // 1000万
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
// 如果是期望的值,就交换,否则就不交换!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以看出,getAndIncrement的底层是通过自旋锁和CAS算法来实现的。
CAS的缺点
什么是ABA问题?
简单来说就是狸猫换太子,例如有两个线程T1和T2,T1线程希望通过CAS算法将一个变量的值由100更新为1,结果在更新过程中睡眠了3秒,在这三秒中T2线程也通过CAS算法先将该变量值更新由100更新为1,然后又将该变量值由1再次更新为100,整个过程看起来似乎该变量的值没有改变,但是对于T1线程来说,数据已经改动了。
如何解决ABA问题?
可以使用原子类,通过增加一个版本号来解决,原理和乐观锁一样。例如,小明和小花同时更新一个数据,小明先睡了三秒,结果数据先被小花更新了,这时版本号发生变化,小明再去更新就会失败,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA问题 1-100-1
* 通过version字段加1来实现数据的原子性
*/
public class ABADemo {
//version =1
static AtomicStampedReference atomicStampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
//其他人员 小花 需要每次执行完毕+1
new Thread(()->{
int stamp=atomicStampedReference.getStamp();//获得版本号
System.out.println("T1 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp02=>"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp03=>"+atomicStampedReference.getStamp());
},"T1").start();
//乐观的小明,sleep过程中数据被小花改过,版本号发生变化,无法完成更新
new Thread(()->{
int stamp=atomicStampedReference.getStamp();//获得版本号
System.out.println("T2 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result=atomicStampedReference.compareAndSet(100,1,stamp,stamp+1);
System.out.println("T2是否修改成功:"+result);
System.out.println("T2 stamp02=>"+atomicStampedReference.getStamp());
System.out.println("T2 当前获取得最新的值=>"+atomicStampedReference.getReference());
},"T2").start();
}
}
自旋锁是为实现保护共享资源而提出一种锁机制。是为了解决对某项资源的互斥使用。在任何时刻,最多只能有一个执行单元获得锁。如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
Unsafe类源码中的getAndAddInt方法中就使用了自旋锁,源码如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // 1000万
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
// 如果是期望的值,就交换,否则就不交换!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们可以使用自旋锁的方式定义一把锁,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自己定义一把自旋锁
*/
public class CodingLock {
//AtomicInteger默认是0
//AtomicReference默认是null
//锁线程
AtomicReference atomicReference=new AtomicReference<>();
//加锁
public void lock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"==>lock");
//上锁 自旋(不停的循环)
while(!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void unlock(){
Thread thread=Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName()+"==>unlock");
}
}
然后编写测试类测试一下,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.TimeUnit;
/**
* 测试自旋锁
*/
public class CodingLockTest {
public static void main(String[] args) {
CodingLock lock=new CodingLock();
//1 一定要先拿到锁,1解锁后2才可以拿到锁
new Thread(()->{
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
},"T1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2
new Thread(()->{
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
},"T2").start();
}
}
测试结果为T1线程先拿到锁,然后等T1释放锁之后T2线程才能拿到锁。这说明我们自己定义的锁实现了锁的功能。
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
产生死锁的四个条件
互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。
模拟死锁,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.TimeUnit;
/**
* 死锁
* 死锁条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件
* 死锁处理:查看堆栈信息:JVM知识
* //1.获取当前运行的java进程号 jps -l
* //2.查看进程信息 jstack 进程号
* //3.jconsole查看对应的信息(可视化工具)
*/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA="lockA";
String lockB="lockB";
new Thread(new MyLockThread(lockA,lockB),"T1").start();
new Thread(new MyLockThread(lockB,lockA),"T2").start();
}
}
class MyLockThread implements Runnable{
private String lockA;
private String lockB;
public MyLockThread(String lockA,String lockB) {
this.lockA=lockA;
this.lockB=lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get:"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get:"+lockA);
}
}
}
}
遇到死锁问题怎么处理?
1.获取当前运行的java进程号,命令:jps -l
2.查看进程信息,命令:jstack 进程号
3.使用jconsole查看对应的信息(可视化工具)。
4.修改代码,破坏死锁产生的其中一个条件即可。
查看字节码:javap -c xxx.class
查看class源码:javap -p xxx.class
将class文件反编译成java文件(需先将jad.exe拷贝至jdk的bin目录下):jad -sjava xxx.class