0
点赞
收藏
分享

微信扫一扫

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_同步方法

文章目录

  • ​​1、什么是 JUC​​
  • ​​1.1 JUC简介​​
  • ​​1.2 进程与线程​​
  • ​​1.3 线程的状态​​
  • ​​1.3.1 线程状态枚举类​​
  • ​​1.3.2 wait/sleep的区别​​
  • ​​1.4 并发与并行​​
  • ​​1.4.1 串行模式​​
  • ​​1.4.2 并行模式​​
  • ​​1.4.3 并发​​
  • ​​2、Lock 接口​​
  • ​​2.1 Synchronized​​
  • ​​2.1.1 Synchronized 关键字回顾​​
  • ​​2.1.2 售票案例​​
  • ​​2.2 什么是Lock​​
  • ​​2.2.1 Lock接口​​
  • ​​2.2.2 lock接口的常见方法​​
  • ​​2.2.3 如何使用​​
  • ​​2.3 使用Lock和Lambda Express改进卖票案例​​
  • ​​2.4 、总结创建线程的几种方式​​
  • ​​3、Java8之lambda表达式复习​​
  • ​​4、线程间通信​​
  • ​​4.1 synchronized实现​​
  • ​​4.2 Lock 方案​​
  • ​​4.3 线程间定制化调用通信​​
  • ​​5、多线程锁​​
  • ​​6、集合的线程安全​​
  • ​​6.1 ArrayList集合线程不安全演示​​
  • ​​6.1.1 原理​​
  • ​​6.2 解决方案​​
  • ​​6.2.1 Vector​​
  • ​​6.2.2 Collections​​
  • ​​6.2.3 CopyOnWriteArrayList​​
  • ​​6.3 扩展类比HashSet和HashMap​​
  • ​​6.3.1 HashSet​​
  • ​​6.3.2 HashMap​​
  • ​​总结​​

1、什么是 JUC

1.1 JUC简介

在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_java_02

1.2 进程与线程

**进程:**进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

进程/线程例子?

  • 使用QQ,查看进程一定有一个QQ.exe的进程,我可以用QQ和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,QQ支持录入信息的搜索。
  • 大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
  • word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查

1.3 线程的状态

1.3.1 线程状态枚举类

Thread.State

public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,(新建)

/**
* Thread state for a runnable thread. A thread in the runnable
*/
RUNNABLE,(准备就绪)

/**
* Thread state for a thread blocked waiting for a monitor lock.
*/
BLOCKED,(阻塞)

/**
* Thread state for a waiting thread.
*/
WAITING,(不见不散)

/**
* Thread state for a waiting thread with a specified waiting time.
* </ul>
*/
TIMED_WAITING,(过时不候)

/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;(终结)
}

1.3.2 wait/sleep的区别

功能都是当前线程暂停

  • wait放开手去睡,放开手里的锁
  • sleep握紧手去睡,醒了手里还有锁

1.4 并发与并行

1.4.1 串行模式

串行表示所有任务都一 一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。
串行是一次只能取得一个任务,并执行这个任务。

1.4.2 并行模式

并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。

一句话:多项工作一起执行,之后再汇总

**例子:**泡方便面,一边电水壶烧水,一边撕调料倒入桶中

1.4.3 并发

并发(concurrent) 指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。一句话:同一时刻多个线程在访问同一个资源,多个线程对一个点

**例子:**小米9 今天上午10点,限量抢购;春运抢票; 电商秒杀…

2、Lock 接口

2.1 Synchronized

2.1.1 Synchronized 关键字回顾

synchronized 是Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象。

2.1.2 售票案例

//口诀:在高内聚低耦合环境下,线程 操作 资源类
//第一步 创建资源类,定义属性和和操作方法
class Ticket {
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale() {
//判断:是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" : 卖出第:"+(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 就可以办到。

2.2 什么是Lock

2.2.1 Lock接口

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比synchronized 更多的功能。

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_i++_03


Lock 与Synchronized 的区别

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

2.2.2 lock接口的常见方法

  • lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待
  • 关键字synchronized 与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的newContition()方法返回Condition 对象,Condition 类也可以实现等待/通知模式。
    Condition 比较常用的两个方法:
  • await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
  • signal()用于唤醒一个等待的线程。

2.2.3 如何使用

class X {
private final ReentrantLock lock = new ReentrantLock();
// ...

public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}

2.3 使用Lock和Lambda Express改进卖票案例

package com.rg.lock;

import java.util.concurrent.locks.ReentrantLock;

//第一步 创建资源类,定义属性和和操作方法
class LTicket {
//票数量
private int number = 30;

//创建可重入锁
private final ReentrantLock lock = new ReentrantLock(true);
//卖票方法
public void sale() {
//上锁
lock.lock();
try {
//判断是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" : 卖出第:"+(number--)+"张票, 剩下:"+number+"张");
}
} finally {
//解锁
lock.unlock();
}
}
}

public class LSaleTicket {
//第二步 创建多个线程,调用资源类的操作方法
//创建三个线程
public static void main(String[] args) {

LTicket ticket = new LTicket();

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();
}
}

2.4 、总结创建线程的几种方式

  • 继承Thread
    ​​​public class SaleTicket extends Thread​改进: java是单继承,资源宝贵,要用接口方式
  • 使用Thread(Runnable target, String name)
  • 新建类实现runnable接口 — 这种方法会新增类,有更新更好的方法

class MyThread implements Runnable//新建类实现runnable接口
new Thread(new MyThread,...)

  • 匿名内部类 — 这种方法不需要创建新的类,可以new接口

new Thread(new Runnable() {
@Override
public void run() {

}
}, "your thread name").start();

  • lambda表达式 — 这种方法代码更简洁精炼

new Thread(() -> {}, "your thread name").start();

错误的写法:

Thread t1 = new Thread();
t1.start();

补充:调用start时候,线程是否马上进行创建?

不一定,start()底层使用的是native方法,是操作系统的方法.
具体什么时候创建由操作系统决定.

3、Java8之lambda表达式复习

Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称
为 Lambda 操作符或剪头操作符。它将 Lambda 分为 两个部分:

  • 左侧:指定了 Lambda 表达式需要的所有参数
  • 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能

代码演示

package com.atguigu.thread;
@FunctionalInterface
interface Foo{

// public void sayHello() ;

public int add(int x,int y);

//函数式接口可以有多个 default实现
//JDK1.8之后 接口里面可以有方法的实现
default int div(int x,int y) {
return x/y;
}

//可以有多个静态函数
public static int sub(int x,int y) {
return x-y;
}
}

/**
*
* @Description: Lambda Express-----> 函数式编程
* 1 口诀:拷贝小括号(形参列表),写死右箭头 ->,落地大括号 {方法实现}
* 2 什么是Lambda:有且只有一个public方法(@FunctionalInterface注解增强定义)
* 3 JDK1.8之后可以有default方法默认实现
* 4 可以有静态方法实现
*/
public class LambdaDemo
{
public static void main(String[] args)
{
//使用匿名内部类方式调用接口中的方法方式
// Foo foo = new Foo() {
// @Override
// public void sayHello() {
// System.out.println("Hello!!");
// }
// foo.sayHello();

//使用Lambda方式
Foo foo = (x,y)->{
System.out.println("Hello!! lambda !!");
return x+y;
};

int result = foo.add(3,5);
System.out.println("******result="+result);
System.out.println("******result div="+foo.div(10, 2));
System.out.println("******result sub="+Foo.sub(10, 2));

}
}

4、线程间通信

线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。

我们来基本一道面试常见的题目来分析

场景: 两个线程,一个线程对当前数值加1,另一个线程对当前数值减1,要求 用线程间通信

4.1 synchronized实现

package com.rg.sync;

//第一步 创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
while(number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}

//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
while(number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
}

/**
*
* @Description:
* 现在两个线程,
* 可以操作初始值为零的一个变量,实现一个线程对该变量加1,一个线程对该变量减1,交替,来10轮。
* Java里面如何进行工程级别的多线程编写
* 1 多线程编程模板(套路)-----上
* 1.1 在高内聚低耦合的场景下:线程 操作 资源类
* 2 多线程编程模板(套路)-----中
* 2.1 判断
* 2.2 干活
* 2.3 通知
* 3 多线程编程模板(套路)-----下
* 防止虚假唤醒用while,不用if
* 虚假唤醒例子:坐飞机,机长广播飞机上有不安全物品,需要全体人员下来然后工作人员检查.检查完之后,需要重新安检上飞机...
*/
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();

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

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

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

**结果分析:**当只有AA,BB线程时,一切运行正常, AA::1和BB::0 轮流执行

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_JUC_04

当增加CC、DD线程时,会出现虚假唤醒的情况,

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_同步方法_05

原因分析:

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_JUC_06

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_java_07

4.2 Lock 方案

package com.rg.lock;

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

/**
* @author lxy
* @version 1.0
* @Description
* @date 2022/4/25 17:30
*/
//第一步 创建资源类,定义属性和操作方法
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();
}
}
}



/**
*
* @Description:
* 现在两个线程,
* 可以操作初始值为零的一个变量,实现一个线程对该变量加1,一个线程对该变量减1,交替,来10轮。
* Java里面如何进行工程级别的多线程编写
* 1 多线程编程模板(套路)-----上
* 1.1 在高内聚低耦合的场景下:线程 操作 资源类
* 2 多线程编程模板(套路)-----中
* 2.1 判断
* 2.2 干活
* 2.3 通知
* 3 多线程编程模板(套路)-----下
* 防止虚假唤醒用while,不用if
*/
public class ThreadDemo2 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();//加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();//加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();//加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();//加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}

4.3 线程间定制化调用通信

三个线程启动,要求如下:

AA打印5次,BB打印10次,CC打印15次.

接着

AA打印5次,BB打印10次,CC打印15次

…来10轮

实现代码

package com.rg.lock;

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

/**
* @author lxy
* @version 1.0
* @Description
* @date 2022/4/27 16:01
*/

//第一步 创建资源类
class ShareResource{
//定义标志位
private int flag = 1;//1 AA 2 BB 3 CC

//创建Lock锁
private Lock lock = new ReentrantLock();

//创建三个Condition,也就是三把钥匙
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();

//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();

try {
//判断
while (flag!=1){
//等待
c1.await();
}
//干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" : 轮数: "+loop);
}
//通知
flag = 2;
c2.signal();//通知BB线程(精准通知)
}finally {
//释放锁
lock.unlock();
}
}

//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();

try {
//判断
while (flag!=2){
//等待
c2.await();
}
//干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" : 轮数: "+loop);
}
//通知
flag = 3;
c3.signal();//通知CC线程(精准通知)
}finally {
//释放锁
lock.unlock();
}
}

//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();

try {
//判断
while (flag!=3){
//等待
c3.await();
}
//干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" : 轮数: "+loop);
}
//通知
flag = 1;
c1.signal();//通知AA线程(精准通知)
}finally {
//释放锁
lock.unlock();
}
}

}

/**
*
* @Description:
* 多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
*
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* ......来10轮
*
* 在线程定制化通信中,使用Lock相比使用syns可以做到精准定位/精准打击.
* 多线程编程模板(套路)-----上
* 1 在高内聚低耦合的场景下:线程 操作 资源类
* 2 判断/干活/通知
* 3 多线程交互中,必须要防止多线程的虚假唤醒,也即(判断只用while,不能用if)
* 4 标志位
*/
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();

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

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

5、多线程锁

经典的八锁问题

  1. 标准访问,先打印短信还是邮件
  2. 停4秒在短信方法内,先打印短信还是邮件
  3. 普通的hello方法,是先打短信还是hello
  4. 现在有两部手机,先打印短信还是邮件
  5. 两个静态同步方法,1部手机,先打印短信还是邮件
  6. 两个静态同步方法,2部手机,先打印短信还是邮件
  7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
  8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

参考代码

package com.rg.sync;

import java.util.concurrent.TimeUnit;

/**
* @author lxy
* @version 1.0
* @Description
* @date 2022/4/27 18:15
*/
class Phone {
public static synchronized void sendEmail() throws Exception{
try {
TimeUnit.SECONDS.sleep(4);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("------sendEmail");
}

public synchronized void sendSMS()throws Exception{
System.out.println("------sendSMS");
}

public void sayHello(){
System.out.println("------sayHello");
}
}


/**
*
* @Description: 8锁
*
1 标准访问,先打印短信还是邮件 邮件
2 停4秒在短信方法内,先打印短信还是邮件 邮件
3 新增普通的hello方法,是先打短信还是hello hello
4 现在有两部手机,先打印短信还是邮件 短信
5 两个静态同步方法,1部手机,先打印短信还是邮件 邮件
6 两个静态同步方法,2部手机,先打印短信还是邮件 邮件
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件 短信
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件 短信
*
* ==============================解析=================================================
* 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
* 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法 (1,2锁)
*
* 加个普通方法后发现和同步锁无关(3锁) 例:同一个手机,A和B同时枪,但是A要的是手机,B要的是手机壳.
*
* 换成两个对象后,不是同一把锁了。(4锁) 例:A和B每个有都有一个手机,无需抢了,各自用各自的就行.
*
* 都换成静态同步方法后,情况又发生变化, 直接把当前的类模板锁了(5,6锁) 例子:我把学校的大门锁了,里面的所有设施都不能用了.
*
* 一个静态同步,一个普通同步,前者锁的是模板,后者锁的是对象实例. 各自锁的内容不同,彼此无关.(7,8锁) 例:华为手机厂停电与你的手机是否会用无关..
* ===============================总结===============================================
* synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
* 具体表现为以下3种形式。
* 对于普通同步方法,锁是当前实例对象。
* 对于静态同步方法,锁是当前类的Class对象。(不加static锁的是this,加static锁的是X.class)
* 对于同步方法块,锁是Synchonized括号里配置的对象.
*
* 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
*
* 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁.
* 可是别的实例对象的普通同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
*
* 所有的静态同步方法用的也是同一把锁——类对象本身,
* 这两把锁(this/Class)是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的。
* 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
* 而不管是同一个实例对象的静态同步方法之间,
* 还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
*
*/
public class Lock_8{
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();

new Thread(()->{
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();

Thread.sleep(100);

new Thread(()->{
try {
// phone.sendSMS();
// phone.sayHello();
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}

6、集合的线程安全

6.1 ArrayList集合线程不安全演示

/**
* @author lxy
* @version 1.0
* @Description
* 题目:请举例说明集合类是不安全的
*
* 2 导致原因
*
*
* 3 解决方案
* 3.1 Vector
* 3.2 Collections.synchronizedList()
* 3.3 CopyOnWriteArrayList
*
* 4 优化建议(同样的错误,不出现第2次)
* @date 2022/4/28 11:43
*/
public class NotSafeDemo {
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 = 1; i <= 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}

}
}

运行结果:

ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常
并发修改异常

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_java_08

6.1.1 原理

查看arrayList的底层源码

public boolean add(E e) {//没有synchronized,线程不安全
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

6.2 解决方案

6.2.1 Vector

改用​​List <String> list = new Vector <>();​

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_i++_09

原理

public synchronized boolean add(E e) {//vector中为每个方法加上了synchronized修饰,线程安全
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

6.2.2 Collections

改用 ​​List <String> list = Collections.synchronizedList(new ArrayList <>()) ;​

运行结果一切正常!

原理

// 注意:synchronizedList是用同步代码块给传入的集合对象加锁!
// 可以看到所有的操作都是上了锁的,synchronized (mutex),锁对象是mutex是来自SynchronizedCollection父类
public void add(int index, E element) {
synchronized (mutex) { list.add(index, element); }//mutex:锁对象 如果手动传入,则是传入的对象,如果没有传入则是当前对象this
}

扩展

HashMap,HashSet是线程安全的吗? 也不是 , 所以有同样的线程安全方法

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_同步方法_10

注: 转换包装后的list可以实现add,remove,get等操作的线程安全性,但是对于迭代操作,Collections.synchronizedList并没有提供相关机制,所以迭代时需要对包装后的list(必须对包装后的list进行加锁,锁其他的不行)进行手动加锁。

List list = Collections.synchronizedList(new ArrayList());
//必须对list进行加锁
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
......
}

关于 Collections.synchronizedList 更为详尽的说明,请参考 https://blog.csdn.net/weixin_45480785/article/details/118934849

6.2.3 CopyOnWriteArrayList

CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_并发_11

原理

/**
* CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
* 而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
* 添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
* 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
* 所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
*/
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();//解锁
}
}

6.3 扩展类比HashSet和HashMap

6.3.1 HashSet

Set<String> set = new HashSet<>();//线程不安全
Set<String> set = Collections.synchronizedSet(new HashSet<>());//线程安全
Set<String> set = new CopyOnWriteArraySet<>();//线程安全 更推荐使用

补:HashSet底层数据结构是什么?

HashMap

但HashSet的add是放一个值,而HashMap是放K、V键值对

private static final Object PRESENT = new Object();

public HashSet() {
map = new HashMap<>();
}

public boolean add(E e) {
return map.put(e, PRESENT)==null;//元素放在hashMap的key上,value位置上放一个Object常量
}

6.3.2 HashMap

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

关于HashMap底层更为详尽的介绍,请参考 [HashMap深度解析 , 一文让你彻底搞懂HashMap](

总结

OK,今天关于 JUC的知识分享 就到这里,希望本篇文章能够帮助到大家,同时也希望大家看后能学有所获!!!

好了,我们下期见~

《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全_并发_12


举报

相关推荐

0 条评论