Java 无法开启线程,start() 方法调用本地方法 start0() 开启线程,底层是 C++,java 无法直接操作硬件。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
并行与并发
并发(多线程操作同一个资源)
并行(多个人一起行走)
// 获取 CPU 核数
Runtime.getRuntime().availableProcessors()
线程的几种状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死的等
WAITING,
// 超时等待,过期不候
TIMED_WAITING,
// 终止
TERMINATED;
}
wait / sleep 区别
同步方法
public class Test {
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();
}
}
/**
* 资源类
*/
class Ticket {
private Integer num = 30;
public synchronized void sale() {
if (num <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "卖出了第: " + (num--) + "票,剩余: " + num);
}
}
同步代码块
class Drawing implements Runnable {
/**
* 账户
*/
private Account account;
/**
* 取出的钱
*/
private int drawingMoney;
/**
* 手上的钱
*/
private int nowMoney;
public Drawing(Account account, int drawingMoney) {
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
synchronized (account) {
if (account.money < drawingMoney) {
System.out.println("银行卡 " + account.cardName + " 余额不足,无法取款");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingMoney;
nowMoney += drawingMoney;
System.out.println(Thread.currentThread().getName() + " 从银行卡 " + account.cardName + " 取款 " + drawingMoney);
System.out.println(Thread.currentThread().getName() + " 身上的 money 为: " + nowMoney);
System.out.println("银行卡" + account.cardName + "余额为: " + account.money);
}
}
}
下面让我们看看
synchronized
的具体底层实现,以上面的Drawing
为例,切换到Drawing.class
所在目录,使用javap -v [class文件名]
查看字节码文件,如下:
注意上面使用橙色方框框住的部分,这也是添
synchronized
关键字之后独有的。执行同步代码块后,首先要执行的monitorenter
指令,退出的时候执行monitorexit
指令。使用synchronized
进行同步,其关键就是必须要获取对象监视器monitor
,当线程获取monitor
后才能继续往下执行,否则就只能等待。这个获取过程是互斥的,即:同一时刻只有一个线程能够获取到monitor
。synchronized
是可重入锁,即:在同一锁程中,线程不需要再次获取同一把锁。简单来说就是,在调用一个同步方法时,这个同步方法又调用了另一个同步方法,且两个同步方法是同一把锁,那么当线程获取到第一个同步方法的锁后,调用第二个同步方法时由于是同一把锁,将不再需要再获取一次锁。另外注意:
每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法
对象,对象监视器、同步队列以及执行线程之间关系如下:
由图可知:任意线程对 Object 的访问,首先要获取 Object 的监视器;如果获取失败,该线程就会进入同步队列,线程变为 阻塞(BLOCKED) 状态;如果获取成功,则执行,线程执行完成后,释放 Object 监视器;在同步队列中的线程就会有机会重新获取该监视器。
公平锁:十分公平,先来后到
非公平锁:十分不公平,可以插队(默认)
public class ReentrantLockTest {
public static void main(String[] args) {
Ticket2 ticket2 = new Ticket2();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket2.sale();
}
}, "A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket2.sale();
}
}, "B").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket2.sale();
}
}, "C").start();
}
}
/**
* Lock 三步曲
* 1、Lock lock = new ReentrantLock(); 创建锁
* 2、lock.lock(); 加锁
* 3、finally => lock.unlock(); 释放锁
*/
class Ticket2 {
private Integer num = 30;
private Lock lock = new ReentrantLock();
public void sale() {
//加锁
lock.lock();
try {
if (num <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "卖出了第: " + (num--) + "票,剩余: " + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
}
- Synchronized 是内置的 Java 关键字,Lock 是一个 Java 类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,Lock 必须手动释放锁,不释放锁会造成死锁
- Synchronized 若是线程 A 获取锁后进入阻塞状态,线程 B 会一直傻傻的等待线程 A;Lock 锁就不一定会等待下去
- Synchronized 是可重入、不可以中断的、非公平的锁;Lock 可重入、可以判断锁、非公平(默认,可以自己设置)的锁
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码。
生产者和消费者问题
Synchronized
版
public class SynchronizedTest {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
/**
* 判断等待,业务,通知
*/
class Data {
private int num = 0;
/**
* +1
*/
public synchronized void increment() throws InterruptedException {
if (num != 0) {
// 等待
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 +1 完成
this.notifyAll();
}
/**
* -1
*/
public synchronized void decrement() throws InterruptedException {
if (num == 0) {
// 等待
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 -1 完成
this.notifyAll();
}
}
运行以上代码,看结果貌似没什么问题,打印的信息也正常。
这时我们在
main
方法中再启动两个线程,修改如下:
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "D").start();
启动方法,查看打印结果:可以看到出现了预期之外的结果。
这时就产生了,
虚假唤醒
的问题;这时我们需要将this.wait();
的条件判断用while
来替换if
即可解决这个问题,修改如下:
public synchronized void increment() throws InterruptedException {
while (num != 0) {
// 等待
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 +1 完成
this.notifyAll();
}
/**
* -1
*/
public synchronized void decrement() throws InterruptedException {
while (num == 0) {
// 等待
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 -1 完成
this.notifyAll();
}
重新运行 main 方法,查看控制台打印结果:可以看到,打印的结果恢复正常。
个人总结:用
if
判断的话,唤醒后线程会从wait
之后的代码开始运行,但是不会重新判断if
条件,直接继续运行if
代码块之后的代码,而如果使用while
的话,也会从wait
之后的代码运行,但由于使用的是while
,所以唤醒后会重新判断循环条件,如果不成立再执行while
代码块之后的代码块,成立的话继续wait
。结合案例来说:拿两个线程
A
、C
来说,比如A
先执行,执行时调用了wait
方法,那它会进入等待,同时会释放锁;接着C
获得锁并且也执行wait
方法,两个执行加操作
的线程一起进入了等待状态,等待被唤醒。此时减操作
的线程中某一个线程执行完毕,并且唤醒了这两个加线程
,那么这两个加线程
就会执行(不会一起执行)。其中A
获取了锁并加1,执行完毕后C
再执行;如果是if
的话,那么A
修改完num
后,B
不会再去判断num
的值,直接会给 num+1。如果是while
的话,A
执行完之后,B
还会去判断num
的值,由于条件不满足B
不会执行。注意:
this.wait();
需要放在while
中,防止虚假唤醒
问题。
我们可以使用
Lock
替代Synchronized
,使用 Condition 中的await 与 signal
方法替代wait 与 notify
方法。
以上为 JDK1.8 中关于 Condition 接口的介绍,结合 Lock 的官方使用案例如下:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock(); }
}
}
代码实现:
public class LockTest {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "JUC-A").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "JUC-B").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "JUC-C").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "JUC-D").start();
}
}
/**
* 判断等待,业务,通知
*/
class Data2 {
private int num = 0;
/**
* 创建 Lock 锁
*/
private Lock lock = new ReentrantLock();
/**
* 创建 Condition 对象
*/
private Condition condition = lock.newCondition();
/**
* +1
*/
public void increment() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
// 等待
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 +1 完成
condition.signalAll();
} catch (InterruptedException e) {
throw new InterruptedException();
} finally {
lock.unlock();
}
}
/**
* -1
*/
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
// 等待
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "==>" + num);
// 通知其它线程 -1 完成
condition.signalAll();
} catch (InterruptedException e) {
throw new InterruptedException();
} finally {
lock.unlock();
}
}
}
运行以上案例,可以看到控制台输出的结果正常:
但是 Condition 的功能只有如此了吗? **任何一个新的技术,绝对不是仅仅只是覆盖了原有的技术,它具有相应的优势与补充。**以上的虽然能够正常运行,但是可以看到线程的调用完全是没有顺序的,如:AB、AD、CB …,如果我们想要线程按照我们指定的顺序有序的执行,那么需要怎么做呢?下面我们将介绍使用 Condition 实现精准通知唤醒。
public class ConditionTest {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.executeA();
}
}, "ConditionA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.executeB();
}
}, "ConditionB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.executeC();
}
}, "ConditionC").start();
}
}
class Data3 {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
/*
1=> 线程A,2=> 线程B,3=> 线程C
*/
private int flag = 1;
public void executeA() {
lock.lock();
try {
while (flag != 1) {
conditionA.await();
}
flag = 2;
System.out.println(Thread.currentThread().getName() + "====>AAAAAAAA");
conditionB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void executeB() {
lock.lock();
try {
while (flag != 2) {
conditionB.await();
}
flag = 3;
System.out.println(Thread.currentThread().getName() + "===>BBBBBBBB");
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void executeC() {
lock.lock();
try {
while (flag != 3) {
conditionC.await();
}
flag = 1;
System.out.println(Thread.currentThread().getName() + "===>CCCCCCCCC");
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
以上指定的线程执行顺序是:线程A ==> 线程B ==> 线程C,启动 main 方法,可以看到控制台打印的信息:
8锁,就是关于锁的 8 个问题,首先创建一个基础的演示模块;接下来我们将逐个演示 8 锁问题。
public class Lock8Test {
public static void main(String[] args) {
}
}
class Phone {
/**
* synchronized 方法锁的是方法的调用者(对象)
*/
public synchronized void sendSms() {
System.out.println(Thread.currentThread().getName() + "===>发送短信");
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + "===>打电话");
}
}
发送短信
还是 打电话
?修改 main 方法,实例化一个 Phone 对象,并创建两个线程,分别调用两个同步方法,两个线程中间添加一个延时。
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}, "LOCK8-A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}, "LOCK8-B").start();
启动 main 方法运行程序,从打印结果可以看到,先打印的是
发送短信
;无论我们运行多少次,先打印的都是发送短信
。
解释:之所以每次都是先打印
发送短信
,是因为sendSms()
与call()
都是synchronized
标注的同步方法,锁的是方法的调用者(即 phone 实例对象),两个方法都是用的同一个锁。由于sendSms()
方法所在的线程先启动,并且中间做了延时保证sendSms()
方法先获得锁,所以sendSms()
方法先执行,先打印出发送短信
。
发送短信
还是 打电话
?修改
sendSms()
方法,添加延时处理,具体修改如下:
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>发送短信");
}
重新执行
main
方法,可以看到:即便是我们在sendSms()
方法中,添加了延时 4 秒的处理,先打印出来的依然是发送短信
。
解释:原因与问题一基本一致,由于
sendSms()
方法先获得锁,所以sendSms()
方法先执行;必须要等sendSms()
方法执行完成后释放锁,call()
方法才能获取锁并执行,所以发送短信
先打印。
发送短信
还是 hello
?修改 Phone 类,增加一个普通方法 hello(),具体如下:
/**
* 这里没有锁,不是同步方法,不受锁的影响
*/
public void hello() {
System.out.println(Thread.currentThread().getName() + "===>hello");
}
修改 main 方法
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}, "LOCK8-A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.hello();
}, "LOCK8-C").start();
运行
main
方法,可以看到:延时了 1 秒,先打印的是hello
消息。
解释:这是由于
hello()
方法是普通方法不是同步方法,不受锁的约束,可以与同步方法sendSms()
同时执行,且由于sendSms()
方法做了 4 秒的延时,执行时间远大于hello()
方法,所以先打印出来的是hello
消息。
发送短信
还是 打电话
?修改 main 方法,创建两个 Phone 对象与两个线程,并且分别调用
sendSms()
与call()
方法:
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendSms();
}, "LOCK8-A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
}, "LOCK8-D").start();
运行
main
方法,查看打印结果,可以看到先打印的是打电话
。
解释:前面也介绍了,同步方法锁的是方法的调用者,即 Phone 的实例对象;由于
phone
与phone2
是两个实例对象,所以这里有两把锁,phone.sendSms()
获得的是phone
的锁,phone2.call()
获得的是phone2
的锁;由于是不同的锁,所以两个方法可以同时执行,而由于call()
方法执行时间短,所以先打印打电话
。
发送短信
还是 打电话
?修改 Phone 类,新增两个静态同步方法:
public static synchronized void sendSmsStatic() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===>发送短信 static");
}
public static synchronized void callStatic() {
System.out.println(Thread.currentThread().getName() + "===>打电话 static");
}
修改
main
方法,创建两个线程执行这两个方法,并查看打印结果:先打印发送短信
注意:这里为了演示同一个对象,使用的是实例对象调用的静态方法,实际开发中还是以 类名.静态方法 的方式调用静态方法
Phone phone = new Phone();
new Thread(()->{
phone.sendSmsStatic();
}, "LOCK8-E").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.callStatic();
}, "LOCK8-F").start();
解释:由于
sendSmsStatic()
与callStatic()
是静态同步方法,而静态同步方法锁的是 Class,一个类只有一个 class 对象,即 Phone.class 模板;sendSmsStatic()
与callStatic()
锁的都是 Phone.class 对象,所以在执行方法时,由于sendSmsStatic()
先执行,先获得锁,所以必须等sendSmsStatic()
执行完毕释放锁,才能执行callStatic()
方法,所以先打印发送短信
。
发送短信
还是 打电话
?静态同步方法就用之前的,修改
main
方法并执行,执行结果:先打印发送短信
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendSmsStatic();
}, "LOCK8-E").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.callStatic();
}, "LOCK8-G").start();
解释:可以看到,即便是用了不同的实例对象调用
sendSmsStatic()
与callStatic()
,先打印的还是发送短信
;这是因为静态同步方法锁的是 Class 对象,而不是实例对象,一个类只有一个 Class 对象,所以先得到锁的先执行,与实例对象无关。
发送短信
还是 打电话
?修改 main 方法,具体修改如下:
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}, "LOCK8-A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.callStatic();
}, "LOCK8-F").start();
执行
main
方法,可以看到执行结果:先打印打电话
解释:
sendSms()
是同步方法,锁的是调用方法的实例对象,即phone
;callStatic()
方法是静态同步方法,锁的是 Class 对象,即 Phone.class;两个方法所需的是不同的锁,可以同时执行,而由于callStatic()
方法执行的时间短,所以先打印打电话
。
问题八:一个静态的同步方法,一个普通同步方法,两个对象,先打印 发送短信
还是 打电话
?
修改
main
方法并执行,查看执行结果:先打印打电话
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}, "LOCK8-A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.callStatic();
}, "LOCK8-G").start();
解释:原因与
问题七
基本相同,由于两个方法锁的对象不是同一个对象,所以执行时间短的先打印。
普通同步方法
锁的是调用方法的类的实例对象
,静态同步方法
锁的是类的 Class 对象
,一个类只有一个 Class 对象。若方法所需要的锁不是同一个锁,则可以同时执行;若是同一个锁,则需要等上一个方法执行并释放锁,才能执行下一个方法。类的实例对象的锁可以有多个,类的 Class 对象的锁只有一个。实例对象的锁 与 Class 对象的锁可以同时使用。
并发下 ArrayList 是不安全的
public class ListTest {
public static void main(String[] args) {
/**
* 并发下 ArrayList 是不安全的 ConcurrentModificationException
* 解决方案:
* 1、List list = new Vector<>();
* 2、List list = Collections.synchronizedList(new ArrayList<>());
* 3、List list = new CopyOnWriteArrayList<>();
* CopyOnWrite 写入时复制,是计算机程序设计领域的一种优化策略;在写入的时候避免覆盖,造成数据问题。
*/
List<String> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
运行以上案例,可以看到控制台打印了
ConcurrentModificationException
并发修改异常;显然,在并发情况下,ArrayList 并不是线程安全的。解决办法:
- 使用 Vector 替代 ArrayList
- 使用 Collections.synchronizedList() 方法将 ArrayList 转换成线程安全的
- 使用 CopyOnWriteArrayList 替换 ArrayList
个人建议是使用
CopyOnWriteArrayList
因为它的效率更高;下面我们将对这三种方式进入分析:
一、
Vector
,通过源码我们可以看到Vector
是通过synchronized
方法来实现的线程安全。
二、
Collections.synchronizedList(new ArrayList<>())
,通过源码我们可以看到,Collections
是通过synchronized
同步代码块 来保证线程安全。
三、
CopyOnWriteArrayList
,通过源码我们可以看到,CopyOnWriteArrayList
是通过 Lock 锁,以及 CopyOnWrite(写入时复制)来保证线程安全与数据安全。
总结:对比以上三种实现,由于执行效率
Lock 锁 > 同步代码块 > 同步方法
,所以我们选择用CopyOnWriteArrayList
来解决线程安全问题。
与前面介绍的 List 一样,Set 也存在线程安全问题,同样也可通过
Collections
与CopyOnWriteArraySet
来解决线程安全问题,由于原理差不多,这里就不做过多介绍了。
public class SetTest {
public static void main(String[] args) {
/**
* 1、Set set = Collections.synchronizedSet(new HashSet<>());
* 2、Set set = new CopyOnWriteArraySet<>();
* HashSet 本质是 HashMap 的键
*/
Set<String> set = new HashSet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 7));
System.out.println(set);
}).start();
}
}
}
以上个人建议使用
CopyOnWriteArraySet
值得注意的是,HashSet 的本质是 HashMap 的键的集合。
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
同样 Map 也存在线程安全问题
public class MapTest {
public static void main(String[] args) {
/**
* 1、Collections.synchronizedMap(new HashMap<>());
* 2、 Map map = new ConcurrentHashMap<>();
*/
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 7));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
解决方法:
- 使用 Collections.synchronizedMap() 方法将 HashMap 转换成线程安全的
- 使用 ConcurrentHashMap 替换 HashMap
通过查看 JDK1.8 的文档,我们可以发现 Callable 有以下特点:Callable 有返回值、Callable 可以抛出异常、Callable 运行的是 call() 方法;
那么我们如何使用 Callable 创建一个线程呢?
首先我们介绍下 Thread、Runnable、FutureTask、Callable 之间的关系,如下图:
一、通过查看 JDK 文档我们可以发现,
Thread
可以使用Runnable
对象作为构造方法的参数来创建一个线程。
二、同样我们通过 JDK 文档可以查看到
Runnable
接口有一个FutureTask
实现类。
三、同样通过 JDK 文档我们可以看到
FutureTask
可以使用Callable
作为构造参数构造一个FutureTask
对象。
综合以上:我们可以把
Callable
作为构造参数传入FutureTask
创建一个FutureTask
实例对象;而FutureTask
又是Runnable
接口的实现,所以我们可以将FutureTask
实例对象作为构造参数传入Thread
创建一个线程。编码如下:
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* new Thread(new Runnable()).start();
* new Thread(new FutureTask()).start();
* new Thread(new FutureTask(new Callable())).start();
*/
FutureTask<String> futureTask = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(5);
System.out.println("自定义 Callable 实现");
return "Test";
});
new Thread(futureTask, "A").start();
// get() 方法可能会产生阻塞(在 call() 方法执行比较耗时时会阻塞,等待返回结果)
String result = futureTask.get();
System.out.println(result);
// 结果会被缓存,效率高
new Thread(futureTask, "B").start();
System.out.println("----------");
}
}
运行以上代码,查看控制台打印信息:
可以发现两个细节:
结果会被缓存,效率高:通过运行程序我们可以看到,启动了两个线程,但是只打印出来一条提示信息;
获取返回值的 get() 方法可能会阻塞等待:通过运行程序我们可以看到,在通过 get() 方法获取 Callable 返回值时,主线程进入阻塞等待状态;在线程 A 执行完成返回结果后,主线程才继续执行 输出打印信息。为了验证这个,我们可以注释掉获取并输出返回值的代码,再运行程序:
// String result = futureTask.get();
// System.out.println(result);
可以看到,控制台先打印
------
,即证明:这时主线程并不会阻塞等待线程 A 的执行结果。
倒计时、减法计数器;
使用示例:
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() + " 离开");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("学生全部离开,关门");
}
}
以上示例中:一个教室中共有 6 个学生,当 6 个学生都离开教室后,执行关门操作;
原理:
countDownLatch.countDown();
// 数量 -1
countDownLatch.await();
// 等待计数器归零,然后再向下执行每次有线程调用
countDown()
数量 -1,当计数器变为 0 ,countDownLatch.await()
就会被唤醒,继续执行。
加法计数器
使用示例:
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " 收集了 " + temp + "颗龙珠");
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "akieay-" + i).start();
}
}
}
运行程序,打印结果如下:
以上示例中:我们创建了一个数值为 7 的加法计数器,当数值增加到 7 的时候,执行我们设置的
CyclicBarrier
的第二个构造参数中的任务;当任务执行完成后,会唤醒处于等待状态中的 7 个线程,并继续往下执行;每执行一次cyclicBarrier.await()
加法计数器自增 1。
信号量计数器
使用示例:
public class SemaphoreTest {
public static void main(String[] args) {
// 线程数量:停车位、限流
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(4);
System.out.println(Thread.currentThread().getName() + " 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
}, "akieay" + i).start();
}
}
}
运行程序,结果如下:
以上示例中,限制信号量为 3,即当前面三个线程获取到 信号量时,后续的线程必须等待;等待前面的线程执行完成后释放信号量,后续的线程才能获信号量并继续执行。
原理:
semaphore.acquire()
,获得,假如已经满了,则等待,等待被释放为止。
semaphore.release()
释放,会将当前信号量释放 + 1,然后唤醒等待的线程。使用场景:多个共享资源互斥时使用、并发限流,控制最大的线程数等。
读的时候,允许多个线程同时读;写的时候,只允许一个线程同时写。
以下是一个没加锁的示例:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 写操作
for (int i = 1; i <= 6; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp + "", "akieay" + temp);
}, String.valueOf(i)).start();
}
//读操作
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
Object o = myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
/**
* 存储,写
* @param key
* @param value
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + " 写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入Ok");
}
/**
* 读取,读
* @param key
* @return
*/
public Object get(String key) {
System.out.println(Thread.currentThread().getName() + " 读取" + key);
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取Ok");
return value;
}
}
运行程序,查看控制台打印的信息:可以看到写入操作是无序的,存在着多个线程同时写入的情况,还存在着读写同时进行的情况;这种问题再实际开发中是必须避免的,下面我们会使用读写锁解决以上问题。
新建一个添加了读写锁的缓存,并将 main 方法中的 MyCache 对象修改为 MyCacheLock 对象:
class MyCacheLock {
private volatile Map<String, Object> map = new HashMap<>();
/**
* 读写锁:更加细粒度的控制
*/
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 存储,写入的时候,只希望同时只有一个线程写
* @param key
* @param value
*/
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入Ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
/**
* 读取,读取的时候,所有人都可以读
* @param key
* @return
*/
public Object get(String key) {
lock.readLock().lock();
Object value = null;
try {
System.out.println(Thread.currentThread().getName() + " 读取" + key);
value = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取Ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return value;
}
}
MyCacheLock myCache = new MyCacheLock();
重新运行应用程序,查看控制台打印信息:可以看到写入的顺序已经保证,在一个线程写入的时候不会存在另一个线程同时写入的情况;读取依然可以多个线程同时读取,保证了效率。
总结:独占锁(写锁)一次只能被一个线程占有,共享锁(读锁)多个线程可以同时占有;
ReentrantReadWriteLock
拥有比ReentrantLock
更加细粒度的控制,写锁保证了不会存在并发修改的问题,读锁与volatile
又保证了并发线程能同时读取到有效且正确的数据。读-读 可以共存、读-写 不能共存、写-写 不能共存
阻塞队列
写入:如果队列满了,就必须阻塞等待
取:如果队列是空的,必须阻塞等待生产
方法 | 抛出异常 | 有返回值 | 阻塞 等待 | 超时 等待 |
---|---|---|---|---|
添加 | add(E e) | offer(E e) | put(E e) | offer(E e, long timeout, TimeUnit unit) |
移除 | remove() | poll() | take() | poll(long timeout, TimeUnit unit) |
检测队首元素 | element() | peek() | ---- | ---- |
add(E e)
、remove()
、element()
/**
* 抛出异常
*/
public static void test1() {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.add("A"));
System.out.println(arrayBlockingQueue.add("B"));
System.out.println(arrayBlockingQueue.add("C"));
// 抛出异常:IllegalStateException: Queue full
// System.out.println(arrayBlockingQueue.add("D"));
System.out.println("++++++++++++++++++++++++++++");
System.out.println(arrayBlockingQueue.element());
System.out.println("++++++++++++++++++++++++++++");
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
// 抛出异常:NoSuchElementException
// System.out.println(arrayBlockingQueue.remove());
}
- add(E e):在队列尾部插入元素。若队列还存在可用的空间,则插入成功,返回 true;若队列已满,则抛出异常
IllegalStateException
。- remove():获取并删除此队列首部元素。 若队列中还存在元素,则删除并返回此队列的首部元素;若队列为空,则抛出异常
NoSuchElementException
。- element():获取但不删除此队列首部元素。若队列中还存在元素,则返回此队列首部元素;若队列为空,则抛出异常
NoSuchElementException
。
offer(E e)
、poll()
、peek()
/**
* 有返回值,不抛出异常
*/
public static void test2() {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.offer("A"));
System.out.println(arrayBlockingQueue.offer("B"));
System.out.println(arrayBlockingQueue.offer("C"));
// false 不抛出异常
System.out.println(arrayBlockingQueue.offer("D"));
System.out.println("===============================");
System.out.println(arrayBlockingQueue.peek());
System.out.println("===============================");
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
// null 不抛出异常
System.out.println(arrayBlockingQueue.poll());
}
- offer(E e):在队列尾部插入元素。若队列还存在可用的空间,则插入成功,返回 true;若队列已满,则插入失败,返回 false。
- poll():获取并删除此队列首部元素。 若队列中还存在元素,则删除并返回此队列的首部元素;若队列为空,返回 null。
- peek():获取但不删除此队列首部元素。若队列中还存在元素,则返回此队列首部元素;若队列为空,则返回 null。
put(E e)
、take()
/**
* 阻塞 等待
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
arrayBlockingQueue.put("A");
arrayBlockingQueue.put("B");
arrayBlockingQueue.put("C");
// 若队列已满,则一直阻塞,等待队列空出空间
// arrayBlockingQueue.put("D");
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
// 若队列已空,则一直阻塞,等待获取元素
// System.out.println(arrayBlockingQueue.take());
}
- put(E e):在该队列的尾部插入指定的元素,如果队列已满,则会阻塞等待空间变为可用【一直阻塞等待】。
- take() :获取并删除此队列的首部元素。如果队列中存在元素,则获取并删除队列首部元素;若队列为空,则一直等待,直到获取并删除首部元素。
offer(E e, long timeout, TimeUnit unit)
、poll(long timeout, TimeUnit unit)
/**
* 阻塞 超时
* @throws InterruptedException
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.offer("A", 2, TimeUnit.SECONDS));
System.out.println(arrayBlockingQueue.offer("B", 2, TimeUnit.SECONDS));
System.out.println(arrayBlockingQueue.offer("C", 2, TimeUnit.SECONDS));
// 插入元素,若队列已满,等待两秒,若两秒内队列产生了可用空间,则插入元素,返回 true,否则插入失败,返回 false。
System.out.println(arrayBlockingQueue.offer("D", 2, TimeUnit.SECONDS));
System.out.println("----------------------");
System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
// 获取并删除队列头,若存在元素则删除,若不存在则等待两秒,若两秒内插入了元素,则获取并删除队列头,否则返回 null。
System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
}
- offer(E e, long timeout, TimeUnit unit):在该队列的尾部插入指定的元素。若队列存在可用空间,则插入成功,返回 true;若队列已满,则等待
指定超时时间
,若指定超时时间
内队列产生了可用空间,则插入元素,返回 true,否则插入失败,返回 false。- poll(long timeout, TimeUnit unit):获取并删除队列头。若存在元素,则获取并删除队列头;若不存在,则等待
指定超时时间
,若指定超时时间
内插入了元素,则获取并删除队列头,否则返回 null。
SynchronousQueue
与其它BlockingQueue
不同,它没有内容;每个插入操作必须等待另一个线程相应的删除操作,反之亦然,SynchronousQueue
只能同时存在一个元素。示例:
public class SynchronousQueueTest {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + " put " + i);
blockingQueue.put(i + "");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Provider").start();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
try {
TimeUnit.SECONDS.sleep(3);
String take = blockingQueue.take();
System.out.println(Thread.currentThread().getName() + " take " + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Consumer").start();
}
}
运行示例,查看控制台打印信息,可以发现:每次 put 插入一个元素后,必须等待另一个线程 take 取出这个元素,才能继续 put 其它元素,否则会进入等待。
程序的运行,本质:需要占用系统资源。为了优化资源的使用,我们可以使用池化技术。池化技术:事先准备好一些资源,在需要使用时直接获取,用完之后需要返还。示例如:数据库连接池、线程池、内存池等。
线程池的好处:
降低资源的消耗。频繁的创建和销毁线程是非常耗费资源的,使用线程池可以避免线程的频繁创建,同时也提高了线程的复用。
提高响应速度。使用线程池,在需要一个线程执行任务的时候,可以直接从线程池中获取线程,而不用手动创建,节省了线程的创建时间。
方便管理。使用线程池可以对线程进行统一的管理,可以控制最大并发数等。
- Executors.newSingleThreadExecutor():创建一个单个线程的线程池。
- Executors.newFixedThreadPool(int nThreads):创建一个固定大小的线程池。 在任何时候,最多nThreads 个线程将处于主动处理任务。 如果所有线程都处于执行状态,这时来了新的任务,则这些任务将进入等待队列中直到线程可用。
- Executors.newCachedThreadPool():创建一个有伸缩性的线程池。 当池中线程全被占用,没有可用的线程时,将创建一个新的线程并将其添加到该池中。 未使用时长达到六十秒的线程,将被终止并从线程池中删除。
public class ThreadPoolTest {
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 < 20; i++) {
// 使用线程池后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 程序结束,线程池用完,关闭线程池
threadPool.shutdown();
}
}
}
可分别运行创建的三种线程池,查看控制台打印信息:可以看到 单个线程池 中始终只有一个线程执行,固定线程池中 最多有 5 个线程同时执行,可伸缩的线程则随着任务的执行逐渐增加线程的数量。
注意:根据 阿里巴巴开发手册 的规范,线程池需要使用 ThreadPoolExecutor 来创建,规避 OOM 造成资源耗尽的风险。
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>());
}
// 本质 ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大线程池大小
long keepAliveTime, // 超时时间,超过该时间没被调用就会释放
TimeUnit unit, // 超时时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂,创建线程的,一般不动
RejectedExecutionHandler handler // 拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到通过
三大方法
创建线程的操作,底层也是调用ThreadPoolExecutor
来创建线程池,只不过指定了不同的参数;所以,我们创建线程池时建议使用ThreadPoolExecutor
,自己根据需要指定相关参数。
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)参数介绍:
- corePoolSize:核心线程池大小
- maximumPoolSize: 最大线程池大小
- keepAliveTime:超时时间,超过该时间没被调用就会释放
- unit:超时时间单位
- workQueue:阻塞队列
- threadFactory:线程工厂,创建线程的,一般不动
- handler:拒绝策略
线程池的最大负载为:最大线程池大小 + 阻塞队列大小
当新的任务提交到线程池时,线程池首先检测
核心线程池
中是否有空闲的线程;若有,则将任务交给其执行;若没有,则检测阻塞队列
是否已满;若没满,则将任务添加到阻塞队列
;若已满,则查看线程池中线程数是否达到最大线程池大小
,若没有达到,则创建新的线程执行任务,若已经达到,则按指定的拒绝策略
拒绝任务。
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
threadPool.shutdown();
}
}
}
线程池线中资源的使用优先级为:核心线程 > 阻塞队列 > 其它线程;即:任务首先都交给核心线程执行,若 核心线程使用完了,则添加到阻塞队列,若阻塞队列也满了,就在最大线程数范围内创建新的线程,若线程数达到了最大线程数,则按指定的拒绝策略拒绝。
此时的线程数为 5 个,正好满足
核心线程池大小
+阻塞队列大小
,所以不需要额外创建线程;执行打印的结果也是两个线程,如下:
此时,将线程数修改为 6,重新执行打印结果如下:可以看到启动了 3 个线程
修改为 8 再执行,查看打印信息:可以看到这时启动了 5 个线程
修改为 9 再执行,由于线程池的最大负载为 8,此时已经超出了最大负载,多余的任务将按照指定的拒绝策略拒绝(任务满了,多余的任务不做处理,抛出异常)
ThreadPoolExecutor.AbortPolicy()
:任务满了,多余的任务不做处理,抛出异常new ThreadPoolExecutor.CallerRunsPolicy()
:任务满了,哪个线程抛过来的任务,返回给哪个线程执行。new ThreadPoolExecutor.DiscardPolicy()
:任务满了,丢掉任务,不会执行也不会抛出异常。new ThreadPoolExecutor.DiscardOldestPolicy()
:任务满了,尝试和最早的竞争,竞争成功则执行,竞争失败则丢弃,也不会抛出异常。
一、
ThreadPoolExecutor.AbortPolicy()
这种策略前面案例已经做了演示就不重复做了。
二、将前面案例的决绝策略修改为
new ThreadPoolExecutor.CallerRunsPolicy()
,再启动程序,查看打印结果如下:可以看到多余的任务返回给了主线程执行。
三、将拒绝策略修改为
new ThreadPoolExecutor.DiscardPolicy()
,再启动应用程序,并查看打印信息:可以看到只执行了8次,多余的任务没有执行也没有抛出异常,直接丢弃掉了。
四、将拒绝策略修改为
new ThreadPoolExecutor.DiscardOldestPolicy()
,它是第三种策略的改进版,比第三种更人性化,当任务已满时,会与最早的竞争,成功则执行,失败则抛弃。启动应用程序,并查看打印信息:可以看到只执行了8次,说明竞争失败任务被抛弃了。
最大线程池大小(
maximumPoolSize
) 如何设置:CPU 密集型:几核就是几,可以保持 CPU 的效率最高!获取 CPU 核数的方法:
Runtime.getRuntime().availableProcessors()
IO 密集型:一般为 (程序中十分耗 IO 的线程的数量) * 2,即:若有 15 个十分耗费 IO 资源的线程,则设置为 30
函数式接口:只有一个方法的接口,如:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
函数型接口,有一个输入参数,有一个返回值
使用示例:
public class FunctionTest {
public static void main(String[] args) {
// 工具类,将输入的字符串转化为大写字符串
// Function function = new Function() {
// @Override
// public String apply(String s) {
// return s.toUpperCase();
// }
// };
// lambda 表达式简化
Function<String, String> function = (str)->{
return str.toUpperCase();
};
System.out.println(function.apply("akieay.com"));
}
}
以上使用函数型接口创建了一个 将字符串转化为大写字符串 的转换器。
断定型接口:有一个输入参数,返回值只能是 布尔值。
使用示例:
public class PredicateTest {
private static Pattern PATTERN_MOBILE = Pattern.compile("^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$");
public static void main(String[] args) {
// 判断字符串是否是手机号
// Predicate predicate = new Predicate() {
// @Override
// public boolean test(String mobile) {
// Matcher matcher = PATTERN_MOBILE.matcher(mobile);
// return matcher.matches();
// }
// };
// lambda 表达式简化
Predicate<String> predicate = (mobile)->{
Matcher matcher = PATTERN_MOBILE.matcher(mobile);
return matcher.matches();
};
System.out.println(predicate.test("1234567890"));
System.out.println(predicate.test("18692264704"));
}
}
以上使用断定型接口创建了一个 判断给定字符串是否是手机号 的判断器。
Supplier:供给型接口,没有参数,只有返回值
Consumer:消费型接口,没有返回值,只有参数
使用示例:
public class SupplierConsumerTest {
public static void main(String[] args) {
// 使用供给型接口定义一个生产 ID 的ID生成器
Supplier<String> supplier = () -> {
return UUID.randomUUID().toString().replace("-", "");
};
// 使用消费型接口定义一个 uuid 消费器
Consumer<String> consumer = (uuid) -> {
System.out.println("uuid: " + uuid);
};
// 消费者 消费 生产者生产的 UUID
consumer.accept(supplier.get());
}
}
以上使用 供给型接口 创建了一个 uuid 生成器,使用 消费型接口 创建了一个 uuid 消费者。
修饰符和类型 | 方法和描述 |
---|---|
boolean |
allMatch(Predicate super T> predicate) 返回此流的所有元素是否与提供的谓词匹配。 |
boolean |
anyMatch(Predicate super T> predicate) 返回此流的任何元素是否与提供的谓词匹配。 |
static |
builder() 返回一个 Stream 的构建器。 |
|
collect(Collector super T,A,R> collector) 使用 Collector对此流的元素执行 mutable reduction Collector 。 |
|
collect(Supplier 对此流的元素执行 mutable reduction操作。 |
static |
concat(Stream extends T> a, Stream extends T> b) 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。 |
long |
count() 返回此流中的元素数。 |
Stream |
distinct() 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。 |
static |
empty() 返回一个空的顺序 Stream 。 |
Stream |
filter(Predicate super T> predicate) 返回由与此给定谓词匹配的此流的元素组成的流。 |
Optional |
findAny() 返回描述流的一些元素的Optional 如果流为空,则返回一个空的Optional 。 |
Optional |
findFirst() 返回描述此流的第一个元素的Optional 如果流为空,则返回一个空的Optional 。 |
|
flatMap(Function super T,? extends Stream extends R>> mapper) 返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。 |
DoubleStream |
flatMapToDouble(Function super T,? extends DoubleStream> mapper) 返回一个 DoubleStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。 |
IntStream |
flatMapToInt(Function super T,? extends IntStream> mapper) 返回一个 IntStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。 |
LongStream |
flatMapToLong(Function super T,? extends LongStream> mapper) 返回一个 LongStream ,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。 |
void |
forEach(Consumer super T> action) 对此流的每个元素执行操作。 |
void |
forEachOrdered(Consumer super T> action) 如果流具有定义的遇到顺序,则以流的遇到顺序对该流的每个元素执行操作。 |
static |
generate(Supplier 返回无限顺序无序流,其中每个元素由提供的 Supplier 。 |
static |
iterate(T seed, UnaryOperator 返回有序无限连续 Stream 由函数的迭代应用产生 f 至初始元素 seed ,产生 Stream 包括 seed , f(seed) , f(f(seed)) ,等 |
Stream |
limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过 maxSize 。 |
|
map(Function super T,? extends R> mapper) 返回由给定函数应用于此流的元素的结果组成的流。 |
DoubleStream |
mapToDouble(ToDoubleFunction super T> mapper) 返回一个 DoubleStream ,其中包含将给定函数应用于此流的元素的结果。 |
IntStream |
mapToInt(ToIntFunction super T> mapper) 返回一个 IntStream ,其中包含将给定函数应用于此流的元素的结果。 |
LongStream |
mapToLong(ToLongFunction super T> mapper) 返回一个 LongStream ,其中包含将给定函数应用于此流的元素的结果。 |
Optional |
max(Comparator super T> comparator) 根据提供的 Comparator 返回此流的最大元素。 |
Optional |
min(Comparator super T> comparator) 根据提供的 Comparator 返回此流的最小元素。 |
boolean |
noneMatch(Predicate super T> predicate) 返回此流的元素是否与提供的谓词匹配。 |
static |
of(T... values) 返回其元素是指定值的顺序排序流。 |
static |
of(T t) 返回包含单个元素的顺序 Stream 。 |
Stream |
peek(Consumer super T> action) 返回由该流的元素组成的流,另外在从生成的流中消耗元素时对每个元素执行提供的操作。 |
Optional |
reduce(BinaryOperator 使用 associative累积函数对此流的元素执行 reduction ,并返回描述减小值的 Optional (如果有)。 |
T |
reduce(T identity, BinaryOperator 使用提供的身份值和 associative累积功能对此流的元素执行 reduction ,并返回减小的值。 |
U |
reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 执行 reduction在此流中的元素,使用所提供的身份,积累和组合功能。 |
Stream |
skip(long n) 在丢弃流的第一个 n 元素后,返回由该流的 n 元素组成的流。 |
Stream |
sorted() 返回由此流的元素组成的流,根据自然顺序排序。 |
Stream |
sorted(Comparator super T> comparator) 返回由该流的元素组成的流,根据提供的 Comparator 进行排序。 |
Object[] |
toArray() 返回一个包含此流的元素的数组。 |
A[] |
toArray(IntFunction generator) 使用提供的 generator 函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。 |
/**
* @Author: akieay
* @Description:
* 现在有 5 个用户,筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于 23 岁
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只输出一个用户!
*/
public class StreamTest {
public static void main(String[] args) {
User user1 = new User(1, "a", 21);
User user2 = new User(2, "b", 22);
User user3 = new User(3, "c", 23);
User user4 = new User(4, "d", 24);
User user5 = new User(6, "e", 25);
// 集合负责存储
List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
// 计算交给 stream 流
list.stream()
.filter(user -> {
return user.getId() % 2 == 0;})
.filter((user -> {
return user.getAge() > 23;}))
.map(user -> {
user.setName(user.getName().toUpperCase());
return user;
})
.sorted((u1, u2) -> {
return u2.getName().compareTo(u1.getName());})
.limit(1)
.forEach(System.out::println);
}
}