Java中的JUC是啥,JUC并发编程

JUC是什么?

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

dd0e3b0e4cae

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则可以尝试获取锁,失败了之后就放弃。

dd0e3b0e4cae

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文档中对该问题也有说明。

dd0e3b0e4cae

虚假唤醒

需要特别注意的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的文档说明。

dd0e3b0e4cae

lSynchronized和Lock的线程通信示意图

dd0e3b0e4cae

Condition.png

我们使用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 list = new Vector<>(); //jdk1.0 就存在的!效率低

* 2、List list = Collections.synchronizedList(new ArrayList<>());

* 3、List list = new CopyOnWriteArrayList<>();

*

* 什么是 CopyOnWrite; 写入是复制 (思想 COW)

* 多个调用者同时要相同的资源;这个有一个指针的概念。

* 读写分离的思想:

*/

public class UnSafeList {

public static void main(String[] args) {

// List list = Arrays.asList("a", "b", "c");

// list.forEach(System.out::println);

// List list = new ArrayList<>();

List 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 list = Collections.synchronizedList(new ArrayList<>());

List 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 set = new HashSet<>();

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

// Set 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 set = Collections.synchronizedSet(new HashSet<>());

Set 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 map = new HashMap<>();

Map 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 map = new ConcurrentHashMap<>();

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值