JUC并发编程

JUC是什么?

JUC,即java.util.concurrent包的缩写,是java原生的并发包和一些常用的工具类。
JUC

线程基础知识

线程和进程
进程:计算机中运行中的程序,如QQ.exe等。
线程:进程中执行的具体的任务,如打字、自动保存等。
一个进程可以包含多个线程,一个进程至少有一个线程。Java程序至少有两个线程:GC线程Main线程
并发和并行
并发:多个线程操作同一个资源并且交替执行的过程。
并行:多个线程同时执行,只有在多核CPU下才能完成。
使用多线程或者并发编程的目的:提高效率,让CPU一直工作,达到最高的处理性能。
线程的状态
线程有6种状态,我们可以从源码中查看具体是哪6种状态。

  public enum State {
    // java能够创建线程吗? 不能!
 	// 新建
    NEW,

    // 运行
    RUNNABLE,

    // 阻塞
    BLOCKED,

    // 等待
    WAITING,

    // 延时等待
    TIMED_WAITING,

    // 终止!
    TERMINATED;
}

很显然,线程的六种状态分别是:新建(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITTING)、延时等待(TMED_WAITTING)、终止(TERMINATED)。
wait和sleep的区别

  • 类不同
    wait是属于Object类的方法,sleep是Thread类的方法。在JUC编程中,线程休眠的实现代码是:
TimeUnit.SECONDS.sleep(3)
  • 是否会释放资源
    sleep会一直持有锁,不会释放锁,wait则会释放锁。
  • 使用范围不同
    wait和notify是一组,一般在线程通信的时候使用。sleep是单独的方法,在任何地方都可以使用。
  • 是否需要捕获异常
    sleep需要捕获中断异常,wait不需要。

Lock锁

传统方式一般采用synchronized关键字来加锁,如以下代码:

package com.coding.demo01;

// 传统的 Synchronized
// Synchronized 方法 和 Synchronized 块

/*
 * 我们的学习是基于企业级的开发进行的;
 * 1、架构:高内聚,低耦合
 * 2、套路:线程操作资源类,资源类是单独的
 */
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        // 1、新建资源类
        Ticket ticket = new Ticket();

        // 2、线程操纵资源类
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"A").start();


        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"C").start();
    }
}

// 单独的资源类,属性和方法!
// 这样才能实现复用!
class Ticket{

    private int number = 30;

    // 同步锁,厕所 =>close=>
    public synchronized void saleTicket(){
        if (number>0){
            System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
        }
    }
}

现在,我们也可以使用Lock来加锁。

Lock lock=new ReentrantLock()

ReentrantLock,即可重入锁(相当于回家的时候只要开了大门的锁,卧室,厕所不需要解锁就能进入),其默认是非公平锁(不公平,后面的线程可以插队)。如以下代码:

package com.coding.demo01;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
 * JUC之后的操作
 * Lock锁 + lambda表达式!
 */
public class Demo02 {
    public static void main(String[] args) {
        // 1、新建资源类
        Ticket2 ticket = new Ticket2();
        // 2、线程操作资源类 , 所有的函数式接口都可以用 lambda表达式简化!
        // lambda表达式 (参数)->{具体的代码}
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"A").start();
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"B").start();
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"C").start();

    }
}


// 依旧是一个资源类
class Ticket2{
    // 使用Lock,它是一个对象
    // ReentrantLock 可重入锁:回家:大门 (卧室门,厕所门...)
    // ReentrantLock 默认是非公平锁!
    // 非公平锁: 不公平 (插队,后面的线程可以插队)
    // 公平锁: 公平(只能排队,后面的线程无法插队)
    private Lock lock = new ReentrantLock();

    private int number = 30;

    public void saleTicket(){
        lock.lock(); // 加锁
        try {
            // 业务代码
            if (number>0){
                System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

synchronized和Lock的区别
1.synchronized是一个关键字,Lock是一个对象。
2.synchronized无法尝试获取锁,Lock可以尝试获取锁并判断。
3.synchronized会自动释放锁(a线程执行完毕,b如果出现异常也会释放锁),Lock锁必须手动进行释放,不释放就会变成死锁。
4.使用synchronized时,如果线程a获得锁并阻塞,线程b会一直进行等待,使用Lock则可以尝试获取锁,失败了之后就放弃。
tryLock方法
5.synchronized一定是非公平的,但Lock锁可以是公平的,需要通过参数进行设置。
6.代码量特别大时,一般使用Lock实现精准控制,synchronized适合代码量较小的同步问题。

生产者消费者问题

线程和线程之间本来是不能通信的,但有时我们需要线程之间进行协调操作。
比如有两个线程:A、B ,还有一个值初始为0,实现两个线程交替执行,对该变量 + 1,-1;交替10次。
先来看使用synchronized实现线程之间通信的版本,代码如下:

package com.coding.demo01;

// Synchronized 版
/*
目的: 有两个线程:A  B ,还有一个值初始为0,
       实现两个线程交替执行,对该变量 + 1,-1;交替10次
 */
public class Demo03 {
    public static void main(String[] args) {
        Data data = new Data();

        // +1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // -1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

// 资源类
// 线程之间的通信: 判断  执行  通知
class Data{

    private int number = 0;

    // +1
    public synchronized void increment() throws InterruptedException {
        if (number!=0){ // 判断是否需要等待
            this.wait();
        }
        number++; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
        if (number==0){ // 判断是否需要等待
            this.wait();
        }
        number--; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
}

那么问题来了,这四条线程可以实现交替吗?答案是不能!因为会产生虚假唤醒问题,jdk文档中对该问题也有说明。
虚假唤醒
需要特别注意的if和while的区别,当两个线程同时执行if判断,if只会判断一次,而while会对每一个线程都进行判断。显然,上面的if应该改为while,代码如下:

package com.coding.demo01;

// Synchronized 版
/*
目的: 有两个线程:A  B ,还有一个值初始为0,
       实现两个线程交替执行,对该变量 + 1,-1;交替10次       
传统的 wait 和 notify方法不能实现精准唤醒通知!
 */
public class Demo03 {
    public static void main(String[] args) {
        Data data = new Data();

        // +1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        // -1
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();


    }
}

// 资源类
// 线程之间的通信: 判断  执行  通知
class Data{

    private int number = 0;

    // +1
    public synchronized void increment() throws InterruptedException {
        while (number!=0){ // 判断是否需要等待
            this.wait();
        }
        number++; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){ // 判断是否需要等待
            this.wait();
        }
        number--; // 执行
        System.out.println(Thread.currentThread().getName()+"\t"+number);
        // 通知
        this.notifyAll(); //唤醒所有线程
    }
}

问题又来了,从测试的结果可以看出,传统的 wait 和 notify方法不能实现精准唤醒通知
这时我们就需要考虑使用JUC来实现了,先来看看JUC中的一个重要的接口Condition的文档说明。
Synchronized和Lock的线程通信示意图
Condition
我们使用Lock锁和Condition来实现精准唤醒线程,代码如下:

package com.coding.demo01;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
实现线程交替执行!
主要的实现目标:精准的唤醒线程!
    三个线程:A B C
    三个方法:A p5  B p10   C p15 依次循环
 */
public class Demo04 {
    public static void main(String[] args) {
        Data2 data = new Data2();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print10();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    data.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
    }
}

// 资源类
class Data2{
    private int number = 1; // 1A 2B  3C
    private Lock lock = new ReentrantLock();
    // 实现精准访问
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5() throws InterruptedException {

        lock.lock();

        try {
            // 判断
            while (number!=1){
                condition1.await();
            }
            // 执行
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知第二个线程干活!
            number = 2;
            condition2.signal(); // 唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 一定要解锁
        }
    }

    public void print10() throws InterruptedException {
        lock.lock();
        try {
            // 判断
            while (number!=2){
                condition2.await();
            }
            // 执行
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知3干活
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void print15() throws InterruptedException {
        lock.lock();
        try {
            // 判断
            while (number!=3){
                condition3.await();
            }
            // 执行
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 通知 1 干活
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

测试结果说明,使用Lock锁很容易就解决上述问题,由此我们可以得到一个结论:一个新技术的出现,一定是为了替换一些旧的技术的!

锁对象的判断方法

1.被synchronized修饰的方法,锁的对象是方法的调用者,当两个方法调用的对象是同一个时,先调用的先执行。
2.没有被synchronized修饰的方法,不是同步方法,不受锁的影响。
3.只要方法被static修饰,不管是否同时被synchronized修饰,锁的对象就是Class模板对象,这个对象是全局唯一的。
4.synchronized锁的是调用的对象,static锁的是这个类的Class模板,这是两个不同的锁。

不安全的集合类

只要在并发环境下,List、Map、Set这些类都是不安全的。
List不安全的代码示例:

package com.coding.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 故障现象:ConcurrentModificationException 并发修改异常
 * 导致原因:add方法没有锁!
 * 解决方案:
 * 1、List<String> list = new Vector<>(); //jdk1.0 就存在的!效率低
 * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
 * 3、List<String> list = new CopyOnWriteArrayList<>();
 * 
 * 什么是 CopyOnWrite; 写入是复制 (思想 COW)
 * 多个调用者同时要相同的资源;这个有一个指针的概念。
 * 读写分离的思想:
 */
public class UnSafeList {

    public static void main(String[] args) {
//        List<String> list = Arrays.asList("a", "b", "c");
//        list.forEach(System.out::println);
//        List<String> list = new ArrayList<>();

        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,3));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

如上述代码所示,解决List不安全问题的方法有两种:

List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWrite(COW),写入是复制,多个调用者同时要相同的资源,这是一种读写分离的思想,其源码如下:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

Set不安全的代码示例:

package com.coding.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

// ConcurrentModificationException
public class UnSafeSet {
    public static void main(String[] args) {
        // HashSet 底层是什么 就是 HashMap
        // add,就是 HashMap 的 key;
        Set<String> set = new HashSet<>();

//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
//        Set<String> set = new CopyOnWriteArraySet();

        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,3));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

如上述代码所示,解决Set不安全问题的方法有两种:

Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet();

Map不安全的代码示例:

package com.coding.unsafe;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

//ConcurrentModificationException
public class UnsafeMap {
    public static void main(String[] args) {
        // new HashMap<>() 工作中是这样用的吗? 不是
        // 加载因子0.75f;,容量 16; 这两个值工作中不一定这样用!
        // 优化性能!
        // HashMap 底层数据结构,链表 + 红黑树
        // = = = = = = =
//        Map<String, String> map = new HashMap<>();
        Map<String, String> map = new ConcurrentHashMap<>();

        // 人生如程序,不是选择就是循环,时常的自我总结十分重要!
        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,3));
                System.out.println(map);
            },String.valueOf(i)).start();
        }

    }
}

解决Map不安全问题的方法是使用ConcurrentHashMap来替代HashMap:

Map<String, String> map = new ConcurrentHashMap<>();

综上所述,要解决一般集合的线程不安全的问题,核心思路就是使用JUC并发包下面的并发安全的集合去替代这些不安全的集合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wunianisme

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值