本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1. 什么是JUC
JUC就是java.util.concurrent工具包的简称。这是一个处理线程的工具包,JDK1.5之后出现。
我们平常在使用线程时一般会使用以下几种方式:
- 继承Thread
- 实现Runnable接口
- 实现Callable接口
以前写过一篇博客,相信大家看完之后可以对多线程有一个清晰地认识:
2. 线程和进程
- 进程:一个程序,QQ.exe,Music.exe程序的集合
- 一个进程往往包含多个线程,至少一个(正在运行的程序可以理解为进程,那么线程就是一个程序的不同的操作)
- java默认有两个线程,main、GC
- 线程:开了一个进程Typora,写字、保存这就是不同的线程。
- 在java中Thread、Runnable、Callable操作线程
# 我们经常用java程序写多线程的例子,那么java真的可以开启多线程吗?
- 答案是不,我们可以从源码中查看答案。(java中开启线程的方式是start()方法,所以我们深入start()方法的源码看一下)
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(); //本地方法,调用底层c++,java无法直接操作硬件
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 */
}
}
}
并发和并行
-
并发:单核cpu,模拟出来多条线程,快速交替。
- 简单地说,就是一个人做多件事,举个例子,如果许多个用户同时访问服务器,如果采取串行方式,那么用户体验感会极差,但是如果采用并发,那么很快会给每一个用户带来一定的反馈,为了维护用户的体验感,也为了更合理cpu的资源,对一个人的访问还没有全部响应,就去执行对另一个人的响应。
- 并发在一定情况下是可以提高cpu的利用率的,比如说在有I/O等待的情况下,在I/O等待的时候去做另外的事情就提高了cpu的利用率。
-
并行:多核cpu,多个线程同时执行。
- 简单地说,就是多个人做多件事。
//下面这段代码可以获取cpu的核数
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
}
并发编程的本质:充分利用cpu的资源
线程的状态数(6个)
//下面是源码中的呈现出的线程状态数
public enum State {
//线程新生状态
NEW,
//线程运行
RUNNABLE,
//线程阻塞
BLOCKED,
//线程等待,死死的等
WAITING,
//线程等待,超过一定的时间就不等了
TIMED_WAITING,
//线程中止状态
TERMINATED;
}
wait和sleep的区别:
-
二者来自不同的类
- wait -> Object
- sleep -> Thread
-
关于锁的释放
- wait会释放锁
- sleep睡觉了会抱着锁,不会释放锁
-
使用的范围是不同的
- wait必须在同步代码块中使用
- sleep可以在任何地方睡觉
3. Synchronized锁
传统的Synchronized锁
package juc.demo;
// synchronized
//下面是一个卖票的例子
public class demo1 {
/*
真正的多线程开发,公司中的开发
多线程就是一个单独的资源类,没有任何附属的方法
属性,方法(资源类中要包含必要的方法,不包含不必要的(附属)方法)
*/
public static void main(String[] args) {
//并发,多个线程同时操作一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "C").start();
}
}
//资源类 OOP
class Ticket{
//属性、方法
private int number = 50;
//卖票的例子
//synchronized本质:队列、锁
public synchronized void sale(){
if(number > 0){
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number + "票");
}
}
}
我们可以在自己的电脑上运行一下这个程序,结果是符合我们的实际情况的,也就说明了我们上面的编程是合理的。
4. Lock锁
我们看一下API中关于Lock接口的信息。
我们看到Lock有三个实现类,有可重入锁、读锁、写锁。
这里我们说一下什么是可重入锁:
下面是
可重入锁是一种特殊的互斥锁,它可以被同一个线程多次获取,而不会产生死锁。
1. 首先它是互斥锁:任意时刻,只有一个线程锁。即假设A线程已经获取了锁,在A线程释放这个锁之前,B线程是无法获取到这个锁的,B要获取这个锁就会进入阻塞状态。
2. 其次,它可以被同一个线程多次持有。即,假设A线程已经获取了这个锁,如果A线程在释放锁之前又一次请求获取这个锁,那么是能够获取成功的。
我们来看一下可重入锁ReentrantLock的一部分源码:
public ReentrantLock() {
sync = new NonfairSync(); //非公平锁,线程可以插队
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
//这里的new FairSync()是公平锁,线程不可以插队
}
- 公平锁:十分公平,线程执行顺序按照先来后到顺序
- 非公平锁:十分不公平;线程可以插队
接下来我们可以将上面的用Synchronized修饰的方法卖票例子改为使用Lock实现,并认识一下Lock和Synchronized的区别和联系
package juc.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// Lock锁
public class demo02 {
public static void main(String[] args) {
Ticket02 ticket = new Ticket02();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for(int i = 0; i < 60; i++){
ticket.sale();
}
}, "C").start();
}
}
/*
Lock加锁的使用方式
1. 首先创建实现类,new ReentrantLock();
2. lock.lock();加锁
3. 在try/catch中写业务代码
4. 在finally中解锁,lock.unlock();
*/
class Ticket02{
//属性、方法
private int number = 50;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); //加锁
try{
if(number > 0){
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number + "票");
}
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
}
Synchronized和Lock的区别
- Synchronized是java内置的关键字,Lock是一个java接口。
- Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
- Synchronized会自动释放锁,Lock必须要手动释放锁(如果不释放锁,死锁)。
- Synchronized线程1(获得锁,如果线程1阻塞)、线程2(傻傻的等);Lock锁就不一定会等待下去。
- Synchronized是可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,可以公平也可以非公平(自己设置,不加参数默认非公平)。
- Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。
额,,,这里用一句话简单的总结一下,Synchronized就是汽车自动挡,Lock就是汽车手动挡。
5. 生产者和消费者问题
# 首先解释一下什么是生产者和消费者问题
生产者消费者问题也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了共享固定大小的两个线程——即所谓的“生产者”和“消费者”
,在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据
。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者不会在缓冲区空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据时,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能推广到多个生产者
和消费者的情形。
该问题需要注意以下几点:
- 在缓冲区为空时,消费者不能再进行消费。
- 在缓冲区为满时,生产者不能再进行生产。
- 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步。
- 注意条件变量与互斥锁的顺序。
接下来我们看一下比较简单情况下的生产者和消费者问题。
Synchronized版的生产者和消费者问题
//这是Synchronized版的生产者和消费者问题
package juc.demo;
// 线程通信
// 传统的生产者、消费者模式
// if改为while : 防止虚假唤醒
public class demo03 {
public static void main(String[] args) {
A a = new A();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class A {
private int number = 0;
public synchronized void increment() throws InterruptedException {
while(number != 0){ //不等于0让其等待
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "->" + number);
//通知其他线程,我+1完毕了
this.notify();
}
public synchronized void decrement() throws InterruptedException {
while(number == 0){ //等于0让其等待
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "->" + number);
//通知其他线程,我-1完毕了
this.notify();
}
}
/*
这里说一下虚假唤醒
虚假唤醒就是在多线程执行过程中,线程间的通信未按照我们幻想的顺序执行,故出现数据不一致等不符合我们预期的结果。
在上面那两个同步方法中我们分别使用了以下的代码,这里使用while的原因是为了防止虚假唤醒。
我们举个例子,我们将whiel改为if,A、C执行加线程,B、D执行减线程,在某一时刻,number为1,此时A线程抢到,但是由于判断,
A线程会等待,之后A又抢到了,且此时number为1,此时if语句之前已经执行过了,A线程不会等待,直接加1,number为2,这显然与
我们的预期不符,这就是虚假唤醒产生的原因,将if改为while可以很好地解决虚假唤醒问题。
while(number != 0){ //不等于0让其等待
//等待
this.wait();
}
while(number != 0){ //不等于0让其等待
//等待
this.wait();
}
*/
Lock版的生产者和消费者问题
//这是Lock版的生产者和消费者问题
package juc.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 线程通信
// Lock版的生产者、消费者模式
// if改为while : 防止虚假唤醒
public class demo04 {
public static void main(String[] args) {
AA a = new AA();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for(int i = 0; i < 10; i++){
try {
a.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class AA {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while(number != 0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "->" + number);
//通知其他线程,我+1完毕了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while(number == 0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "->" + number);
//通知其他线程,我-1完毕了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
# ReentrantLock使用方法 (juc底层这些知识等学完juc基础用法,就会单独看源码学习)
- 提供了无条件的,可轮询的,定时的以及可中断的锁获取操作
- 加锁和解锁都是显式的
- 与Synchronized一样都是可重入锁
Lock lock = new ReentrantLock();
try{
lock.lock();//加锁操作
}finally{
lock.unlock();
}
# 介绍一下Condition类
首先我们需要明白condition对象是依赖于lock对象的,意思就是说condition对象需要通过lock对象进行创建出来(调用Lock对象的newCondition()方法)。consition的使用方式非常的简单。但是需要注意在调用方法前获取锁。Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
Condition精准的通知和唤醒线程
//让A、B、C线程轮流操作
package juc.demo;
import jdk.nashorn.internal.ir.ContinueNode;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class demo05 {
public static void main(String[] args) {
Data03 date = new Data03();
new Thread(()->{
for(int i = 0; i <10; i++){
date.printA();
}
}, "A").start();
new Thread(()->{
for(int i = 0; i <10; i++){
date.printB();
}
}, "B").start();
new Thread(()->{
for(int i = 0; i <10; i++){
date.printC();
}
}, "C").start();
}
}
class Data03{
private int number = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA(){
lock.lock();
try {
//业务判断->执行->通知
while(number != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "->" + number);
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while(number != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "->" + number);
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (number != 3){
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "->" + number);
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//生产线:下单->支付->物流
6. 8锁现象
Synchronized锁的对象是方法的调用者
例子1:
package juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁:就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信还是 先打印 打电话? 1/发短信 2/打电话
* 1、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
*/
public class Main {
public static void main(String[] args) {
Phone phone = new Phone();
// 锁的存在
new Thread(()->{
phone.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
// synchronized 锁的对象是方法的调用者!、
// 两个方法用的是同一个对象调用(同一个锁),谁先拿到锁谁执行!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);// 抱着锁睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
//线程在执行前必须先拿到锁才可以执行,执行完之后要释放锁。
/*
上面代码的执行结果是先发短信后打印,因为对于Synchronized修饰的普通方法锁的是对象,发短信线程先拿到锁,发短信执行完之后释放锁,
之后打电话线程才可以拿到锁执行。
*/
例子2:
package juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、 增加了一个普通方法后!先执行发短信还是Hello?
* 4、 两个对象,两个同步方法, 发短信还是 打电话?
*/
public class Main {
public static void main(String[] args) {
// 两个对象,两个调用者,两把锁!
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
new Thread(()->{
phone2.hello();
},"C").start();
}
}
class Phone2{
// synchronized 锁的对象是方法的调用者!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
// 这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
/*
结果是:
先打电话
在说hello
最后发短信
*/
例子3:
对于静态方法来说,Synchronized锁的是类的Class模板
package juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?
* 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
*/
public class Main {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone3{
// synchronized 锁的对象是方法的调用者!
// static 静态方法
// 锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
/*
虽然是两个不同的对象,但是由于锁的是静态方法,锁的对象是类的Class模板,也就是说,锁只有一个,
必须等其中一个拿到锁的线程执行完,另一个线程才能继续执行。
*/
例子4:
package juc.lock8;
import java.util.concurrent.TimeUnit;
/**
* 7、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
* 8、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
*/
public class Main {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone4{
// 静态的同步方法 锁的是 Class 类模板
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通的同步方法 锁的调用者(对象),二者锁的对象不同,所以不需要等待
public synchronized void call(){
System.out.println("打电话");
}
}
/*
结果:
先打电话后发短信,因为发短信线程会等4s
*/
- 对于Synchronized修饰的普通方法来说,Synchronized锁的是对象
- 对于Synchronized修饰的静态方法来说,Synchronized锁的是类的Class
7. 集合类不安全
# 并发修改异常
并发就是同一时刻发生,并发修改的意思就是同一时刻发生并修改。当方法检测到对象的并发修改,但不允许这种修改时,会抛出此异常。
List不安全
List、ArrayList等在并发多线程条件下,不能实现数据共享。多个线程调用同一个list对象就会出现并发修改异常
java.util.ConcurrentModificationException:并发修改异常
package juc.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
//java.util.ConcurrentModificationException并发修改异常
/**
* 解决方案:
* 1. List<String> list = new Vector<>();
* 2. List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3. List<String> list = new CopyOnWriteArrayList<>();
*/
public class ListTest {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for(int i = 0; i < 10; i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Set不安全
package juc.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* java.util.ConcurrentModificationException 并发修改异常
* 解决方案:
* 1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
* 2.
*/
public class SetTest {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for(int i = 0; i < 100; i++){
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
HashSet底层就是HashMap,HashMap底层的知识我以前写过博客,具体可以看我以前写过的博客啊
Map不安全
package juc.unsafe;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* java.util.ConcurrentModificationException 并发修改异常
* 解决方案:
* 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
* 2. Map<String, String> map = new ConcurrentHashMap<>();
*/
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
for(int i = 0; i < 30; i++){
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
8. Callable
- Callable 是 java.util 包下 concurrent 下的接口,有返回值,可以抛出被检查的异常
- Runable 是 java.lang 包下的接口,没有返回值,不可以抛出被检查的异常
- 二者调用的方法不同,run()/ call()
同样的 Lock 和 Synchronized 二者的区别,前者是java.util 下的接口 后者是 java.lang 下的关键字。
使用Callable的步骤:
- 首先创建一个Callable的实现类,并实现call()方法(将要执行的逻辑写在其中)
- 创建一个FutureTask对象并将Callable的实现类作为参数传递给它的构造方法
- 创建一个Thread对象并将上面的FutureTask对象作为参数传递给它的构造方法
- Thread执行**start()**方法
package juc.clllable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTestImpl call = new CallableTestImpl();
FutureTask futureTask = new FutureTask(call);
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
Integer o = (Integer) futureTask.get();
System.out.println(o);
}
}
class CallableTestImpl implements Callable<Integer> {
@Override
public Integer call(){
System.out.println("call");
return 1024;
}
}
9. 常用的辅助类
9.1 CountDownLatch
CountDownLatch是一个减法计数器,实现调用几次其他线程后再出发某一个任务,
# 什么是 CountDownLatch
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。
当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待
的线程就可以恢复执行接下来的任务。
package juc.add;
import java.util.concurrent.CountDownLatch;
public class CountDownLanchTest {
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() + " go out");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("close door");
}
}
原理:
- CountDownLatch countDownLatch = new CountDownLatch(6); 设置需要等待的线程数为6
- countDownLatch.countDown();计数器减一
- countDownLatch.await();等待计数器归零再向下执行
[CountDownLatch学习](CountDownLatch的理解和使用 - Shane_Li - 博客园 (cnblogs.com))
9.2 CyclicBarrier
加法计数器
让指定数量的线程等待完成后才能执行下一步的动作
package juc.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠线程
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();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
9.3 Semaphore
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
package juc.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for(int i = 1; i <= 6; i++){
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
- semaphore.acquire();获得,假设已经满了则等待,等待其他线程释放.
- semaphore.release();释放,会将当前信号的释放量加1,然后等待其他线程争抢。
10. ReadWriteLock读写锁
读可以被多个线程共享,写只能有一个线程去写
ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。
package juc.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWritedemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
class MyCache{
private volatile Map<String, Object> map = new HashMap<>();
//存、写
public void put(String key, Object value){
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
//取、读
public void get(String key){
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
class MyCacheLock{
private volatile Map<String, Object> map = new HashMap<>();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存、写
public void put(String key, Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//取、读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
11. BlockingQueue
BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
阻塞队列
四组API:
| 方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时等待 |
|---|---|---|---|---|
| 添加 | add | offer() | put() | offer(,) |
| 移除 | remove | poll() | take() | poll(,) |
| 检测队首元素 | element | peek() | - | - |
package juc.bq;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.*;
public class BlockQueue {
public static void main(String[] args) throws InterruptedException {
test4();
}
public static void test1(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
System.out.println(queue.element());
//队列已满,抛出异常java.lang.IllegalStateException: Queue full
//System.out.println(queue.add("d"));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
//队列为空, 抛出异常java.util.NoSuchElementException
//System.out.println(queue.remove());
}
public static void test2(){
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
//队列已满
//System.out.println(queue.offer("d"));
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.peek());
//队列为空
System.out.println(queue.poll());
}
public static void test3() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
queue.put("a");
queue.put("b");
queue.put("c");
//queue.put("d");
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
}
public static void test4() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println(queue.offer("d", 2, SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
}
}
SynchronizedQueue
同步队列
没有容量,当put进去一个元素之后,必须take消费一个才能继续put(简而言之就是容量为1的ArrayBlockingQueue)
package juc.bq;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* Synchronized同步队列
* put了一个元素,必须从里面先删除一个元素,否则不能继续put值
*/
public class SynchronizedQueueDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " put 1");
queue.put(1);
System.out.println(Thread.currentThread().getName() + " put 2");
queue.put(2);
System.out.println(Thread.currentThread().getName() + " put 3");
queue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->" + queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->" + queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "->" + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}