JUC高并发编程
线程和进程的概念
进程和线程
进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;
线程:是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。
线程的状态
- NEW(新建)
- RUNNABLE(准备就绪)
- BLOCKED(阻塞)
- WAITING(不见不散)
- TIMED_WAITING(过时不候)
- TERMINATED(终结)
wait和sleep的区别
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 它们都可以被interrupted方法中断
并发和并行
- 串行模式:串行表示所有任务都–按先后顺序进行
- 并行模式:并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。
- 并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
- 例子:春运抢票,电商秒杀…
- 并行:多项工作一起执行,之后再汇总
- 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
管程
管程:在操作系统中叫Monitor(监视器),在java中叫锁。是一种同步机制,保证同一时间,只有一个线程访问被保护数据或者代码。
用户线程和守护线程
用户线程:自定义线程,我们平时使用的都是用户线程
public class Main_ {
public static void main(String[] args) {
Thread aaa = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
while (true){
}
}, "aaa");
//isDaemon:返回false就是用户线程,返回true就是守护线程
aaa.start();
System.out.println(Thread.currentThread().getName()+"执行完了");
}
}
/*
main执行完了
aaa::false
*/
主线程结束了,用户线程还在运行,jvm存活
守护线程:比如垃圾回收线程
public class Main_ {
public static void main(String[] args) {
Thread aaa = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
while (true){
}
}, "aaa");
//isDaemon:返回false就是用户线程,返回true就是守护线程
aaa.setDaemon(true); //守护线程
aaa.start();
System.out.println(Thread.currentThread().getName()+"执行完了");
}
}
/*
main执行完了
*/
没有用户线程了,都是守护线程,jvm结束
Lock接口
复习Synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
Synchronized实现卖票:3个售票员卖30张票
1、创建资源类,定义属性和方法
2、创建多个线程、调用资源类的操作方法
class Ticket {
//第一步:创建资源类,定义属性和方法
private int number = 30;
//操作方法:卖票
public synchronized void sale(){
//判断是否有票
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出了:"+(30-number)+"票"+"还剩:"+number--);
}
}
}
public class SaleTicket {
public static void main(String[] args) {
//第二步:创建Ticket对象
Ticket ticket = new Ticket();
//创建三个线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"CC").start();
}
}
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock
就可以办到。
什么是Lock接口
为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器
Lock与的Synchronized区别
-
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
-
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
使用Lock实现卖票例子
class Ticket {
private int number = 30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
//卖票方法
public void sale(){
try {
//上锁
lock.lock();
//判断是否有票
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出了:"+(30-number)+"还剩余:"+number--+"张票");
}
}finally {
//解锁
lock.unlock();
}
}
}
public class LSaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//创建三个线程
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
/*
start()方法:当我们new Thread并调用start()方法的时候,这个线程创建了吗?是不一定的,可能创建了,可能过一会创建,在start()底层源码里,
有一个native关键字,当我们方法到这,java就无能为力了,native会去调操作系统来完成,具体什么创建,看操作系统
*/
Lock和synchronized有以下几点不同:
-
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
-
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
-
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
-
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
-
Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
创建线程的多种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口
- 使用线程池
线程间通信
上面我们实现了两个步骤,现在我们要在中间加上一个步骤:资源类操作方法
第一步:创建资源类,在资源类创建属性和操作方法
第二步:在资源类操作方法
(1)判断
(2)干活
(3)通知
第三步:创建多个线程,调用资源类的操作方法
例子:有两个线程:实现对一个初始值是0的变量,一个线程对值+1,一个线程对值-1
然后+1 再-1
通过synchronized实现:
class Share {
//第一步:创建资源类,定义属性和方法
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步:判断干活,通知
if (number != 0){ //判断number值是否是0,如果不是0,等待
this.wait();
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
if (number != 1){
this.wait();
}
//如果number值是1,就-1操作
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
notifyAll();
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
}
}
虚假唤醒问题
synchronized实现
上面的代码是有问题的:当我们再增加两条线程CC 和DD,CC执行+1操作,DD执行-1操作,四条线程同时执行,我们会发现,居然出现了-1这种现象,我们程序的设计是不应该出现-1的啊!
我们的wait()方法有一个特点就是在哪里睡就在哪里醒,假如AA先得到锁进行+1操作后,释放锁,刚好被CC抢到,但是这时的CC处于等待状态,所以CC就释放锁,然后被AA拿到,此时的AA也是处于等待状态,AA会释放锁,刚好又被CC抢到锁,但是这时的CC就不会进行判断,他是在判断以后睡着的,在哪里睡就在哪里醒,所以他会直接往下执行操作,这种现象叫做虚假唤醒问题!怎么防止呐?
把if改为while,while循环,不管你在什么地方睡,醒来都要经过判断。
Lock实现
使用ReentrantLock
实现,lock里面有一个方法lock.newCondition();
class Share{
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo02 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
线程间定制化通信
启动三个线程,按照如下要求:
- AA打印5次,BB打印10次,CC打印15次
- AA打印5次,BB打印10次,CC打印15次
- AA打印5次,BB打印10次,CC打印15次
- 进行5轮
class ShareResource{
//定义标志位
private int flag = 1;
//创建lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 1){
condition1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" 轮数 :"+loop);
}
//修改标志位
flag = 2;
//通知BB线程
condition2.signal();
}finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2){
condition2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" 轮数 :"+loop);
}
//修改标志位
flag = 3;
//通知CC线程
condition3.signal();
}finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3){
condition3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" 轮数 :"+loop);
}
//修改标志位
flag = 1;
//通知CC线程
condition1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo03 {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
try {
resource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
try {
resource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
try {
resource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
}
}
集合的线程安全
ArrayList集合线程不安全演示
我们使用ArrayList演示:因为ArrayList是不安全的,他的方法里面没有synchronized关键字
public class ThreadDemo04 {
public static void main(String[] args) {
List<String> list = new ArrayList();
for (int i = 0; i < 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
当我们从集合中取值的时候,可能还没有添加进去,或者有多个线程对这个数据进行操作!会造成并发修改异常 ``ConcurrentModificationException
解决方案:Vector
Vector是线程安全的:它的方法上面都有synchronized关键字,但是我们一般都不使用这种方法
public class ThreadDemo04 {
public static void main(String[] args) {
// List<String> list = new ArrayList();
List<String> list = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
解决方案:Collections
Collections中有一个方法synchronizedList
public class ThreadDemo04 {
public static void main(String[] args) {
// List<String> list = new ArrayList();
// List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
这种方式也是有一点古老,但是也能解决问题,我们经常使用一个类CopyOnWriteArrayList
解决方案::CopyOnWriteArrayList
CopyOnWriteArrayList
源码:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
public class ThreadDemo04 {
public static void main(String[] args) {
// List<String> list = new ArrayList();
// List<String> list = new Vector<>();
// List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
HashSet线程不安全问题
public class ThreadDemo05 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
在我们的java.util.concurrent
这个包下有一个类CopyOnWriteArraySet
是线程安全的,我们可以使用它
public class ThreadDemo05 {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
HashMap线程不安全问题
public class ThreadDemo05 {
public static void main(String[] args) {
Map<String,String> map = new HashMap();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
map.put(key,UUID.randomUUID().toString().substring(0,8));
System.out.println(map); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
在我们的java.util.concurrent
这个包下有一个类ConcurrentHashMap
是线程安全的,我们可以使用它
public class ThreadDemo05 {
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
map.put(key,UUID.randomUUID().toString().substring(0,8));
System.out.println(map); //并发修改异常 ConcurrentModificationException
},String.valueOf(i)).start();
}
}
}
Synchronized锁的八种情况
class Phone{
public synchronized void sendSMS() throws InterruptedException {
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail(){
System.out.println("------sendEmail");
}
public void getHello(){
System.out.println("------getHello");
}
}
public class EightSynchronized {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone1 = new Phone();
new Thread(()->{
try {
phone.sendSMS();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
//phone.sendEmail();
//phone.getHello();
phone1.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
},"BB").start();
}
}
/*
1.标准访问,先打印短信还是邮件(先执行sendSMS(),锁的是当前这个对象this,phone,假如是两个对象phone1,那么久不是同一把锁)
------sendSMS
------sendEmail
2.停4秒在短信方法内,先打印短信还是邮件(同上)
------sendSMS
------sendEmail
3.新增普通方法的hello方法,是先发短信还是输出hello(hello方法没有锁,所以它直接执行,先输出)
------getHello
------sendSMS
4.现在有两部手机,先打印短信还是邮件(两部手机就是两个对象,不是同一把锁)
------sendEmail
------sendSMS
5.两个静态同步方法,1部手机,先打印短信还是邮件(静态同步方法加synchronized,锁的是当前类的Class对象(字节码文件),可以把static想象成一栋楼的锁,synchronized想象成楼里面一个房间的锁)
------sendSMS
------sendEmail
6.两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7.1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件(锁的范围不同,用的锁不同,锁住了大楼,但是房间的锁和它没关系)
------sendEmail
------sendSMS
8.1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS
*/
公平锁和非公平锁
我们上面有一个卖票的例子,我们运行程序发现我们的AA几乎把所有的票都卖出去了,我们这里就用的是非公平锁。
非公平锁:会导致我们其他线程饿死,但是效率高
公平锁:体现了一种阳光普照的意思,每个线程都有票卖,效率相对低
怎么设置我们的锁是公平的还是非公平的呐?
ReentrantLock lock = new ReentrantLock();
这里面可以设置参数:是一个布尔类型
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
通过源码我们可以发现,当我们使用无参构造创建锁的时候它其实是一个非公平锁,有参构造创建锁的时候他会判断你传入的布尔类型是true还是false,然后通过三元运算符判断是公平还是非公平,当我们传入true的时候就是公平锁
效果:
死锁
1、什么是死锁:两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去
2、产生死锁的原因:
第一:系统资源不足
第二:进程运行推进顺序不合适
第三:资源分配不当
实现一个死锁
public class DeadLock {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 持有锁a ,试图获取锁b");
synchronized (b){
System.out.println(Thread.currentThread().getName()+" 获取锁b");
}
}
},"AA").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+" 持有锁b ,试图获取锁a");
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 获取锁a");
}
}
},"BB").start();
}
}
发现程序停止不了
那在工作中怎么证明我们的程序停止不了是死锁造成的,还是死循环,或者其他的原因,我们可以通过两个命令行
第一个是jps
,第二个是jstack
是jvm自带堆栈跟踪工具,我们去试一下
找到我们带有我们类信息的那条记录,然后用jstack+记录前面的编号
然后就可以看到我们类的消息向下翻会发现,jstack发现了一个死锁:并指出相关信息
Callable接口
Runnable接口和Callable接口比较
接口 | 是否有返回值 | 是否抛出异常 | 实现方法 |
---|---|---|---|
Runnable | 没有返回值 | 不抛出异常 | run() |
Callable | 有返回值 | 无法计算结果会抛出异常 | call() |
我们创建两个类来实现两个接口Runnable接口和Callable接口
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable{
@Override
public Integer call() throws Exception {
return 200;
}
}
然后我们创建两个线程,一个放Runnable接口的实现类MyThread1,一个放Callable接口的实现类MyThread2
new Thread(new MyThread1()).start();
new Thread(new MyThread2()).start(); 会报错
我们发现第二种方式会报错,是因为Thread的构造方法中没有Callable的,只有Runnable接口的
我们需要一个中间商,构造方法里面既有Runnable接口又有Callable接口,它就是FutureTask
我们用FutureTask来实现一下Runnable和Callable接口创建线程
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" come in callable");
return 200;
}
}
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(new MyThread1()).start();
//new Thread(new MyThread2()).start(); 会报错
FutureTask futureTask1 = new FutureTask<>(new MyThread2());
//lamb表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in Callable");
return 1024;
});
//创建一个线程
new Thread(futureTask2,"lucy").start();
new Thread(futureTask1,"mark").start();
//判断计算是否完成
// while (!futureTask2.isDone()){
// System.out.println("wait.....");
// }
//通过get方法来获得返回值
System.out.println(futureTask2.get());
//上面输出返回值的时候进行了一系列计算,第二次获得的时候直接输出
System.out.println(futureTask2.get());
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
JUC强大的辅助类
减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
场景:6个同学陆续离开教室后班长才可以关门
public class CountDownLatchDemo {
public static void main(String[] args) {
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
},String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName()+"班长关门走人了!");
}
}
当我们没有使用CountDownLatch的时候我们的输出结果会出现班长关门了,结果教室还有人的情况,改进
public class CountDownLatchDemo {
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()+" 号同学离开了教室");
//计数 -1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"班长关门走人了!");
}
}
6 号同学离开了教室
2 号同学离开了教室
1 号同学离开了教室
5 号同学离开了教室
3 号同学离开了教室
4 号同学离开了教室
main班长关门走人了!
循环栅栏CyclicBarrier
CyclicBarrier看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数就是目标障碍数,每次执行CyclicBarrier一次障碍数就会加一,如果到达了目标障碍数,才会执行cyclicBarrier.await()之后的语句,可以将CyclicBarrier理解为加一操作。
场景:集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
//设置固定值
private static final int NUMBER = 7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("集齐七颗龙珠可以召唤神龙");
});
for (int i = 1; i <= 7; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 颗龙珠被集齐了");
//等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
5 颗龙珠被集齐了
2 颗龙珠被集齐了
3 颗龙珠被集齐了
1 颗龙珠被集齐了
6 颗龙珠被集齐了
4 颗龙珠被集齐了
7 颗龙珠被集齐了
集齐七颗龙珠可以召唤神龙
信号灯Semaphore
6辆车抢3个停车位:
public class SemaphoreDemo {
public static void main(String[] args) {
//设置三个车位
Semaphore semaphore = new Semaphore(3);
//6辆车去抢
for (int i = 1; i <= 6 ; i++) {
new Thread(()->{
try {
//抢占车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 抢到了车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+" -----------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
1 抢到了车位
2 抢到了车位
4 抢到了车位
4 -----------离开了车位
3 抢到了车位
1 -----------离开了车位
2 -----------离开了车位
6 抢到了车位
5 抢到了车位
6 -----------离开了车位
3 -----------离开了车位
5 -----------离开了车位
读写锁
悲观锁
第一个人在对账户进行操作的时候他会对账户进行上锁,那么其他人就不能对账户进行操作,别人就是阻塞或者等待状态,只有当上锁的人把锁解开以后,其他人才能对账户进行操作。不管哪个人对账户进行操作前都会先上锁,然后解锁。好处是能解决并发中的各种问题,缺点是不支持并发操作,效率低
乐观锁
两个或者多个人可以对账户进行操作,乐观锁会对修改前的账户加上一个版本号,第一个每个人都会获得相同的版本号,当有一个人对事务进行提交以后,他会对版本号进行一个修改,另外一个人再提交之前会对版本号进行比较,比较自身的版本号和数据库的版本号是否相同,如果不相同就提交失败。
表锁
一张表里面有很多行数据,当对其中一行表进行操作的时候,他会对整张表进行上锁,表锁不会发生死锁
行锁
假如想对第一条数据进行操作,那就只对第一行数据进行上锁,别人就不能对第一条数据进行操作,但是可以对其他行数据进行操作,行锁会发生死锁
读锁
共享锁:会发生死锁
写锁
独占锁:会发生死锁
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享
ReadWriteLock rwLock = new ReentrantReadWriteLock();
class MyCache{
//创建map集合
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key , Object value){
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
//读取数据
public Object get(String key){
rwLock.readLock().lock();
Object result = null;
//暂停一会
try {
System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5 ; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5 ; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
锁降级:将写锁降级为读锁
获取写锁–>获取读锁–>释放写锁–>释放读锁
在写的时候,可以读,但是在读的时候不能写
public class Demo01 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//1.获取写锁
writeLock.lock();
System.out.println("write-----");
//2.获取读锁
readLock.lock();
System.out.println("read-------");
//3.释放写锁
//4.释放读锁
}
}
我们在写的时候可以读,程序正常执行
然后我们把第三步和第四步写上,程序也可以正常执行,这个过程就是把写锁降级为写锁
但是读锁不能升级为写锁
public class Demo01 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//2.获取读锁
readLock.lock();
System.out.println("read-------");
//1.获取写锁
writeLock.lock();
System.out.println("write-----");
//3.释放写锁
//writeLock.unlock();
//4.释放读锁
//readLock.unlock();
}
}
我们程序停不下来了,证明我们读的时候是不能写的
BlockingQueue阻塞队列
阻塞队列,顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出:
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图将空的队列中获取元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使得队列变得空闲起来并后续新增
常见的BlockingQueue
ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常见的阻塞队列,处理一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置,总的来说是由数组结构组成的有界阻塞队列
LinkedBlockingQueue
由链表结构组成的有界阻塞队列(大小默认Integer.MAX_VALUE)
DelayQueue
使用优先级队列实现的延迟无界阻塞队列
PriorityBlockingQueue
支持优先级排序的无界阻塞队列
SynchronousQueue
不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue
由链表组成的无界阻塞队列
LinkedBlockingDeque
由链表组成的双向阻塞队列
BlockingQueue核心方法
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//创建阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//第一组
System.out.println(blockingQueue.add("a")); //true
System.out.println(blockingQueue.add("b")); //true
System.out.println(blockingQueue.add("c")); //true
//System.out.println(blockingQueue.element());
//System.out.println(blockingQueue.add("w")); //抛出异常--> IllegalStateException: Queue full
System.out.println(blockingQueue.remove()); //a
System.out.println(blockingQueue.remove()); //b
System.out.println(blockingQueue.remove()); //c
//System.out.println(blockingQueue.remove()); //抛出异常--> NoSuchElementException
//第二组
System.out.println(blockingQueue.offer("a")); // true
System.out.println(blockingQueue.offer("b")); // true
System.out.println(blockingQueue.offer("c")); // true
System.out.println(blockingQueue.offer("www")); // false
System.out.println(blockingQueue.poll()); // a
System.out.println(blockingQueue.poll()); // b
System.out.println(blockingQueue.poll()); // c
System.out.println(blockingQueue.poll()); // null
//第三组
blockingQueue.put("a"); //a
blockingQueue.put("b"); //b
blockingQueue.put("c"); //c
blockingQueue.put("w"); //程序一直阻塞,不能停止
System.out.println(blockingQueue.take()); //a
System.out.println(blockingQueue.take()); //b
System.out.println(blockingQueue.take()); //c
System.out.println(blockingQueue.take()); //程序一直阻塞,不能停止
//第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d",3L, TimeUnit.SECONDS));//阻塞超过3秒,就自动结束程序
}
}
ThreadPool线程池
什么是线程池?
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。
线程池特点:
(1)降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的优势:
线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
线程池使用方式:
Executors.newFixedThreadPool(int)一池N线程
public class ThreadPoolDemo01 {
public static void main(String[] args) {
//一池五线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool1.execute(()->{//execute里面是Runnable接口,所以可以使用lamb表达式
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool1.shutdown();
}
}
}
pool-1-thread-1 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
pool-1-thread-5 办理业务
pool-1-thread-2 办理业务
pool-1-thread-5 办理业务
pool-1-thread-4 办理业务
pool-1-thread-3 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
Executors.newSingleThreadExecutor()一池一线程
public class ThreadPoolDemo01 {
public static void main(String[] args) {
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool2.execute(()->{//execute里面是Runnable接口,所以可以使用lamb表达式
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool2.shutdown();
}
}
}
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
Executors.newCachedThreadPool()一池可扩容线程
public class ThreadPoolDemo01 {
public static void main(String[] args) {
//一池可扩容线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool3.execute(()->{//execute里面是Runnable接口,所以可以使用lamb表达式
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool3.shutdown();
}
}
}
他们的底层原理都是去new了一个ThreadPoolExecutor,通过它来实现我们线程池的创建
线程池的七个参数
public ThreadPoolExecutor(int corePoolSize, //常驻线程数量或核心线程数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,用于创建线程
RejectedExecutionHandler handler) //拒绝策略
线程池工作流程和拒绝策略
线程池工作流程:
四种拒绝策略:
1、 AbortPolicy: 直接抛异常,阻止系统正常工作;
2、 CallerRunsPolicy: 将任务分给调用线程来执行,运行当前被丢弃的任务,这样做不会真的丢弃任务,但是提交的线程性能有可能急剧下降 (谁让你来的,你就去找谁)
3、 DiscardOldestPolicy: 丢弃队列中最老的任务, 尝试再次提交当前任务;
4、 DiscardPolicy: 默默丢弃无法处理的任务,不予任何处理;
自定义线程池
public class ThreadPoolDemo02 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(()->{//execute里面是Runnable接口,所以可以使用lamb表达式
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
分支合并框架
Fork/Join
Fork/Join它可以将一个大的任务拆分成多个子任务进行合并处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join框架主要完成两件事情:
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
举例:1+2+3+…+100
class MyTask extends RecursiveTask<Integer>{
//拆分值不能超过10,计算10以内的运算
private static final Integer VALUE = 10;
private int begin;//拆分开始值
private int end;//拆分结束值
private int result;//返回结果
//创建有参构造
public MyTask(int begin,int end){
this.begin = begin;
this.end = end;
}
//拆分和合并过程
@Override
protected Integer compute() {
//判断相加两个数值是否大于10
if ((end-begin<=VALUE)){
for (int i = begin; i <= end; i++) {
result = result+i;
}
}else { //进一步拆分
//获取中间值
int middle = (begin+end)/2;
//拆分左边
MyTask task01 = new MyTask(begin, middle);
//拆分右边
MyTask task02 = new MyTask(middle+1, end);
//调用方法进行拆分
task01.fork();
task02.fork();
//合并结果
result = task01.join()+task02.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
MyTask myTask = new MyTask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);
//获取最终合并之后结果
Integer result = submit.get();
System.out.println(result);//5050
//关闭池对象
forkJoinPool.shutdown();
}
}
异步回调
异步回调一般有专门的工具:mq消息队列,这里简单写一下同步和异步的输入输出
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//同步调用
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+":completableFuture1");
});
completableFuture1.get(); //ForkJoinPool.commonPool-worker-3:completableFuture1
//同步调用
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+":completableFuture2");
//异常模拟
int i = 10/0;
return 1024;
});
completableFuture2.whenComplete((t,u)->{
System.out.println("------t"+t); //t代表我们的返回值
System.out.println("------u"+u); //u代表我们的异常 --->ujava.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).get();
}
}
“十一月要买漂亮衣服,十二月有白雪和圣诞树,一月里来是新年,二月天气回暖,樱花和春天,或许还有爱情。
一点一点数着过,日子也就不难熬了。 ”