一、什么是JUC?
JUC就是Java的并发包java.util.concurrent的简写,是关于并发编程的API。
与JUC相关的有三个包:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。
二、并发和并行的区别
-
并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。(同一时间段)(通俗地讲:CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替)
-
并行:在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。(同一时刻)(通俗地讲:CPU 多核 ,多个线程可以同时执行)
下边这张图可以更形象地表示并发(上图)和并行(下图)的区别:
并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机
案例演示
获取CPU核数
public class Test1 { public static void main(String[] args) {
// 获取cpu的核数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
三、虚假唤醒
假如生产者/消费者问题中存在不止一个生产者和不止一个消费者,那么就会出现虚假唤醒的情况。
案例演示
class Shop{
public int num=0;
public synchronized void add(){//店员进货
if(num!=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName()+"进货,货物余量:"+num);
this.notifyAll();
}
public synchronized void buy(){//客人买货
if(num==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println(Thread.currentThread().getName()+"买货,货物余量:"+num);
this.notifyAll();
}
}
public class MyTest{
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.add();
}
},"店员1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.buy();
}
},"客人1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.add();
}
},"店员2").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.buy();
}
},"客人2").start();
}
}
这种情况出现了产品数为负的情况,肯定不合适,这就是虚假唤醒。因为假设这时num==0,然后两个消费者线程A、B都wait(),此时生产者C执行num++后,再唤醒却是唤醒了所有等待的线程,此时这两个消费者线程A、B抢占资源后立马执行wait()之后的代码。假设线程A速度比较快,先执行了num--,而线程B并不知道此时num已经为0,它只是依照指示被唤醒执行了wait()后的代码num--,这就会出现产品为负的情况。
为了防止虚假唤醒,我们应该让wait()在while循环中。因为wait()在while循环中的话,线程被唤醒后while还会多判断一次此时是否满足条件才执行后面的代码。
案例演示
class Shop{
public int num=0;
public synchronized void add(){//店员进货
while(num!=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName()+"进货,货物余量:"+num);
this.notifyAll();
}
public synchronized void buy(){//客人买货
while(num==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println(Thread.currentThread().getName()+"买货,货物余量:"+num);
this.notifyAll();
}
}
public class MyTest{
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.add();
}
},"店员1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.buy();
}
},"客人1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.add();
}
},"店员2").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shop.buy();
}
},"客人2").start();
}
}
四、Lock锁
-
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock的对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象 -
所有已知实现类:
ReentrantLock(可重入锁,最常用), ReentrantReadWriteLock.ReadLock(读锁), ReentrantReadWriteLock.WriteLock (写锁)
(一)ReentrantLock
- ReentrantLock(可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
- ReentrantLock有两种构造方法:
空参构造默认使用非公平锁,有参构造可以指定公平锁/非公平锁。
- 公平锁:十分公平,可以先来后到
非公平锁:十分不公平,可以插队 (默认)
- 公平锁:十分公平,可以先来后到
- 格式
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
} finally{
lock.unlock();
}
}
}
案例演示
import java.util.concurrent.locks.ReentrantLock;
public class UnsafeBuyTicket implements Runnable {
private int ticket=10;
private final ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
while(ticket>0){
System.out.println(Thread.currentThread().getName()+"抢到了第"+(11-ticket)+"张票");
ticket--;
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
UnsafeBuyTicket b = new UnsafeBuyTicket();
new Thread(b,"小明").start();
new Thread(b,"老师").start();
new Thread(b,"黄牛党").start();
}
}
(二)synchronized与Lock的区别
- Lock是显式锁(需要手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 如果线程1获得锁,那么线程2会阻塞并等待,一直傻傻的等;Lock锁就不一定会等待下去(
boolean tryLock()
只有在调用时释放该锁,才能获取锁。 ) - Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,是否公平可以
自己设置 - Synchronized 适合锁少量的同步代码;Lock 适合锁大量的同步代码!
五、JUC解决生产者/消费者问题
1.
Lock接口里有一个抽象方法:
Condition newCondition()
返回一个新的 Condition实例绑定到该 Lock实例。
(我们知道Lock用来取代 synchronized方法和语句的使用,那么Condition就用来取代对象监视器的使用方法。 )
2.
Condition类里又有以下方法:
void await()
使当前线程等待,直到被唤醒或 interrupted。
void signal()
唤醒一个等待线程。
void signalAll()
唤醒所有等待线程。
案例演示
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Shop{
private int num=0;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
public void add() throws InterruptedException {//店员进货
lock.lock();
try {
while(num!=0){
condition.await();//如果店里有货,店员等待
}
num++;
System.out.println(Thread.currentThread().getName()+"进货,货物余量:"+num);
condition.signalAll();//进货后通知客人来买货
} finally {
lock.unlock();
}
}
public void buy() throws InterruptedException {//客人买货
lock.lock();
try {
while(num==0){
condition.await();//如果店里没货,客人等待
}
num--;
System.out.println(Thread.currentThread().getName()+"买货,货物余量:"+num);
condition.signalAll();//买货后通知店员进货
} finally {
lock.unlock();
}
}
}
public class MyTest{
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"店员A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"客人B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"店员C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"客人D").start();
}
}
那么你可能会问,那这个方法和synchronized方法不就没啥区别吗?
其实,任何一项新技术绝对不仅仅只是覆盖了原来的技术,一定有它的优势或补充。
问:假如我们希望店员A进货完通知来买货的一定是客人B;而客人B买完货通知进货的一定是店员C;店员C进货后通知来买货的一定是客人D该怎么实现呢?
synchronized就实现不了,这里就得用JUC的方法来解决:
六、Condition实现精准通知唤醒
案例演示
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Shop{
private int num=1;//假设对应:1A 2B 3C 4D
Lock lock=new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
Condition condition3=lock.newCondition();
Condition condition4=lock.newCondition();
public void addA() throws InterruptedException {//店员A进货
lock.lock();
try {
while(num!=1){
condition1.await();//如果店里有货,店员A等待
}
num=2;
System.out.println(Thread.currentThread().getName()+"进货");
condition2.signal();//进货后通知客人B来买货
} finally {
lock.unlock();
}
}
public void buyB() throws InterruptedException {//客人b买货
lock.lock();
try {
while(num!=2){
condition2.await();//如果店里没货,客人B等待
}
num=3;
System.out.println(Thread.currentThread().getName()+"买货");
condition3.signal();//买货后通知店员C进货
} finally {
lock.unlock();
}
}
public void addC() throws InterruptedException {//店员C进货
lock.lock();
try {
while(num!=3){
condition3.await();//如果店里有货,店员C等待
}
num=4;
System.out.println(Thread.currentThread().getName()+"进货");
condition4.signal();//进货后通知客人D来买货
} finally {
lock.unlock();
}
}
public void buyD() throws InterruptedException {//客人D买货
lock.lock();
try {
while(num!=4){
condition4.await();//如果店里没货,客人D等待
}
num=1;
System.out.println(Thread.currentThread().getName()+"买货");
condition1.signal();//买货后通知店员A进货
} finally {
lock.unlock();
}
}
}
public class MyTest{
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.addA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"店员A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.buyB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"客人B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.addC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"店员C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shop.buyD();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"客人D").start();
}
}
这就是精准通知唤醒
七、读写锁ReadWriteLock
ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。read lock可以由多个阅读器线程同时进行,只要没有写入者。 write lock只能有一个线程写。 (读的时候多个线程同时读,写的时候一个线程写)
案例演示1
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
public static void main(String[] args){
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int temp=i;
new Thread(()->{
myCache.put(temp+"",temp+10);
},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{
//volatile原子性 后面讲
private volatile Map<String,Integer> map = new HashMap();
public void put(String s,Integer i){//添加键值对到我的缓存中
System.out.println(Thread.currentThread().getName()+"正在写入");
map.put(s,i);
System.out.println(Thread.currentThread().getName()+"写入成功!");
}
public void get(String s){//通过键查找值
System.out.println(Thread.currentThread().getName()+"正在读取");
Integer i = map.get(s);
System.out.println(Thread.currentThread().getName()+"读取成功:"+i);
}
}
通过结果我们发现,1线程还没写入成功就被其他线程插队了,这不是我们想要的
这时我们就需要加入读写锁:
案例演示2
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
public static void main(String[] args){
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int temp=i;
new Thread(()->{
myCache.put(temp+"",temp+10);
},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{
//volatile 后面讲
private volatile Map<String,Integer> map = new HashMap();
// 读写锁: 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String s,Integer i){//添加键值对到我的缓存中
readWriteLock.writeLock().lock();//写锁,写入的时候只希望有一个线程在写
try {
System.out.println(Thread.currentThread().getName()+"正在写入");
map.put(s,i);
System.out.println(Thread.currentThread().getName()+"写入成功!");
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String s){//读,所有线程都可以同时读
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在读取");
Integer i = map.get(s);
System.out.println(Thread.currentThread().getName()+"读取成功:"+i);
} finally {
readWriteLock.readLock().unlock();
}
}
}
八、线程八锁问题
(一)场景一:一个对象,两个同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo1 {
public static void main(String[] args) {
Phone1 phone = new Phone1();
new Thread(()->{phone.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);//JUC版的sleep
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();}).start();
}
}
class Phone1{
public synchronized void sendMessage(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
解析:先执行发短信,因为synchronized锁的是调用该方法的对象,而此程序里两个方法调用的对象是同一个,先调用的先执行!
(二)场景二:一个对象,两个同步方法,其中一个加了延时
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{phone.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();}).start();
}
}
class Phone2{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
解析:即使发短信有延时也是先执行发短信,因为synchronized锁的是调用该方法的对象,而此程序里两个方法调用的对象是同一个,先调用的先执行!
(三)场景三:一个对象,一个同步方法,一个普通方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(()->{phone.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();}).start();
}
}
class Phone3{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通方法
public void call(){
System.out.println("打电话");
}
}
解析:先执行打电话,因为call()方法没有锁!不是同步方法,不受锁的影响
(四)场景四:两个对象,两个同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo4 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{phone1.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone2.call();}).start();
}
}
class Phone4{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
解析:这里调用方法的对象是两个不同的对象,那么锁的就是这两个不同的调用者,所以互不影响。
(五)场景五:一个对象,两个静态同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo5 {
public static void main(String[] args) {
Phone5 phone = new Phone5();
new Thread(()->{phone.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();}).start();
}
}
class Phone5{
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
解析:只要同步方法被 static 修饰,锁的对象就是 Class模板对象,这个全局唯一!所以说这里是同一个锁,这里程序会从上往下依次执行
(六)场景六:两个对象,两个静态同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo6 {
public static void main(String[] args) {
Phone6 phone1 = new Phone6();
Phone6 phone2 = new Phone6();
new Thread(()->{phone1.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone2.call();}).start();
}
}
class Phone6{
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
解析:虽然是两个对象,但是这里锁的对象还是同一个,是 Class模板对象,这里程序会从上往下依次执行。
(七)场景七:一个对象,一个静态同步方法,一个同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo7 {
public static void main(String[] args) {
Phone7 phone = new Phone7();
new Thread(()->{phone.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();}).start();
}
}
class Phone7{
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
解析:被static修饰的同步方法锁的是Class模板,而synchronized修饰的方法锁的是调用此方法的对象,这里锁的是两种对象,互不影响,由于sendMessage()有延时,因此打电话先执行。
(八)场景八:两个对象,一个静态同步方法,一个同步方法
案例演示
import java.util.concurrent.TimeUnit;
public class LockDemo8 {
public static void main(String[] args) {
Phone8 phone1 = new Phone8();
Phone8 phone2 = new Phone8();
new Thread(()->{phone1.sendMessage();}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone2.call();}).start();
}
}
class Phone8{
public static synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4); //JUC版的sleep,有延时能更好体现八锁问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
解析:被static修饰的同步方法锁的是Class模板,而synchronized修饰的方法锁的是调用此方法的对象,这里锁的是两种对象,互不影响,由于sendMessage()有延时,因此打电话先执行。
总结:
同步方法锁的是调用此方法的对象,静态同步方法锁的是Class模板对象。
九、集合线程安全化
(一)List集合
1.Collections.synchronizedList
我们知道,ArrayList是不安全的,看个例子:
案例演示
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ListDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(new Random().nextInt(100));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
此时会报错:ConcurrentModificationException并发修改异常,因为ArrayList是线程不安全的,多个线程同时进行add操作,有可能在add过程中出现覆盖的情况。
那么,如何让它变得安全呢?有人肯定想到Vector集合是线程安全的,Vector的add()方法加上了synchronized:
但是实际上Vector出现于JDK1.0,而ArrayList出现于JDK1.2,那开发人员早就知道可以用synchronized解决而没有在ArrayList加上一定有原因,因此Vector集合在这个问题上并不是一个完美的解决方案
在Collections工具类中有一个静态方法:
static <T> List<T> synchronizedList(List<T> list)
返回由指定集合支持的同步(线程安全)集合。
它可以将集合转换成线程安全的集合
案例演示
import java.util.*;
public class ListDemo {
public static void main(String[] args) {
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(new Random().nextInt(100));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
这就解决了。
2.CopyOnWriteArrayList
CopyOnWrite顾名思义,写入时复制,就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。
案例演示
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListDemo {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(new Random().nextInt(100));
System.out.println(list);
list.toString();
},String.valueOf(i)).start();
}
}
}
这个集合也是线程安全的,通过源码我们可以看到它的add()方法是用了Lock锁
CopyOnWriteArrayList 底层实现添加的原理是通过创建底层数组的新副本来实现的。当 List 需要add的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将add的内容写入副本中。写完之后,再将add完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
(二)Set集合
同样的,Set集合(如HashSet)是线程不安全的,多线程下使用同样会报并发修改异常,和List集合一样,有以下两种方式使Set集合线程安全
1.Collections.synchronizedSet
static <T> Set<T> synchronizedSet(Set<T> s)
返回由指定集合支持的同步(线程安全)集。
案例演示
import java.util.Collections;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(new Random().nextInt(100));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
2.CopyOnWriteArraySet
案例演示
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class SetDemo {
public static void main(String[] args) {
Set<Integer> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(new Random().nextInt(100));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
(三)Map集合
同样的,Map集合(如HashMap)也是线程不安全的,多线程下使用同样会报并发修改异常,解决方式也是两种:
1.Collections.synchronizedMap
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定Map集合支持的同步(线程安全)Map集合。
案例演示
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class MapDemo {
public static void main(String[] args) {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),new Random().nextInt(100));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
2.ConcurrentHashMap
第二个方法和上面的有些不同
案例演示
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class MapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),new Random().nextInt(100));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
十、Callable
Callable与Runnable创建线程方式不同的是,它可以抛出异常,且有返回值
我们都知道,Runnable方式是通过:new Thread(Runnable子实现类的对象).start();
来启动线程的,那么Callable可不可以也用这种方法启动线程呢?可以。
Runnable有个子实现类叫做FutureTask,它得其中一种构造方法是:
FutureTask(Callable<V> callable)
创建一个 FutureTask ,它将在运行时执行给定的 Callable 。
这样,我们就知道怎么启动线程了:new Thread(new FutureTask(Callable子实现类的对象)).start();
案例演示
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1024;
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask(myThread);
//在两个线程里传入同一个futureTask,只会执行一次
//结果会被缓存,效率高
new Thread(futureTask,"A").start();
new Thread(futureTask,"AA").start();
//想要执行两次,就需要再来一个不一样的FutureTask
FutureTask futureTask1 = new FutureTask(myThread);
new Thread(futureTask1,"B").start();
Integer o = (Integer)futureTask.get(); //这个get方法可能会产生阻塞!把他放到最后
System.out.println(o);
}
}
十一、JUC中常用的辅助类
(一)CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CountDownLatch用给定的计数初始化。线程通过await()方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await()调用立即返回。
“所有人都离开教室了才能锁门”
可以理解为“减法计数器”
案例演示
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"离开了教室");
countDownLatch.countDown();//计数器-1
},String.valueOf(i)).start();
}
//如果没有await(),会提前锁门,把人关在教室里
countDownLatch.await();//等到计数器归零后,才执行下面的代码
System.out.println("锁门");
}
}
(二)CyclicBarrier
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。它的作用就是会让所有线程都等待完成后才会继续下一步行动。
“吃饭时要等全家人都上座了才动筷子”
也可以理解为“加法计数器”
案例演示
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test1 {
public static void main(String[] args) {
//CyclicBarrier(int parties, Runnable barrierAction)
//创建一个新的 CyclicBarrier,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->System.out.println("召唤神龙成功!"));
//当有7条线程等待时,它跳闸并且执行第二个参数里的run方法
for (int i = 1; i <= 7; i++) {
final int temp=i;
new Thread(()->{
//在lambda表达式中引用的变量应当是最终变量,且不可修改变量的值
//不能直接用i
System.out.println(Thread.currentThread().getName()+"拿到了第"+temp+"颗龙珠");
try {
cyclicBarrier.await();//等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
(三)Semaphore
Semaphore:信号量
Semaphore管理着一组虚拟的许可集合,这种许可可以作为某种凭证,来管理资源,在一些资源有限的场景下很有实用性,比如数据库连接。
任何线程想要访问资源必须首先通过acquire() 获得一个许可,这个操作可能会阻塞线程直到成功获得一个许可,因为资源是有限的,没有获得资源就需要阻塞,等待其他线程通过 release() 方法来归还这个许可,release会唤醒一个等待在Semaphore上的一个线程来尝试获得许可。
如果想要达到一种互斥的效果,比如任何时刻只能有一个线程获得许可,那么就可以初始化Semaphore的数量为1,一个线程获得这个许可之后,任何到来的通过acquire()来尝试获得许可的线程都会被阻塞直到这个持有许可的线程调用了release()方法来释放许可为止。
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
案例演示
模拟抢车位:六辆车,三个车位
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args){
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();//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();//release 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程
}
},String.valueOf(i)).start();
}
}
}
十二、阻塞队列BlockingQueue
-
什么是阻塞队列?
写入:如果队列满了,就必须阻塞等待直到队列有元素被取出
取出:如果队列为空,就必须阻塞等待直到队列有元素被写入 -
队列采用FIFO,数据从队列尾输入,从队列头输出,先进去的就先出来,就像水在水管里流一样。
-
什么情况下我们会使用阻塞队列:多线程并发处理,线程池!
(一)继承关系
(二)BlockingQueue的四组API
1.抛出异常 | 2.不抛出异常,有返回值 | 3.阻塞等待 | 4.超时等待 | |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,,) |
移除 | remove() | poll() | take() | poll(,) |
检测队首元素 | element() | peek() | - | - |
案例演示1
会抛出异常
import java.util.concurrent.ArrayBlockingQueue;
public class MyTest{
public static void main(String[] args) {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//队列大小为3
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
//System.out.println(queue.add("d"));会抛出异常 IllegalStateException: Queue full队列满
System.out.println(queue.element());//检测队首元素
System.out.println("================");
System.out.println(queue.remove());
System.out.println(queue.element());//移除了a以后,队首是b
System.out.println(queue.remove());
System.out.println(queue.remove());
//System.out.println(queue.remove());会抛出异常 java.util.NoSuchElementException没有此元素
}
}
案例演示2
不抛出异常,有返回值
import java.util.concurrent.ArrayBlockingQueue;
public class MyTest{
public static void main(String[] args) {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//队列大小为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"));//不会抛出异常,有返回值false
System.out.println(queue.peek());//检测队首元素
System.out.println("================");
System.out.println(queue.poll());
System.out.println(queue.peek());//移除了a以后,队首是b
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());//不会抛出异常,有返回值null
}
}
案例演示3
阻塞等待
import java.util.concurrent.ArrayBlockingQueue;
public class MyTest{
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//队列大小为3
queue.put("a");//此方法无boolean返回值
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());程序不会停止,会一直阻塞在这
}
}
案例演示4
超时等待
这里用到offer和poll方法的重载
boolean offer(E e, long timeout, TimeUnit unit)
在该队列的尾部插入指定的元素,等待指定的等待时间,以使空间在队列已满时变为可用。
E poll(long timeout, TimeUnit unit)
检索并删除此队列的头,等待指定的等待时间(如有必要)使元素变为可用。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class MyTest{
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);//队列大小为3
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
//等待,超过两秒如果队列还是满的,则停止等待,并返回false
System.out.println(queue.offer("d",2, TimeUnit.SECONDS));
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
//等待,超过3秒如果队列还是空的,则停止等待,并返回null
System.out.println(queue.poll(3, TimeUnit.SECONDS));
}
}
(三)SychronousQueue同步队列
- 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素
- put了一个元素,必须从里面先take取出来,否则不能再put进去元素!
案例演示
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class MyTest{
public static void main(String[] args) {
BlockingQueue<String> queue = new SynchronousQueue();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put a");
queue.put("a");
System.out.println(Thread.currentThread().getName()+" put b");
queue.put("b");
System.out.println(Thread.currentThread().getName()+" put c");
queue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" take "+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" take "+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" take "+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
T1写了一个元素,不会紧接着再写一个,会等到被取出来才继续写。