大数据学习之路 JUC篇(1)
前提说明
本人是一名学生,茫茫it行业的一名卑微的小白,这是我第一次写博客。其原因是学着学着知识发现回顾的时候差不多全忘记了!!为了总结、复习自己以往学到过的有关大数据方面的知识,以便在未来回想自己当初都学过什么的时候,不会那么无助。同时也希望这些知识能够帮助那些在寻求技术之路上的朋友们。也欢迎各位大佬前来纠错指正!
正文
JUC:
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
线程的几种状态
Thread.State 枚举类查看线程的各种状态
NEW(新建);
RUNNABLE(就绪);
BLOCKED(阻塞);
WAITING(等待)一直等待,不见不散;
TIMED_WAITING(超时等待)等待一定的时候后,不再等待;
TERMINATED(终止)。
wait/sleep 都会导致线程的阻塞,有什么区别呢?
wait放开手去睡,放开手里的锁。
sleep握紧手去睡,醒了手里还有锁
多线程编程的企业级套路+模板
在高内聚低耦合的情况下, 线程 操作(对外暴露的调用方法) 资源类
- 高内聚低耦合的前提下,线程操作资源类
- 判断+干活+通知
- 防止线程的虚假唤醒,只要有wait/await需要用while判断
题目1:三个售票员 卖出 30张票
一言不合 先定义一个资源类。而 高内聚的内涵是: 将对资源类的操作封装在资源类本身中(高内聚)
class Ticket2 //资源类=实例变量+实例方法
{
private int number = 30;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t 卖出第:" + number-- + "票\t还剩下:"+number+"张票");}
}
}
main()线程中定义三个售票员进行买票
public class SaleTicketDemo01 {
public static void main(String[] args) //main线程,一切程序的入口
{
//创建资源类对象
Ticket2 ticket = new Ticket2();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 40; j++) {
ticket.sale();
}
}
}, “Thread”+i).start();
}
}
}
换成lambda表达式:
public static void main(String[] args) //main线程,一切程序的入口
{
//创建资源类对象
Ticket2 ticket = new Ticket2();
//使用lambda表达式
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();
}
}
lambda 表达式 口诀(阳哥的口诀就是爽!)
口诀:拷贝小括号;写死右箭头;落地大括号。(参数只有一个的时候可以省略类型和小括号)
lambda 适用于函数式接口(接口中只有一个未实现的抽象方法!)(@FunctionalInterface 函数式接口用此注解进行修饰)
Java8 中 函数式接口允许 有部分方法的实现(使用default 修饰词),也允许静态方法的实现(类名.调用)
@FunctionalInterface
interface Foo {
void sayHello();
default int mul(int x, int y) {
return x * y;
}
default int mul1(int x, int y) {
return x * y;
}
static float div(int a, int b) {
return a / b;
}
static float div2(int a, int b) {
return a / b;
}
}
interface Foo2 {
int add(int x, int y);
}
public class lambda {
public static void main(String[] args) {
Foo foo = new Foo() {
@Override
public void sayHello() {
System.out.println("sayHello方法");
}
};
foo.sayHello();
//转换成lambda表达式
//TODO 口诀:拷贝小括号;写死右箭头;落地大括号
Foo foo1 = () -> {
System.out.println("sayHello方法");
};
foo1.sayHello();
System.out.println(foo1.mul(5, 5));
System.out.println(Foo.div(8, 2));
Foo2 foo2 = new Foo2() {
@Override
public int add(int x, int y) {
return x + y;
}
};
System.out.println(foo2.add(5, 7));
foo2 = (int x, int y) -> {
return x + y;
};
System.out.println(foo2.add(5, 7));
}
}
JAVA四大函数式接口
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class FunctionInterDemo {
public static void main(String[] args) {
//TODO 1.消费型接口,有一个输入参数,但是没有返回值,使用accept接受参数
Consumer<String> consumer = s -> {
System.out.println(s);
};
consumer.accept("消费型接口");
//TODO 2.供给型接口,有没有输入参数,但是有返回值,使用get获取返回值
Supplier<String> supplier = () -> { return "供给型接口";};
System.out.println(supplier.get());
//TODO 3.断定型接口,有输入参数,有返回值,但是返回值类型必须是boolean,使用test获取返回值
Predicate<String> predicate = s -> {return s.isEmpty();};
System.out.println(predicate.test("断定型接口"));
//TODO 4.函数型接口,有输入参数,有返回值,使用apply获取返回值
Function<String,Integer> function = s -> {return 1024;};
System.out.println(function.apply("函数型接口"));
}
}
结果:
消费型接口
供给型接口
false
1024
资源类中使用JUC中的Lock进行加锁
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + “\t 卖出第:” + number-- + “票\t还剩下:”+number+“张票”);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}结果
A 卖出第:30票 还剩下:29张票
A 卖出第:29票 还剩下:28张票
A 卖出第:28票 还剩下:27张票
A 卖出第:27票 还剩下:26张票
A 卖出第:26票 还剩下:25张票
A 卖出第:25票 还剩下:24张票
A 卖出第:24票 还剩下:23张票
A 卖出第:23票 还剩下:22张票
A 卖出第:22票 还剩下:21张票
A 卖出第:21票 还剩下:20张票
A 卖出第:20票 还剩下:19张票
A 卖出第:19票 还剩下:18张票
A 卖出第:18票 还剩下:17张票
A 卖出第:17票 还剩下:16张票
A 卖出第:16票 还剩下:15张票
A 卖出第:15票 还剩下:14张票
A 卖出第:14票 还剩下:13张票
A 卖出第:13票 还剩下:12张票
A 卖出第:12票 还剩下:11张票
B 卖出第:11票 还剩下:10张票
B 卖出第:10票 还剩下:9张票
B 卖出第:9票 还剩下:8张票
B 卖出第:8票 还剩下:7张票
B 卖出第:7票 还剩下:6张票
B 卖出第:6票 还剩下:5张票
B 卖出第:5票 还剩下:4张票
B 卖出第:4票 还剩下:3张票
B 卖出第:3票 还剩下:2张票
B 卖出第:2票 还剩下:1张票
B 卖出第:1票 还剩下:0张票
使用synchronize(隐式锁,同步方法或者同步代码块)关键字进行 锁 ,会锁定整个方法体下的所有代码。这时我们使用更加灵活的锁 Lock(显式锁)
JUC中的Lock:ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock
ReentrantLock(可重入锁) 现实生活中的例子:小明上厕所,给门上了一把锁。同时小刚也想上厕所,这时就要等待这个锁解锁。等小明解锁 开门后,小刚也要给门上的锁加锁,出来时也要解锁。(可重入锁)ReentrantLock构造函数中提供两种锁:创建公平锁和非公平锁(默认)
ReentrantLock有三个内部类 Sync、NonfairSync和FairSync类。
Sync继承AbstractQueuedSynchronized抽象类
NonfairSync(非公平锁)继承Sync抽象类。
FairSync(公平锁)继承Sync抽象类。
公平锁,线程按照发出的请求的顺序获取锁;非公平锁,允许插队。
公平锁上,若有另外一个线程持有锁或有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。非公平锁性能高于公平锁性能的原因
在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟
假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于锁被 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此 B 会再次尝试获取这个锁。与此同时,如果线程 C 也请求这个锁,那么 C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面,B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也提高了。
当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁
公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
**ReadWriteLock(读写锁)**维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
ReadWriteLock读取操作通常不会改变共享资源,但是执行写入操作时,必须独占方式来获取锁。对于读取操作占多数的数据结构。ReadWriteLock能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变形可以完全不需要考虑加锁操作。
写写/读写 需要“互斥(不能共存)
读读 不需要互斥(可以共存)
private ReadWriteLock lock = new ReentrantReadWriteLock();
//读
public void get() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " : " + number);
} finally {
lock.readLock().unlock();
}
}
//写
public void set(int number) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName());
this.number = number;
} finally {
lock.writeLock().unlock();
}
}
举个例子:资源类MyCache中有一个HashMap,有两个方法:读get方法,写put方法。主线程中有6个写线程,6个读线程。代码如下:
没有加读写锁时代码:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
class MyCache
{
private volatile Map<String, String> map = new HashMap<>();
public void put(String key, String value)
{
System.out.println(Thread.currentThread().getName()+"写入数据");
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入数据成功!");
}
public void get(String key)
{
System.out.println(Thread.currentThread().getName()+"读取数据");
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
String s = map.get(""+key);
System.out.println(Thread.currentThread().getName()+"读取数据成功!"+s);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 定义资源类
MyCache myCache = new MyCache();
for (int i = 0; i < 6; i++) {
final int tmpi = i;
new Thread(()->{
myCache.put(""+tmpi, String.valueOf(tmpi));
}, "put"+String.valueOf(i)).start();
}
for (int i = 0; i < 6; i++) {
final int tmpi = i;
new Thread(()->{
myCache.get(""+tmpi);
}, "get"+String.valueOf(i)).start();
}
}
}
输出结果:
put0写入数据
put1写入数据
put2写入数据
put3写入数据
put4写入数据
put5写入数据
get0读取数据
get1读取数据
get2读取数据
get3读取数据
get4读取数据
get5读取数据
put0写入数据成功!
put1写入数据成功!
put2写入数据成功!
put4写入数据成功!
get0读取数据成功!0
put5写入数据成功!
put3写入数据成功!
get1读取数据成功!1
get3读取数据成功!3
get2读取数据成功!2
get5读取数据成功!5
get4读取数据成功!4
Process finished with exit code 0
0线程还没有写完成,其他写线程就开始了对这个资源类进行写操作。这样就没法保证这个map的准确性。(类似于数据库中事务的 原子性)
使用读写锁后的代码以及结果:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache {
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, String value) {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写入数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入数据成功!");
readWriteLock.writeLock().unlock();
}
public void get(String key) {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "读取数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
String s = map.get("" + key);
System.out.println(Thread.currentThread().getName() + "读取数据成功!" + s);
readWriteLock.readLock().unlock();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 定义资源类
MyCache myCache = new MyCache();
for (int i = 0; i < 6; i++) {
final int tmpi = i;
new Thread(() -> {
myCache.put("" + tmpi, String.valueOf(tmpi));
}, "put" + String.valueOf(i)).start();
}
for (int i = 0; i < 6; i++) {
final int tmpi = i;
new Thread(() -> {
myCache.get("" + tmpi);
}, "get" + String.valueOf(i)).start();
}
}
}
结果:
put0写入数据
put0写入数据成功!
put1写入数据
put1写入数据成功!
put2写入数据
put2写入数据成功!
put3写入数据
put3写入数据成功!
put4写入数据
put4写入数据成功!
put5写入数据
put5写入数据成功!
get0读取数据
get1读取数据
get2读取数据
get3读取数据
get4读取数据
get5读取数据
get0读取数据成功!0
get1读取数据成功!1
get2读取数据成功!2
get3读取数据成功!3
get4读取数据成功!4
get5读取数据成功!5
结果显示: 读读不互斥(可以同时存在);读写/写写互斥(不能同时存在)
Lock 接口中的 抽象方法
void lock(); //获取锁
void lockInterruptibly() throws InterruptedException; //如果当前线程未被中断,则获取锁
boolean tryLock(); //只有锁在空闲状态时才能获取该锁。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //锁在给定时间内空闲,且当前线程未被中断,则获取锁
void unlock(); //释放锁
Condition newCondition(); //返回绑定到此Lock实例的新 Condition 实例。
线程间的通信
题目2:现在两个线程,可以操纵初始值为0的一个变量,实现一个线程对该变量加1,一个线程对该变量减1,实现交替,来10轮,变量初始值为0;
题目3:4个线程, 两个加,两个减 ,生产一个消费一个,最终变量初始值为0
针对于题目3,如果使用if来控制wait或者await,可能会产生虚假唤醒的可能
产生虚假唤醒的原因分析
//1. 判断
if (number != 0)
{
condition.await();
}
//2. 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3. 通知
condition.signalAll();
上述代码想要实现的是 number 加1、减1 循环10轮。但是使用if来进行条件判断时,number的数值可能超过1。其原因如下图:
所以在多线程环境,如果想要防止线程的虚假唤醒,在遇到wait,await的时候,进行条件判断时,使用while循环,每次唤醒后将当前线程重新拉回后再进行标志位判断(类似于重新安检),进而防止上述线程的虚假唤醒。
完整代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源类
class Aircondition {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increament1() throws Exception {
lock.lock();
try {
//1. 判断
while (number != 0)
{
condition.await();
}
//2. 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3. 通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement1() throws Exception
{
lock.lock();
try
{
// 判断
while (number == 0)
{
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
condition.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public synchronized void increment() throws Exception {
//1 判断
// if(number >=2)
// {
// this.wait(); //等待
// }
//TODO 多个线程同一个操作时,可能导致虚假唤醒, 可通过while解决
while (number > 0) {
this.wait();
}
//2 干活
this.number++;
//干完了吆喝一声
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3 通知
this.notifyAll(); //唤醒其他所有线程(通知)
}
public synchronized void decrement() throws Exception {
//1 判断
while (number == 0) {
this.wait();
}
//2 干活
this.number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
//3 通知
this.notifyAll();
}
}
public class CommunicationBetweenThread {
public static void main(String[] args) throws Exception {
Aircondition aircondition = new Aircondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// aircondition.increment();
aircondition.increament1();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Producer1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// aircondition.increment();
aircondition.increament1();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Producer2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// aircondition.decrement();
aircondition.decrement1();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Consumer1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// aircondition.decrement();
aircondition.decrement1();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Consumer2").start();
}
}
上述代码运行结果如下:
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer1 1
Consumer1 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Producer2 1
Consumer2 0
Process finished with exit code 0
synchronize与lock(Condition)在线程通信(等待和唤醒)中,写法的区别
synchronized 与 wait() nofity() notifyAll() 配套
Lock 与Condition对象与 await() signal() signalAll()配套
为什么会出现老版本和新版本的写法(wait–await;notify–signal)?
Condition可以实现唤醒特定的线程(精确打击;精确控制:实现线程之间的按顺序调用)
condition 类似于备用钥匙,一把锁可以有多个备用钥匙。
题目:多线程之间按顺序调用, 实现 A线程->B线程->C线程。
三个线程启动,要求如下:
AA打印5次, BB打印10次,CC打印15次
接着
AA打印5次, BB打印10次,CC打印15次
来10轮
完整代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
private int number = 1; //A:1,B:2, C:3
private Lock lock = new ReentrantLock();
// 一把锁开多份备用钥匙
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print_c(int count) {
lock.lock();
switch (count) {
case 5: {
try {
//1. 判断
while (number != 1) {
c1.await();
}
//2. 干活
for (int i = 0; i < count; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3. 先修改标志位再通知
number = 2;
c2.signal(); //TODO 精确打击,只唤醒 c2
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
break;
}
case 10: {
try {
//1. 判断
while (number != 2) {
c2.await();
}
//2. 干活
for (int i = 0; i < count; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3. 先修改标志位再通知
number = 3;
c3.signal(); //TODO 精确打击,只唤醒 c3
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
break;
}
case 15: {
try {
//1. 判断
while (number != 3) {
c3.await();
}
//2. 干活
for (int i = 0; i < count; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3. 先修改标志位再通知
number = 1;
c1.signal(); //TODO 精确打击,只唤醒 c1
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
break;
}
}
}
}
public class ConditionDemo {
public static void main(String[] args) {
ShareData sd=new ShareData();
for (int i = 0; i < 10; i++) {
new Thread(()->{sd.print_c(5);}, "AA").start();
new Thread(()->{sd.print_c(10);}, "BB").start();
new Thread(()->{sd.print_c(15);}, "CC").start();
}
}
}
lock 配备了三个condition:c1,c2,c3;分别用来控制AA,BB,CC线程的执行顺序。
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
number为标志位(判断等待(await)的条件):1–AA;2–BB;3–CC
number!=1时,AA线程通过c1 condition实现等待
while (number != 1) {
c1.await();
}
number!=2时,BB线程通过c2 condition实现等待
while (number != 2) {
c2.await();
}
number!=3时,CC线程通过c3 condition实现等待
while (number != 3) {
c3.await();
}
当number=1时,AA线程打印5次,然后修改标志位number=2,通过c2 condition唤醒BB线程。
当number=2时,AA线程打印10次,然后修改标志位number=3,通过c3 condition唤醒CC线程。
当number=3时,AA线程打印15次,然后修改标志位number=1,通过c1 condition唤醒AA线程。
两轮结果如下:
AA 0
AA 1
AA 2
AA 3
AA 4
BB 0
BB 1
BB 2
BB 3
BB 4
BB 5
BB 6
BB 7
BB 8
BB 9
CC 0
CC 1
CC 2
CC 3
CC 4
CC 5
CC 6
CC 7
CC 8
CC 9
CC 10
CC 11
CC 12
CC 13
CC 14
AA 0
AA 1
AA 2
AA 3
AA 4
BB 0
BB 1
BB 2
BB 3
BB 4
BB 5
BB 6
BB 7
BB 8
BB 9
CC 0
CC 1
CC 2
CC 3
CC 4
CC 5
CC 6
CC 7
CC 8
CC 9
CC 10
CC 11
CC 12
CC 13
CC 14
多线程的 8锁现象
假设目前有一个 手机phone 资源类。这个资源类 可以 发短信 和 发邮件
第一种锁的情况:标准访问(先打印Email还是SMS)
package org.JVM_JUC;
class Phone1{
public synchronized void sendEmail(){
System.out.println("----------sendEmail----------");
}
public synchronized void sendSMS(){
System.out.println("----------sendSMS----------");
}
}
public class New8Lock {
public static void main(String[] args) {
Phone1 phone1 = new Phone1();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone1.sendEmail();
}, "A").start();
new Thread(()->{
phone1.sendSMS();
}, "B").start();
}
}
}
请问先调用哪个方法:是先发Email还是先发SMS?
答:;这个由 操作系统来控制
结果如下:
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendSMS----------
Process finished with exit code 0
为了保证能让Email先执行,SMS后执行,中间sleep一下
public class New8Lock {
public static void main(String[] args) throws InterruptedException {
Phone1 phone1 = new Phone1();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone1.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
phone1.sendSMS();
}, "B").start();
}
}
}
第二种锁的情况:sendEmail方法中sleep 4秒,这时谁先打印,谁后打印?
资源类代码
class Phone2{
public synchronized void sendEmail(){
// 暂停一会儿线程
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----------sendEmail----------");
}
public synchronized void sendSMS(){
System.out.println("----------sendSMS----------");
}
}
main方法核心代码
Phone2 phone2 = new Phone2();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone2.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
phone2.sendSMS();
}, "B").start();
}
分析:由于sleep不释放对象的锁,所以依然按顺序执行
运行结果
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
第三种锁的情况:新增一个普通方法sayHello方法,sendEmail方法依然睡眠4秒,这时先打印那个方法中的内容?
资源类代码如下:
class Phone3{
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----------sendEmail----------");
}
public synchronized void sendSMS(){
System.out.println("----------sendSMS----------");
}
public void sayHello(){
System.out.println("----------sayHello----------");
}
}
mian方法核心代码
Phone3 phone3 = new Phone3();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone3.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
phone3.sayHello();
}, "B").start();
}
资源类非同步方法先打印,睡眠的sendEmail后打印
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
----------sayHello----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
Process finished with exit code 0
第四种锁的情况:两部手机,一个线程操作一部手机发邮件,另一个线程操作另一部手机发短信,这时哪个先打印,哪个后打印?
main核心代码:
Phone1 p1 = new Phone1();
Phone1 p2 = new Phone1();
for (int i = 0; i < 10; i++) {
new Thread(()->{
p1.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
p2.sendSMS();
}, "B").start();
}
}
由于不存在资源竞争,所以按顺序执行
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
Process finished with exit code 0
第五种锁的情况:同一部手机,两个静态同步方法,请问先打印邮件还是先打印短信?
class Phone4{
public static synchronized void sendEmail(){
System.out.println("----------sendEmail----------");
}
public static synchronized void sendSMS(){
System.out.println("----------sendSMS----------");
}
}
main核心代码
Phone4 p4 = new Phone4();
for (int i = 0; i < 10; i++) {
new Thread(()->{
p4.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
p4.sendSMS();
}, "B").start();
}
静态方法加锁,锁的是整个类,由于两个线程之间有sleep,强制使得两个线程按顺序执行。
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
Process finished with exit code 0
第六种锁的情况:两部手机,两个静态同步方法,请问先打印邮件还是先打印短信?
Phone4 p4 = new Phone4();
Phone4 p41 = new Phone4();
for (int i = 0; i < 10; i++) {
new Thread(()->{
p41.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
p4.sendSMS();
}, "B").start();
}
结果:
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
----------sendEmail----------
----------sendSMS----------
Process finished with exit code 0
与第五种相同,因为静态方法加锁锁的是整个类,而不是单独的对象。
第七种锁的情况:一个普通同步方法,一个静态同步方法,一部手机,请问先打印邮件还是短信?
class Phone5{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"睡眠"+4+"秒后");
System.out.println("----------sendEmail----------");
}
public synchronized void sendSMS(){
System.out.println("----------sendSMS----------");
}
}
main核心方法:
Phone5 phone5 = new Phone5();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone5.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
phone5.sendSMS();
}, "B").start();
}
结果:
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
Process finished with exit code 0
第八种锁的情况:一个普通同步方法,一个静态同步方法,两部手机,请问先打印邮件还是短信?
Phone5 phone5 = new Phone5();
Phone5 phone51 = new Phone5();
for (int i = 0; i < 10; i++) {
new Thread(()->{
phone5.sendEmail();
}, "A").start();
Thread.sleep(100);
new Thread(()->{
phone51.sendSMS();
}, "B").start();
}
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
----------sendSMS----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
A睡眠4秒后
----------sendEmail----------
Process finished with exit code 0
八锁 结果分析
对于 第一种和第二种锁:
一个对象里面如果有多个synchronize方法,某个时刻内,只要一个线程去调用其中的一个synchronize方法了,
其他的线程都只能等待,换句话说,某个时刻内,只能有唯一一个线程去访问这些synchronize方法
锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronize方法。
对于 第三种锁:
生活中的例子: 班长要用手机发邮件, 学委只要手机壳, 所以不参加资源竞争。
添加的普通方法与同步锁无关
对于 第四种锁
换成两个对象后,不是同一把锁了,没有资源竞争
对于 第五种和第六种锁
static 锁的是全局锁,而不是对象锁(对于同一个类的不同对象,static空间 只有一份)
synchronize实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为一下3中形式:
1.对于普通的同步方法,锁是当前实例对象(对象锁)
2.对于同步方法块,锁是synchronize括号中配置的对象
3.对于静态同步方法,锁的是整个类(全局锁), 该类的所有实例对象都会受到该锁的限制
对于 第七种和第八种锁
静态同步方法与非静态同步方法之间是不会有竞争条件的(锁不一样)
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁
Java 多线程的实现(操作类有几种)?
老版本中有2种;新版本中有4种。
- 继承Thread类,实现run方法
- 实现Runnable接口,实现run方法
- 实现Callable接口,实现call方法。注意:新建Thread的时候,Thread的构造方法中没有接收Callable的。(中间商赚差价!!!)所有我们需要找到一个既可以联系Runnable接口又联系Callable接口的类(FutureTask))
- 线程池 ExecutorService(ThreadPoolExcutor类)
Callbale接口 实现多线程
class MyThread implements Runnable
{
@Override
public void run() {
System.out.println("Runnable 的 run方法");
}
}
class MyThread2 implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("Callable 的 call方法正在执行。。。。");
return 1024;
}
}
Callable接口与Runnable接口的区别:
- Callable接口的call方法有返回值;而Runnable的run方法没有返回值。
- call会抛出异常;而run方法不会抛出异常。
- 实现方法不同。一个是call方法,一个是run方法。
完整代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyThread implements Runnable
{
@Override
public void run() {
System.out.println("Runnable 的 run方法");
}
}
class MyThread2 implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("Callable 的 call方法正在执行。。。。");
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask, "CallableThread").start();
Integer result = (Integer) futureTask.get(); //获取线程的call方法的返回值
System.out.println(result);
new Thread(new MyThread(), "RunnableThread").start();
}
}
结果:
Callable 的 call方法正在执行。。。。
1024
Runnable 的 run方法
futureTask.get()方法可以获取call方法的返回值
为什么要使用Callable接口?
Callable细节
get() 方法获取线程的返回值时,一般放在最后。因为调用了get()方法,则会阻塞线程。一直等待线程计算完成后才能执行下面的后续代码。
futureTask.get() 方法放到最后的结果
class MyThread2 implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("Callable 的 call方法正在执行。。。。");
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName()+"睡了"+"4s后"+"返回数值");
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask, "CallableThread").start();
System.out.println("main线程:futureTask线程已经start");
Integer result = (Integer) futureTask.get(); //获取线程的call方法的返回值
}
}
结果:main线程会先打印"main线程:futureTask线程已经start"
main线程:futureTask线程已经start
Callable 的 call方法正在执行。。。。
CallableThread睡了4s后返回数值
Process finished with exit code 0
futureTask.get()方法放到前面的结果:
class MyThread2 implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("Callable 的 call方法正在执行。。。。");
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName()+"睡了"+"4s后"+"返回数值");
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask, "CallableThread").start();
Integer result = (Integer) futureTask.get(); //获取线程的call方法的返回值
System.out.println("main线程:futureTask线程已经start");
}
}
Callable 的 call方法正在执行。。。。
CallableThread睡了4s后返回数值
main线程:futureTask线程已经start
Process finished with exit code 0
同一个futureTask对象只能被线程调用一次,当有新的线程调用了已经被调用过的futureTask对象时,这次只会复用上次的结果。(只会执行一次)
举个例子:老师上课途中,让班长去买水。班长买水的过程中,老师继续讲课,两个线程同时进行(第一次让班长买水,合情合理)。上课途中:学委又让班长第二次买水(有点过分了,班长也要听课!),这时班长不会再次买水。(JAVA中充满着人生的哲学)
class MyThread2 implements Callable<Integer>
{
@Override
public Integer call() throws Exception {
System.out.println("Callable 的 call方法正在执行。。。。");
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName()+"睡了"+"4s后"+"返回数值");
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask, "AA").start();
new Thread(futureTask, "BB").start();
System.out.println("main线程:futureTask线程已经start");
Integer result = (Integer) futureTask.get(); //获取线程的call方法的返回值
}
}
结果:
main线程:futureTask线程已经start
Callable 的 call方法正在执行。。。。
AA睡了4s后返回数值
Process finished with exit code 0
JUC强大的辅助类
countDownLatch类
countDownLatch类:使一个线程等待其他线程各自执行完毕后再执行。
它是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
CountDownLatch 类的原理
CountDownlatch类主要有两个方法:当一个线程或多个线程调用await方法时,这些线程会被阻塞。
其他线程调用countDown方法时将计数器减一(线程不会被阻塞)
当计数器为0时,因await方法阻塞的线程会被唤醒,继续执行。
举个例子:班级里面有7名学生,包括一名班长;等待其他6名学生上完自习离开教室后,班长负责锁门。
正常不使用countDownLatch工具类时:
public class CountDownLatchDemo {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"上完自习,离开教室");
}, String.valueOf(i)).start();
}
System.out.println("班长离开教室并锁上了门");
}
}
结果:
1上完自习,离开教室
班长离开教室并锁上了门
0上完自习,离开教室
2上完自习,离开教室
3上完自习,离开教室
4上完自习,离开教室
5上完自习,离开教室
班长提前离开了教室并锁上了门,剩余的5名同学只能被困在教室中做多人运动!!
使用了CountDownLatch辅助类之后:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
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()+"上完自习,离开教室");
countDownLatch.countDown(); //每个线程执行完毕后,计数器减一
}, String.valueOf(i)).start();
}
countDownLatch.await(); //当计数器数值不为0的时候会一直的等待(阻塞),另一个线程无法执行,直至计数器为0;
System.out.println("班长离开教室并锁上了门");
}
}
结果:
1上完自习,离开教室
0上完自习,离开教室
3上完自习,离开教室
2上完自习,离开教室
4上完自习,离开教室
5上完自习,离开教室
班长离开教室并锁上了门(防止了多人运动的发生!!!)
CyclicBarrier类
一句话概括CyclicBarrier类的作用:集齐七颗龙珠—方可召唤神龙
CountDownLatch类是 等所有的线程都执行完成后再执行最后的线程。而CyclicBarrier类实现的是 只有一定量的线程启动了才执行后续的线程。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("七颗龙珠已经集齐,召唤神龙");
});
for (int i = 1; i <= 7; i++) {
final int tmpi = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"集齐"+tmpi+"星龙珠!");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "线程"+String.valueOf(i)).start();
}
}
}
结果:
线程1集齐1星龙珠!
线程2集齐2星龙珠!
线程5集齐5星龙珠!
线程7集齐7星龙珠!
线程4集齐4星龙珠!
线程3集齐3星龙珠!
线程6集齐6星龙珠!
七颗龙珠已经集齐,召唤神龙
CyclicBarrier类与CountDownLatch类的区别
CountDownLatch | CyclicBarrier |
---|---|
减计数方式 | 加计数方式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时,无法重置 | 计数达到指定值时,计数置为0重新开始 |
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 | 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 |
不可重复利用 | 可重复利用 |
现实生活中的例子:王者荣耀,LOL中,加载时,双方10人,都要等他们加载到100%的时候才能开始游戏
更具体的使用可以参考这位大神的文章:CyclicBarrier 使用详解
Semaphore类(信号量:类似于pv操作)
现实生活中的例子: 有6辆车,争抢3个车位;先抢到的停车(处理业务逻辑),没有抢到的车子等待。抢到车位的车子离开时,没有抢到的要进行下一轮的争抢
先上代码:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); //模拟资源类, 有3个车位
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire(); // 请求资源
System.out.println(Thread.currentThread().getName()+"\t抢占到车位,停车");
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t3s后离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); //释放资源
}
}, String.valueOf(i)).start();
}
}
}
结果::
1 抢占到车位,停车
0 抢占到车位,停车
2 抢占到车位,停车
0 3s后离开了车位
2 3s后离开了车位
1 3s后离开了车位
3 抢占到车位,停车
4 抢占到车位,停车
5 抢占到车位,停车
3 3s后离开了车位
4 3s后离开了车位
5 3s后离开了车位
Process finished with exit code 0
0 抢占到车位,停车
1 抢占到车位,停车
2 抢占到车位,停车
2 3s后离开了车位
3 抢占到车位,停车
1 3s后离开了车位
5 抢占到车位,停车
0 3s后离开了车位
4 抢占到车位,停车
3 3s后离开了车位
5 3s后离开了车位
4 3s后离开了车位
Process finished with exit code 0
Semaphore类的原理
acquire(获取):当一个线程调用acquire操作时,它要么通过成功,获取信号量(信号量减1);要么一直等待下去,直至有线程释放了信号量,或者超时
release(释放):实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个作用:
一个是用于对多个共享资源的互斥使用
另一个是用于并发线程数的控制。
Semaphore semaphore = new Semaphore(1);当只有一份资源时,相当于synchronized;
当如果有一个需求,多个线程争抢一份资源,抢到的线程持有这份资源20s钟。可以使用semaphore类(信号量)轻松实现。
BlockingQueue(阻塞队列)
现实生活中,类似于火锅店顾客吃饭时没有位置,需要等待。这些顾客就会被安排到BlockingQueue中。
线程1往阻塞队列里面添加元素(生产);线程2从阻塞队列中获取元素(消费);
当队列为空的时候,从队列中获取元素的操作将会被阻塞;
当队列是满的时候,往队列中添加元素的操作将会被阻塞。
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往这个空的队列中插入元素。
试图向已经满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变的空闲起来并后续新增。
阻塞队列的用处
在多线程领域:所谓阻塞,在某些情况下会 挂起 线程(即阻塞),一旦满足条件,被挂起的线程又会被自动唤醒。
为什么需要BlockingQueue,好处是我们不需要关心什么时候 需要阻塞线程,什么时候需要唤醒线程因为这一切BlockingQueue都给你一手包办了
BlockingQueue UML图
实现类 | 说明 |
---|---|
ArrayBlockingQueue | 由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列 |
SynchrousQueue | 不存储元素的阻塞队列,也即单个元素的阻塞队列 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的延迟无界阻塞队列 |
LinkedTransferQueue | 由链表组成的无界阻塞队列 |
LinkedBlockingDeque | 由链表组成的双向阻塞队列 |
BlockingQueue核心方法
抛出异常代码演示
add 代码说明:
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.add("d"));
}
}
结果:
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at org.JVM_JUC.BlockingQueue.BlockingQueueDemo.main(BlockingQueueDemo.java:15)
Process finished with exit code 1
remove 代码说明:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.add("d"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
}
结果:
true
true
true
a
b
c
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at org.JVM_JUC.BlockingQueue.BlockingQueueDemo.main(BlockingQueueDemo.java:19)
Process finished with exit code 1
element方法 返回 队首元素。队列为空的时候会抛出异常。
特殊值代码演示
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> 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(blockingQueue.offer("d"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
}
结果:
true
true
true
false
a
b
c
null
阻塞 说明(put,take)
队列满的时候,再次put,线程会一直阻塞;
队列空的时候,再次take,线程会一直阻塞。
超时 说明(offer,poll)
队列满的时候,再次offer,线程会阻塞指定的时间后,如果成功写入返回true,如果写入失败返回false;
队列空的时候,再次take时,原理同上。
大数据学习之路 JUC篇(2) 集合类的不安全性
ArrayList 线程不安全
回顾一下集合类
ArrayList集合中 底层的实现是 数组 数组的类型是Object。
数组的内存开辟空间初始值是多少:java8中是空引用:类似于懒加载, 当添加第一个元素时,初始空间是10。
当添加的元素超过初始值时, ArrayList会进行扩容, 第一次扩容的时候先扩原始值的一半(15),
然后通过Arrays.copyOf(r, i); 将数据复制到扩容的ArrayList中;第二次扩容扩容到(15 + 15/2 =22)
16:是HashMap的初始值, HashMap扩容为上一次原始值的一倍
线程不安全举例:
代码说明:
只有一个main主线程时,添加数据不会出错。
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.stream().forEach(System.out::println);
}
}
结果:
1
2
3
Process finished with exit code 0
多个线程操作ArrayList时:
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,10));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
第一次结果:
[6542b20c-0]
[6542b20c-0, d33da8c7-2]
[6542b20c-0, d33da8c7-2, 4a749f39-4]
Process finished with exit code 0
第二次结果:
[null, 727955a9-9]
[null, 727955a9-9]
[null, 727955a9-9]
第三次结果:
[ae981e3f-b, 408f08a8-a]
[ae981e3f-b, 408f08a8-a, 3d704549-0]
[ae981e3f-b, 408f08a8-a]
Process finished with exit code 0
从上述三次结果可以看出,三次输出的结果都不一致!!!
随着线程数的增加,可能会报java.util.ConcurrentModificationException
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 300; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,10));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
结果:
[bf19612a-8, d6b0e43d-3, d39f752d-3, 5ff66553-1, 8569d610-2, a022c98c-9, 43e3ea26-5, a775bc96-1, 4a9e738e-2, 94f6bcfd-6, 4bcd5e3c-7, ca3a4849-5, b1cc8c8d-e, 05a6f9de-b, 49832dbe-5, 9ae3d6cf-7, 0cba4d3b-1, 1b3966a8-f, 36ac8060-0, 3cfa92c9-4, 79733374-3, 885dad74-2, c3c07e28-0, af50c524-c, 3ad899a0-7, 9fd474fe-1, bacb4c71-7, 592184c8-3, 41becbc3-b, 31f00a15-9, a7e5e7e2-e, 61aa2479-e, aa8e8aba-c, 8db4841b-7, 5c5eff3f-4, ec2c33eb-7, 124524ae-a, 22a1a157-f, ed721b7d-8, b5aa5dfa-b, 45c9cbfd-c, e1a5babe-9, 7931f844-4, 3b039c15-7, 0bf158e0-e, 4010fd6e-3, 91564ee2-5, 7381165f-e, 4fe4ea89-c, 5a6083cc-1, da8beceb-9, 93d78246-7, 4617fcda-8, 147e1576-a, 171b554f-b, 8b5f8cd8-d, e0151427-8, e90ed3a6-8, e2015a1d-6, f5ba66b3-4, 481ca580-2, 3c4ad842-d, be7e6412-f, 1d8514d4-2, fa4e1138-3, b02b0d56-d, fae95fe2-d, a883bedc-4, aee3b444-d]
Exception in thread "10" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at org.JVM_JUC.Collection_Safe.ArrayListDemo.lambda$main$0(ArrayListDemo.java:13)
at java.lang.Thread.run(Thread.java:745)
导致原因
ArrayList 中的 boolean add(E e);没有加锁,所以多个线程争抢资源时就会发生Java.util.ConcurrentModificationException(俗称:并发修改异常!!)
解决方案
List list = new ArrayList<>(); // 效率高,不支持并发
List list = new Vector<>(); // 线程安全,但是效率低
List list = Collections.synchronizedList(new ArrayList<>()); // 线程安全,小数量完全可以
List list = new CopyOnWriteArrayList<>(); // 线程安全,JUC包下的类(写时复制),适用于多线程环境
HashSet 线程不安全
Set 与 ArrayList类似,也是线程不安全的
HashSet 底层实现是 HashMap, key=e(键值是你要添加的值,而value值是一个常量PRESENT=new Object())
解决方案
Set set = new HashSet<>(); // 效率高,但是线程不安全
Set set = Collections.synchronizedSet(new HashSet<>()); // 线程安全
Set set = new CopyOnWriteArraySet<>(); // 线程安全,JUC包下的类
同理HashMap也是线程不安全的
HashMap 默认的初始空间为16,负载因子为:0.75。当hashmap里面的元素达到了16*0.75时,进行扩容,HashMap扩容为上一次原始值的一倍。使用HashMap的时候尽量初始空间设置大一些,避免频繁的扩容。
HashMap解决线程安全问题与ArrayList和HashSet有一点不同
Map<String, String> map = new HashMap<>(); // 效率高,多线程不安全
Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>(); //多线程安全,JUC包下的类
写时复制(原理)
写时复制:(读写分离)
CopyOnWrite容器即写时复制的容器,往一个容器里面添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后新的容器为Object[] newElements,然后新的容器Object[] newElements里面添加元素,添加完元素之后,再将原容器的应用指向新的容器:setArray(newElements);
这样做的好处是可对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
写时复制的源码
public boolean add(E e)
{
final ReentrantLock lock = this.lock;
try
{
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}finally{
lock.unlock();
}
}
从源码里可以看出,每次扩容一个空间,然后将原来的数据通过 Arrays.copyOf 复制到新的容器中。在写的时候和复制的时候 加上了锁。防止了java.util.ConcurrentModificationException异常。
本质上就是 读的容器 和写的容器是分离开的,允许多个线程同时读。但写被加上了锁,同一时刻只允许一个线程写。
JUC(3)线程池(JUC的半壁江山)
线程池的优势:
线程池做的工作是要控制运行的线程数量,处理过程中将任务放入队里, 然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的主要特点:
线程复用
控制最大并发数
管理线程
- 降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配,调优和监控。
线程池的使用(ExecutorService 接口)
ExecutorService 接口 通过线程池的工具类 Executors来实例化(类似于集合的工具类:Arrays,Collections)。
线程池的架构
线程池真真正正要玩的就是ThreadPoolExecutor 类
Java通过Executors提供了四种线程池:
- newSingleThreadExecutor:单线程化线程池。创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
- newFixedThreadPool:固定线程数的线程池。创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool:周期性线程池。线程池支持定时以及周期性执行任务。
- newCachedThreadPool:可缓存线程池。用来创建一个可以无限扩大的线程池。执行很多短期的异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用他们。可扩容,遇强则强(机器性能越强悍,缓存线程池越强)
三种经典的线程池的代码示例
Executors.newFixedThreadPool()代码示例
需求:模拟银行业务窗口。一共10个用户,5个业务窗口。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService pools = Executors.newFixedThreadPool(5); //线程池有5个受理线程,类似于一个银行有5个受理窗口
try {
for (int i = 0; i < 10; i++) {
final int tmpi = i;
pools.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "号窗口给顾客" + tmpi + "办理业务");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pools.shutdown();
}
}
}
结果:
pool-1-thread-2 号窗口给顾客1办理业务
pool-1-thread-1 号窗口给顾客0办理业务
pool-1-thread-3 号窗口给顾客2办理业务
pool-1-thread-5 号窗口给顾客4办理业务
pool-1-thread-4 号窗口给顾客3办理业务
pool-1-thread-4 号窗口给顾客5办理业务
pool-1-thread-5 号窗口给顾客6办理业务
pool-1-thread-3 号窗口给顾客7办理业务
pool-1-thread-1 号窗口给顾客8办理业务
pool-1-thread-2 号窗口给顾客9办理业务
Process finished with exit code 0
从结果可以看出,newFixedThreadPool线程池中有5个线程,每个线程可以处理不同的业务。线程池中的每个线程都可以复用。(使用线程池,相对于直接单一使用线程,节省了资源申请和资源释放的响应时间。)
Executors.newSingleThreadExecutor()代码示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo {
public static void main(String[] args) {
// ExecutorService pools = Executors.newFixedThreadPool(5); //线程池有5个受理线程,类似于一个银行有5个受理窗口
ExecutorService pools = Executors.newSingleThreadExecutor(); //线程池只有单独一个线程可以处理业务
try {
for (int i = 0; i < 10; i++) {
final int tmpi = i;
pools.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "号窗口给顾客" + tmpi + "办理业务");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pools.shutdown();
}
}
}
结果:
pool-1-thread-1 号窗口给顾客0办理业务
pool-1-thread-1 号窗口给顾客1办理业务
pool-1-thread-1 号窗口给顾客2办理业务
pool-1-thread-1 号窗口给顾客3办理业务
pool-1-thread-1 号窗口给顾客4办理业务
pool-1-thread-1 号窗口给顾客5办理业务
pool-1-thread-1 号窗口给顾客6办理业务
pool-1-thread-1 号窗口给顾客7办理业务
pool-1-thread-1 号窗口给顾客8办理业务
pool-1-thread-1 号窗口给顾客9办理业务
Process finished with exit code 0
Executors.newCachedThreadPool()代码示例
代码类似于上述代码,这里直接附上结果(模拟1000个用户办理业务)
pool-1-thread-1 号窗口给顾客0办理业务
pool-1-thread-2 号窗口给顾客1办理业务
pool-1-thread-3 号窗口给顾客2办理业务
pool-1-thread-4 号窗口给顾客3办理业务
pool-1-thread-5 号窗口给顾客4办理业务
pool-1-thread-6 号窗口给顾客5办理业务
pool-1-thread-7 号窗口给顾客6办理业务
pool-1-thread-8 号窗口给顾客7办理业务
pool-1-thread-9 号窗口给顾客8办理业务
pool-1-thread-10 号窗口给顾客9办理业务
pool-1-thread-11 号窗口给顾客10办理业务
pool-1-thread-12 号窗口给顾客11办理业务
pool-1-thread-13 号窗口给顾客12办理业务
pool-1-thread-14 号窗口给顾客13办理业务
pool-1-thread-15 号窗口给顾客14办理业务
pool-1-thread-16 号窗口给顾客15办理业务
pool-1-thread-17 号窗口给顾客16办理业务
pool-1-thread-18 号窗口给顾客17办理业务
pool-1-thread-19 号窗口给顾客18办理业务
pool-1-thread-20 号窗口给顾客19办理业务
pool-1-thread-21 号窗口给顾客20办理业务
pool-1-thread-22 号窗口给顾客21办理业务
pool-1-thread-23 号窗口给顾客22办理业务
pool-1-thread-24 号窗口给顾客23办理业务
pool-1-thread-25 号窗口给顾客24办理业务
pool-1-thread-26 号窗口给顾客25办理业务
pool-1-thread-27 号窗口给顾客26办理业务
pool-1-thread-227 号窗口给顾客226办理业务
pool-1-thread-228 号窗口给顾客227办理业务
pool-1-thread-226 号窗口给顾客225办理业务
pool-1-thread-225 号窗口给顾客224办理业务
pool-1-thread-224 号窗口给顾客223办理业务
pool-1-thread-223 号窗口给顾客222办理业务
pool-1-thread-222 号窗口给顾客221办理业务
pool-1-thread-6 号窗口给顾客305办理业务
pool-1-thread-221 号窗口给顾客220办理业务
pool-1-thread-220 号窗口给顾客219办理业务
pool-1-thread-219 号窗口给顾客218办理业务
pool-1-thread-218 号窗口给顾客217办理业务
pool-1-thread-217 号窗口给顾客216办理业务
pool-1-thread-216 号窗口给顾客215办理业务
pool-1-thread-215 号窗口给顾客214办理业务
pool-1-thread-214 号窗口给顾客213办理业务
pool-1-thread-5 号窗口给顾客354办理业务
从上述结果可以看出,已经创建的线程可以被复用(而不是一直创建新线程)
newScheduledThreadPool:可以 延时启动 、定时启动 、可以自定义最大线程池数量
关于newScheduledThreadPool线程池的使用,可以参考java线程池之newScheduledThreadPool
线程池(ThreadPoolExecutor源码)本质
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从源码上可以看出,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()和Executors.newCachedThreadPool()这三个Executors提供的线程池实际上就是一个线程池:ThreadPoolExecutor类
线程池(ThreadPoolExecutor)的七大参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
//7大参数
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池七大参数说明
- corePoolSize :线程池中的常驻核心线程数。(比如银行窗口,最多有5个窗口可办理业务,但是由于今天客流量少,只开2个窗口,所以今天的常驻核心线程数就是2)
- maximumPoolSize :线程池中能够容纳同时执行的最大线程数,这个值必须大于等于1。(常驻线程数为2工作一段时间,突然客流量剧增,这时就需要 扩容,maximumPoolSize 表示最多有几个窗口)
- keepAliveTime:多余的空闲线程的存活时间。当前线程池中线程数量超过corePoolSize 时,当空闲时间达到,对于线程会被销毁直到只剩下corePoolSize个线程为止。(当前线程池中的线程数多于常驻线程数,但是客流量较少,常驻线程数就可以完成,当多于的线程空闲时间达到这个值的时候就会被销毁,这时就需要 缩容)
- unit:keepAliveTime的单位
- workQueue:队伍队列,被提交但尚未被执行的任务(类似于银行的候客区:用户需要排队等待,等轮到时候,窗口会提示:请 5号顾客到1号窗口办理业务)
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程。一般默认的即可
- handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒接请求执行的Runnable的策略。(候客区满了,即阻塞队列满了。银行就会通知:目前该网点正处于业务高峰期,没有进入候客区的顾客需要至少等待2个小时,请您到其他网点办理业务)
线程池的底层工作原理
下面这个图是结合现实生活中,银行办理业务,来通俗的说明线程池的原理
线程池的工作流程
流程文字版说明.特别重要、特别重要、特别重要
- 在创建了线程池后,开始等待请求。
- 当调用execute() 方法添加一个请求任务时,线程池会做出如下判断
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务。
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务 放入阻塞队列
2.3 如果这个时候阻塞队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会 启动饱和拒绝策略来执行 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize的大小。
线程池用哪个?生产中如何设置合理参数?
线程池的拒接策略
线程池的拒绝策略是什么?
等待队列已经排满了,再也塞不下新任务了。同时,线程池中的max线程也达到了,无法继续为新任务服务。这个时候我们就需要 拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略?(4种拒接策略)
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。(回退,谁让你找我的,你回去找谁)
public class UDFThreadPool {
public static void main(String[] args) {
ExecutorService threadpool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
for (int i = 0; i < 10; i++) {
final int tmpi = i;
threadpool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "号窗口给顾客" + tmpi + "办理业务");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadpool.shutdown();
}
}
}
结果:
pool-1-thread-1 号窗口给顾客0办理业务
main 号窗口给顾客8办理业务
pool-1-thread-2 号窗口给顾客1办理业务
pool-1-thread-4 号窗口给顾客6办理业务
pool-1-thread-3 号窗口给顾客5办理业务
pool-1-thread-5 号窗口给顾客7办理业务
main 号窗口给顾客9办理业务
pool-1-thread-2 号窗口给顾客2办理业务
pool-1-thread-1 号窗口给顾客3办理业务
pool-1-thread-3 号窗口给顾客4办理业务
Process finished with exit code 0
CallerRunsPolicy这种策略当队列满了而且线程数已经达到了最大的maximumPoolSize,这是,会把这个任务返还给任务提交者那个线程进行处理。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
以上内置拒绝策略均实现了RejectedExecutionHandle接口
在工作中 单一的/固定数的/可变的三种创建线程池的方法哪个用的多??(超级大坑)
答:一个都不用,在工作中只能使用自定义的线程池
Executors中JDK已经给你提供了,为什么不用呢?
阿里巴巴java开发手册中规定:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2)CacheThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
在工作中如何使用线程池,是否自定义过线程池?
public class UDFThreadPool {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadpool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors()+1,
2L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
for (int i = 0; i < 10; i++) {
final int tmpi = i;
threadpool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "号窗口给顾客" + tmpi + "办理业务");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadpool.shutdown();
}
}
}
JAVA流式计算(不是IO流哦!!!切记)
流(Stream)到底是什么呢?
它是数据渠道,用于操作数据源(集合,数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算”
流的特点
- Stream自己不会存储元素
- Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。(有点类似于Spark中的RDD)
怎么用?
阶段
- 创建一个Stream:一个 数据源 (数组,集合)
- 中间操作:一个中间操作,处理 数据源数据。
- 终止操作:一个终止操作,执行中间操作链,产生结果。
源头=>中间流水线=>结果
使用java流式计算可以方便的实现对一些sql的实现。
题目:一个User类,有 id name,age三个字段。目前有5个对象,要求,选出id为偶数的,且age大于等于24的对象,并将name变成大写,最后输出排序最前面的那个 name。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
class User
{
private int id;
private String name;
private int age;
}
public class StreamDemo {
// 流式计算
public static void main(String[] args) {
/**
* 需求:选出id为偶数的,年龄大于等于24的 且将选择出来的用户的名字变成大写,最后返回最大的那个名字
*/
// TODO 1. 首先创建数据源
User user1 = new User(11,"a",22);
User user2 = new User(12,"b",23);
User user3 = new User(13,"c",24);
User user4 = new User(14,"d",28);
User user5 = new User(16,"e",26);
// 需求
List<User> list = Arrays.asList(user1,user2,user3,user4,user5);
list.stream().filter(user -> {return user.getId()%2==0;}).filter(user -> {return user.getAge()>=24;}).map(user -> {return user.getName().toUpperCase();}).sorted((o1, o2) -> {return o2.compareTo(o1);}).limit(1).forEach(System.out::println);
}
}
结果:
E
Process finished with exit code 0
分支合并框架 ForkJoin(大事化小 小事化了)
ForkJoinPool
既然任务是被逐渐的化小,那就需要把这些任务存在一个池子里面,这个池子就是ForkJoinPool,它与其它的ExecutorService区别主要在于它使用“工作窃取“,那什么是工作窃取呢?
工作窃取:一个大任务会被划分成无数个小任务,这些任务被分配到不同的队列,这些队列有些干活干的块,有些干得慢。于是干得快的,一看自己没任务需要执行了,就去隔壁的队列里面拿去任务执行。
ForkJoinTask(类似于futureTask,这个抽象类实现了Future接口,可以get() 返回值)
ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:RecursiveAction和RecursiveTask。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果,
RecursiveTask(public abstract class RecursiveTask extends ForkJoinTask)(自己调用自己,类似于分治法)**
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
class MyTask extends RecursiveTask<Integer>
{
public static final Integer ADJUST_VALUE = 10;
private int begin;
private int end;
private int result;
public MyTask(int begin, int end) {
this.begin=begin;
this.end=end;
}
@Override
protected Integer compute() {
if((end-begin)<=ADJUST_VALUE)
{
for (int i = begin; i < end; i++) {
result = result+i;
}
}else
{
int middle = (begin+end)/2;
MyTask myTask1 = new MyTask(begin, middle);
MyTask myTask2 = new MyTask(middle+1, end);
myTask1.fork(); // 自动调用compute方法
myTask2.fork(); //自动调用compute方法
// join 获取返回值
result = myTask1.join()+myTask2.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//TODO 线程操作资源类
MyTask myTask = new MyTask(0, 1000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> joinTask = forkJoinPool.submit(myTask);
System.out.println(joinTask.get());
forkJoinPool.shutdown();
}
}
结果:
435927
Process finished with exit code 0
JAVA中的异步回调(CompletableFuture)
这里我只写了一个Demo,关于CompletableFuture类的详细介绍,可以参考有了CompletableFuture,使得异步编程没那么难了
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// CompletableFuture.runAsync(); runAsync:异步回调,没有返回值
// CompletableFuture.supplyAsync(); supplyAsync:异步回调,有返回值
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t没有返回值");
});
future1.get();
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
return Thread.currentThread().getName();
});
System.out.println(future2.whenCompleteAsync((s, throwable) -> {
System.out.println("********s:" + s);
System.out.println("********throwable:" + throwable);
}).exceptionally(throwable -> {
return throwable.getMessage();
}).get());
}
}
结果:
ForkJoinPool.commonPool-worker-1 没有返回值
********s:ForkJoinPool.commonPool-worker-1
********throwable:null
ForkJoinPool.commonPool-worker-1
Process finished with exit code 0
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// CompletableFuture.runAsync(); runAsync:异步回调,没有返回值
// CompletableFuture.supplyAsync(); supplyAsync:异步回调,有返回值
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t没有返回值");
});
future1.get();
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
int a = 10/0;
return Thread.currentThread().getName();
});
System.out.println(future2.whenCompleteAsync((s, throwable) -> {
System.out.println("********s:" + s);
System.out.println("********throwable:" + throwable);
}).exceptionally(throwable -> {
return throwable.getMessage();
}).get());
}
}
结果
ForkJoinPool.commonPool-worker-1 没有返回值
********s:null
********throwable:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
Process finished with exit code 0