目录
01.什么是JUC?
所谓JUC就是java并发工具包:java.util.concurrent
,学习JUC主要学习三个包的使用:
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
02.回顾多线程
进程和线程
进程:
- 进程是指在内存中运行的一个应用程序(例如:微信),每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。
- 进程不依赖于线程而独立存在,一个进程中可以启动多个线程。
线程:
- 线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如:Java程序默认有2个线程:mian线程和GC线程。
- 线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起
共享
分配给该进程的所有资源。
java开启多线程的两种方法
第一种:通过继承Thread类创建线程类,步骤如下:
- 1.定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的行体。
- 2.创建该类的实例对象,即创建一个线程对象。
- 3.使用线程对象调用start()方法来启动线程。
- 注意:也可以使用匿名内部类的方式创建和启动线程
- 一般的实现举例:
//使用这种方式不能达到资源共享的目的
public class Test {
public static void main(String[] args) {
//第二步:
MyThread myThread=new MyThread();
//第三步:
myThread.start();
}
}
//第一步:
class MyThread extends Thread{
@Override
public void run() {
System.out.println("线程执行的内容。。。");
}
}
- 使用匿名内部类的方式实现
public class Test {
public static void main(String[] args) {
//方式一,使用匿名继承Thread
new Thread("Thread_A") {
@Override
public void run() {
System.out.println("Thread_A线程执行的内容。。。");
}
}.start();
//方式二,使用lambda表达式
new Thread(()->{
System.out.println("Thread_B线程执行的内容。。。");
},"Thread_B").start();
}
}
第二种,通过实现Runnable接口创建线程类,步骤如下:
- 1.定义一个类实现Runnable接口;
- 2.创建该类的实例对象obj;
- 3.将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
- 4.调用线程对象的start()方法启动该线程;
//注意:使用这种方式可以达到资源共享的目的
public class Test {
public static void main(String[] args) {
//第二步:
MyThread myThread=new MyThread();
//第三步:
Thread thread=new Thread(myThread,"Thread_A");
//第四步:
thread.start();
//第三步和第四步的合并写法
new Thread(myThread,"Thread_B").start();
}
}
//第一步:
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("线程执行的内容。。。");
}
}
总结:
- Thread类本身也实现了Runable接口,并重写了Runbale接口提供的run方法。
- 第一种方式是继承Thread类,第二种方式是实现Runable接口。
- 虽然上面说的是两种方式启动多线程,但是本质都是使用了JDK提供的Thread类来启动多线程的。
- 第二种方式的实现是因为Thread类提供了入参为Runable类型的构造方法。
线程的状态:
Thread类中有一个内部枚举类state
,在该类中列出了一个线程可有的状态。
public enum State {
NEW, //新建
RUNNABLE, //运行
BLOCKED, //阻塞
WAITING, //等待
TIMED_WAITING, //超时等待
TERMINATED; //终止
}
线程等待:wait和sleep的对比
1.来自不同的类:
- Object.wait() :是线程进入等待状态。线程会放弃对象锁,等待被其他线程唤醒。
- Thread.sleep() :使线程暂停执行指定的时间,让出cpu给其它线程。但是它的监控状态依然保持,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
2.关于锁被释放
- wait会释放锁
- sleep不会释放锁
3.使用的范围不同
- wait必须在同步代码块中
- sleep可以在任何地方休眠
03.java.util.concurrent.locks.Lock(Lock锁)
Synchronized
在说Lock锁之前,先说一下Synchronized关键字。
场景一:把资源放在线程类中,如下(不建议使用):
public class Test001 {
public static void main(String[] args) {
TestThread testThread=new TestThread();
testThread.start();
}
}
class TestThread extends Thread{
private int ticket=50;//资源
@Override
public void run() {
if(ticket>0){
ticket--;
System.out.println(Thread.currentThread().getName()+ticket);
}
}
}
场景二:把资源和线程类分开,使用的时候直接把资源丢进线程中即可,为了安全使用synchronized
关键字给sale方法解锁,
public class SaleTicketDemo01 {
public static void main(String[] args) {
//并发:多线程操作同一个资源,把资源直接丢入线程
Ticket ticket=new Ticket();
new Thread(()->{//每个线程都买60次
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();
}
}
//资源类
class Ticket{
//定义票数
private int ticket=50;
//synchronized 本质:排队
public synchronized void sale(){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":sale:"+(ticket--)+" ticket,剩余:"+ticket);
}
}
}
Lock锁
Lock是java.util.concurrent.locks
包下的一个接口
,这个包下面一共三个接口
Lock接口有三个实现类
ReentrantLock :可重入锁
ReentrantReadWriteLock:可重入读写锁的两个内部类
- ReentrantReadWriteLock.ReadLock:可重入读写锁的读锁
- ReentrantReadWriteLock.WriteLock :可重入读写锁的写锁
使用方法:使用Lock接口的一个实现类ReentrantLock
使用Lock锁的三步:
1.Lock lock=new ReentrantLock();
2.lock.lock(); // 加锁
3.在finally代码块中使用:lock.unlock();// 解锁
示例:
public class SaleTiketDemo02 {
public static void main(String[] args) {
//并发:多线程操作同一个资源,把资源直接丢入线程
Ticket2 ticket=new Ticket2();
new Thread(()->{//每个线程都买60次
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();
}
}
//资源类
class Ticket2{
//定义票数
private int ticket=50;
//从底层源码可以看出,不加参数为非公平锁,加参数True为公平锁
Lock lock=new ReentrantLock();//第一步
public void sale(){
lock.lock();//第二步:
try {//业务代码
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":sale:"+(ticket--)+" ticket,剩余:"+ticket);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ReentrantLock :可重入锁,看一下这个类的构造方法
//有两个构造方法
//第一个构造方法,不穿参数,默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//第二个构造方法,传参数,true为公平锁,false为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:有先来后到的说法,要排队
非公平锁:可以插队
Synchronized 和 Lock 区别
Synchronized和Lock都是Java中用于实现线程同步的机制,但它们在实现方式、功能和性能上存在显著差异。
特性 | Synchronized | Lock(ReentrantLock) |
---|---|---|
实现层次 | JVM层面,Java内置关键字 | JDK层面,Java代码实现 |
锁的获取和释放 | 自动获取,自动释放(代码块执行结束或者异常时自动释放) | 手动获取(Lock.lock),手动释放(在finally代码块中调用Lock.unlock释放) |
锁类型 | 非公平锁 | 可选公平和非公平 |
锁状态查询 | 无法判断获取锁的状态 | 可以判断是否获取到了锁 |
锁等待机制 | 获取不到一直等待 | 可以设置获取锁的方式,lock()方法会一直等待,tryLock()方法完全不等待,tryLock(timeout, unit)有限时间等待 |
中断响应 | 不支持中断等待 | 支持 |
性能 | Java 6 后优化,性能近Lock | 高竞争情况下性能更好 |
代码灵活性 | 简单但是不灵活 | 灵活单逻辑复杂 |
使用场景 | 同步少量代码块 | 同步大量代码 |
04.线程之间的通讯(生产者和消费者问题)
生产者和消费者的问题,其实就是线程之间的通信问题。线程间通信是多线程编程中的核心问题,Java提供了多种机制来实现线程间的协调与数据交换。
synchronized版本
synchronized版本的生产消费问题:synchronized ,wait,notify
线程交替执行,四个线程A,B,C,D操作一个变量num(值在0-1之间)
* 生成者线程A,C执行:num+1
* 消费者线程B,D执行:num-1
问题:
* 当生产者线程A生产完之后,可能会唤醒生产者C,这时候num的值就有可能为2
* 当消费者线程B消费完之后,可能会唤醒消费者D,这时候num的值就有可能为-1
解决办法:
* 方法一:如果使用【if】的话,只会判断一次,可能还会出现上面的问题(虚假唤醒)
* 方法二:如果使用【循环】的话,把等待放在循环中,每次其他线程执行完,唤醒一个线程的时候都会进行判断
结论:
*使用if会出现虚假唤醒解决不了上面的问题,使用while循环则可以
为何使用if不行的具体原因:if中的条件只会判断一次,而while则会每次都判断,具体如下
* 1.num初始值为0。
* 2.线程A拿到锁,此时线程A开始执行加1操作,加1完成之后唤醒其他线程。
* 3.假如唤醒了线程C,由于num的值为1,进入判断条件,调用wait方法,释放对象锁。
* 4.假如此时B拿到锁,执行减1操作,减1完成之后唤醒其他线程
* 5.假如此时唤醒了线程A,此时线程A开始执行加1操作,加1完成之后唤醒其他线程。
* 6.假如唤醒了线程C,此时num的值虽然为1,但是if条件不会再次判断,此时线程C还会对num进行加1操作,从而出现上面的为2的情况。
* 7.如果使用while则需要再次判断,就可以避免上面的问题。
示例代码:
public class test01 {
public static void main(String[] args) throws InterruptedException{
//并发:多个线程共享一个资源资源
//创建多个线程,把资源直接丢入线程
Data data=new Data();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"A:producer").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"B:consumer").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"C:producer").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"D:consumer").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);
this.notifyAll();//通知
}
//-1
public synchronized void decrement()throws InterruptedException{
while(num==0){
this.wait();//等待
}
num--;//业务
System.out.println(Thread.currentThread().getName()+"==>"+num);
this.notifyAll();//通知
}
}
虚假唤醒是如何产生的
把 while (num != 0) {}
换成 if (num == 0) {}
就会出现虚假唤醒。官方文档有标注;
为什么if判断会出现虚假唤醒?
1. 因为if只会判断一次
2. 而while每次都会判断
JUC 版本
JUC 版本的生产消费问题:Lock,await,single
Lock:代替synchronized关键字的作用
通过lock.newCondition创建一个条件
condition.await:代替wait
condition.single:代替notify
通过刚才的演示可以发现,这种实现的效果和使用synchronized,wait,notify的效果是一样的
注意:里面也要使用while进行判断
任何一种新的技术绝不会只是对原来技术的一种覆盖,肯定有它的优势,和对原来技术的补充,
public class Test02 {
public static void main(String[] args) throws InterruptedException{
//并发:多个线程共享一个资源资源
//创建多个线程,把资源直接丢入线程
Data2 data=new Data2();
new Thread(()->{
for(int i=0;i<15;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"A:producer").start();
new Thread(()->{
for(int i=0;i<15;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"B:consumer").start();
new Thread(()->{
for(int i=0;i<15;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"C:producer").start();
new Thread(()->{
for(int i=0;i<15;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"D:consumer").start();
}
}
/*等待,业务,通知*/
class Data2{ //数据类,资源类
private int num=0;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
//+1
public void increment() throws InterruptedException{
try {
lock.lock();
while(num!=0) {
condition.await();//等待
}
num++;//业务
System.out.println(Thread.currentThread().getName()+"==>"+num);
condition.signalAll();//通知
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
//-1
public synchronized void decrement()throws InterruptedException{
try {
lock.lock();
while(num==0){
condition.await();//等待
}
num--;//业务
System.out.println(Thread.currentThread().getName()+"==>"+num);
condition.signalAll();//通知
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
java.util.concurrent.locks.Condition(精准唤醒)
java.util.concurrent.locks
一共有三个接口
Lock
Condition
ReadWriteLock
示例:
//Condition:实现精准的通知和唤醒作用
/*A执行完调用B
* B执行完调用C
* C执行完调用A
* */
public class Test03 {
public static void main(String[] args) throws InterruptedException{
//并发:多个线程共享一个资源资源
//创建多个线程,把资源直接丢入线程
Data3 data=new Data3();
new Thread(()->{
for(int i=0;i<12;i++){
try {
data.printA();
} catch (Exception e) {
e.printStackTrace();
}
}},"Thread_A").start();
new Thread(()->{
for(int i=0;i<12;i++){
try {
data.printB();
} catch (Exception e) {
e.printStackTrace();
}
}},"Thread_B").start();
new Thread(()->{
for(int i=0;i<12;i++){
try {
data.printC();
} catch (Exception e) {
e.printStackTrace();
}
}},"Thread_C").start();
}
}
/*等待,业务,通知*/
class Data3{ //数据类,资源类
private int flag=1;//flag为1 A线程执行,2 B 线程执行,3 C 线程执行
Lock lock=new ReentrantLock();
Condition conditionA=lock.newCondition();
Condition conditionB=lock.newCondition();
Condition conditionC=lock.newCondition();
public void printA(){
try {
lock.lock();
while (flag!=1){
conditionA.await();//等待
}
System.out.println(Thread.currentThread().getName()+".....AAAAA....");
flag=2;//唤醒B线程
conditionB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
try{
lock.lock();
while (flag!=2){
conditionB.await();//等待
}
System.out.println(Thread.currentThread().getName()+".....BBBBB....");
flag=3;//唤醒B线程
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
try {
lock.lock();
while (flag!=3){
conditionC.await();//等待
}
System.out.println(Thread.currentThread().getName()+".....CCCCC....");
flag=1;//唤醒B线程
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
总结
- 任何一种新的技术绝不会只是对原来技术的一种覆盖,肯定有它的优势,和对原来技术的补充。
- synchronized版本如法实现精准唤醒,虽然JDK提供了notify方法和notifyAll方法,但是这两种方法都不能实现精准唤醒
- notify方法:唤醒一个等待的线程,选择唤醒哪个线程不确定,也就是随机唤醒一个线程。
- notifyAll方法:唤醒所有等待的线程,它们都有机会争夺资源,具体那个线程能争夺到资源也不确定。
- JUC 版本可以实现synchronized版本的功能,还能在此基础上实现线程的精准唤醒。