JUC
Lock 锁:(重点)
回忆synchronized 锁:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 模拟卖票 */ public class Demo1 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()->{for (int i = 1; i < 10; i++) ticket.sale(); },"A").start(); new Thread(()->{for (int i = 1; i < 10; i++) ticket.sale(); },"B").start(); } } class Ticket{ private int num = 10; public synchronized void sale(){ if (num > 0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+(num--)+"票,剩余"+num+"张票"); } } }
Lock 锁:
使用三步骤:
1、new ReentrantLock(); // 点进去这个类,可以看到默认是非公平的!
2、加锁 lock.lock();
3、解锁 lock.unlock();
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 模拟卖票 */ public class Demo1 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()->{for (int i = 1; i < 10; i++) ticket.sale(); },"A").start(); new Thread(()->{for (int i = 1; i < 10; i++) ticket.sale(); },"B").start(); } } class Ticket{ private int num = 10; Lock lock = new ReentrantLock(); public void sale(){ // 加锁 lock.lock(); try { // 业务代码 if (num > 0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+(num--)+"票,剩余"+num+"张票"); } } catch (Exception e) { e.printStackTrace(); }finally { // 解锁 lock.unlock(); } } }
synchronized 和 Lock 区别:
1、synchronized 是内置的java 关键字,lock 是java 类
2、synchronized 无法判断 获取锁的状态,lock 可以判断是否获取锁
3、synchronized 会自动释放锁,lock 不会自动释放,如果不释放,就会死锁
4、synchronized 如果线程阻塞,另一个会一直等,lock 就不一定了,它有 .tryLock() 方法,尝试获取锁
5、synchronized 可重入的,不可以中断,非公平,lock 可以判断锁,可以自己设置是否公平的锁;new ReentrantLock 时候给参数
6、synchronized 适合少量代码同步,lock 适合大量同步代码块
传统的 生产者、消费者,以及虚假唤醒问题
线程之间的通信问题:生产者和消费者问题。 等待唤醒,通知唤醒
比如说:线程交替 A B 操作同一个变量 num = 0
/** * 线程之间的通信问题:生产者和消费者问题。 等待唤醒,通知唤醒 * 线程交替 A B 操作同一个变量 num = 0 * A num+1 * B num-1 */ public class Demo2 { public static void main(String[] args) { Data data = new Data(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for (int i = 0; i < 10; i--) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); } } // 数字资源类 写法:判断等待、业务代码、通知 class Data{ private int num = 0; // +1 public synchronized void increment() throws InterruptedException { if (num != 0){ this.wait(); } num++; System.out.println(Thread.currentThread().getName()+"->"+num); // 通知其他线程 this.notifyAll(); } // -1 public synchronized void decrement() throws InterruptedException { if (num == 0){ this.wait(); } num--; System.out.println(Thread.currentThread().getName()+"->"+num); // 通知其他线程 this.notifyAll(); } }
这样打印的是正常的,因为只有两个线程,
如果再加两个线程 C D,那就会出现意向不到的问题,打印的结果就会乱了,if 判断会造成虚假唤醒
解决办法很简单,把if 换成 while
Lock 锁 版的生产者和消费者
还是Lock 用法的三步,new ReentrantLock(); 它里面有.newCondition 这个方法,
这个方法里有await 和 signal两个方法,和wait 跟 notify 这两个是一样的,
然后还是和正常使用Lock 锁是一样的,加锁、释放。
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 线程之间的通信问题:生产者和消费者问题。 等待唤醒,通知唤醒 * 线程交替 A B 操作同一个变量 num = 0 * A num+1 * B num-1 */ public class Demo3 { public static void main(String[] args) { Data3 data = new Data3(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for (int i = 0; i < 10; i--) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for (int i = 0; i < 10; i--) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); } } // 数字资源类 写法:判断等待、业务代码、通知 class Data3{ private int num = 0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // +1 public void increment() throws InterruptedException { 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() throws InterruptedException { 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(); } } }
这样还是存在一个问题,这样跟传统的sync 同步锁是打印的没有去别的,因为四个线程的
执行顺序不是按ABCD 顺序执行的,它没有实现精准唤醒下一个线程执行的一个效果。
记住,任何的新出来的技术,绝对不只是替换原来的旧技术!
Condition 实现精准唤醒
A执行完调用B,B执行完调用C,C执行完调用A,依次循环
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Demo4 { public static void main(String[] args) { Data4 data4 = new Data4(); new Thread(()->{for (int i = 0; i < 10; i++) data4.printA();},"A").start(); new Thread(()->{for (int i = 0; i < 10; i++) data4.printB();},"B").start(); new Thread(()->{for (int i = 0; i < 10; i++) data4.printC();},"C").start(); } } class Data4{ private Lock lock = new ReentrantLock(); // 3个监视器,来判断 Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); // 1的话让A执行,2让B,3让C,初始是1 private int num = 1; public void printA(){ lock.lock(); try { // 业务、判断-》执行-》通知 while (num != 1){ condition1.await(); } System.out.println(Thread.currentThread().getName()+"-->AAA"); // 唤醒,唤醒指定的人 num = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printB(){ lock.lock(); try { // 业务、判断-》执行-》通知 while (num != 2){ condition2.await(); } System.out.println(Thread.currentThread().getName()+"-->BBB"); // 唤醒,唤醒指定的人 num = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printC(){ lock.lock(); try { // 业务、判断-》执行-》通知 while (num != 3){ condition3.await(); } System.out.println(Thread.currentThread().getName()+"-->CCC"); // 唤醒,唤醒指定的人 num = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
这样就可以实现精准唤醒。
CopyOnWriteArrayList
首先想一下,ArrayList 真的安全吗,如果是单线程运行的话,是肯定安全的,如果多线程呢?
来看下这段错误的代码:
import java.util.ArrayList; import java.util.List; import java.util.UUID; public class Demo5 { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 1; i <= 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } } }
这个代码就会报错:java.util.ConcurrentModificationException 修改并发编程异常!
于是有一下几种解决方案:
import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; public class Demo5 { public static void main(String[] args) { List<String> list = new ArrayList<>(); /** * 解决方案: */ // 1、 List<String> list = new Vector<>(); // 2、 List<String> list = Collections.synchronizedList(new ArrayList<>()); // 3、 List<String> list = new CopyOnWriteArrayList<>(); for (int i = 1; i <= 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } } }
不用说,肯定是第三章方案 CopyOnWriteArrayList 比较好,那么它有什么牛逼的地方呢?
前两种方案,不妨点进去看源码可以发现都是用的 synchronize 关键字,那么,只要有这个 synchronize 就会影响效率,但是 CopyOnWriteArrayList 用的是Lock 锁,相比较起来好一点吧。
Vector 和 ArrayList 我们经常做比较它们的区别,其实Vector 比 ArrayList 出的还要早,实在1.0 后出的,ArrayList 是在1.2 出的
CopyOnWriteArraySet
和CopyOnWriteArrayList 一样的,我们先看一下这个错误的代码:
import java.util.HashSet; import java.util.Set; import java.util.UUID; public class SetTest { public static void main(String[] args) { Set<String> set = new HashSet<>(); for (int i = 1; i <= 30; i++) { new Thread(()->{ set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } } }
解决办法同理:
import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; public class SetTest { public static void main(String[] args) { Set<String> set = new HashSet<>(); /** * 解决方案: * 1、 Set<String> set = Collections.synchronizedSet(new HashSet<>()); * 2、 Set<String> set = new CopyOnWriteArraySet<>(); */ for (int i = 1; i <= 30; i++) { new Thread(()->{ set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } } }
原理一样的,虽然Collections 这个类下对应的方法可以解决问题,但是用的还是synchronize 关键字,所以推荐使用 CopyOnWriteArraySet
HashSet 底层是什么呢?
是HashMap,它的源码里就是new 了一个HashMap,就是这个map 的key ,因为key 是无法重复的,这就是set 的不可重复的特性的原因,它底层就是这个map.add 添加进去了。
ConcurrentHashMap
map 也不安全,这个要知道。还是先来看一下错误代码:
import java.util.HashMap; import java.util.Map; import java.util.UUID; public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); /** * map 是这样用的吗?它的默认等价于什么?这个如果不知道,面试的时候就直接拜拜了! * 肯定不是这样用的,因为我们工作过不用HashMap, * HashMap 有个初始化容量和一个 加载因子。 * 它等价于:new HashMap<>(16, 0.75); 点进源码可以寻找到答案,一个位运算和一个默认加载0.75 */ for (int i = 1; i <= 30; i++) { new Thread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5)); System.out.println(map); },String.valueOf(i)).start(); } } }
这个代码运行也是会报错并发编程异常,
解决办法:
import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); /** * 解决办法: * Map<String, String> map = new ConcurrentHashMap<>(); */ for (int i = 1; i <= 30; i++) { new Thread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5)); System.out.println(map); },String.valueOf(i)).start(); } } }
Callable
它是线程创建的一种方式,它和Runnable 的区别就是:
1、可以有返回值
2、可以抛出异常
3、执行方法是 call() Runnable 是run 方法执行的
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread thread = new MyThread(); // 启动Callable 一样要用一个适配类,它就是 FutureTask FutureTask futureTask = new FutureTask(thread); // 启动 new Thread(futureTask,"A").start(); new Thread(futureTask,"B").start(); // 如果两个线程都去启动,只会打印一个call 的字段,Callable 是有缓存的 // 获取Callable 的返回结果 String value = (String) futureTask.get(); // 这个get 方法可能会产生阻塞,因为那个call 的方法里如果有耗时的操作, // 所以这个 .get 方法最好放在后面执行,或者使用异步操作处理 System.out.println(value); // abc } } /** * 用法是 实现Callable,它有泛型,泛型里输入的是什么类型,返回值就一定要是什么类型的 * 就是重写它里面call 这个方法的返回值类型 */ class MyThread implements Callable<String>{ @Override public String call() throws Exception { System.out.println("call"); return "abc"; } }
常用辅助类
1、CountDownLatch
它是计数用的。倒计时减数用的,一个减法计数器
import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 初始值给 6 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"go"); // 线程数量 -1 countDownLatch.countDown(); },String.valueOf(i)).start(); } // 这个方法千万别忘记,代表计数器归零,然后就可以向下执行,不然会乱的 countDownLatch.await(); System.out.println("close"); } }
它里面主要有两个方法:
.countDown(); 数量 -1
.await(); 用法,等待计数器归零,才可以向下执行,如果计数器没有归零就唤醒这个方法,会乱的。所以这个方法写在了那个for 循环的外面
2、CyclicBarrier
也是计数器,加法计数器。
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CylicBarrierDemo { public static void main(String[] args) { /** * 集齐7个龙珠召唤神龙 */ CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("神龙召唤成功"); }); for (int i = 1; i <= 7; i++) { final int tem = i; // lambda 的这个表达式是new 了一个Thhread 类,它是是拿不到 i 的 new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集了"+tem+"颗龙珠"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
加法计数器的.await(); 这个方法是写在for 循环里面了,它要等待加到7 ,初始时候设置的。
3、Semaphore
信号量。个人理解是,开闭了一个数据量的大小,进行造作他的数量。
import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreDemo { public static void main(String[] args) { // 制作一个限流 Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 6; i++) { new Thread(()->{ // acquire 得到 try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"拿到了一个"); TimeUnit.SECONDS.sleep(2); // 线程延迟2 秒 System.out.println(Thread.currentThread().getName()+"离开了"); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 释放 semaphore.release(); } },String.valueOf(i)).start(); } } }
ReadWriteLock -- 读写锁
目前只有一个实现类:ReentrantReadWreteLock
ReadWriteLock 维护一对关联的Lock,也就是两个,一个用于读,一个用于写。读的时候可以多线程同时读,写的时候只能一个线程去写。
package com.biao; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockDemo { public static void main(String[] args) { MyCache2 myCache = new MyCache2(); // 写入 for (int i = 1; i <= 5; i++) { final int tem = i; new Thread(()->{ myCache.put(tem+"",tem+""); },String.valueOf(i)).start(); } // 读取 for (int i = 1; i <= 5; i++) { final int tem = i; new Thread(()->{ myCache.get(tem+""); },String.valueOf(i)).start(); } } } /** * 自定义伪造一个 加锁的 缓存 */ class MyCache2{ private volatile Map<String,Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 存 写入时候,希望只有一个线程去写 public void put(String key, Object value){ // 写入锁时候用.writeLock() 方法,然后加锁 .lock() 就是给读写锁解锁 readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"写入"+key); map.put(key,value); System.out.println(Thread.currentThread().getName()+"写入OK"); } catch (Exception e) { e.printStackTrace(); } finally { // 解锁 readWriteLock.writeLock().unlock(); } } // 取 读,所有人都可以读,同时多线程读 public void get(String key){ readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"读取"+key); Object o = map.get(key); System.out.println(Thread.currentThread().getName()+"读取OK"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } } /** * 自定义伪造一个缓存 */ class MyCache{ private volatile Map<String,Object> map = new HashMap<>(); // 存 public void put(String key, Object value){ System.out.println(Thread.currentThread().getName()+"写入"+key); map.put(key,value); System.out.println(Thread.currentThread().getName()+"写入OK"); } // 取 public void get(String key){ System.out.println(Thread.currentThread().getName()+"读取"+key); Object o = map.get(key); System.out.println(Thread.currentThread().getName()+"读取OK"); } }
如果用的不是解锁的缓存那个类,就会在写入的时候乱套,写的时候不可以乱套的。但读 的时候,是可以多线程同时读,所以可以乱套。
阻塞队列 BlockIngQueue
分为阻塞、队列,先理解是什么意思。
队列:先进先出,排队的。
阻塞:就是等待的意思。
就是说,如果队列满了,就必须要阻塞了,要停止了。如果队列是空的,就必须不能阻塞,赶紧运行开工啦。
BlockIngQueue 这个类是个集合类,在Collection 类下的一个子类,Collection 下都是到有一个List 和 Set,其实还有一个同等级的类,就是BlockIngQueue。
List 下有ArrayList 和 LinkedList,这个BlockIngQueue 下也有 ArrayBlockIngQueue 和 LinkedBlockIngQueue
所以这个BlockingQueue 并不是新的东西,如果你不知道,那是你原来就不知道。
BlockIngQueue 四组 API
方式 | 抛出异常 | 不抛出异常 | 阻塞、等待 | 超时等待 |
添加 | add | offer | put | offer(,,) |
移除 | remove | poll | take | poll(,) |
检测队首元素 | element | peek |
示例:
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; import static java.util.concurrent.TimeUnit.SECONDS; public class Test { public static void main(String[] args) throws InterruptedException { test1(); test2(); test3(); test4(); } /** * 抛出异常 */ public static void test1(){ // 给出队列大小 ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3); // 给队列添加元素,返回true 表示成功 System.out.println(arrayBlockingQueue.add("a")); System.out.println(arrayBlockingQueue.add("b")); System.out.println(arrayBlockingQueue.add("c")); // 再加一个就会有异常报错,队列已满 // System.out.println(arrayBlockingQueue.add("d")); // 查看队首是哪个,也就是第一个元素是谁 System.out.println(arrayBlockingQueue.element()); // a // 移除队列里的元素,表示移除3次,返回元素,表示移除的是哪个 a b c System.out.println(arrayBlockingQueue.remove()); System.out.println(arrayBlockingQueue.remove()); System.out.println(arrayBlockingQueue.remove()); // 如果再次移除,就会报错,队列里的元素不存在 // System.out.println(arrayBlockingQueue.remove()); } /** * 不抛出异常 */ public static void test2(){ // 给出队列大小 ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3); // 存入队列,成功返回true System.out.println(arrayBlockingQueue.offer("a")); System.out.println(arrayBlockingQueue.offer("b")); System.out.println(arrayBlockingQueue.offer("c")); // 返回false,不抛出异常 // System.out.println(arrayBlockingQueue.offer("d")); // 查看队首 System.out.println(arrayBlockingQueue.peek()); // 从队列弹出/移除3次 System.out.println(arrayBlockingQueue.poll()); System.out.println(arrayBlockingQueue.poll()); System.out.println(arrayBlockingQueue.poll()); // 返回null,不报异常 // System.out.println(arrayBlockingQueue.poll()); } /** * 阻塞、等待 */ public static void test3() throws InterruptedException { // 给出队列大小 ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3); // 给队列添加元素,这个没有返回值,就不输出打印了 arrayBlockingQueue.put("a"); arrayBlockingQueue.put("b"); arrayBlockingQueue.put("c"); // 这个不会报错,它会一直等,什么时候弹出一个元素,这个就会添加进去 // arrayBlockingQueue.put("d"); // 线程不会停止,开发中如果这样,就等于写死了。。。 // 弹出元素,返回值是当前弹出的元素 a b c System.out.println(arrayBlockingQueue.take()); System.out.println(arrayBlockingQueue.take()); System.out.println(arrayBlockingQueue.take()); // 不会报错,会一直阻塞,什么时候队列里有,就直接弹出 // System.out.println(arrayBlockingQueue.take()); } /** * 超时退出 */ public static void test4() throws InterruptedException { // 给出队列大小 ArrayBlockingQueue<Object> arrayBlockingQueue = new ArrayBlockingQueue<>(3); // 给队列添加元素,没有返回值 arrayBlockingQueue.offer("a"); arrayBlockingQueue.offer("b"); arrayBlockingQueue.offer("c"); // 会一直等待,如果2 秒后队列还是满的,就直接退出不执行了 // arrayBlockingQueue.offer("d",2, SECONDS); // 取出,返回值是元素 a b c System.out.println(arrayBlockingQueue.poll()); System.out.println(arrayBlockingQueue.poll()); System.out.println(arrayBlockingQueue.poll()); // 超过2 秒内,如果队列里还没有,就退出不去拿了 // arrayBlockingQueue.poll(2, SECONDS); } }
同步队列 SynchronousQueue
这个相比 阻塞队列要简单,api 很少
import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; public class SynchronousQueueDemo { public static void main(String[] args) { // 同步队列,是同步的,这边存,那边就要立马去取出来 SynchronousQueue synchronousQueue = new SynchronousQueue(); // 存 new Thread(()->{ try { System.out.println(Thread.currentThread().getName()+"put 1"); synchronousQueue.put("1"); System.out.println(Thread.currentThread().getName()+"put 2"); synchronousQueue.put("2"); System.out.println(Thread.currentThread().getName()+"put 3"); synchronousQueue.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } },"T1").start(); // 取 new Thread(()->{ try { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"=>"+synchronousQueue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"=>"+synchronousQueue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"=>"+synchronousQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } },"T2").start(); } }
线程池(重点)3大方法、7大参数、4种拒绝策略
池化技术:就是提前建立多个连接,用的时候就直接用了,避免了一直重复的连接、关闭,避免消耗资源。比如连接池、线程池、内存池。。。
Executors:线程池的工具类,这个在阿里巴巴开发手册说不推荐使用。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Executors 工具类 3 大方法 */ public class ThredPool { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); // 单个线程的线程池 // ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个5个线程的线程池 // ExecutorService executor = Executors.newCachedThreadPool(); // 可伸缩大小的线程池,遇强则强,遇弱则弱。。 try { for (int i = 0; i < 10; i++) { executor.execute(()->{ System.out.println(Thread.currentThread().getName()+"ok"); }); } } catch (Exception e) { e.printStackTrace(); } finally { // 用完后需要关闭线程池的连接 executor.shutdown(); } } }
Executors的那三个方法的源码里,本质都是用的一个 ThreadPoolExecutor
7 大参数和自定义线程池
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; }
所以我们应该用 ThreadPoolExecutor
import java.util.concurrent.*; /** * Executors 工具类 3 大方法 */ public class ThredPool { public static void main(String[] args) { // 自定义线程池 ExecutorService executor = new ThreadPoolExecutor( 2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 拒绝策略,有好几种,具体用哪种看业务场景 ); try { for (int i = 0; i < 10; i++) { executor.execute(()->{ System.out.println(Thread.currentThread().getName()+"ok"); }); } } catch (Exception e) { e.printStackTrace(); } finally { // 用完后需要关闭线程池的连接 executor.shutdown(); } } }
cpu 密集型 和 IO 密集型(调优)
cpu 密集型:看电脑是几核的就是几,这样可以保持cpu 的效率最高
获取电脑上cpu 的核数: Runtime.getRuntime().availableProcessors() // 返回值就是cpu 核数的数量
io 密集型: 判断程序中十分耗 IO 的线程有多少个,然后给出相应的线程数量来处理,一般是双倍。就是很耗io 线程的数量的双倍。
四大函数式接口lambda(必须要会)
比如:lambda表达式、链式编程、函数式接口、Stream流式计算,这些都是函数式接口。
Function:函数式接口:意思就是只有一个方法的接口,这种的在java里非常的多,底层大量的运用。
import java.util.function.Function; public class Demo { public static void main(String[] args) { // Function<String, String> function = new Function<String, String>() { // 参数1:给的参数类型,参数2:返回的参数类型 // @Override // public String apply(String s) { // 这里的返回参数类型一定要和上面的一样 // return s; // } // }; // lambda 表达式 Function<String,String> function = s -> {return s;}; System.out.println(function.apply("abc")); } }
Predicate:断定型接口:比如有一个输入参数,返回值就只能是 布尔值
import java.util.function.Predicate; public class Demo { public static void main(String[] args) { Predicate<String> predicate = new Predicate<String>() { @Override public boolean test(String s) { return s.isEmpty(); } }; System.out.println(predicate.test("abc")); // 输入有值就返回false,没有值就返回true } }
Consumer:消费型接口:只有输入,没有返回值
import java.util.function.Consumer; public class Demo { public static void main(String[] args) { // Consumer<String> consumer = new Consumer<String>() { // @Override // public void accept(String s) { // System.out.println(s); // } // }; // lambda 表达式写法 Consumer<String> consumer = s -> {System.out.println(s);}; consumer.accept("abc"); // 返回的就直接打印了 } }
Supplier:供给型接口:没有参数,只有返回值,就是返回给别人一个东西
import java.util.function.Supplier; public class Demo { public static void main(String[] args) { // Supplier<Integer> supplier = new Supplier<Integer>() { // @Override // public Integer get() { // return 123; // } // }; // lambda 表达式写法 Supplier supplier = ()->{return 123;}; System.out.println(supplier.get()); } }
Stream 流式计算
大数据:存储 + 计算
计算都应该交给 流 来操作。
示例:一个实体类
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class User { private int id; private String name; private int age; }
import java.util.Arrays; import java.util.List; /** * 要求:一分钟内完成此题,只能用一行代码实现。 * 筛选: * 1、id 必须是偶数 * 2、年龄必须大于23岁 * 3、用户名转为大写字母 * 4、用户名字母倒序排序 * 5、只输出一个用户 */ public class StreamTest { public static void main(String[] args) { User u1 = new User(1, "a", 21); User u2 = new User(2, "b", 22); User u3 = new User(3, "c", 23); User u4 = new User(4, "d", 24); User u5 = new User(5, "e", 25); // 存储在集合 List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // 计算交给Stream list.stream() .filter(user -> {return user.getId()%2==0;}) // id 必须式偶数 .filter(user -> {return user.getAge() > 23;}) // 年龄大于 23 .map(user -> {return user.getName().toUpperCase();}) // 名字转为大写 .sorted((uu1,uu2)->{return uu2.compareTo(uu1);}) // 倒序排列 .limit(1) // 只输出一个,分页实现 .forEach(System.out::println); // 打印 } }
ForkJoin
在jdk1.7 之后出现的,可以并行执行任务,提高效率,适用于大数据量。
大数据:Map Reduce--意思就是 把大任务拆分成小任务,把计算出的多个结果再合并成一个最终的结果。这种思想就是 ForkJoin
特点:工作窃取。就是可以提高效率。意思就是一个双端队列(两端都可以取出),两边都可以操作,一边执行完了以后,另一边还没有执行完,执行完的那一边,就会把这边没执行完的任务拿过来做。
import java.util.concurrent.RecursiveTask; /** * 求和计算 * 优秀的人用ForkJoin,牛逼的人用Stream * 使用ForkJoin:1、forkjoinPool 来执行 * 2、使用它里面的 .execute(ForkJoinTask task) 计算任务 * 3、记得首先要使用的这个类,要去继承 RecursiveTask<>,然后去重写里面的方法 compute */ public class ForkJoinDemo extends RecursiveTask<Long> { private Long start; // 开始值 private Long end; // 结束值 private Long tem = 10000L; // 临界值 public ForkJoinDemo(Long start, Long end) { this.start = start; this.end = end; } // 重写计算方法,返回值就是 继承的类的 泛型 @Override protected Long compute() { if ((end - start) < tem){ // 如果 结束值 - 开始值 < 临界值,就分支合并计算 Long sum = 0L; for (Long i = start; i <= end; i++) { sum += i; } return sum; }else { // 分支合并计算 long middle = (start + end) / 2; // 中间值 ForkJoinDemo task1 = new ForkJoinDemo(start, middle); task1.fork(); // 拆分任务了,把任务压入线程队列 ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end); task1.fork(); // task2 也要压入 return task1.join() + task2.join(); } } }
测试类:
import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.stream.LongStream; public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { // test1(); // test2(); test3(); } // 普通人 public static void test1(){ Long sum = 0L; long start = System.currentTimeMillis(); // 时间 for (Long i = 0L; i <= 10_0000_0000; i++) { sum += i; } long end = System.currentTimeMillis(); System.out.println("sum="+sum+"时间="+(end-start)); } // 优秀的人,用ForkJoin public static void test2() throws ExecutionException, InterruptedException { long start = System.currentTimeMillis(); // 时间 ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L); ForkJoinTask<Long> submit = forkJoinPool.submit(task); // 这个式异步提交 .execute 是同步 Long sum = submit.get(); long end = System.currentTimeMillis(); System.out.println("sum="+sum+"时间="+(end-start)); } // 牛逼的人:一行代码解决,并且效率极高 public static void test3(){ long start = System.currentTimeMillis(); // 时间 // Stream 并行流 long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum); long end = System.currentTimeMillis(); System.out.println("sum="+sum+"时间="+(end-start)); } }
异步回调
异步调用首先想到的式ajax,但是java线程也是可以异步调用的。
CompletableFuture:
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * 异步请求 */ public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 没有返回值的: // 发送一个请求,没有返回值,泛型应该写Void,V 大写 // CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> { // try { // TimeUnit.SECONDS.sleep(2); // 休眠2 秒 // } catch (InterruptedException e) { // e.printStackTrace(); // } // System.out.println(Thread.currentThread().getName()+"runAsync=>Void"); // }); // System.out.println("111"); // 这个111 会先打印,因为上面休眠了2 秒,它就会异步先来执行下面的 // // 获得阻塞结果 // completableFuture.get(); // 有返回值的异步回调, CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName()+"runAsync=>Integer"); return 1024; }); // 有返回值回调的话,就分返回成功 和 返回失败 completableFuture.whenComplete((t,u)->{ System.out.println("t=>"+t); // t 是正常的 System.out.println("u=>"+u); // u 是错误的信息 }).exceptionally((e)->{ e.printStackTrace(); System.out.println(e.getMessage()); return 500; }).get(); } }
JMM java内存模型
JMM 的同步约定:
1、线程解锁前:必须把共享变量 立刻 刷回主内存
2、线程加锁前:必须读取主内存中的最新值到工作内存中
3、加锁和解锁式同一把锁
线程内存分:工作内存、主内存
8 种操作:
如果两条线程都去主内存去拿,中间改变了Flag 的值,主内存没有来得及刷新,不能改变它的可见性。
这些指令必须承对出现,不可以单独使用。
先看下这个错误的代码:
import java.util.concurrent.TimeUnit; public class JMMDemo { private static int num = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ // 现在有线程1 和 main 线程 while (num == 0){ // num 改变成1 ,线程就不会停止 } }).start(); TimeUnit.SECONDS.sleep(1); // 让线程1 睡一秒 num = 1; System.out.println(num); } }
这个问题是:num 的值发生变化,但主内存不知道,没有确保主内存变量的可见性。
volatile
import java.util.concurrent.TimeUnit; public class JMMDemo { // 添加volatile 关键字,确保它的可见性 private volatile static int num = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ // 现在有线程1 和 main 线程 while (num == 0){ // num 改变成1 ,线程就不会停止 } }).start(); TimeUnit.SECONDS.sleep(1); // 让线程1 睡一秒 num = 1; System.out.println(num); } }
volatile:是java 关键字,java虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性(要么成功,要么失败)
3、禁止指令重排
验证:volatile 不能保证原子性:
public class Demo2 { // 就算加上volatile 是不能保证原子性 private volatile static int num = 0; // 如果在这个方法加 synchronized 可以确保num 原子性 public static void add(){ // 只要调用add 方法,num++ num++; } public static void main(String[] args) { // num 理论上应该是 20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } while (Thread.activeCount() > 2){ // 如果线程大于2 Thread.yield(); // 让线程迭让 } System.out.println(Thread.currentThread().getName()+" "+num); } }
那么,如果不用synchronized 或者Lock 锁,怎么确保原子性??
原子类:比如int,就用AtomicInteger()
import java.util.concurrent.atomic.AtomicInteger; public class Demo2 { // 原子类的Integer private volatile static AtomicInteger num = new AtomicInteger(); public static void add(){ // 只要调用add 方法,num++ // num++; num.getAndIncrement(); // 这个getAndIncrement 并不是简单用了+1 操作,用的是CAS,效率极高 } public static void main(String[] args) { // num 理论上应该是 20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { add(); } }).start(); } while (Thread.activeCount() > 2){ // 如果线程大于2 Thread.yield(); // 让线程迭让 } System.out.println(Thread.currentThread().getName()+" "+num); } }
这样就可以确保原子性,原子类的底层非常牛逼,直接和操作系统挂钩,直接在内存中修改值!Unsafe 类是一个很特殊的存在,往下看!
volatile - 禁止指令重排
指令重排:意思就是你写的程序,计算机并不是按照你写的那样去执行的。
虽然我们正常写的代码可能永远都不会重排,但这个几率是一定存在的。
volatile 加上以后,可以禁止重排序。
单例模式
volatile 的内存屏障 在单例模式使用的最多,单例模式分 懒汉模式、饿汉模式
懒汉式里有个DCL懒汉式
饿汉式单例回顾:
/** * 饿汉 单例模式 */ public class Hungry { private Hungry() { } private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } }
懒汉式单例:
package com.biao.single; /** * 懒汉式单例 */ public class LazMan { private LazMan(){ System.out.println(Thread.currentThread().getName()+"ok"); } private static LazMan lazMan; public static LazMan getInstance(){ if (lazMan == null){ lazMan = new LazMan(); } return lazMan; } // 单线程下,单例确实没毛病,但是多线程下,是有问题的 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazMan.getInstance(); }).start(); } } }
怎么解决单线程下的单例模式? 双重检验锁!
package com.biao.single; /** * 懒汉式单例 */ public class LazMan { private LazMan(){ System.out.println(Thread.currentThread().getName()+"ok"); } private volatile static LazMan lazMan; // 双重检验锁,锁它的class DCL 懒汉式 public static LazMan getInstance(){ if (lazMan == null){ synchronized (LazMan.class){ if (lazMan == null){ lazMan = new LazMan(); // 它不是原子性操作,所以一定要在上面加 volatile,避免重排序 } } } return lazMan; } // 单线程下,单例确实没毛病,但是多线程下,是有问题的 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazMan.getInstance(); }).start(); } } }
静态内部类:
/** * 静态内部类 */ public class Holder { private Holder() { System.out.println(Thread.currentThread().getName()+"ok"); } public static Holder getInstance(){ return InnerClass.HOLDER; } public static class InnerClass{ private static final Holder HOLDER = new Holder(); } }
单例模式式不安全的,可以通过反射破坏,最好的解决办法是:枚举
CAS 理解
cas 是cpu 的并发原理。
import java.util.concurrent.atomic.AtomicInteger; public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); // 比较并更换,默认是2020,如果是2020的话就返回2021 System.out.println(atomicInteger.compareAndSet(2020, 2021)); // true System.out.println(atomicInteger.get()); // 返回2021 System.out.println(atomicInteger.compareAndSet(2020, 2021)); // false System.out.println(atomicInteger.get()); // 还是返回2021,因为上面已经修改了,所以条件是2020 不成立 } }
这个 .compareAndSet(,) 方法,就是CAS的缩写,这个方法底层调用的 Unsafe 类,它调用的是内存里的方法,所以效率非常快,里面的 getAndAddInt 方法,是个自旋锁。
说到CAS,面试时候可能会问ABA 的问题。
ABA:说白了就是狸猫换太子。
CAS 的缺点:循环耗时(自旋锁)、一次只能保证一个共享变量的原子性、ABA问题
原子引用解决ABA问题
引用原子引用,对应的是乐观锁 的思想
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class CASDemo { public static void main(String[] args) { // 如果泛型是一个包装类,注意对象引用问题,正常工作中比较的都是一个对象,如果用Integer,会是一个大坑 AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1); new Thread(()->{ int stamp = atomicStampedReference.getStamp();// 获得版本号 System.out.println("a1=>"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println("a2=>"+atomicStampedReference.getStamp()); System.out.println(atomicStampedReference.compareAndSet(2,1,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)); System.out.println("a3=>"+atomicStampedReference.getStamp()); },"a").start(); } }
就是改完以后,如果再要改,那就会失败,因为原有的基础上已经发生的改变,所以要在第一改完以后,再给改回来。
可重入锁
又称 递归锁,意思是,拿到了外面的锁,就可以打开里面的锁,自动获得。有了大门钥匙,就可以进入房间里的门。
// synchronized public class Demo1 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone{ public synchronized void sms(){ System.out.println(Thread.currentThread().getName()+"sms"); call(); // 这里调用cass 方法里的锁 } public synchronized void call(){ System.out.println(Thread.currentThread().getName()+"call"); } }
如果使用Lock,就等于加了两把锁,每次都是锁上后再释放,但synchronized 是自动释放锁,所以感觉是,虽然是一把锁,但是只进入锁一次。
Lock 锁:如果 .lock() 方法锁了两次,一定也要在finally 里面释放两次,不然会是死锁。
自旋锁
CAS 自定义的自旋锁:
package com.biao.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * 自旋锁 */ public class SpinlockDemo { // Thread 默认值是null AtomicReference<Thread> atomicReference = new AtomicReference<>(); // 加锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"=>mylock"); // 自旋锁 while (!atomicReference.compareAndSet(null,thread)){ // 条件不成立,就一直去执行 } } // 解锁 public void myUnLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"=>myUnLock"); // 解锁不用自旋,如果里面有线程,就给它 null atomicReference.compareAndSet(thread,null); } // 测试:T1 和T2 都去执行,T1先拿到锁,T1 解锁之后,T2 才能解锁 public static void main(String[] args) { SpinlockDemo lock = new SpinlockDemo(); new Thread(()->{ // 加锁,自己写的锁 lock.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 解锁 lock.myUnLock(); } },"T1").start(); new Thread(()->{ // 加锁,自己写的锁 lock.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 解锁 lock.myUnLock(); } },"T2").start(); } }
死锁
两个锁掐架了。。。
都去试图获取对方的锁
排除死锁:
import java.util.concurrent.TimeUnit; /** * 死锁 */ public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; // A 想获得B,B想获得A,结果死锁 new Thread(new MyThread(lockA,lockB),"T1").start(); new Thread(new MyThread(lockB,lockA),"T2").start(); } } class MyThread implements Runnable{ private String lockA; private String lockB; public MyThread(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA){ System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>"+lockB); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB){ System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>"+lockA); } } } }
解决问题:
jps 定位进程号
jstack 进程号 然后就可以查看锁的信息