1、JUC简介
指java.util包下的三个工具类
java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks
实现多线程共有三种方式,我们传统的是:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
使用实现Runnable几口的方式,代码没有返回值,效率相比较于实现Callable也比较低
2、线程和进程
一个进程包含多个线程
Java默认有两个线程:main线程、GC线程
Java是不能够开启线程的:他的底层调用的start0()是一个native方法,由底层的C++方法编写
private native void start0();
并发和并行
并发:CPU单核,多线个程操作同一个资源,通过快速交替的方式,达到一种并行的假象
并行:CPU多核,多个线程同时执行
我们可以使用代码的方式查看我们的电脑核数
package pers.mobian.demo01;
public class test01 {
public static void main(String[] args) {
//打印我们电脑的核数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:充分利用CPU的资源
线程的状态
根据源码可得,状态共六种:
public enum State {
//新生态
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待(死等)
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
wait和sleep的区别
1、来自不同的类
wait:来自Object类
sleep:来自Thread类
2、所的释放不同
wait:会释放锁(清醒着的人好说话)
sleep:不会释放锁(睡着了的人,你说好话求情,他听不见)
3、使用的范围是不同的
wait:只能在同步代码块中使用
sleep:可以在任何地方使用
4、概念上的不同
sleep是Thread类的静态方法。sleep的作用是让线程休眠指定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件时恢复线程执行。wait是Object的方权法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。
5、是否会抛出异常
很多地方都说wait()不会抛出异常,但是我自己在实验的时候,也是需要抛出异常的,这个点有待斟酌…
3、Lock锁(重点)
3.1、传统的synchronized锁:队列锁
利用synchronized锁资源的代码实现:
package pers.mobian.demo01;
public class test01 {
public static void main(String[] args) throws InterruptedException {
// 并发:多线程操作同一个资源类,
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口
new Thread(() -> {
for (int i = 1; i < 20; i++) {
//把资源类丢入线程
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket {
//共卖40张票
private int number = 30;
// 卖票的方式
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
}
}
3.2、Lock接口
根据JDK帮助文档,我们可得:
Lock锁是一个接口,其所有的实现类为:
ReentrantLock(可重入锁),ReentrantReadWriteLock.ReadLock(可重入读写锁中的读锁),ReentrantReadWriteLock.WriteLock(可重入读写锁中的写锁)
该锁的使用方式
//通过接口实例化Lock锁的具体实现类
Lock l = ...;
//加锁
l.lock();
try {
// access the resource protected by this lock
}
finally {
//解锁
l.unlock();
}
将上面的synchronized锁方法进行改写
public class lock {
public static void main(String[] args) throws InterruptedException {
//System.out.println(Runtime.getRuntime().availableProcessors());
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket2 ticket = new Ticket2();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket2 {
private int number = 30;
//1.实例化锁
Lock l = new ReentrantLock();
public void sale() {
//2.加锁
l.lock();
try {
//3.编写业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.解锁
l.unlock();
}
}
}
补充:公平锁非公平锁
当我们实例化ReentrantLock(可重入锁)时,可以根据我们传递得参数,继而使用公平锁和非公平锁。
public ReentrantLock() {
//无参构造非公平锁,默认使用非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
//传递为false时,使用公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:大家都十分得公平,遵循先来后到
非公平锁:大家不是同样的公平,可以出现插队的现象
3.3、synchronized和Lock锁的区别
1、synchronized是Java内置的关键字;Lock锁是Java的一个类
2、synchronized无法获取到锁的状态;Lock锁可以判断是否获取到了锁
3、synchronized会自动的释放锁;Lock必须要手动的去释放锁,如果不释放,就会造成死锁
4、synchronized中,线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着;Lock锁出现这种情况,可以使用tryLock()尝试获取锁
5、synchronized是不可中断的、非公平的、可重入锁;Lock锁是非公平的(可以自己设置)、可判断的、可重入锁
6、synchronized适合锁少量的同步代码;Lock锁适合锁大两的同步代码
7、synchronized有代码块锁和方法锁;Lock只有代码块锁
8、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(拥有更多的子类)
4、生产者和消费者问题
4.1、生产者和消费者:synchronized版
代码实现:
package pers.mobian.providerconsumer;
public class SynTest01 {
public static void main(String[] args) {
Num num = new Num();
//编写两个线程类
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
class Num {
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//业务代码
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//业务代码
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
}
在我们编写的一个简单的生产者和消费者的类中,如果我们出现两个以上的线程类的时候,就又会出现资源的不同步问题。
我们查阅JDK的帮助文档可得:
它推荐我们使用while,于是我们就可以将我们的代码修改为
package pers.mobian.providerconsumer;
public class SynTest01 {
public static void main(String[] args) {
Num num = new Num();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Num {
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number != 0) {
//业务代码
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
//业务代码
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
this.notifyAll();
}
}
总结:使用while代替if就可以防止虚假唤醒
4.2、JUC版的生产者和消费者
两种方式的转变:
修改synchronized锁的测试代码:
package pers.mobian.providerconsumer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest01 {
public static void main(String[] args) {
Num2 num = new Num2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Num2 {
private int number = 0;
//实例化锁
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//业务代码
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//业务代码
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
//通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
对比于传统的synchronized版本的形式,JUC版本使用起来更加的复杂,不过随之而来的是更多的优势!!!
4.3、Conditional实现精准的通知唤醒
参考JDK帮助文档可得:
对应的编写我们的业务代码:
package pers.mobian.providerconsumer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest02 {
public static void main(String[] args) {
Num3 num = new Num3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
num.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
class Num3 {
private int number = 2;
private Lock lock = new ReentrantLock();
//用这三个conditional对象,来标记不同
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
//具体方法的实现步骤:业务判断 --> 执行 --> 通知
//A
public void printA() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 1) {
condition1.await();
}
number = 2;
System.out.println(Thread.currentThread().getName() + "=>AAAAAAAAAAA");
//通知其他线程
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//B
public void printB() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 2) {
condition2.await();
}
number = 3;
System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBBBB");
//通知其他线程
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//C
public synchronized void printC() throws InterruptedException {
lock.lock();
try {
//业务代码
while (number != 3) {
condition3.await();
}
number = 1;
System.out.println(Thread.currentThread().getName() + "=>CCCCCCCCCC");
//通知其他线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5、关于锁的八种现象
我们传统的线程模板
public class Test01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
for (int i = 0; i < 45; i++) {
phone.call();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 45; i++) {
phone.send();
}
}, "B").start();
}
}
class Phone {
public void send() {
System.out.println("发短信");
}
public void call() {
System.out.println("打电话");
}
}
这种访问方式,是随机访问的
下文会使用sleep方法,主要的目的是为了方法实际的运行顺序
现象一:我们对方法使用的synchronized关键字,先执行打电话
现象二:让我们的线程sleep4秒钟,结果依然还是先执行打电话
package pers.mobian.lock8;
import java.util.concurrent.TimeUnit;
public class Test01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.call();
}, "A").start();
new Thread(() -> {
phone.send();
}, "B").start();
}
}
class Phone {
public synchronized void send() {
System.out.println("发短信");
}
public synchronized void call() {
//现象二时添加,让线程先睡4秒种
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
结论:synchronized锁的是我们方法的调用者,并且我们开启的两个线程方法使用的是同一把锁,那么就会出现谁先拿到,谁执行的现象。以至于我们让call方法sleep了四秒,依然是call方法先执行。
现象三:在之前代码的基础上,新添加一个普通的方法,此时先执行hello方法
现象四:我们新实例化一个Phone对象,使用不同得对象去调用方法,此时先执行发短信
package pers.mobian.lock8;
import java.util.concurrent.TimeUnit;
public class Test02 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
// phone1.hello();
phone2.send();
}, "B").start();
}
}
class Phone2 {
public synchronized void send() {
System.out.println("发短信");
}
public synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
总结:现象三中,由于这是一个普通的方法,所以就没有锁的效果,又由于时间的延迟,导致先打印hello。现象四中,由于我们使用了不同的对象,以至于synchronized锁的对象不是同一个,所以先打印发短信。
现象五:在之前代码的基础上,我们在方法前添加static关键字,此时先执行打电话
现象六:我们再添加一个对象,使用不同的对象进行方法的打印,此时依然是先执行打电话
package pers.mobian.lock8;
import java.util.concurrent.TimeUnit;
public class Test03 {
public static void main(String[] args) {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
//phone1.send();
phone2.send();
}, "B").start();
}
}
class Phone3 {
public static synchronized void send() {
System.out.println("发短信");
}
public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
总结:当我们添加了static关键字以后,此时我们先打印打电话,不仅是因为我们拿到的是同一个对象的锁,还因为我们的锁是直接锁的该类的Class模板。当我们再新添加一个对象的时,由于我们使用了static修饰,直接锁到了模板上,所以依然是先执行打电话。
现象七:当我们去掉一个锁方法的static关键字以后,此时先打印发短信
现象八:我们再新建一个对象后,使用不同的对象去调用,此时依然是先打印发短信
package pers.mobian.lock8;
import java.util.concurrent.TimeUnit;
public class Test04 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
// phone1.send();
phone2.send();
}, "B").start();
}
}
class Phone4 {
public synchronized void send() {
System.out.println("发短信");
}
public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
总结:现象七中,我们只添加一个static关键字,即模板Class中只有一部分被锁,则先打印出发短信。现象八中,我们又新建了一个对象,同理,由于我们锁的东西不一样(打电话锁模板,发短信锁对象),所以不冲突,那么就先执行发短信。
只有当锁的对象或者模板是同一个的时候,才能够借助调用的顺序来执行。
6、不安全的集合类
6.1、List
List是不安全的集合类
如何将它变得安全?有如下几种方法:
- 将ArrayList集合变成Vector集合
- 使用Collections.synchronizedList( new ArrayList<>() )的方式创建集合
- 使用CopyOnWriteArrayList<>()
第三种方式为写入时复制。它相比较于Vector,效率更加高效,因为Vector底层使用的是synchronized锁,而在JDK1.8中CopyOnWriyteArrayList底层是Lock锁(不过JDK11中貌似使用的又是synchronized锁)
6.2、Set
Set也是不安全的集合类
同样,将不安全的集合变成安全集合的方法:
- 使用Collections.synchronizedList( new HashSet<>() )的方式创建集合
- 使用CopyOnWriteArraySet<>()
补充:Set的底层是什么?(后面我也会写一些关于这三个集合的底层源码)
//默认的空参初始化方法
public HashSet() {
map = new HashMap<>();
}
//使用HashSet的add方法,依然是调用HashMap的底层put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
即HashSet的底层就是HashMap
6.3、Map
Map也是不安全的集合类
将不安全的集合变成安全集合的方法:
- 使用Collections.synchronizedMap( new HashMap() )的方式创建集合
- 使用ConcurrentHashMap<>()
7、Callable
由JDK帮助文档可得
相比较于之前的实现Runnable方法,实现Callable有哪些好处?
- Callable含有返回值
- Callable可以抛出异常
- Runnable使用的是run方法,而Callable则是call方法
由底层的源码可知,我们是没有办法直接调用这个实例的,我们需要在两者之间添加一层关系。
我们在Runnable的实现类中找到了一个FutureTask类,并且发现FutereTask是可以接受Callable对象的。
即,我们只需要创建一个FutureTask的实例对象,就可以用来接收实现Callable的线程类的实例对象,最后就只需要将FutureTask的实例放入线程中开启就可以了。
具体代码如下:
package pers.mobian.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread mythread = new Mythread();
FutureTask<String> stringFutureTask = new FutureTask<>(mythread);//适配类
new Thread(stringFutureTask,"A").start();
//如果再添加一条语句,结果还是打印一条结果,因为有缓存的存在,用于提高效率
new Thread(stringFutureTask,"B").start();
//这个get方法,可能会产生阻塞,应该放在代码的最后,或者使用异步通信来处理
System.out.println(stringFutureTask.get());
}
}
//传递的参数类型,就是我们需要返回时的参数类型
class Mythread implements Callable<String> {
public String call() {
System.out.println("成功调用call方法");
return "OK";
}
}
8、常用的辅助类
8.1、CountDownLatch
简单理解为减法计数器
测试代码实例:
package pers.mobian.add;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest01 {
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();//数量-1
}, String.valueOf(i)).start();
}
//等待计数器归零以后,再继续向下执行
countDownLatch.await();
System.out.println("Close door");
}
}
总结:countDownLatch.countDown()每执行一次,数量就会 -1 ,当数量变成0以后,countDownLatch.await()就会被唤醒,继而执行后面的方法
8.2、CyclicBarrier
减法器
package pers.mobian.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest01 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("上了五天课,可以回家休息了");
});
for (int i = 1; i <= 5; i++) {
//使用lambda表达式,相当于重新编写了一个类,是没有办法直接使用到上级的变量的
//此时需要我们自己定义一个临时变量来接受
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();
}
}).start();
}
}
}
总结:当i变成5以后,就会执行cyclicBarrier对象中的接口方法。值得注意的是,如果我们设置的parties比较小的话,就会提前执行该方法
8.3、Semaphore
计数器
package pers.mobian.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreTest01 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
//获取
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "占位三秒钟");
TimeUnit.SECONDS.sleep(3);
System.out.println("三秒钟到" + Thread.currentThread().getName() + "离开位置");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
总结:semaphore.acquire()假如我们的信号量已经满了,就会等待,直到semaphore释放;semaphore.release()会将当前的信号量释放,然后唤醒等待的线程。
作用:多个共享资源互斥的使用、并发限流、控制最大的线程数。
转载:https://blog.csdn.net/qq_44377709/article/details/105845427