本章继续讲多线程
目录
一、线程同步
当多个线程共享数据时,由于CPU负责线程的调度,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致。
1、为什么需要线程同步
上一章学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法,不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为;否则,不能保证程序运行结果的正确性。
示例:
public class Storage {
public final int MAX_COUNT = 3;//最大库存三车
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
private int count = 0;//库存量
//对外向果商发货
public void get() {
if (count > 0) {
count--;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//2.显示数据
System.out.println(Thread.currentThread().getName() + ":采购了第" + (MAX_COUNT - count) + "车水果");
}
}
//果园向仓库供货
public void put() {
if (count == 0) {
count = MAX_COUNT;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":已向仓库发送" + count + "车水果");//显示数据
}
}
}
class BusiThread implements Runnable {
Storage storage;
public BusiThread(Storage storage) {
this.storage = storage;
}
public void run() {
while (true) {
this.storage.get();//仓库供应一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class FarmerThread implements Runnable {
Storage storage;
public FarmerThread(Storage storage) {
this.storage = storage;
}
public void run(){
while (true) {
this.storage.put();//向仓库运送一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Test7{
public static void main(String[] args) {
Storage storage=new Storage();//创建仓库类对象
Thread busi1=new Thread(new BusiThread(storage),"莲花超市");
Thread busi2=new Thread(new BusiThread(storage),"果果香水果店");
Thread farmer=new Thread(new FarmerThread(storage),"果农老王");
farmer.start();
busi1.start();
busi2.start();
}
}
在以上代码中,变量count存储现有仓库库存;get()方法实现当仓库库存大于零时,对外向果商或商超发货,每次发货一车;put()方法实现当仓库库存为零时,由果园的果农向仓库供货,每次供货三车。为了展示更多的数据方便观察效果,在示例代码中,FarmerThread类和BusiThread类中存在死循环。
运行结果:
果农老王:已向仓库发送1车水果
果果香水果店:采购了第2车水果
莲花超市:采购了第2车水果
莲花超市:采购了第0车水果
果农老王:已向仓库发送2车水果
莲花超市:采购了第1车水果
果果香水果店:采购了第1车水果
果果香水果店:采购了第0车水果
莲花超市:采购了第0车水果
果农老王:已向仓库发送1车水果
果果香水果店:采购了第2车水果
莲花超市:采购了第2车水果
莲花超市:采购了第1车水果
果果香水果店:采购了第2车水果
果农老王:已向仓库发送1车水果
莲花超市:采购了第3车水果
果果香水果店:采购了第0车水果
从运行结果可以发现,运行结果中存在如下数据问题:
- 不是从第一车水果开始供货。
- 出现果果香水果店和莲花超市共同采购同一车水果的情况。
- 果农老王应该运输三车水果到仓库,但数据显示只发送两车。
多个线程共同操作同一共享资源会带来数据不安全问题。具体原因是在Storage类中存在以下情况:
- 存在多个线程共同操作的变量count。
- 在get()方法和put()方法中,存在修改数据和显示数据两步操作。
二、如何实现线程同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保资源某一时刻只能被一个线程使用,这被称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突。
1、同步代码块
代码块即使用“{}”括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块。
语法结构:
synchronized(obj){
//需要同步的代码
}
示例:
package d10;
public class Storage2 {
public final int MAX_COUNT = 3;//最大库存三车
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
private int count = 0;//库存量
//对外向果商发货
public void get() {
synchronized (this) {
if (count > 0) {
count--;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//2.显示数据
System.out.println(Thread.currentThread().getName() + ":采购了第" + (MAX_COUNT - count) + "车水果");
}
}
}
//果园向仓库供货
public void put() {
synchronized (this) {
if (count == 0) {
count = MAX_COUNT;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":已向仓库发送" + count + "车水果");//显示数据
}
}
}
}
class BusiThread2 implements Runnable {
Storage2 storage;
public BusiThread2(Storage2 storage) {
this.storage = storage;
}
public void run() {
while (true) {
this.storage.get();//仓库供应一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class FarmerThread2 implements Runnable {
Storage2 storage;
public FarmerThread2(Storage2 storage) {
this.storage = storage;
}
public void run() {
while (true) {
this.storage.put();//向仓库运送一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Test8 {
public static void main(String[] args) {
Storage2 storage = new Storage2();//创建仓库类对象
Thread busi1 = new Thread(new BusiThread2(storage), "莲花超市");
Thread busi2 = new Thread(new BusiThread2(storage), "果果香水果店");
Thread farmer = new Thread(new FarmerThread2(storage), "果农老王");
farmer.start();
busi1.start();
busi2.start();
}
}
运行结果:
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
2、同步方法
如果一个方法的所有代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。
语法结构:
package d10;
public class Storage3 {
public final int MAX_COUNT = 3;//最大库存三车
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
private int count = 0;//库存量
//对外向果商发货
public synchronized void get() {
if (count > 0) {
count--;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//2.显示数据
System.out.println(Thread.currentThread().getName() + ":采购了第" + (MAX_COUNT - count) + "车水果");
}
}
//果园向仓库供货
public synchronized void put() {
if (count == 0) {
count = MAX_COUNT;//1.修改数据
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":已向仓库发送" + count + "车水果");//显示数据
}
}
}
class BusiThread3 implements Runnable {
Storage3 storage;
public BusiThread3(Storage3 storage) {
this.storage = storage;
}
public void run() {
while (true) {
this.storage.get();//仓库供应一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class FarmerThread3 implements Runnable {
Storage3 storage;
public FarmerThread3(Storage3 storage) {
this.storage = storage;
}
public void run(){
while (true) {
this.storage.put();//向仓库运送一车水果
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Test9{
public static void main(String[] args) {
Storage3 storage=new Storage3();//创建仓库类对象
Thread busi1=new Thread(new BusiThread3(storage),"莲花超市");
Thread busi2=new Thread(new BusiThread3(storage),"果果香水果店");
Thread farmer=new Thread(new FarmerThread3(storage),"果农老王");
farmer.start();
busi1.start();
busi2.start();
}
}
运行结果:
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
果农老王:已向仓库发送3车水果
果果香水果店:采购了第1车水果
莲花超市:采购了第2车水果
果果香水果店:采购了第3车水果
3、线程同步特征
所谓线程之间保持同步,是指不同的线程在执行之间以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征:
- 当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。
- 如果多个线程访问的不是同一共享资源,则无需同步。
- 当一个线程访问Object对象的同步代码块或同步方法时,其他线程仍可以访问该Object对象的非同步代码块及非同步方法。
综上所述,synchronized关键字就是为当前的代码块声明一把锁,获得这把锁的线程可以执行代码块里的指令,其他线程只能等待取锁,然后才能执行相同的操作。
三、线程安全的类型
若程序所在的进程中有多个线程,而当这些线程同时运行时,每次的运行结果和单线程的运行结果是一样的,而且其他变量的值夜和预期相同,那么当前程序就是线程安全的。
一个类在被读线程访问时,不管运行时对这些线程有怎样的时序安排,它必须是以固定的、一致的顺序执行,这样的类型被称为线程安全的类型。
1、ArrayList是常用的集合类型,它是否线程安全的呢?
答案是ArrayList是非线程安全的类型。
而ArrayList集合添加一个元素主要完成如下两步操作:
- 判断列表容量是否足够,是否需要扩容。
- 将元素添加到列表的元素数组里。
以上两步操作并非不可分割,这样也就出现了导致线程不安全的隐患。在多个线程执行add()方法进行添加元素操作时,可能会导致elementData数组越界。
2、对比 Hashtable和HashMap
1、是否线程安全
Hashtable是线程安全的,其方法时同步的,可查看Hashtable类型源码中操作数据的方法为同步方法。
而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable。
如果使用HashMap,就要自行增加同步处理。
2、效率比较
由于Hashtable是线程安全的,其方法是同步的,而HashMap是非线程安全的,重速度,轻安全,所以当只需要单线程时,使用HashMap的执行速度要搞过Hashtable。
3、对比StringBuffer和StringBuilder
StringBuffer和StringBuilder都可用来存储字符串变量,是可变的对象。它们的区别是StringBuffer是线程安全的,而StringBuilder是非线程安全的。因此,在单线程环境下StringBuilder执行效率更高。
四、线程的状态转换
线程的状态转换就是利用了Thread常用方法,
在Java语言使用Thread类及其子类的对象表示线程,新建的线程通常会在五钟状态中转换,即新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡。
这里就不多讲了!!!
总结
多线程允许程序员编写出可最大程度利用CPU的高效程序。
- 在Java程序启动时,一个线程立刻运行,该线程通常被成我饿程序的主线程。主线程是产生其他子线程的线程。
- 通常,主线程必须最后完成运行,因为它执行各种关闭动作。
可通过两种方式创建线程。
- 声明一个继承了Thread类的子类,在此子类中,重写Thread类的run()方法。
- 声明一个实现Runnable接口的类,然后实现run()方法。
每一个线程均会处于新建、就绪、运行、阻塞、死亡五钟状态之一。
在Java实现的多线程应用程序中,可以通过调用Thread类中的方法实现对线程类对象的操作。
- 调整线程的优先级:在同等情况下,优先级高的线程会获得较多的运行机会,优先级低的线程则相反。Java线程优先级用1~10的整数表示。
- 线程休眠:sleep(long millis)方法使线程转到阻塞状态。
- 线程的强制运行:join()方法可以让某一线程强制运行。
- 线程礼让:yield()方法,暂停当前正在执行的线程类对象,把执行机会让给相同或更高优先级的线程。
当多个线程类对象操作同一共享资源时,要使用synchronized关键字进行资源的同步处理,可以使用同步代码块或同步方法实现线程同步。
标签:count,水果,下篇,Thread,storage,线程,new,java,多线程 From: https://blog.csdn.net/AE_BD/article/details/136886117