Java并发编程之线程安全、线程通信

简介: Java多线程开发中最重要的一点就是线程安全的实现了。所谓Java线程安全,可以简单理解为当多个线程访问同一个共享资源时产生的数据不一致问题。为此,Java提供了一系列方法来解决线程安全问题。synchronizedsynchronized用于同步多线程对共享资源的访问,在实现中分为同步代码块和同步方法两种。

Java多线程开发中最重要的一点就是线程安全的实现了。所谓Java线程安全,可以简单理解为当多个线程访问同一个共享资源时产生的数据不一致问题。为此,Java提供了一系列方法来解决线程安全问题。

synchronized

synchronized用于同步多线程对共享资源的访问,在实现中分为同步代码块和同步方法两种。

同步代码块

 1 public class DrawThread extends Thread {
 2     
 3     private Account account;
 4     private double drawAmount;
 5     public DrawThread(String name, Account account, double drawAmount) {
 6         super(name);
 7         this.account = account;
 8         this.drawAmount = drawAmount;
 9     }
10     @Override
11     public void run() {
12         //使用account作为同步代码块的锁对象
13         synchronized(account) {
14             if (account.getBalance() >= drawAmount) {
15                 System.out.println(getName() + "取款成功, 取出:" + drawAmount);
16                 try {
17                     TimeUnit.MILLISECONDS.sleep(1);
18                 } catch (InterruptedException e) {
19                     e.printStackTrace();
20                 }
21                 account.setBalance(account.getBalance() - drawAmount);
22                 System.out.println("余额为: " + account.getBalance());
23             } else {
24                 System.out.println(getName() + "取款失败!余额不足!");
25             }
26         }
27     }
28 }

同步方法

使用同步方法,即使用synchronized关键字修饰类的实例方法或类方法,可以实现线程安全类,即该类在多线程访问中,可以保证可变成员的数据一致性。

同步方法中,隐式的锁对象由锁的是实例方法还是类方法确定,分别为该类对象或类的Class对象。

 1 public class SyncAccount {
 2     private String accountNo;
 3     private double balance;
 4     //省略构造器、getter setter方法
 5     //在一个简单的账户取款例子中, 通过添加synchronized的draw方法, 把Account类变为一个线程安全类
 6     public synchronized void draw(double drawAmount) {
 7         if (balance >= drawAmount) {
 8             System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
 9             try {
10                 TimeUnit.MILLISECONDS.sleep(1);
11             } catch (InterruptedException e) {
12                 e.printStackTrace();
13             }
14             balance -= drawAmount;
15             System.out.println("余额为: " + balance);
16         } else {
17             System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
18         }
19     }
20     //省略HashCode和equals方法
21 }

同步锁(Lock、ReentrantLock)

Java5新增了两个用于线程同步的接口Lock和ReadWriteLock,并且分别提供了两个实现类ReentrantLock(可重入锁)和ReentrantReadWriteLock(可重入读写锁)。

相比较synchronized,ReentrantLock的一些优势功能:

1. 等待可中断:指持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。

2. 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序依次获取。synchronized是非公平锁,ReentrantLock可以通过参数设置为公平锁

3. 多条件锁:ReentrantLock可通过Condition类获取多个条件关联

Java 1.6以后,synchronized性能提升较大,因此一般的开发中依然建议使用语法层面上的synchronized加锁。

Java8新增了更为强大的可重入读写锁StampedLock类。

比较常用的是ReentrantLock类,可以显示地加锁、释放锁。下面使用ReentrantLock重构上面的SyncAccount类。

 1 public class RLAccount {
 2     //定义锁对象
 3     private final ReentrantLock lock = new ReentrantLock();
 4     private String accountNo;
 5     private double balance;
 6     //省略构造方法和getter setter
 7     public void draw(double drawAmount) {
 8         //加锁
 9         lock.lock();
10         try {
11             if (balance >= drawAmount) {
12                 System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
13                 try {
14                     TimeUnit.MILLISECONDS.sleep(1);
15                 } catch (InterruptedException e) {
16                     e.printStackTrace();
17                 }
18                 balance -= drawAmount;
19                 System.out.println("余额为: " + balance);
20             } else {
21                 System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
22             } 
23         } finally {
24             //通过finally块保证释放锁
25             lock.unlock();
26         }
27     }
28 }

死锁

当两个线程相互等待地方释放锁的时候,就会产生死锁。关于死锁和线程安全的深入分析,将另文介绍。

线程通信方式之wait、notify、notifyAll

Object类提供了三个用于线程通信的方法,分别是wait、notify和notifyAll。这三个方法必须由同步锁对象来调用,具体来说:

1. 同步方法:因为同步方法默认使用所在类的实例作为锁,即this,可以在方法中直接调用。

2. 同步代码块:必须由锁来调用。

wait():导致当前线程等待,直到其它线程调用锁的notify方法或notifyAll方法来唤醒该线程。调用wait的线程会释放锁。

notify():唤醒任意一个在等待的线程

notifyAll():唤醒所有在等待的线程

 1 /*
 2  * 通过一个生产者-消费者队列来说明线程通信的基本使用方法
 3  * 注意: 假如这里的判断条件为if语句,唤醒方法为notify, 那么如果分别有多个线程操作入队\出队, 会导致线程不安全.
 4  */
 5 public class EventQueue {
 6     
 7     private final int max;
 8     
 9     static class Event{
10         
11     }
12     //定义一个不可改的链表集合, 作为队列载体
13     private final LinkedList<Event> eventQueue = new LinkedList<>();
14     
15     private final static int DEFAULT_MAX_EVENT = 10;
16     
17     public EventQueue(int max) {
18         this.max = max;
19     }
20     
21     public EventQueue() {
22         this(DEFAULT_MAX_EVENT);
23     }
24     
25     private void console(String message) {
26         System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
27     }
28     //定义入队方法
29     public void offer(Event event) {
30         //使用链表对象作为锁
31         synchronized(eventQueue) {
32             //在循环中判断如果队列已满, 则调用锁的wait方法, 使线程阻塞
33             while(eventQueue.size() >= max) {
34                 try {
35                     console(" the queue is full");
36                     eventQueue.wait();
37                 } catch (InterruptedException e) {
38                     e.printStackTrace();
39                 }
40             }
41             console(" the new event is submitted");
42             eventQueue.addLast(event);
43             this.eventQueue.notifyAll();
44         }
45     }
46     //定义出队方法
47     public Event take() {
48         //使用链表对象作为锁
49         synchronized(eventQueue) {
50             //在循环中判断如果队列已空, 则调用锁的wait方法, 使线程阻塞
51             while(eventQueue.isEmpty()) {
52                 try {
53                     console(" the queue is empty.");
54                     eventQueue.wait();
55                 } catch (InterruptedException e) {
56                     e.printStackTrace();
57                 }
58             }
59             Event event = eventQueue.removeFirst();
60             this.eventQueue.notifyAll();
61             console(" the event " + event + " is handled/taked.");
62             return event;
63         }
64     }
65 }

线程通信方式之Condition

如果使用的是Lock接口实现类来同步线程,就需要使用Condition类的三个方法实现通信,分别是await、signal和signalAll,使用上与Object类的通信方法基本一致。

 1 /*
 2  * 使用Lock接口和Condition来实现生产者-消费者队列的通信
 3  */
 4 public class ConditionEventQueue {
 5     //显示定义Lock对象
 6     private final Lock lock = new ReentrantLock();
 7     //通过newCondition方法获取指定Lock对象的Condition实例
 8     private final Condition cond = lock.newCondition();
 9     private final int max;
10     static class Event{ }
11     //定义一个不可改的链表集合, 作为队列载体
12     private final LinkedList<Event> eventQueue = new LinkedList<>();
13     private final static int DEFAULT_MAX_EVENT = 10;
14     public ConditionEventQueue(int max) {
15         this.max = max;
16     }
17     
18     public ConditionEventQueue() {
19         this(DEFAULT_MAX_EVENT);
20     }
21     
22     private void console(String message) {
23         System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
24     }
25     //定义入队方法
26     public void offer(Event event) {
27             lock.lock();
28             try {
29                 //在循环中判断如果队列已满, 则调用cond的wait方法, 使线程阻塞
30                 while (eventQueue.size() >= max) {
31                     try {
32                         console(" the queue is full");
33                         cond.await();
34                     } catch (InterruptedException e) {
35                         e.printStackTrace();
36                     }
37                 }
38                 console(" the new event is submitted");
39                 eventQueue.addLast(event);
40                 cond.signalAll();;
41             } finally {
42                 lock.unlock();
43             }
44         
45     }
46     //定义出队方法
47     public Event take() {
48             lock.lock();
49             try {
50                 //在循环中判断如果队列已空, 则调用cond的wait方法, 使线程阻塞
51                 while (eventQueue.isEmpty()) {
52                     try {
53                         console(" the queue is empty.");
54                         cond.wait();
55                     } catch (InterruptedException e) {
56                         e.printStackTrace();
57                     }
58                 }
59                 Event event = eventQueue.removeFirst();
60                 cond.signalAll();
61                 console(" the event " + event + " is handled/taked.");
62                 return event;
63             } finally {
64                 lock.unlock();
65             }
66     }
67 }

Java 1.5开始就提供了BlockingQueue接口,来实现如上所述的生产者-消费者线程同步工具。具体介绍将另文说明。

 

目录
相关文章
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。
|
4天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
22 3
|
4天前
|
Java 开发者
在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。
【10月更文挑战第13天】在Java编程中,正确的命名规范不仅能提升代码的可读性和可维护性,还能有效避免命名冲突。本文将带你深入了解Java命名规则,包括标识符的基本规则、变量和方法的命名方式、常量的命名习惯以及如何避免关键字冲突,通过实例解析,助你写出更规范、优雅的代码。
25 3
|
4天前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
12 3
|
4天前
|
Java 程序员 编译器
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。
在Java编程中,保留字(如class、int、for等)是具有特定语法意义的预定义词汇,被语言本身占用,不能用作变量名、方法名或类名。本文通过示例详细解析了保留字的定义、作用及与自定义标识符的区别,帮助开发者避免因误用保留字而导致的编译错误,确保代码的正确性和可读性。
15 3
|
4天前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
20 3
|
3天前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
13天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
32 1
C++ 多线程之初识多线程
|
28天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
13天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
36 6