JUC就是java.util .concurrent工具包的简称。
1 进程和线程
1.1 相关概念
进程:一个程序,一个进程往往可以包含多个线程,至少包含一个
线程:比如说idea,你运行的同时还可以编写代码
java默认有2个线程:main线程,gc线程
java自身是无法开启线程的,需要调用底层的c++开启线程
private native void start0();
1.2 线程的状态
一共有6种状态,如下:
public enum State {
NEW, //新建
RUNNABLE, //运行
BLOCKED, //阻塞
WAITING, //等待
TIMED_WAITING, //超时等待
TERMINATED; //终止
}
状态之间的转换如下图所示:
1.3 wait()与sleep()
- wait()方法会释放锁,但sleep()方法不会释放锁
- wait()必须在同步代码块中使用,sleep()可以在任意地方使用
- wait()不用捕获异常,sleep()必须捕获异常
2 锁
2.1 公平锁与非公平锁
- 公平锁:不能插队,先来先占用锁。
- 非公平锁:可以插队,看谁先抢到锁。
public ReentrantLock() {
sync = new NonfairSync();
}
//可以通过传入参数来选择创建公平锁还是非公平锁,默认创建非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2 可重入锁(递归锁)
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class)。可重入锁的意义便在于防止死锁。
2.3 自旋锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。CAS底层用的就是自旋锁。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//自旋锁
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.4 死锁
两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
2.5 Synchronized
主要是为了解决线程安全的问题。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
有三种应用方式:
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
- 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
- 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
ps:static锁class模板,其它锁的是实例(8锁问题)
class Aircondition{
private static int temperature = 20;
static {
}
//普通同步方法(实例方法)
public synchronized void increase(){
temperature++;
}
//同步方法块
public void decrease(){
synchronized (this){
temperature--;
}
}
//静态同步方法
public synchronized static void open(){
System.out.println("open");
}
}
2.6 ReentrantLock
最主要的就是ReentrantLock可以实现公平锁机制而synchronized只能非公平锁。
Lock lock = new ReentrantLock();//创建锁
lock.lock();//加锁
lock.unlock();//解锁 最好放在finally中执行
**与synchronized的区别
- Synchronized 内置的Java关键字, Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下
去; - Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
2.7 读写锁ReadWriteLock
多线程可以同时读操作,但是写只能有一个线程去写
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class ReadWriteLockTest {
public static void main(String[] args) {
MyMap myMap = new MyMap();
//写入
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myMap.put(finalI+"", UUID.randomUUID().toString().substring(0,5));
},""+i).start();
}
//读取
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myMap.get(finalI+"");
},""+i).start();
}
}
}
class MyMap{
private volatile Map map = new HashMap<>();//加volatile是为了保证可见性
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,String value){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写完了");
readWriteLock.writeLock().unlock();
}
public void get(String key){
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"读取"+key);
System.out.println(Thread.currentThread().getName()+"读到的值"+map.get(key));
readWriteLock.readLock().unlock();
}
}
3 生产者消费者问题
3.1 synchronized版
class Prosumer {
private int number = 0;
public synchronized void increatment() throws InterruptedException {
//必须用while判断,否则会存在虚假唤醒的问题
while (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
this.notifyAll();
}
public synchronized void decreatement() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
this.notifyAll();
}
}
//synchronized版
public class ProducerConsumer {
public static void main(String[] args) {
Prosumer prosumer = new Prosumer();
//1-5线程用于生产
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
prosumer.increatment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "").start();
}
//6-10线程用于消费
for (int i = 6; i <= 10; i++) {
new Thread(() -> {
try {
prosumer.decreatement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "").start();
}
}
}
执行结果如下:一个生产一个消费,消费完毕后在生产。
3.2 ReentrantLock版
class Prosumer{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increatment() {
lock.lock();
//必须用while判断,否则会存在虚假唤醒的问题
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decreatement() {
lock.lock();
//必须用while判断,否则会存在虚假唤醒的问题
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//ReentrantLock版
public class ProducerConsumer {
public static void main(String[] args) {
Prosumer prosumer = new Prosumer();
//1-5线程用于生产
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
prosumer.increatment();
}, i + "").start();
}
//6-10线程用于消费
for (int i = 6; i <= 10; i++) {
new Thread(() -> {
prosumer.decreatement();
}, i + "").start();
}
}
}
执行结果如下:一个生产一个消费,消费完毕后在生产。
ps:这里要重点关注虚假唤醒的问题,切记线程等待一定要出现在循环中,而不是条件判断中
4 集合类线程不安全
4.1 List不安全
首先在多线程下使用普通ArrayList测试
public class ListUnsafe {
public static void main(String[] args) {
List list = new ArrayList<>();
//同时创建10个线程分别往list中插入数据
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,10));
System.out.println(list);
},i+"").start();
}
}
}
执行结果如下图。
这里程序报错,java.util.ConcurrentModificationException,即并发修改异常,这是由于一个线程正在写入的同时另一个线程又来抢占资源,造成数据不一致。
可采用以下途径解决该问题。
public class ListUnsafe {
public static void main(String[] args) {
// List list = new ArrayList<>(); //错误方式
//方式1,使用Vector
// List list = new Vector<>();
//方式2,使用Collections集合工具类将其变为线程安全
// List list = Collections.synchronizedList(new ArrayList<>());
//方式3,使用并发编程类CopyOnWriteArrayList替换ArrayList
List list = new CopyOnWriteArrayList<>();
/**
*CopyOnWriteArrayList 写入时复制
*添加元素时,会先加锁,复制该集合,往新的集合中添加元素,添加完毕后再将原容器的引用指向新容器,然后释放锁
* 这样只有在写入加锁,读不加锁,读写不同的容器,是一种读写分离的思想
*
*/
////同时创建10个线程分别往list中插入数据
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,10));
System.out.println(list);
},i+"").start();
}
}
}
4.2 Set不安全
Set与List情况类似,这里直接给出代码
public class SetUnsafe {
public static void main(String[] args) {
// Set set = new HashSet<>(); //不安全
//方法1,使用Collections集合工具类将其变为线程安全
// Set set = Collections.synchronizedSet(new HashSet<>());
//方法2,使用并发编程类CopyOnWriteArraySet替换HashSet
Set set = new CopyOnWriteArraySet<>();
////同时创建10个线程分别往list中插入数据
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,10));
System.out.println(set);
},i+"").start();
}
}
}
ps:Hastset底层本质上是创建了一个HashMap,利用了HashMap的key不能重复的特点,来达到去重的目的。
4.3 Map不安全
Map与List情况类似,这里直接给出代码
public class MapUnsafe {
public static void main(String[] args) {
// Map map = new HashMap<>(); //不安全
//方法1,使用Collections集合工具类将其变为线程安全
// Map map = Collections.synchronizedMap(new HashMap<>());
//方法2,使用并发编程类ConcurrentHashMap替换HashMap
Map map = new ConcurrentHashMap();
////同时创建10个线程分别往list中插入数据
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,10));
System.out.println(map);
},i+"").start();
}
}
}
5 常用的辅助类
5.1 CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。CountDownLatch用给定的计数初始化, 计数无法重置。即会等待所设置的计数的线程数执行完毕后才会继续往下执行,比如所有人走完才能关门。
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" ok");
countDownLatch.countDown(); //数量-1
},""+i).start();
}
countDownLatch.await(); //等待计数器归零,才继续执行
System.out.println("已全部运行完");
}
}
执行结果如下:
5.2 CyclicBarrier
允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。 比如七颗龙珠集齐了才能召唤神龙。
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("龙珠召唤成功");
});
for (int i = 0; i < 7; i++) {
int finalI = i+1;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集到第"+ finalI +"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},""+i).start();
}
}
}
执行结果如下:
5.3 Semaphore信号量
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。(限流,抢车位,10个车抢5个车位)
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();//得到一个信号量
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放该信号量
}
}, "" + i).start();
}
}
}
执行结果如下:
6 阻塞队列
写入:若队满则阻塞等待取出
取出:若对空则阻塞等待写入
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer |
移除 | remove | poll | take | poll |
检测队首元素 | element | peek |
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
test1();
test2();
test3();
test4();
}
//超时等待,过期不候
private static void test4() throws InterruptedException {
BlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a",2, TimeUnit.SECONDS);
blockingQueue.offer("b",2, TimeUnit.SECONDS);
blockingQueue.offer("c",2, TimeUnit.SECONDS);
//会等待1秒查看是否能插入
blockingQueue.offer("d",2, TimeUnit.SECONDS);
System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
//会等待1秒查看是否依旧为空,为空返回null
System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
}
//等待,阻塞,死死的等
private static void test3() throws InterruptedException {
BlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//会一直阻塞,直到阻塞队列有位置了才会插入d,死死的等
// blockingQueue.put("d");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//会一直阻塞,直到阻塞队列非空了才能取出,死死的等
System.out.println(blockingQueue.take());
}
//有返回值,无异常
private static void test2(){
BlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//下面语句执行不会抛出异常,返回false
System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//下面语句执行不会抛出异常,返回null
System.out.println(blockingQueue.poll());
//下面语句执行不会抛出异常,返回null
System.out.println(blockingQueue.peek());
}
//抛出异常
private static void test1() {
BlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//下面语句执行会抛出异常,因为此时阻塞队列已满Exception in thread "main" java.lang.IllegalStateException: Queue full
// System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.element());//获取队首元素
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//下面语句执行会抛出异常,因为此时阻塞队列已空Exception in thread "main" java.util.NoSuchElementException
// System.out.println(blockingQueue.remove());
//下面语句执行会抛出异常,因为此时阻塞队列已空Exception in thread "main" java.util.NoSuchElementException
System.out.println(blockingQueue.element());
}
}
7 线程池
事先创建好一些线程,如果有人要用,直接来来拿,用完后在还给我,优化资源使用。
好处:
- 降低资源消耗
- 提高响应速度
- 方便管理
- 线程复用,可以控制最大并发数,管理线程
Executors提供了一些方法来创建不同的线程池,如下:
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //创建单个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10); //创建固定个数线程的线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); //创建可动态伸缩的线程池
其底层是通过ThreadPoolExecutor进行创建
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大线程池大小
long keepAliveTime,//多长时间不使用自动释放
TimeUnit unit,//超时单位
BlockingQueue 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;
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
通过观察源代码我们发现,这三种线程池在创建时最大线程数都设置为Integer的最大值,导致内存溢出,因此不应该使用以上三种方法创建线程池,而是应该通过ThreadPoolExecutor进行创建。
其中RejectedExecutionHandler handler拒绝策略共有四种
new ThreadPoolExecutor.AbortPolicy(); //如果当前线程池已满,又拿线程,不给直接跑异常
new ThreadPoolExecutor.CallerRunsPolicy();//哪来的回哪
new ThreadPoolExecutor.DiscardPolicy() //阻塞队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //阻塞队列满了,尝试去和最早的竞争,也不会抛出异常!
使用范例:
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
8 JMM
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
JMM同步约定:
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
主要存在可见性问题,即其他线程不知道主内存中的值是否已被修改。而volatile关键字的出现就是为了解决该问题。
9 Volatile
Volatile有三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
通过使用java.util.concurrent.atomic下的原子包装类型可以实现原子操作,如AtomicBoolean,AtomicInteger,AtomicLong等,而不需要在使用synchronized来保证原子性,底层原理是CAS(CAS会存在ABA问题,可以采用带版本号的原子操作来解决)