java高并发复习笔记

1 篇文章 0 订阅

JMM

JMM即为JAVA 内存模型(java memory model)) 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。

JMM 同步规定

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须由工作内存进行。
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200709164839950.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyOTUwMTQ5,size_16,color_FFFFFF,t_7
JMM 的特性

  1. 可见性
  2. 原子性
  3. 有序性

Volatile

什么是Volatile
是java虚拟机提供的轻量级的同步机制

有三大特性

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

可见性

public class VolatileDemo {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " coming...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.addOne();
            System.out.println(Thread.currentThread().getName() + " updated...");
        }).start();

        while (data.a == 0) {
        
        }
        System.out.println(Thread.currentThread().getName() + " job is done...");
    }
}

class Data {
    // int a = 0;  
    volatile int a = 0;
    void addOne() {
        this.a += 1;
    }
}

如果不加 volatile 关键字,则主线程会进入死循环,加 volatile 则主线程能够退出,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。

不保证原子性
什么是原子性?
不可分割,完整性,也就是某个线程正在做某个具体的业务时,中间不可以被加塞或者分割,需要整体完整,要么同时成功,要么同时失败

怎么解决不保证原子性?
使用同步锁或者atomic
atomic底层(unsafel类 cas自旋)

public class VolatileDemo {
    public static void main(String[] args) {
       // test01();
       test02();
    }

    // 测试原子性
    private static void test02() {
        Data data = new Data();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.addOne();
                }
            }).start();
        }
        // 默认有 main 线程和 gc 线程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(data.a);
    }
}

class Data {
    volatile int a = 0;
    void addOne() {
        this.a += 1;
    }
}

发生输出的并不是20000,没有保证原子性

禁止指令重排
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象

计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种

  1. 编译器优化的重排
  2. 指令并行的重排
  3. 内存系统的重排

单线程环境里面确保程序最终执行的结果和代码执行的结果一致
处理器在进行重排序时必须考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测

public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;
    
    public void method01() {
        a = 1;           // flag = true;
                         // ----线程切换----
        flag = true;     // a = 1;
    }

    public void method02() {
        if (flag) {
            a = a + 3;
            System.out.println("a = " + a);
        }
    }

}

如果两个线程同时执行,method01 和 method02 如果线程 1 执行 method01 重排序了,然后切换的线程 2 执行 method02 就会出现不一样的结果。

DCL单例模式

public class DCLSingleton {
	private static volatile DCLSingleton dclSingleton = null;
	
	private DCLSingleton(){
		
	}
	
	public static DCLSingleton getsSinglton(){
		if (dclSingleton == null){
			synchronized (DCLSingleton.class) {
				if (dclSingleton == null){
					dclSingleton = new DCLSingleton();
				}
			}	
		}
		return dclSingleton;
	}
}

如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。

CAS

CAS是什么?
全称compare and swap 比较并交换 它是一条cpu并发原语
判断内存中某个位置的真实值是否为预期值,如果是则更改为新的值 ,该操作是原子性的
CAS底层原理
自旋锁 unsafe类
unsafe类
Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问,Unsafe 类相当一个后门,基于该类可以直接操作特定内存的数据。
Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 指针一样直接操作内存,因为 Java 中 CAS 操作执行依赖于 Unsafe 类。

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(3);
        // 获取真实值,并替换为相应的值
        boolean b = atomicInteger.compareAndSet(3, 2019);
        System.out.println(b); // true
        boolean b1 = atomicInteger.compareAndSet(3, 2020);
        System.out.println(b1); // false
        atomicInteger.getAndIncrement();
    }
}
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);//valueOffset为内存偏移量,即地址
}
//obj为atomicInteger类,valueOffset为内存偏移量,即地址,val需要加的值
public final int getAndAddInt(Object obj, long valueOffset, long expected, int val) {
    int temp;
    do {
        temp = this.getIntVolatile(obj, valueOffset);  // 获取快照值
    } while (!this.compareAndSwap(obj, valueOffset, temp, temp + val));  // 判断obj的值是否等于temp,相等返回true,obj的值变为temp+val,推出循环,不相等返回false继续循环获取内存中的值
    return temp;
}

cas的缺点

  1. 循环时间长开销很大
    如果 CAS 失败,会一直尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销(比如线程数很多,每次比较都是失败,就会一直循环),所以希望是线程数比较小的场景。
  2. 只能保证一个共享变量的原子操作
    对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
  3. ABA 问题

ABA
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程T1从主内存中取出A,线程T2从主内存中取出A,并且T2进行了一些操作把主内存的值变成了B,然后T2又将主内存中的B变为A,这时候线程T1进行cas操作发现内存中还是A。这就是ABA问题

ABA问题的后果
小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。

原子引用
AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3= new User("z3", 18);
        User l4= new User("l4", 20);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, l4)); // true
        System.out.println(atomicReference.get()); // User(userName=l4, age=20)
    }
}

怎么解决ABA问题?
使用原子时间戳AtomicStampedReference

public class ABADemo2 {
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
        }).start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);  //第三个参数判断当前stamp是否为
            System.out.println(b); // false
            System.out.println(atomicStampedReference.getReference()); // 100
        }).start();
    }
}

ArrayList 的安全问题

public class ContainerDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                list.add(random.nextInt(10));
                System.out.println(list);
            }).start();
        }
    }
}

多个线程去写list,发生并发修改异常,报ConcurrentModificationException异常
解决方案

  1. 使用vector,因为vector的add方法加了同步锁synchronized,但是保证了安全性,降低了并发性
  2. Collections.synchronizedList(new ArrayList<>());
    同理Collections.synchronizedSet(new HashSet<>());Collections.synchronizedMap(new HashMap<>());分别可以解决HashSet和HashMap的不安全问题
  3. CopyOnWriteArrayList
    同理CopyOnWriteArraySet可以解决HashSet不安全问题,底层是由CopyOnWriteArrayList实现的,ConcurrentHashMap是解决HashMap的不安全问题

CopyOnWriteArraySet的部分代码

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
    .....

CopyOnWriteArrayList添加源码

	// CopyOnWriteArrayList添加源码
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();  
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

写时复制
CopyOnWrite容器即写时复制的容器。往容器添加元素的时候,先加锁,不直接往容器Object[]添加,而是先将当前容器Object复制出一个新的容器Object[] newlements,然后在新的容器里面添加元素,添加元素之后,通过setArray方法将原容器的引用指向新的容器。
这样做的好处是可以对CopyOnWrite容器进行并发的写,而不需要加锁

1.优点
对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。

2.缺点
数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。

内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。

公平锁和非公平锁
是什么?
公平锁 是指多个线程按照申请锁的顺序来获取锁,先进先出原则,类似排队打饭,先来后到,
非公平锁 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,有可能会造成优先级反转或者饥饿现象

synchronized只能是非公平锁。而ReentrantLock可以实现公平锁和非公平锁两种,默认是非公平锁。

可重入锁(递归锁)

是什么? 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
synchronized,ReentrantLock都是可重入锁
作用:防止死锁

public class demo1 {
	public static void main(String[] args) {	
		demo2 d = new demo2();
		for (int i=0;i<100;i++){
			new Thread(()->{
				d.send1();
			}).start();
		}
	}
}
class demo2{
	public synchronized void send1(){
		System.out.println(Thread.currentThread().getName()+"send1");
		send2();
	}
	public  synchronized void send2(){
		System.out.println(Thread.currentThread().getName()+"send2");
	}
}

自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
好处:减少线程上下文切换的消耗
缺点:循环会消耗CPU

手写一个自旋锁

public class SpinLockDemo {
	AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
	public void myLock(){
		Thread th = Thread.currentThread();
		System.out.println(Thread.currentThread().getName()+"  lock");
		while(!atomicReference.compareAndSet(null, th)){
			
		}
	}
	
	public void myUnlock(){
		Thread th = Thread.currentThread();
		atomicReference.compareAndSet(th, null);
		System.out.println(Thread.currentThread().getName()+"  unlock");
	}
	
	public static void main(String[] args) {
		SpinLockDemo spinLockDemo = new SpinLockDemo();
		new Thread(()->{
			spinLockDemo.myLock();
			try {
				Thread.sleep(3000);
			} catch (Exception e) {
				e.printStackTrace();
			}
			spinLockDemo.myUnlock();
		},"A").start();	
		
		new Thread(()->{
			spinLockDemo.myLock();
			spinLockDemo.myUnlock();
		},"B").start();
	}
}

独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁(写锁) 指该锁一次只能被一个线程所持有
共享锁(读锁) 指该锁可以被多个线程所持有
对ReentrantReadWriteLock而言读锁是共享锁,写锁是独占锁

public class MyCache {
	private volatile Map<String,Object> map = new HashMap<>();
	private ReentrantReadWriteLock rwlock =  new ReentrantReadWriteLock();
	
	public void put(String key,Object value){
		rwlock.writeLock().lock();
		try {
			System.out.println(Thread.currentThread().getName()+"\t 正在写入"+key);
			try {Thread.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();
		}	
	}
	
	public void get(String key){
		rwlock.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName()+"\t 正在读取"+key);
			try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println(Thread.currentThread().getName()+"\t 读取完成"+map.get(key));
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			rwlock.readLock().unlock();
		}	
	}
	
	public static void main(String[] args) {
		MyCache myCache = new MyCache();
		for (int i = 0; i < 10; i++) {
			final int tempInt = i;
			new Thread(()->{
				myCache.put(tempInt+"", tempInt+"");
			},String.valueOf(i)).start();
		}
		
		for (int i = 0; i < 10; i++) {
			final int tempInt = i;
			new Thread(()->{
				myCache.get(tempInt+"");
			},String.valueOf(i)).start();
		}
	}
}

Synchronized与lock的区别
1.synchronized是关键字属于jvm层次的
synchronized 是由一对 monitorenter/monitorexit 指令实现的,
monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。
但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能
lock是一个接口,位于java.util.concurrent.locks下
2.synchronized不需要用户手动释放锁 线程会自动释放
lock需要手动释放锁,若没有及时释放,可能会造成死锁
3.synchronized不可中断 除非抛出异常或者完成
lock可中断 a.设置超时方法trylock(long timeout,TimeUnit) b.调用interrupted()方法
4.Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁
5.synchronized不能实现精确唤醒线程,要么随机唤醒一个,要么全部唤醒,reentrantlock可以通过绑定condition来精确唤醒线程
在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

案例

package com.citms;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程顺序执行
 * A 打印5次-》B打印10次-》C打印15次-》A打印5次-》B打印5次-》C打印15次 。。。。。
 */
class ShareData{
    private int num = 1; // A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5(){
        lock.lock();
        try {
            while (num != 1){
                condition1.await();
            }
            for (int i = 0; i < 5 ; i++) {
                System.out.print(Thread.currentThread().getName()+"\t"+(i+1)+"\t");
            }
            System.out.println();
            num = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try {
            while (num != 2){
                condition2.await();
            }
            for (int i = 0; i < 10 ; i++) {
                System.out.print(Thread.currentThread().getName()+"\t"+(i+1)+"\t");
            }
            System.out.println();
            num = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try {
            while (num != 3){
                condition3.await();
            }
            for (int i = 0; i < 15 ; i++) {
                System.out.print(Thread.currentThread().getName()+"\t"+(i+1)+"\t");
            }
            System.out.println();
            num = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ThreadChain {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.print5();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.print10();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.print15();
            }
        },"C").start();
    }
}


在这里插入图片描述

死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象
在这里插入图片描述

package com.citms;

class HoldLockThread implements Runnable{
    private String lockA;
    private String lockB;
    public HoldLockThread(String lockA,String lockB){
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"持有"+lockA+"\t尝试获取"+lockB);
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"持有"+lockB+"\t尝试获取"+lockA);
            }
        }
    }
}
public class DeadLock {
    public static void main(String[] args) {
        new Thread(new HoldLockThread("lockA", "lockB")).start();
        new Thread(new HoldLockThread("lockB", "lockA")).start();
    }

}

在这里插入图片描述
怎么判断是死锁呢?
通过jps命令找到该进程
在这里插入图片描述
通过jstack来分析原因
在这里插入图片描述
在这里插入图片描述

ReentrantLock/CountDownLatch/CyclicBarrier/Semaphore

ReentrantLock
ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全

CountDownLatch
让一些线程堵塞直到另一些线程完成一系列操作后才被唤醒。CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,调用线程会被堵塞,其他线程调用 countDown 方法会将计数减一(调用 countDown 方法的线程不会堵塞),当计数其值变为零时,因调用 await 方法被堵塞的线程会被唤醒,继续执行。

假设我们有这么一个场景,教室里有班长和其他6个人在教室上自习,怎么保证班长等其他6个人都走出教室在把教室门给关掉。

public class CountDownLauchDemo {
	public static void main(String[] args) throws InterruptedException {
		
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + " 离开了教室...");
            }, String.valueOf(i)).start();
        }
 
        countDownLatch.await();

        System.out.println("班长把门给关了,离开了教室...");
 
    }
}

CyclicBarrier
CyclicBarrier 字面意思可循环使用的屏障,主要是让一组线程到达一个屏障时被阻塞,直到最后一个线程达到屏障,屏障才会打开。通过**await()**方法进入屏障
与countdownlatch的区别:
countdownlatch是一次性的,CyclicBarrier是循环使用的
countdownlatch不同线程负责不同职责,有的负责等待(await),有的负责倒计时(countdown),而cyclicbarrier所有线程的职责都一样

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> {
            System.out.println("车满了,开始出发...");
        });
        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 开始上车...");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这里插入图片描述
Semaphore 信号量[ˈseməfɔːr]
Semaphore 是 synchronized 的加强版,作用是控制同时访问某个资源的线程数量
Semaphore维护了一个计数器,线程可以通过调用acquire()方法来获取Semaphore中的许可证,当计数器为0时,调用acquire()的线程将被阻塞(tryacquire不会阻塞,会立即返回),直到有其他线程释放许可证;线程可以通过调用release()方法来释放Semaphore中的许可证,这会使Semaphore中的计数器增加,从而允许更多的线程访问共享资源。
相关场景:
1.限流
2.资源池

public class SemaphoreDemo {
  public static void main(String[] args) {
      Semaphore semaphore = new Semaphore(3);
      for (int i = 0; i < 6; i++) {
          new Thread(() -> {
              try {
                  semaphore.acquire(); // 获取一个许可
                  System.out.println(Thread.currentThread().getName() + " 抢到车位...");
                  Thread.sleep(3000);
                  System.out.println(Thread.currentThread().getName() + " 离开车位");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  semaphore.release(); // 释放一个许可
              }
          }).start();
      }
  }
}

在这里插入图片描述

AQS

AbstractQueuedSynchronizer(抽象队列同步器),是java自带的除synchronize之外的锁机制,在java.util.concurrent.locks包下
AQS的核心思想
如果请求的共享资源空闲,则将当前请求该资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果请求的共享资源被占用,就需要一套线程阻塞等待以及唤醒时锁分配的机制,这个机制是用AQS中的CLH队列实现的,即暂时获取不到锁的线程加入到队列中。
通俗的来讲,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变state,成功则获取锁成功,失败则进入等待队列,等待被唤醒
CLH队列
AQS内部维护着一个FIFO的队列,即CLH队列。AQS的同步机制就是依靠CLH队列实现的。CLH队列是FIFO的双端双向队列,实现公平锁。线程通过AQS获取锁失败,就会将线程封装成一个Node节点,通过CAS将tail重新指向新的尾部节点。当有线程释放锁时,修改head节点指向下一个获得锁的节点

AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore 信号量[ˈseməfɔːr]、CountDownLatch、CyclicBarrier

自定义同步器
同步器的设计基于模板方法模式,自定义同步器通过继承AbstractQueuedSynchronizer重写指定的方法,而模板方法会调用使用者重新的方法。
重写哪些方法?
自定义同步器只需要实现共享资源state的获取以及释放方式就可以了
isHeldExclusively 判断该线程是否正在独占资源 用到condition才需要实现
tryAcquire(int) 独占方式 获取资源 成功true 失败false
tryRelease(int) 独占方式 释放资源 成功true 失败false
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

AQS部分转自于https://blog.csdn.net/mulinsen77/article/details/84583716

阻塞队列

什么是阻塞队列?
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当阻塞队列是满时,往队列里添加元素的操作将会被阻塞

为什么需要BlockingQueue?
我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都包办好了
在concurrent包发布之前,多线程环境下,需要我们控制这些细节,同时兼顾效率和安全,会给程序带来一定的复杂度

BlockingQueue的架构
在这里插入图片描述
BlockingQueue类似于List接口,BlockingQueue有七大实现类
ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列
LinkedBlockingQueue 是一个基于链表结构的有界阻塞队列,但是是默认大小为Integer.MAX_VALUE的阻塞队列,慎用!
SynchronousQueue 是一个不存储元素的阻塞队列(每一个put操作后必须等待一个take操作,否则不能继续添加元素,反之消费也是这样)
另外四个不做描述,知道前3个就是了

阻塞队列api
在这里插入图片描述

三种生产者消费者模式
第一种 wait notifyall synchronized
第二种 await signalAll lock
第三种 阻塞队列 BlockQueue
三种模式的代码地址

Callable接口和线程池

Callable接口案例

package com.citms;
import java.util.concurrent.*;

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("进到call里面来了");
        TimeUnit.SECONDS.sleep(5);
        return 520;
    }
}
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
        new Thread(futureTask).start();
        int result01 = 794;
        int result02 = futureTask.get(); // 如果call没执行完这里会阻塞
        System.out.println(result01+result02);
    }
}

在这里插入图片描述
线程池
线程池的核心参数
在这里插入图片描述
线程池的底层是ThreadPoolExecutor 其中的7个参数就是线程池的核心参数
1.corePoolSize 线程池中常驻的核心线程数
2.maximumPoolSize 线程池能够容纳同时执行的最大线程数
3.keepAliveTime 多余的空闲线程的存活时间
当前线程池数量超过corePoolSize时,并且空闲时间到达keepAliveTime值时,
多余的线程会被销毁直到只剩下corePoolSize个线程为止
4.unit keepAliveTime的单位
5.workQueue 任务队列,被提交但尚未被执行的任务存放处
6.threadFactory 表示生成线程池中工作线程的线程工厂,一般用默认的就可
7.hander 拒绝策略,当队列满了并且工作线程大于等于线程池的最大线程数,线程池启动拒绝策略
a.AbortPolicy 直接抛出RejectedExecutionException异常阻止系统正常运行, 这是默认的拒绝策略
b.CallerRunsPolicy "调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
c.DiscardOldesPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
d.DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案

线程池的底层流程
在这里插入图片描述

1.在创建了线程池后,等待提交过来的任务
2.当调用execute方法添加一个请求任务时,线程池会做如下判断
a.如果正在运行的线程数量小于coolPoolSize 那么马上创建线程运行这个任务
b.如果正在运行的线程数量大于或者等于coolPoolSize,那么将这个任务放入队列
c.如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程来运行任务
d.如果队列满了,正在运行的线程数量大于或者等于maximuxPoolSize,那么线程池会启动饱和拒绝策略来执行
3.当一个线程完成任务,从队列中取下一个任务执行
4.当一个线程无事可做找过一定的时间(keepAliveTime),线程池就会判断
当前线程池数量超过corePoolSize时,并且空闲时间到达keepAliveTime值时,
多余的线程会被销毁直到只剩下corePoolSize个线程为止

面试问题:你用单一的/固定数的/可变的三种创建线程池的方法,你用哪个多? 或者问实际开发中用哪种线程池
一个都不用,而是用 ThreadPoolExecutor 创建
因为Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

合理配置线程池你是如何考虑的?
首先判断任务是CPU密集型(大量计算的任务)还是IO密集型(I/O (硬盘/内存) 的读/写操作)
CPU密集型:CPU核数+1个线程线程池
IO密集型:CPU核数*2

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值