Java中的多线程详解(超级简单理解)(下篇)

使用工具 IntelliJ IDEA Community Edition 2023.1.4

使用语言 Java8 

代码能力快速提升小方法,看完代码自己敲一遍,十分有用 

目录

1.线程同步

1.1 线程同步的作用

1.2 为什么需要线程同步 

1.3 实现线程的同步

1.3.1 加锁的方式

1.4 线程同步贯穿示例 

1.5 线程同步的特征 

1.6 线程安全的类型 

1.6.1 ArrayList是否是线程安全的

 1.6.2 对比Hashtable和HashMap

1.6.3 对比StringBuffer和StringBuilder 


1.线程同步

1.1 线程同步的作用

当多个线程共享数据时,由于CPU负责线程的调度,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致
使用线程同步可以多个线程操作同一个数据,比如,一个电影院只有5张票,我们不能让每一个用户线程都抢到5张票,所以我们要使用线程同步来解决这个问题;

1.2 为什么需要线程同步 

前面学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法,不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为;否则,不能保证程序结果的正确性。
在日常生活中,也常常会出现竞争共享资源的情况,例如,商场的试衣间就是常常被顾客竞争的资源,假如在同一时刻,两位顾客都需要同一个试衣间试衣服,而试衣间同一时刻只能容纳一人。如何解决这一问题呢?这就需要用到线程同步技术。 

1.3 实现线程的同步

当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这被称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突。
以上加锁的过程使用线程同步实现,有同步代码块和同步方法两种方式,这两种方式都使用到synchronized关键字; 

1.3.1 加锁的方式

同步代码块

代码块即使用{}括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块,其语法如下:
synchronized(obj){
  //需要同步的代码
}

如果一个代码块带有synchronized(obj)标记,那么当线程执行此代码块时,必须先获得obj变量所引用的对象的锁,其可以针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。
假如这把锁没有被其他线程占用,当前线程就会获得这把锁,开始执行同步代码块。在一般情况下,只有获得锁的线程才可以操作共需数据,执行完同步代码块中的所有代码才会释放锁,使其他线程获得锁。 

同步方法

如果一个方法的所有代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。其语法如下: 

访问修饰符 synchronized 返回类型 方法名(参数列表){}
synchronized 访问修饰符 返回类型 方法名(参数列表){} 

1.4 线程同步贯穿示例 

示例代码 

 运行结果

1.5 线程同步的特征 

所谓线程之间保持同步,是指不同的线程在执行同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征。

从以上内容中使用了同步方法和同步代码块实现线程同步,这两者从实现结果看没有区别,只是同步方法便于阅读理解,而同步代码块可以更精确地限制访问区域,这样会更高效。

线程同步的特征 

当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。

如果多个线程访问的不是同一共享资源,则无需同步。

当一个线程访问Object对象的同步代码块或同步方法时,其他线程仍可以访问该Object对象的非同步代码块及非同步方法。 

synchronized关键字就是为当前的代码块声明一把锁,获得这把锁的线程可以执行代码块里的指令,其他的线程只能等待获取锁,然后才能执行相同的操作。

1.6 线程安全的类型 

1.6.1 ArrayList是否是线程安全的

若程序所在的进程中有多个线程,而当这些线程同时运行时,每次的运行结果和单线程时的运行结果是一样的,而且其他变量的值也和预期相同,那么当前程序就是线程安全的。
一个类在被多线程访问时,不管运行时对这些线程有怎样的时序安排,它必须是以固定的、一致的顺序的执行,这样的的类型被称为线程安全的类型。
ArrayList是常用的集合类型,它是否是线程安全的呢?答案是ArrayList是非线程安全的类型。 

从以上源码中,ensureCapacityInternal(size+1)方法的作用是判断将当前的新元素加到列表后面,列表的elementData数组大小是否足够。如果size+1长度大于elementData数组的实际长度,那么就要对elementData数组扩容由此可以看出,添加一个元素的主完成如下两步操作

  • 判断列表容量是否足够,是否需要扩容
  • 将元素添加到列表的元素数组里

可能导致线程不安全的现象分析如下:假定有一个ArrayList对象定义长度为10,其中已存储了9个元素

  • 1.ArrayList对象已存储了9个元素,即size为9
  • 2.线程A执行add()方法,获得size值为9,调用ensureCapacityInternal(size+1)方法进行容量判断
  • 3.线程B也执行了add()方法,获得size值为9,同样调用ensureCapacityInternal(size+1)方法。
  • 4.由于ArrayList对象定义的长度为10,即数组elementData长度为10,线程A、B判断的结果都是可以容纳的,不需要扩容。
  • 5.线程A进行添加数据操作,执行elementData[size++]=e语句,此时,size的值变为10。
  • 6.线程也开始进行添加数据操作,执行elementData[10]=e语句,而elementData没有进行扩容,最大下标为9,于是就会报出数据越界异常。

elementData[size++]=e拆分如下:
elementData[size]=e;
size=size+1; 

 1.6.2 对比Hashtable和HashMap

是否线程安全 

Hashtable是线程安全的,其方法是同步的,可查看Hashtable类型源码中操作数据的方法为同步方法,如下所示: 

而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable。如果使用HashMap,就要自行增加同步处理。

效率比较 

由于使用Hashtable是线程安全的其方法是同步的,其方法是同步的,而HashMap是非线程安全的,中速度,轻安全,所以当只需单线程时,使用HashMap的执行速度要高过Hashtable。 

1.6.3 对比StringBuffer和StringBuilder 

StringBuffer和StringBuilder都可用来存储字符串变量,是可变的对象。它们的区别是StringBuffer是线程安全的,而StringBuilder是非线程安全的。因此在单线程环境下StringBuilder执行效率更高。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值