一、什么是JUC
JUC是Java中的三个包,java.util.concurrent,
业务:普通的线程代码 Thread 调用Runnable没有返回值,效率较Callable低,所以企业中Runnable使用少,大多情况下使用Callable。
二、线程和进程
-
进程:一个程序
一个进程包含多个线程,最少包含一个
Java默认有2个线程:Main、GC -
线程:
对于Java而言,开启线程:Thread、Runnable、Callable
Java真的可以开启线程吗?开不了,通过调用底层C++本地方法start0()开启;
并发和并行
并发:多线程操作同一资源(CPU一核,交替进行)
并行:多个人一起行走(CPU多核,多线程同时执行):线程池
//获取cpu核数
System.out.println(Runtime.getRuntime().availableProcessors)
并发编程的本质:充分利用CPU资源
线程有几个状态
源码中有6个
NEW:新生
RUNNABLE:就绪
RUNNING:运行
BLOCKED:阻塞
WATTING:等待(死等)
TIMED_WATTING:超时等待
TERMINATED:终止
wait/sleep区别
- 来自的类不同:Object和Thread。企业中一般不会使用Thread.sleep(),而用TimeUnit.DAYS(SECONDS).sleep(1);
- 锁的释放:wait释放锁,sleep不会释放
- 适用范围:wait必须在同步代码块,sleep可以在任何地方
- 异常捕获:wait不需要,sleep必须捕获
三、Lock(重点)
实现买票
传统的Synchronized锁
public class saleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP面向对象,资源对象只有属性、方法
class Ticket {
private int num = 30;
public synchronized void sale() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + num-- + "票,剩余" + num);
}
}
}
Lock锁
使用方式
实现Lock接口的类
ReentrantLock:可重入锁
有两个构造函数
公平锁:可以先来后到 (3h 3s那3s的线程就要等待3h)
非公平锁:可以插队(默认)
//Lock 三部曲
//1、 new ReentrantLock();
//2、 加锁 lock.lock(); 业务代码放在try中间
//3、 解锁 finally-》lock.unlock();
package com.zzz;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class saleTicketLock {
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"C").start();
}
}
//Lock 三部曲
//1、 new ReentrantLock();
//2、 加锁 lock.lock(); 业务代码放在try中间
//3、 解锁 finally-》lock.unlock();
class Ticket2 {
private Lock lock = new ReentrantLock();
private int num = 30;
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区别
- Synchronized内置关键字,Lock是一个Java类
- Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
- Synchronized会自动释放锁,Lock必须手动释放,如果不释放会产生死锁
- Synchronized 线程1(获得锁、阻塞)线程2(等待锁、死等),Lock 不会一直等待(lock.tryLock();)
- Synchronized 可重入锁,不可以中断,非公平。Lock可重入锁,可以判断锁,非公平(可自己设置)。
- Synchronized 适合锁少量代码同步问题,Lock适合锁大量的同步代码。
生产者消费者问题
线程通信
线程交替执行 A B操作同一变量
传统的Synchronized锁
// A num+1
// B num-1
public class ProductTest {
public static void main(String[] args) {
Data data = new Data();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.add();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.del();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
}
}
//判断等待 业务 通知
class Data {
private int num;
public synchronized void add() throws InterruptedException {
if (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
this.notifyAll();
}
public synchronized void del() throws InterruptedException {
if (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + ":" + num);
this.notifyAll();
}
}
但是如果线程数量超过2个就会出现问题(虚假唤醒)
if 改为 while:因为if只会判断一次
JUC 版Lock锁
之前使用的是wait和notify通信
而Lock使用的是await和signal来通信
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//线程通信
//线程交替执行 A B操作同一变量
// A num+1
// B num-1
public class ProductLockTest {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.add();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.del();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.add();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"C").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
data.del();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"D").start();
}
}
//判断等待 业务 通知
class Data2 {
private int num;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
public void del() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + ":" + num);
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
仍存在的问题:ABCD四个线程随机访问,想让有序,A-B-C-D,精准的通知
解决:新建3个Condition,执行完A后唤醒Condition2,通过num依次唤醒
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
package com.zzz;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// ABCD线程有序输出
//当num=1执行A num=2 B 3 C
public class ProductLockOrdered {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data3.printA();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data3.printB();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data3.printC();
}
}
},"C").start();
}
}
class Data3 {
private int num = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA(){
lock.lock();
try {
while (num!=1){
condition1.await();
}
System.out.println("AAAA");
num = 2;
condition2.signal();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num!=2){
condition2.await();
}
System.out.println("BBBB");
num = 3;
condition3.signal();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num!=3){
condition3.await();
}
System.out.println("CCCC");
num = 1;
condition1.signal();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
四、8锁现象
什么是锁?如何判断锁的是谁?
8锁其实就是锁的八个问题。
1、标准情况下两个线程先打印 发短信还是打电话?
答:发短信
2、sendMsg延迟4s,两个线程打印,中间休息1s,先打印谁?
答:发短信
原因:synchronized 锁的对象是方法的调用者:phone。两个方法用得是同一个锁,谁先拿到谁执行。
import java.util.concurrent.TimeUnit;
//8锁,就是锁的八个问题
//1、标准情况下,两个线程打印,中间休息1s,先打印谁? 发短信
//2、sendMsg延迟4s,两个线程打印,中间休息1s,先打印谁? 发短信
public class Q1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendMsg();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone.call();},"B").start();
}
}
class Phone{
public synchronized void sendMsg() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
3、增加一个普通方法,先执行普通方法还是发短信?
答:普通方法
原因:hello方法非同步,没有锁
...
new Thread(()->{phone.hello();},"B").start();
...
public void hello(){
System.out.println("hello");
}
...
4、两个对象,两个同步方法,先执行打电话还是发短信?
答:打电话
原因:有两个对象,锁的对象不是同一个,不会占用锁
public class Q2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{
try {
phone1.sendMsg();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();},"B").start();
}
}
class Phone2{
public synchronized void sendMsg() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
5、给同步方法加上static,只有一个对象,先打印打电话还是发短信?
答:发短信
6、给同步方法加上static,两个对象,先打印打电话还是发短信?
答:发短信
原因:加上static(静态方法)类一加载就有了 ,锁的是Class ,Phone3只有全局唯一的对象
public class Q3 {
public static void main(String[] args) throws InterruptedException {
// Phone3 phone = new Phone3();
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{
try {
phone1.sendMsg();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();},"B").start();
}
}
class Phone3{
// 加上static(静态方法)类一加载就有了 ,锁的是Class ,Phone3只有全局唯一的对象
public static synchronized void sendMsg() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
7、一个静态同步方法,一个普通同步方法,一个对象,先打印打电话还是发短信?
答:打电话
8、一个静态同步方法,一个普通同步方法,两个对象,先打印打电话还是发短信?
答:打电话
原因:静态同步方法锁的是Class,普通锁的是对象。所以不会占用一个锁。
public class Q4 {
public static void main(String[] args) throws InterruptedException {
// Phone4 phone = new Phone4();
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{
try {
phone1.sendMsg();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();},"B").start();
}
}
class Phone4{
// 静态同步方法
public static synchronized void sendMsg() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
// 普通同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
小节
锁的有两个:new出来的对象和Class唯一的模板
关键看是否是同一个对象或者是否是static
五、集合的不安全
ArrayList
ArrayList在多线程不安全 报java.util.ConcurrentModificationException并发修改异常
解决方式:
- Vector:使用new Vector<>(),但是该方法面试不加分
- Collections.synchronizedList :使用Collections.synchronizedList(new ArrayList());
- CopyOnWriteArrayList : 使用juc中的 new CopyOnWriteArrayList<>(); CopyOnWrite写入时复制一份,避免多线程写入时被覆盖造成数据问题
public class ArrayListTest {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
问:CopyOnWriteArrayList为什么比Vector好?
答:Vector的add()方法源码中是使用synchronized修饰的,使用synchronized修饰的效率都会降低。CopyOnWriteArrayList使用的是Lock锁,执行了 取——复制——存 的操作。
Set
//HashSet 不安全
// 解决方法 :1. 使用Collections 2.使用CopyOnWrite
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
public class SetTest {
public static void main(String[] args) {
// HashSet<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString());
System.out.println(set);
}).start();
}
}
}
HashSet底层 就是HashMap
add方法本质就是map的put,所以Set的本质就是Map的key,是无法重复的
Map
HashMap有三个构造,默认HashMap = new HashMap<>(16,0.75) 初始容量16、 加载因子0.75
解决方法 :1. 使用Collections 2.ConcurrentHashMap
public class MapTest {
public static void main(String[] args) {
// HashMap<String,String> map = new HashMap<>();
//HashMap 不安全
// 解决方法 :1. 使用Collections 2.ConcurrentHashMap
// Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString());
System.out.println(map);
}).start();
}
}
}
六、Callable
有返回值,可以抛出异常
要实现implements Callable,此处的String即为返回值类型。
- new Thread的构造中只能传入Runnable,不能传入Callable
- 所以通过一个适配类来传入Callable,即FutureTask
- 获取call返回结果通过FutureTask.get()获取,但是他会产生阻塞,要放到最后一行或通过异步通信来处理
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<String> futureTask = new FutureTask<>(myThread); // 适配类
new Thread(futureTask).start();
new Thread(futureTask).start();
String s = futureTask.get();
System.out.println(s);
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
return "call()";
}
}
七、常用的辅助类(高并发必会)
CountDownLatch
减法计数器,等待计数器归0后执行后续任务。
等待所有人都出教室后关门
- new CountDownLatch(6); 默认计数器为6
- 每执行一次,countDown 减一
- countDownLatch.await();等待计数器归0后执行后续任务,如果没有这句话计数器无用。
public class CountDownLatchTest {
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() + " out");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("关门");
}
}
CyclicBarrier
加法计数器
CyclicBarrier有两个构造函数,
- 一个参数:第一个参数是需要几个线程await()
- 两个参数:第一个参数是需要几个线程await(),第二个参数是Runnable,表示满足几个线程后执行该Runnable。
集卡,集齐7卡可获得奖品
public class CyclicBarrierTest {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("奖品发放");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(() -> {
// 这里不能直接使用i,可以通过final来转接使用。因为作用域不同
System.out.println(Thread.currentThread().getName() + temp + " 集卡成功");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}
Semaphore
信号量。可以理解为通行证。限流
acquire() 得到,占有。如果已经满了,等待被释放为止。
release() 释放车位,将当前信号量+1,然后唤醒等待的线程
抢车位。
//抢车位
public class SemeaphoreTest {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
// acquire 得到
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位!");
TimeUnit.SECONDS.sleep(2);
// release 释放
System.out.println(Thread.currentThread().getName() + "离开了车位!");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},String.valueOf(i)).start();
}
}
}
八、读写锁
自定义一个缓存,模拟读写过程
未加读写锁之前:写被插队,期望成功写入后再下一个写。
加入读写锁后:保证了写入成功后再下一个写
- 新建ReentrantReadWriteLock():ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- 写操作加写锁:readWriteLock.writeLock().lock();并释放
- 读操作加读锁:readWriteLock.readLock().lock();并释放
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写操作
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(Thread.currentThread().getName(), temp);
},String.valueOf(i)).start();
}
//读操作
for (int i = 0; i < 5; i++) {
new Thread(()->{
myCache.get(Thread.currentThread().getName());
},String.valueOf(i)).start();
}
}
}
//自定义模拟缓存
class MyCache{
private HashMap<String,Object> hashMap = new HashMap<>();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println("写入"+key);
hashMap.put(key,value);
System.out.println("写入"+key + "成功");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public Object get(String key){
readWriteLock.readLock().lock();
try {
System.out.println("读取"+key+",值为"+hashMap.get(key));
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
readWriteLock.readLock().unlock();
}
return hashMap.get(key);
}
}
独占锁(写锁):一次只能被一个线程占有
共享锁(读锁):多个线程可以同时占用
九、阻塞队列
BlockingQueue
-
什么情况下使用它?
多线程并发处理、线程池 -
如何使用它?
添加、移除
有四组API:抛出异常、不会抛出异常、阻塞等待、超时等待
方式 | 抛出异常 | 不抛异常,有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,) |
移除 | remove() | poll() | take() | poll(,) |
查看队首元素 | element() | peek() | ------ | ----- |
- 抛出异常
/**
* 抛出异常
*/
public static void throwException(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
// 抛出异常java.lang.IllegalStateException: Queue full
// System.out.println(queue.add("d"));
System.out.println("----------");
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// 抛出异常java.util.NoSuchElementException
// System.out.println(queue.remove());
}
- 不抛出异常,有返回值
offer返回false
poll 返回null
/**
* 抛出异常
*/
public static void noThrowAndHasReturn(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
// 不抛出异常,返回false
// System.out.println(queue.offer("d"));
System.out.println("----------");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
// 不抛出异常,返回null
System.out.println(queue.poll());
}
- 等待(一直阻塞)
/**
* 等待:一直阻塞
*/
public static void waitAlways() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
queue.put("a");
queue.put("b");
queue.put("c");
// 一直阻塞
// queue.put("d");
System.out.println("----------");
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
// 一直阻塞
// System.out.println(queue.take());
}
- 超时等待
/**
* 等待:超时退出
*/
public static void waitTime() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
queue.offer("a");
queue.offer("b");
queue.offer("c");
// 等待2秒
// queue.offer("d",2, TimeUnit.SECONDS);
System.out.println("----------");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
// 等待2秒
// System.out.println(queue.poll(2,TimeUnit.SECONDS));
}
}
SynchronousQueue
同步队列
和其他BlockingQueue 不同,SynchronousQueue不存储元素
put了一个元素,必须先take取出来,否则不能put进值
public class SynchronousQueueTest {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println("A put 1");
queue.put("1");
System.out.println("A put 2");
queue.put("2");
System.out.println("A put 3");
queue.put("3");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("B get " + queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println("B get " + queue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println("B get " + queue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"B").start();
}
}
十、线程池(重点)
面试题:三大方法、七大参数、四种拒绝策略
好处:
- 降低资源消耗
- 提高响应速度
- 方便管理
- 线程复用、可以控制最大并发数、管理线程
三大方法
-
单一线程池:Executors.newSingleThreadExecutor();
-
固定线程池:Executors.newFixedThreadPool(5);
-
缓存线程池:Executors.newCachedThreadPool();可伸缩,遇强则强,遇弱则弱
线程池用完一定要关闭,为保证程序运行完毕,可以将其放到finnaly中。
public class ThreeMethod {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 100; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " OK");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
threadPool.shutdown();
}
}
}
七大参数
原因是因为三大方法的源码都是调用了new ThreadPoolExecutor(),其实为封装的方法,但其中使用到的参数是Integer.MAX_VALUE,即大小约为21亿,会导致OOM,所以不允许使用。
图例
- 超时等待即为所有人业务都办完了,除核心线程数外,超过超时时间后就会关闭窗口
- 候客区即为阻塞队列,new
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), //阻塞队列,最大为3
Executors.defaultThreadFactory(), //默认线程工程,一般不会动
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
四种拒绝策略
AbortPolicy抛出异常
new ThreadPoolExecutor.AbortPolicy()
CallerRunsPolicy返回
new ThreadPoolExecutor.CallerRunsPolicy()
哪里来的去哪里
DiscardPolicy 丢掉新任务,不抛出异常
new ThreadPoolExecutor.DiscardPolicy()
DiscardOldestPolicy与最早来的竞争,不抛出异常
new ThreadPoolExecutor.DiscardPolicy()
如果竞争成功则处理,不成功丢弃
如何定义最大线程数
- CPU密集型。几核就是几,可以保证CPU效率最高。
- 可以在任务管理器——性能——CPU——逻辑处理器查看
- (常用)通过代码获取:Runtime.getRuntime().availableProcessors();
- IO密集型。程序中有几个大型任务执行IO操作(非常耗时),一般设置为IO任务数的2倍。
十一、四大函数式接口(必须掌握)
新时代程序员必会:lambda表达式、链式编程、函数式接口、Stream流
函数式接口:只有一个方法的接口
例如:Runnable、foreach
使用场景:Stream流中的函数
例如:
.filter(Predicate p)
.forEach(Consumer c)
Function
public class FuncitonTest {
public static void main(String[] args) {
// Function func = new Function<String,String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
Function<String,String> function = (str)->{
return str;
};
System.out.println(function.apply("aaa"));
}
}
断定型接口 Predicate
有一个参数输入,返回值只能是boolean
public class PredicateTest {
public static void main(String[] args) {
// Predicate func = new Predicate<String>() {
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
Predicate<String> function = (str)->{
return str.isEmpty();
};
System.out.println(function.test("aaa"));
}
}
消费型接口 Consumer
public class ConsumerTest {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = (str)->{
System.out.println(str);
};
}
}
供给者接口 Supplier
public class SupplierTest {
public static void main(String[] args) {
// Supplier<String> supplier = new Supplier<String>() {
// @Override
// public String get() {
// return "aaa";
// }
// };
Supplier<String> supplier = ()->{
return "aaa";
};
System.out.println(supplier.get());
}
}
十二、Stream流
大数据时代基本的操作都是存储+计算
存储:集合、Mysql
计算:Stream
以下习题运用到了【新时代程序员必会:lambda表达式、链式编程、函数式接口、Stream流】
User:id\name\age
/**
* 题目要求:一分钟内完成此题,只能用一行代码实现!
* 现在有5个用户!筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于23岁
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只输出一个用户!
*/
public class Test {
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, "d", 25);
User u6 = new User(6, "f", 26);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5, u6);
//lambda表达式、链式编程、函数式接口、Stream流
list.stream()
.filter((u) -> u.getId() % 2 == 0)
.filter((u) -> u.getAge() > 23)
.map(u -> u.getName().toUpperCase())
.sorted((uu1,uu2)->uu2.compareTo(uu1))
// .sorted(Comparator.reverseOrder())
.limit(1)
.forEach(System.out::println);
}
}
十三、ForkJoin
必须在大任务量的情况下使用。并行执行任务,提高效率
大任务拆分成小任务
特点:工作窃取
当线程B工作完成之后会将A未完成的部分偷过来继续执行
如何使用?
//如何使用ForkJoin
//1.通过ForkJoinPool执行
//2.计算任务 ForkJoinPool.execute(ForkJoinTask task)
//3.计算类要继承ForkJoin方法,实现compute方法 (RecursiveTask递归任务)
计算1-十亿的和
// 分3(普通计算) 6(ForkJoin) 9(Stream流)等
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
// 分3(普通计算) 6(ForkJoin) 9(Stream并行流)等
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//普通计算
// test1(); //时间:8340
// test2();//6726
test3();//422
}
public static void test1(){
long start = System.currentTimeMillis();
Long sum = 0L;
for (Long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println((end - start));
}
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println((end - start));
}
public static void test3(){
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0, 10_0000_0000).parallel().reduce(0L, Long::sum);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println((end - start));
}
}
//如何使用ForkJoin
//1.通过ForkJoinPool执行
//2.计算任务 ForkJoinPool.execute(ForkJoinTask task)
//3.计算类要继承ForkJoin方法,实现compute方法 (RecursiveTask递归任务)
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if(end - start < temp) {
Long sum = 0L;
for (Long i = start; i < end; i++) {
sum +=i;
}
return sum;
}else {
// forkjoin
long middle = (start + end) / 2;
// 拆分任务,把子任务压入队列
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork();
long sum = task1.join() + task2.join();
return sum;
}
}
}
十四、JMM
什么是JMM?
Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步约定:
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程加锁前,必须读取主存中的最新值,到线程工作内存中
- 加锁跟解锁是同一把锁
线程的工作内存与主存之间的8个操作:
存在问题:线程B修改了Flag值,但A仍然在Flag为true的内存中不能及时可见。
十五、Volatile
Volatile是Java虚拟机提供的轻量级同步机制 .
1、保证可见性
//Volatile保证可见性,如果不加Volatile则程序不会停止,即num感知不到其他线程修改了值。
public class Demo01 {
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(num==0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
2、不保证原子性
不保证原子性,即加了Volatile结果也不是2w。加上Synchonized则结果正确。
public class Demo02 {
private static volatile int num = 0;
public static void main(String[] args) {
//期望结果应该是2w
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num);
}
public static void add(){
num++;
}
}
如果不加lock和Synchonized如何保证原子性
javap -p .class 反编译查看如下。
使用原子类、解决原子性问题 。这些类的底层都与操作系统挂钩,直接在内存中修改值(Unsafe类)。
例如int num的++,可以使用AtomicInteger
private static volatile AtomicInteger num = new AtomicInteger();
num.incrementAndGet(); //AtomicInteger +1方法, 使用的是CAS方法,在内存中修改值
3、禁止指令重排序
什么是指令重排?
源代码——编译器优化的重排——指令并行也可能重排——内存系统也会重排——执行
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
所以Volatile可以保证禁止指令重新排序,通过添加内存屏障。在单例模式中使用最多
十六、单例模式
饿汉式
//饿汉式
public class HungryTest {
private static HungryTest hungryTest = new HungryTest();
private HungryTest(){}
public static HungryTest getInstance() {
return hungryTest;
}
}
懒汉式
public class LazyTest {
private LazyTest(){
System.out.println(Thread.currentThread().getName() +" ok");
}
private static LazyTest lazyTest;
public static LazyTest getInstance(){
if(lazyTest == null){
lazyTest = new LazyTest();
}
return lazyTest;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyTest.getInstance();
}).start();
}
}
}
单线程下单例模式这样写没问题,多线程有问题。
应该采用DCL单例模式。即二次判断加锁,并防止指令重排加Volatile。
DCL单例模式
二次判断加锁,并防止指令重排加Volatile。
public class LazyDCLTest {
private LazyDCLTest(){
System.out.println(Thread.currentThread().getName() +" ok");
}
private volatile static LazyDCLTest lazyTest;
public static LazyDCLTest getInstance(){
if(lazyTest == null){
synchronized (LazyDCLTest.class){
if(lazyTest == null){
lazyTest = new LazyDCLTest();//但是该行不是原子性操作
/*
我们期望它的步骤:
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
不是原子性操作原因是他有可能指令重排,步骤为132。当A线程执行了13时,B线程加入会判断到当前对象!=null,但此时还未完成构造
所以要在对象上加上Volatile
*/
}
}
}
return lazyTest;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyDCLTest.getInstance();
}).start();
}
}
}
结果正常
但是这些单例模式都是不安全的,是可以通过反射来破坏的。可以使用枚举单例来防反射。
public enum EnumTest {
INSTANCE;
private EnumTest(){}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumTest instance1 = EnumTest.INSTANCE;
//获取无参构造,但是提示没有NoSuchMethodException,反编译后发现为有参
// Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(null);
Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumTest instance2 = declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance2);
}
}
十七、深入理解CAS
什么是CAS?
compareAndSet,比较并交换
如果是这个值,就重新赋值。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2000);
System.out.println(atomicInteger.compareAndSet(2000, 3000));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2000, 3000));
System.out.println(atomicInteger.get());
}
}
源码:
atomicInteger.getAndIncrement();(num++):
Unsafe类
CAS缺点:
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
- ABA问题
ABA问题(狸猫换太子)(乐观锁)
A拿到1,要替换为2。但在中间B也拿到了1,替换为了3,又替换为了1,此时A是毫不知情的,继续处理。
但是我们期望有人改了之后告诉别人——即引入原子引用
十八、原子引用
带版本号的原子操作
原子引用
解决上述ABA问题
public class CASDemo2 {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(1,1);
new Thread(()->{
int stamp = atomicReference.getStamp();
System.out.println("A,last:" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicReference.compareAndSet(1, 2, atomicReference.getStamp(), atomicReference.getStamp() + 1));
System.out.println("A,modify1:" + atomicReference.getStamp());
System.out.println(atomicReference.compareAndSet(2, 1, atomicReference.getStamp(), atomicReference.getStamp() + 1));
System.out.println("A,modify2:" + atomicReference.getStamp());
},"A").start();
new Thread(()->{
int stamp = atomicReference.getStamp();
System.out.println("B,last:" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicReference.compareAndSet(1, 2, stamp, stamp + 1));
System.out.println("B,modify1:" + atomicReference.getStamp());
},"B").start();
}
}
十九、各种锁
公平锁、非公平锁
公平锁:不能插队,必须先来后到。
非公平锁:可以插队,3s 3h(默认非公平)
Lock reentrantLock = new ReentrantLock();
Lock reentrantLock = new ReentrantLock(true);
可重入锁
又叫递归锁, 拿到外面的锁之后,就可以拿到里面的锁
所有的锁都是可重入锁
可重入锁的主要目的是解决在递归调用或嵌套代码中的锁定问题。当一个线程已经获得了锁,但在持有锁的代码块中又调用了另一个需要同样锁的方法时,如果使用非可重入锁,线程会因为无法再次获得同一个锁而陷入死锁状态。而可重入锁允许线程多次获得同一个锁,避免了死锁问题。
自旋锁
什么是自旋锁?
当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
为什么要使用自旋锁?
多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。
我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
死锁
制造死锁:
public class DeadLock {
public static void main(String[] args) {
MyThread myThreadA = new MyThread("lockA", "lockB");
MyThread myThreadB = new MyThread("lockB", "lockA");
new Thread(myThreadA,"A").start();
new Thread(myThreadB,"B").start();
}
}
class MyThread implements Runnable{
String lockA = "";
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 + "=>want to get " + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " lock: " + lockB + "=>get" + lockA);
}
}
}
}
如何解决?
1、使用jps -l定位进程号
2、使用 jstack +进程号,找到死锁问题