学的B站狂神的JUC教程,以及自己另外看书记的笔记,对视频中的内容做了更为详细的笔记
目录
一、什么是JUC
1.1 JUC简介
我们常说的JUC就是Java.util.concurrent包,他是一个处理线程的工具包,从jdk1.5之后开始出现
1.2进程与线程
进程:是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器;
线程:是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位;
1.3.1线程的状态
1.NEW(新生)
2.RUNNABLE(运行)
3.BLOCKED(阻塞)
4.WAITING(不见不散,不来就一直等)
5.TIMED_WAITING(过时不候)
6.TERMINATED(终结)
1.3.2 wait/sleep的区别
①sleep是Thread类的静态方法,wait是Object类的方法,任何对象实例都可以调用这两个方法;
②sleep不会释放锁,也不需要占用锁;wait会释放锁,但是调用它的前提是当前线程占有了锁的位置(即代码要在synchronized同步代码块中)
③它们都可以interrupted方法中断;
④sleep必须要处理异常;
1.4 并发与并行
并发(concurrent): 是指在同一实体上的多个事件,在一台处理器上“同时处理多个任务”,同一时刻其实只有一个事件发生【一对多】
并行(parallel):是在不同实体上的多个事件,多台处理器上同时处理多个任务,同一时刻各自互不干涉【多对多】
二、LOCK锁
1、reentrantlock可以代替synchronized,使用reentrantlock可以完成和synchronized同样的功能,但是必须得手动释放锁。使用synchronized锁定如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中释放锁。
2、使用reentrantlock可以进行尝试锁定(tryLock),这样无法锁定或者在指定时间内无法锁定的话,线程可以决定是否继续等待。
3、reentrantlock可以指定为公平锁。ReentrantLock lock=new ReentrantLock(true);表示new一个公平锁。默认是非公平锁。
(以卖票为例:)
传统的synchronized锁:
public class SaleTicketDemo01 {
public static void main(String[] args) {
//创建多个线程,调用资源类中的方法
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
class Ticket{
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale(){
//判断是否有票
if (number>0){
number--;
System.out.println(Thread.currentThread().getName()+"卖出1张票"+",还剩"+number+"张票;");
}
}
}
使用了ReetrantLock(可重入锁)之后:
/**
* 使用可重入锁ReentrantLock
*/
public class SaleTicketDemo02 {
public static void main(String[] args) {
//创建多个线程,调用资源类中的方法
Ticket02 ticket = new Ticket02();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
class Ticket02{
//票数
private int number = 30;
//1.new ReentrantLock对象
Lock lock = new ReentrantLock();
//操作方法:卖票
public void sale(){
lock.lock();//2.加锁
//判断是否有票
try {
if (number>0){
number--;
System.out.println(Thread.currentThread().getName()+"卖出1张票"+",还剩"+number+"张票;");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//3.释放锁
lock.unlock();
}
}
}
公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,
就占有锁,否则就会加入到等待队列中,以后会按照FIFO(队列先进先出)的规则从队列中取到锁;
非公平锁:如果一个新来的线程,直接就去抢锁,而不去判断等待队列是否有线程在等待,这就是非公平锁。synchronized都是非公平锁。
新来的线程检查队列与否是区分公平锁和非公平锁的关键!
【Synchronized和Lock的区别:】
1、Synchronized 是Java内置的关键字,Lock是一个Java类;
2、Synchronized无法判断获取锁的状态,Lock 可以判断线程是否获取到了锁;
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,会造成死锁的问题;
4、Synchronized线程1(获得锁,阻塞)、线程2(等待,傻傻的等) ; Lock锁就不一定会等待下去;
5、Synchronized 是可重入锁,不可以中断的,属于非公平锁;Lock也是可重入锁,可以判断锁的状态,默认非公平锁(可以自己设置);
6、Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
三、生产者和消费者问题
1.synchronized版本:
/**
* 线程之间的通信问题,生产者和消费者问题
* 线程A B操作同一个变量 num=0
* A:num+1;
* B:num-1;
*/
public class Producter {
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) {
throw new RuntimeException(e);
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"B").start();
}
}
class Data{
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number!=0){
this.wait();
}
number++;
//通知其他线程,我+1完毕
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number==0){
this.wait();
}
number--;
//通知其他线程,我+1完毕
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();
}
}
这里如果我们再多增加两个线程的话,就会出现虚假唤醒的问题!(这时候在同步方法中就不能用if去进行判断而要用while)
使用if判断的话,线程被唤醒之后会从wait 之后的代码开始运行,不会重新判断if条件;
而使用while的话,也会从wait之后的代码运行,但是会重新执行判断while循环条件,成立的话继续wait;
2.JUC版本的生产者和消费者问题:(换汤不换药,只是取代wait转而变成condition来控制线程的等待与唤醒)
class Data02{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// condition.await();//等待
// condition.signalAll();//唤醒全部线程
public void increment() throws InterruptedException {
try {
lock.lock();
//业务代码
while (number!=0){
//等待
condition.await();
}
number++;
//通知其他线程,我+1完毕
System.out.println(Thread.currentThread().getName()+"=>"+number);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
try{
lock.lock();
while (number==0){
condition.await();//等待
}
number--;
//通知其他线程,我+1完毕
System.out.println(Thread.currentThread().getName()+"=>"+number);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
3.Condition实现精准通知唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | class Data03{ private Lock lock = new ReentrantLock(); //new 3个监视器 private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int number = 1; //1A 2B 3C public void printA(){ lock.lock(); try { //业务,判断,执行,通知 while (number!=1){ condition1.await(); } System.out.println(Thread.currentThread().getName()+"=>"+"AAAAA"); //唤醒指定的人B number = 2; condition2.signal(); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void printB(){ lock.lock(); try { //业务,判断,执行,通知 while (number!=2){ condition2.await(); } System.out.println(Thread.currentThread().getName()+"=>"+"BBBBB"); //唤醒指定的人B number = 3; condition3.signal(); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void printC(){ lock.lock(); try { //业务,判断,执行,通知 while (number!=3){ condition3.await(); } System.out.println(Thread.currentThread().getName()+"=>"+"CCCCC"); //唤醒指定的人B number = 1; condition1.signal(); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } |
三.八锁现象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /** * 1.标准情况下,两个线程先打印发短信还是打电话? //发短信 * 2.sendMsg()延迟4s后,两个线程是先打印发短息还是打电话? //发短信 * 3.增加一个普通方法后,先执行发短信还是hello?//这个时候就不存在抢锁的问题了,看谁的延时少就去执行谁 * 4.两个对象分别去调用两个同步方法,是先发短信还是先打电话?(sendMsg延迟4s,call延迟1s) //打电话啊 * */ public class Test1 { public static void main(String[] args) { Phone phone1 = new Phone(); Phone phone2 = new Phone(); //显然这里一定是发短信方法先拿到对象的锁,所以不管怎么样,都会先执行 new Thread(()->{ phone1.sendMsg(); }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(()->{ phone2.call(); }).start(); } } class Phone{ /** * synchronized锁的对象是方法的调用者 * 两个同步方法用的是同一把锁,谁先拿到就谁先执行! */ public synchronized void sendMsg(){ try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } public void hello(){ System.out.println("Hello"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /** * 5.增加两个静态的同步方法,只有一个对象,先打印发短信还是打电话?//发短信 因为static 是静态方法,在类进行加载的时 * 候就已经初始化了所以这里synchronized是在给class进行加锁,所以会按照调用的先后顺序来进行输出 * 6.new两个对象,两个同步的静态方法,先打印发短信还是打电话? //发短信 */ public class Test2 { public static void main(String[] args) { //这两个对象的类加载器只有一个,static方法锁的是class Phone2 phone1 = new Phone2(); Phone2 phone2 = new Phone2(); //显然这里一定是发短信方法先拿到对象的锁,所以不管怎么样,都会先执行 new Thread(()->{ phone1.sendMsg(); }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(()->{ phone2.call(); }).start(); } } //只有唯一的一个class对象 class Phone2{ /** * synchronized锁的对象是方法的调用者 * static 是静态方法,在类进行加载的时候就已经初始化了 * 所以这里synchronized是在给class进行加锁,所以会按照调用的先后顺序来进行输出 */ public static synchronized void sendMsg(){ try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("发短信"); } public static synchronized void call(){ System.out.println("打电话"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /** * 7.一个静态同步方法,一个普通同步方法,通过一个对象去分别在不同的线程里面调用,谁先谁后? //打电话,因为 * 静态同步方法(类锁)和普通同步方法(对象的锁)用的不是一把锁,所以自然就是先打电话 * 8.两个对象分别去调用一个静态同步方法和普通同步方法,先打印发短信还是打电话?//打电话 因为两个对象不是用的一把锁, * 自然就是谁的延迟小,先打印谁咯 * */ public class Test3 { public static void main(String[] args) { //这两个对象的类加载器只有一个,static方法锁的是class Phone3 phone1 = new Phone3(); Phone3 phone2 = new Phone3(); //显然这里一定是发短信方法先拿到对象的锁,所以不管怎么样,都会先执行 new Thread(()->{ phone1.sendMsg(); }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(()->{ phone2.call(); }).start(); } } //只有唯一的一个class对象 class Phone3{ /** * synchronized锁的对象是方法的调用者 * static 是静态方法,在类进行加载的时候就已经初始化了 * 所以这里synchronized是在给class进行加锁,所以会按照调用的先后顺序来进行输出 */ public static synchronized void sendMsg(){ try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("发短信"); } public synchronized void call(){ System.out.println("打电话"); } } |
四、多线程下的集合类不安全
List
当在单线程中List是安全的,但在并发中ArrayList是不安全的如:
public class ListTest {
public static void main(String[] args) {
//并发操作下list是不安全的
List<String> list = new ArrayList<>();
for (int i = 0; i <=10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
就会出现了:ConcurrentModificationException(并发修改异常),
解决方案如下:
1.使用synchronized给ArrayList的add方法加锁(ArrayList是1.2出来的) ,也就是通过Collections.synchronizedList加锁
2.使用List的子类Vector集合类着里面加入了synchronized(Vector是1.0出来的)
3.使用Collections.synchronizedList()方法将集合转换为安全的集合
4.使用CopyOnWriteArrayList类在java并发包concurrent下!
这里推荐使用CopyOnWriteArrayList而不使用vector是因为:
vector所有的操作都有锁,但是copyonwrite(cow)读操作是没有锁的!
在开发中读比写更频繁,所以说cow好在这里,另外还有一个有点是数组扩容问题。cow不需要扩容
Set
同理,Set集合在多线程的环境下也是不安全的,和list一样会出现java.util.ConcurrentModificationException
解决方案:
1.通过Collections.synchronizedSet给Set加锁;
2.使用CopyOnWriteSet,同样的也在concurrent包下,底层是数组;
3.使用ConcurrentSkipListSet,底层是链表;
【】hashSet的底层就是一个hashMap
Map
new HashMap<>();<==>new HashMap<>(16,0.75);
这里的16是hashMap的默认容量,0.75是加载因子
1.当hashMap达到临界值的时候,会触发扩容机制,扩容的临界值是默认容量x加载因子
也就是16x0.75=12;
2.当位桶中链表的长度大于8时,底层的链表会自动转成红黑树;
3.位桶中链表的节点装换成红黑树时最小的hash表容量为64;
4.链表长度低于6,会从红黑树转成链表
ConcurrentHashMap
1.为什么要使用ConcurrentHashMap?
当然是为了线程安全,为了规避HashMap在多线程模型下的缺点,传统线程安全的HashTable的问题等;
①hashMap在多线程环境下不安全;
②hashTable使用synchronized(非公平锁)给操作加锁,但是在线程
激烈竞争的情况下,hashTable的效率非常低下,会进入阻塞或者轮询状态;(比如说线程A在进行put操作,
那么线程B想要进行put或者get操作的话是不被允许的),所以竞争越激烈,效率越低;
③相较于笨重的hashTable,ConcurrentHashMap就显得很高效;ConcurrentHashMap降低了锁的粒度,在
JDK1.7中,设置了分段锁,来表示不同的数据;1.8以后取消了分段锁,进一步的降低了锁的粒度;
JDK1.8后,ConcurrentHashMap和HashMap的底层数据结构基本都是数组+链表+红黑树,但是在ConcurrentHashMap
里面,红黑树在Node数组里面存储的不是一个TreeNode对象,而是一个TreeBin对象,在TreeBin对象的内部,
维持着一个红黑树;
五、Callable
1.可以有返回值
2.可以抛出异常
3.方法不同,call()/run()
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建适配器
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
//因为FutureTask实现了RunnableFuture接口所以可以通过Thread启动线程
new Thread(futureTask,"A").start();
//获取线程返回值
Integer i = futureTask.get();
System.out.println(i);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}
六、常用辅助类
6.1 CountDownLatch
常用方法:
countDownLatch.countDown(); //表示数量-1
countDownLatch.await(); //等待计数器归零在往下执行!
原理:当每次有线程调用 countDown()方法则数量减一,假设数量变为0,await()方法就会被唤醒继续执行!
coding:
/**
* 测试线程辅助类计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//初始化,总数是6,在必须要执行完某个任务的时候,使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go out");
countDownLatch.countDown(); //数量减一
},String.valueOf(i)).start();
}
//等待计数器归0,然后再向下执行
countDownLatch.await();
System.out.println("close the door");
}
}
总结: countDownLatch.countDown();会让其自动减一 ,而在CountDownLatch下
用await等待会等待其计数器归零时才会再向下执行,防止门提前关了里面的人出不去
6.2 CyclicBarrier
/**
* 测试线程辅助类CyclicBarrierDemo
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
//达到初始化参数的值,执行返回对应的结果
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
int temp = i;
new Thread(()->{
//注意!lambda表达式里面不能直接操作循环变量;需要通过 一个中间量来进行操作
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();
}
}
}
总结:CyclicBarrier可以设置指定最终达到的数值(代表for里的最大数),还能创建线程,
CyclicBarrier下的await会等待其数值达到最终数值才会执行最终数值旁边的Runnable线程
Semaphore:(信号量)
6个车只有3个停车位,进行抢占车位和离开车位的操作;
coding:
/**
* 信号量
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//这里的3是线程总数
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位!");
TimeUnit.SECONDS.sleep(2); //等待2s
System.out.println(Thread.currentThread().getName()+"离开了车位!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放车位
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
总结:Semaphore就是信号量可以填写线程总数,acquire()许可证同意进行操作,release()释放让其离开,
然后会让后面的数值上来代替直到走完。类似于操作系统中的P、V操作。
七、阻塞队列BlockingQueue
写入:如果队列满了,就必须阻塞等待
取:如果队列是空的就必须阻塞等待生产
学会使用队列:四组api
1.抛出异常
2.不会抛出异常
3.阻塞等待
4.超时等待
/**
* 会抛异常的
*/
//如果add操作超出了初始化的容量,会抛java.lang.IllegalStateException
//而如果队列没有数据了的话,依然执行remove操作的话,会抛出java.util.NoSuchElementException
public static void test1(){
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("1"));
System.out.println(blockingQueue.add("2"));
System.out.println(blockingQueue.add("3"));
// System.out.println(blockingQueue.add("3"));
System.out.println("===================");
//这里我们看到遵循了先进先出(FIFO)的原则
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
}
-----------------------------------------------------------------------------------------
/**
* 有返回值,不抛出异常
*/
public static void test2(){
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println("====================");
//如果offer添加操作超过了队列的初始化容量,会返回false,不会抛出异常
// System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.peek()); //查看队列首部元素 a
System.out.println("********************");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//如果队列中没有数据,进行poll移除操作时,会返回null
// System.out.println(blockingQueue.poll());
}
------------------------------------------------------------------------------------------------------------------------
/**
* 等待阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("i");
blockingQueue.put("j");
blockingQueue.put("k");
//队列初始化容量满了再进行put,会一直阻塞
// blockingQueue.put("n");
System.out.println("==================");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//队列没有元素的情况下,再进行take(取)操作,会一直阻塞等待
System.out.println(blockingQueue.take());
}
----------------------------------------------------------------------------------------
/**
* 等待,阻塞(超时等待)
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("11"));
System.out.println(blockingQueue.offer("22"));
System.out.println(blockingQueue.offer("33"));
System.out.println("====================");
//如果offer添加操作超过了队列的初始化容量,在等待1s未果后,会终止添加
System.out.println(blockingQueue.offer("d",1, TimeUnit.SECONDS));
System.out.println("********************");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//如果队列中没有数据时,进行poll移除操作,在等待1s未果后,会自动终止取数据
System.out.println(blockingQueue.poll(1,TimeUnit.SECONDS));
}
八、同步队列
SynchronousQueue:没有容量,进去一个元素,必须等待取出来之后,才能往里面放另一个元素;
coding:
/**
* 测试同步队列
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "put1");
try {
blockingQueue.put("1");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "put2");
try {
blockingQueue.put("2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "put3");
try {
blockingQueue.put("3");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"T1").start();
new Thread(()->{
//******************************
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println(Thread.currentThread().getName()+"=>"+ blockingQueue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//******************************
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println(Thread.currentThread().getName()+"=>" + blockingQueue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//**********************************
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//**********************************
try {
System.out.println(Thread.currentThread().getName()+"=>" + blockingQueue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"T2").start();
}
}
九、线程池(重点)
9.1简介
池化技术:事先准备好一些资源,有需要了从池子中取,取完了再放回去;
程序的运行本质:占用系统的资源,优化资源的使用!
常见的池化技术:线程池、jdbc数据库连接池、内存池、对象池
线程池的好处:
1.降低资源的消耗;
2.提高响应的速度;
3.方便管理;
线程复用,可以控制最大并发数,管理线程
9.2 三大方法:
①newSingleThreadExecutor //创建单个线程
②newFixedThreadPool //创建一个固定大小的线程池
③newCacheThreadPool //创建一个可伸缩的线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学
更加明确线程池的运行规则,规避资源耗尽的风险。
【说明:】Executors各个方法的弊端:
①newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
②newCacheThreadPool和newScheduledThreadPool:
主要问题是线程最大数是Integer.MAX_VALUE(约为21亿),可能会创建非常多的线程,甚至OOM
newCacheThreadPool:
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
*那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,
*此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,
*线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
9.3 七大参数及自定义线程池
源码分析:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
七大参数:
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.AbortPolicy //默认拒绝策略,拒绝任务并抛出任务
②ThreadPoolExecutor.CallerRunsPolicy //使用调用的线程直接运行任务(例如测试的main线程)
③ThreadPoolExecutor.DiscardPolicy //直接拒绝任务,不抛出错误
④ThreadPoolExecutor.DiscardOldestPolicy //触发拒绝策略,只要还有任务新增,
一直会丢弃阻塞队列的最老的任务,并将新的任务加入
coding:
//自定义线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
try {
//最大承载:阻塞队列值+最大线程数(这里是5+3=8)
for (int i = 0; i < 9; i++) {
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
poolExecutor.shutdown();
}
}
【最大线程到底该如何定义?】
1.cpu 密集型:几核就是几,可以充分利用CPU的资源
2.IO密集型:判断程序中十分耗IO的线程,
十、四大函数式接口
10.1 :函数型接口
(只有一个方法的接口)
@FunctionalInterface
public interface Runnable {
public abstract void run();
Runnable接口就是一个典型的例子
@FunctionalInterface(简化编程模型,jdk1.8之后大量使用)
coding:
//判断字符串是否为空
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
};
System.out.println(predicate.test(""));
10.2 断定型接口
(有一个输入参数)
coding:
//判断字符串是否为空
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
};
System.out.println(predicate.test(""));
10.3 消费型接口
(只有输入,无返回值)
coding:
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer1 = (str)->{
System.out.println(str);
};
consumer1.accept("adada");
}
10.4 供给型接口
(没有参数,只有返回值)
// Supplier<Integer> supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier<Integer> supplier1 = ()->{
return 1024;
};
System.out.println(supplier1.get());
}
十一、Stream流式计算
/**
* 题目要求:(一分钟内完成此题,只能用一行代码实现)
* 现在有5个用户,筛选:
* 1. ID必须是偶数;
* 2. 年龄必须大于23岁;
* 3. 用户名转为大写字母;
* 4. 用户名字母倒着排序;
* 5. 只输出一个用户
*/
coding:
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,"e",25);
User u6 = new User(6,"f",26);
List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6);
list.stream().filter((u)->{return u.getId()%2==0;})
.filter((u)->{return u.getAge()>23;})
.map((u)->{return u.getName().toUpperCase();})
.sorted((us1,us2)->{return us2.compareTo(us1);})
.limit(1)
.forEach(System.out::println);
}
}
十二、ForkJoin
11.1:什么是ForkJoin
核心思想就是把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,
其实现思想与MapReduce有异曲同工之妙。
Fork/Join框架使用一个巧妙的算法来平衡线程的负载,称为工作窃取(work-stealing)算法。工作窃取的运行流程图如下:
假如我们要做一个很大的任务,分多个线程处理,其中一些线程处理完毕没有事情了,这时候
空闲的线程会去还在执行任务的线程的尾部"窃取"一个任务来帮忙执行;
工作流程:
1.计算类继承ForkJoinTask的子类---RecursiveTask<>
2.然后通过forkJoinPool.execute(ForkJoinTask task)来执行计算
coding:
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start; // 1
private Long end; // 1990900000
// 临界值
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(); // 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// test1();//5165
// test2();//3608
test3();//167
}
// 基础的for循环
public static void test1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = 1L; 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);// 提交任务
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+"\t时间:"+(end-start));
}
}
十三、JMM
JMM描述的是一组规则,通过这组规则控制程Java序中各个变量在共享数据区域
和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性拓展延伸的。
·关于JMM的一些同步的约定:
1.线程解锁前,必须把共享变量立刻刷回内存;
2.线程加锁前,必须读取主存中的最新值到工作的内存中;
3.加锁和解锁用的是同一把锁;
十四、Volatile
(Java虚拟机提供的轻量级的同步机制)
【volatile的作用:】
1.保证可见性;
2.不保证原子性;
3.由于内存屏障,禁止指令的重排;
保证可见性:
public class JMMDemo {
// private static int num = 0;
//加了volatile关键字后就不会出现死循环了(volatile的保证可见性)
private volatile static int num = 0;
public static void main(String[] args){
new Thread(()->{
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
num=1;
System.out.println(num);
}catch (Exception e){
e.printStackTrace();
}
}
}
不保证原子性:
public class JMMDemo02 {
//volatile不保证原子性
private volatile static int num = 0;
public static void add(){
num++; //不是一个原子性操作
}
public static void main(String[] args) {
//理论上num结果应该为20000
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(Thread.currentThread().getName()+""+num); //19469
}
}
通过加lock锁和synchronized关键字可以保证原子性;
除了上面这两种方式,还可以使用原子类进行操作;
eg:
##为什么Atomic类可以保证原子性?
以AtomicInteger为例。在AtomicInteger中有一个volatile[0]修饰的value变量,
也就是这个整型的值。在调用getAndIncrement()时,AtomicInteger会通过Unsafe类的
getAndAddInt方法对变量value进行一次CAS[1]操作。由于CAS是具有原子性的,
所以AtomicInteger就保证了操作的线程安全。
十五、指令重排
你写的程序,计算机并不是按照你写的那样去执行
源代码->编译器优化->指令并行可能重排->内存系统可能重排->执行
int x=1;
int y=1;
x=x+5;
y=x+x;
我们期望的是1234,但是可能是21344,1324
不可能是4123,==处理器在执行指定重排的时候,考虑数据之间的依赖性
可能造成影响的结果x,y,a,b默认是0
Volitale可以避免指令重排:
1.保证特定的操作执行顺序;
2.可以保证某些变量的内存可见性;
十六、深入理解CAS
16.1 什么是CAS
CAS即Compare And Swap,比较并交换,这是为了解决多线程环境下使用锁而造成性能损耗的一种机制,
它包含三个操作数------内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值的值相匹配,那么处理器
会自动将内存位置的值更新为新值。否则,处理器将不做任何操作;直白点说就是,我认为V这个位置应该有A,如果
有,那么就把B放到这个位置;否则,还是返回原始的值;
图示:
16.2 CAS可能存在的一些问题以及解决
16.2.1 ABA问题
CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化就更新,但是如果一个值原来是A,
变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了 —> 这就是
所谓的ABA问题。
举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,
你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,还好 ; 但是假若你是一个比较讲
卫生的人,那你肯定就不高兴了。。。
ABA问题的解决思路其实也很简单,就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本
号加1,那么A→B→A就会变成1A→2B→3A了。
详情见16.4.3
16.2.2 循环时间长开销大
自旋CAS如果一直不成功的话,会给CPU带来非常大的开销;
16.2.3 只能保证一个共享变量的原子操作
当我们对一个共享变量执行操作时,可以采用循环CAS的方式进行操作;但是如果对多个共享变量进行操作时,
循环CAS就无法保证操作的原子性了,这个时候我们就需要采用锁的概念了;
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,
合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的
原子性,就可以把多个变量放在一个对象里来进行CAS操作。
16.3 JDK中对CAS的支持 — Unsafe类
Java中提供了对CAS操作的支持:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
参数var1:表示要操作的对象
参数var2:表示要操作对象中属性地址的偏移量
参数var4:表示需要修改数据的期望的值
参数var5/var6:表示需要修改为的新值
【注意:】UnSafe类让Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。但是
这个类就如它的名字一样,是不安全的;UnSafe对象不能直接调用,只能哦通过反射获得;
16.4 JDK中的相关原子操作类简介--底层CAS机制
16.4.1 AtomicInteger
·int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
·boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
·int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
·int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
16.4.2 AtomicIntegerArray
这个类提供了更新数组中元素的原子操作,常用方法如下:
·int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
·boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,
则以原子方式将数组位置i的元素设置成update值。
----需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,
所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
16.4.3 更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变量,就需要使用这个
原子更新引用类型的类;
①AtomicReference 原子更新引用类型
②AtomicStampedReference 利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA的问题了
③AtomicMarkableReference 原子更新带有标记位的引用类型,
16.4.4 原子更新字段类
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段
更新。要想原子地更新字段类需要两步。
第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态
方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
第二步,更新类的字段(属性)必须使用public volatile修饰符。
· AtomicIntegerFieldUpdater 原子更新整型字段的更新器
· AtomicLongFieldUpdater 原子更新长整型字段的更新器
· AtomicReferenceFieldUpdater 原子更新引用类型的字段
十七、悲观锁和乐观锁
悲观锁:(总有刁民想谋害朕)
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这
样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁。性能较差!
乐观锁:(从乐观的情况出发)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。
CAS这种机制也可以将其称之为乐观锁。综合性能较好!
CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,
适用于竞争不激烈、多核 CPU 的场景下。
(1)因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
(2)但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。需要综合考虑!