多线程进阶=>JUC并发编程
Posted dxj1016
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程进阶=>JUC并发编程相关的知识,希望对你有一定的参考价值。
多线程进阶=>JUC并发编程
准备环境:创建maven项目,修改项目配置为8,否则lambdas用不了。修改setting中的java compiler为8。
1、什么是JUC
- JUC就是java.util.concurrent下面的工具包,专门用于多线程的开发。(源码+官方文档 面试高频问)
- 在 Java 5.0 提供了 java.util.concurrent (简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
业务:普通的线程代码 Thread
Runable 没有返回值,效率相比Callable相对较低!
2、 线程和进程
一句话概括线程和进程
进程是操作系统中的应用程序、是资源分配的基本单位,线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位
2.1、进程
进程: 在操作系统中运行的应用程序
一个进程往往可以包含多个线程,至少包含一个
2.2、线程
线程: 线程是CPU调度和执行的单位。开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)
- 对于Java而言:Thread、Runable、Callable进行开启线程的。
- Java默认有几个线程?2个线程! main线程、GC线程
提问?JAVA真的可以开启线程吗? 开不了的!
Java是没有权限去开启线程、操作硬件的,点开start的原码可以看到这是一个线程安全的,通过add加入线程组, 点开start0()方法发现这是一个native的一个本地方法,底层是C++。所以java是没有权限直接操作硬件的,由操作系统来开启线程。start()和strat0()方法的源码如下:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//这是一个C++底层,Java是没有权限操作底层硬件的
private native void start0();
2.3、并发
并发编程:并发,并行
并发: 多线程操作同一个资源。
- CPU 只有一核,只能操作一个线程,但是可以模拟出来多条线程。那么我们就可以使用CPU快速交替,来模拟多线程。看起来是同一时间操作多个线程操作同一个资源,其实本质还是一个线程执行,由于速度非常快,所以看起来是多个线程在执行。
- 并发编程的本质:充分利用CPU的资源!
2.4、并行
并行: 多个人一起行走
- CPU多核,多个线程可以同时执行。
- 提高并行效率 我们可以使用线程池!
获取cpu的核数
public class Test1 {
public static void main(String[] args) {
//获取cpu的核数
//CPU密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
2.5、线程的状态
6种状态,Thread.state查看源码如下:
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待,死死等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
2.6、wait/sleep
1、来自不同的类
wait => Object
sleep => Thread
一般情况企业中使用休眠是:
TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
2、关于锁的释放
-
wait 会释放锁;
-
sleep睡觉了,不会释放锁;
3、使用的范围是不同的
-
wait 必须在同步代码块中;
-
sleep 可以在任何地方睡;
4、是否需要捕获异常
- wait和sleep都需要捕获异常
- notify和notifyall都不需要捕获异常
3、Lock锁(重点)
3.1、传统的 synchronized
package com.marchsoft.juctest;
import lombok.Synchronized;
/**
* Description:synchronized
* 基本的卖票例子
* 真正的多线程开发,公司中的开发
* 线程就是一个单独的资源类,没有任何附属的操作
* 1、属性,方法。
**/
public class Demo01 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类
final Ticket ticket = new Ticket();
//new Runnable()看源码可以发现他是一个@FunctionalInterface函数式接口,接口不能实例化,但是可以用匿名内部类实现他,但是这个比价繁琐,所以在jdk1.8,可以使用lambdas表达式(参数)->{代码}实现。
/*
new Thread(new Runnable(){
@Override
public void run(){
}
}).start();
*/
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"C").start();
}
}
// 资源类 OOP
class Ticket {
//属性、方法
private int number = 30;
//卖票的方式
public void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
}
}
- 代码开启了三个线程
- 运行结果出现了不符合实际情况,卖出的票跟剩余的票和总数不对应,原因是多个线程可能争夺同一个资源
- 改变上面的代码,在sale方法加上synchronized,可以解决上述问题。
- synchronized 本质:队列,锁
- 这个锁的是对象或者class
3.2、Lock(重点)
Lock接口
公平锁: 十分公平,必须先来后到~;
非公平锁: 十分不公平,可以插队;(默认为非公平锁)
package com.marchsoft.juctest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
package demo02;
import jdk.nashorn.internal.ir.CallNode;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test1 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类
Ticket2 ticket = new Ticket2();
/*
new Runnable()看源码可以发现他是一个@FunctionalInterface函数式接口,接口不能实例化,
但是可以用匿名内部类实现他,但是这个比价繁琐,
所以在jdk1.8,可以使用lambdas表达式(参数)->{代码}实现。
*/
/*
new Thread(new Runnable(){
@Override
public void run(){
}
}).start();
*/
new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"A").start();
new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"B").start();
new Thread(() ->{ for (int i = 1; i <40 ; i++)ticket.sale(); },"C").start();
}
}
// 资源类 OOP
// 不再使用synchronized实现多线程,而是使用Lock锁。
/*lock三部曲
1、new ReentrantLock();
2、lock.lock();
3、finally=>lock.unlock();
*/
class Ticket2 {
//属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
//卖票的方式
public void sale() {
// 执行之前加锁,执行完毕解锁
lock.lock();
//try-catch-finally:Ctrl+Alt+T
try {
// 业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
3.3、 Synchronized 与Lock 的区别
- Synchronized 内置的Java关键字,Lock是一个接口
- Synchronized 无法判断获取锁的状态,Lock可以判断
- Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!如果不释放锁,可能会遇到死锁
- Synchronized 线程1(获得锁->阻塞)、线程2(一直等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
- Synchronized 是可重入锁,不可以中断的,非公平的;Lock是接口,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
- Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
锁是什么,如何判断锁的是谁?
4、生产者和消费者问题
面试的:单例模式、排序算法、生产者消费者问题,死锁问题。
4.1、Synchronzied 版本
package pc;
/**
* 线程之间的通信问题:生产者和消费者问题
* 线程交替执行 A B操作同一个变量 num=0
* A num+1
* B num-1
*/
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
//判断等待,业务,通知
class Data {//数字,资源类
private int num = 0;
//只要是并发编程一定会有锁。
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
if (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
if (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
/*
执行结果:
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
*/
4.2、存在问题(虚假唤醒)
问题,如果有四个线程,会出现虚假唤醒
两个生产者线程,两个消费者线程,会虚线虚假唤醒,
- if判断流水线状态为空时,线程被阻塞,这时if判断就完成了,线程被唤醒后直接执行线程剩余操作。
- while判断流水线状态为空时,线程被阻塞,这时的while循环没有完成,线程被唤醒后会先进行while判断,然后执行后面的操作。
- if只判断了一次,等待队列的多生产者被消费者唤醒后重新进入同步队列,这时候抢到锁,会从if下面的语句开始执行,相当于没了判断。
- 为什么if改成while之后就不会出现虚假唤醒问题了,因为while会一直判断,线程没有唤醒就出不来,比消费者唤醒生产者,其它2两个线程都会争夺锁,如果其中一个生产者抢到锁了,那么他会从等待位置醒来,while的时候,就会先循环判断条件,如果不满足条件才会出来,否则还会继续等待,但是if就不一样了,if判断一次就结束了,所以从wait醒来的时候,早就过了if判断语句,因此他会执行wait方法下面的操作。
虚假唤醒:就是当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功。
解决方式 ,if 改为while即可,防止虚假唤醒
package com.marchsoft.juctest;
package pc;
/**
* 线程之间的通信问题:生产者和消费者问题
* 线程交替执行 A B操作同一个变量 num=0
* A num+1
* B num-1
*/
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
//判断等待,业务,通知
class Data {//数字,资源类
private int num = 0;
//只要是并发编程一定会有锁。
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
while (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
while (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
以上是关于多线程进阶=>JUC并发编程的主要内容,如果未能解决你的问题,请参考以下文章
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕(
可以覆盖上次的打印内)等待多个远程调用结束)(代码片段