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接口,来实现如上所述的生产者-消费者线程同步工具。具体介绍将另文说明。

 

目录
相关文章
|
5天前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
39 1
|
5天前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
36 1
|
27天前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
68 0
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
152 0
|
4月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
259 5
|
8月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
278 20
|
8月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
10月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
165 1

热门文章

最新文章