Java并发编程之线程安全、线程通信
Java多线程开发中最重要的一点就是线程安全的实现了。所谓Java线程安全,可以简单理解为当多个线程访问同一个共享资源时产生的数据不一致问题。为此,Java提供了一系列方法来解决线程安全问题。
synchronized
synchronized用于同步多线程对共享资源的访问,在实现中分为同步代码块和同步方法两种。
同步代码块
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
//使用account作为同步代码块的锁对象
synchronized(account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取款失败!余额不足!");
}
}
}
}
同步方法
使用同步方法,即使用synchronized关键字修饰类的实例方法或类方法,可以实现线程安全类,即该类在多线程访问中,可以保证可变成员的数据一致性。
同步方法中,隐式的锁对象由锁的是实例方法还是类方法确定,分别为该类对象或类的Class对象。
public class SyncAccount {
private String accountNo;
private double balance;
//省略构造器、getter setter方法
//在一个简单的账户取款例子中, 通过添加synchronized的draw方法, 把Account类变为一个线程安全类
public synchronized void draw(double drawAmount) {
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println("余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
}
}
//省略HashCode和equals方法
}
同步锁(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类。
public class RLAccount {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
//省略构造方法和getter setter
public void draw(double drawAmount) {
//加锁
lock.lock();
try {
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + "取款成功, 取出:" + drawAmount);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= drawAmount;
System.out.println("余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款失败!余额不足!");
}
} finally {
//通过finally块保证释放锁
lock.unlock();
}
}
}
死锁
当两个线程相互等待地方释放锁的时候,就会产生死锁。关于死锁和线程安全的深入分析,将另文介绍。
线程通信方式之wait、notify、notifyAll
Object类提供了三个用于线程通信的方法,分别是wait、notify和notifyAll。这三个方法必须由同步锁对象来调用,具体来说:
1. 同步方法:因为同步方法默认使用所在类的实例作为锁,即this,可以在方法中直接调用。
2. 同步代码块:必须由锁来调用。
wait():导致当前线程等待,直到其它线程调用锁的notify方法或notifyAll方法来唤醒该线程。调用wait的线程会释放锁。
notify():唤醒任意一个在等待的线程
notifyAll():唤醒所有在等待的线程
/*
* 通过一个生产者-消费者队列来说明线程通信的基本使用方法
* 注意: 假如这里的判断条件为if语句,唤醒方法为notify, 那么如果分别有多个线程操作入队\出队, 会导致线程不安全.
*/
public class EventQueue { private final int max; static class Event{ }
//定义一个不可改的链表集合, 作为队列载体
private final LinkedList<Event> eventQueue = new LinkedList<>(); private final static int DEFAULT_MAX_EVENT = 10; public EventQueue(int max) {
this.max = max;
} public EventQueue() {
this(DEFAULT_MAX_EVENT);
} private void console(String message) {
System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
}
//定义入队方法
public void offer(Event event) {
//使用链表对象作为锁
synchronized(eventQueue) {
//在循环中判断如果队列已满, 则调用锁的wait方法, 使线程阻塞
while(eventQueue.size() >= max) {
try {
console(" the queue is full");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
this.eventQueue.notifyAll();
}
}
//定义出队方法
public Event take() {
//使用链表对象作为锁
synchronized(eventQueue) {
//在循环中判断如果队列已空, 则调用锁的wait方法, 使线程阻塞
while(eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
this.eventQueue.notifyAll();
console(" the event " + event + " is handled/taked.");
return event;
}
}
}
线程通信方式之Condition
如果使用的是Lock接口实现类来同步线程,就需要使用Condition类的三个方法实现通信,分别是await、signal和signalAll,使用上与Object类的通信方法基本一致。
/*
* 使用Lock接口和Condition来实现生产者-消费者队列的通信
*/
public class ConditionEventQueue {
//显示定义Lock对象
private final Lock lock = new ReentrantLock();
//通过newCondition方法获取指定Lock对象的Condition实例
private final Condition cond = lock.newCondition();
private final int max;
static class Event{ }
//定义一个不可改的链表集合, 作为队列载体
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 10;
public ConditionEventQueue(int max) {
this.max = max;
} public ConditionEventQueue() {
this(DEFAULT_MAX_EVENT);
} private void console(String message) {
System.out.printf("%s:%s\n",Thread.currentThread().getName(), message);
}
//定义入队方法
public void offer(Event event) {
lock.lock();
try {
//在循环中判断如果队列已满, 则调用cond的wait方法, 使线程阻塞
while (eventQueue.size() >= max) {
try {
console(" the queue is full");
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
cond.signalAll();;
} finally {
lock.unlock();
} }
//定义出队方法
public Event take() {
lock.lock();
try {
//在循环中判断如果队列已空, 则调用cond的wait方法, 使线程阻塞
while (eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
cond.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
cond.signalAll();
console(" the event " + event + " is handled/taked.");
return event;
} finally {
lock.unlock();
}
}
}
Java 1.5开始就提供了BlockingQueue接口,来实现如上所述的生产者-消费者线程同步工具。具体介绍将另文说明。
Java并发编程之线程安全、线程通信的更多相关文章
- Java并发编程:如何创建线程?
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- 【转】Java并发编程:如何创建线程?
一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...
- [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors
[Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...
- [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...
[Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...
- Java 并发编程——Executor框架和线程池原理
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- 2、Java并发编程:如何创建线程
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- 原创】Java并发编程系列2:线程概念与基础操作
[原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...
- Java并发编程的艺术(六)——线程间的通信
多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java ...
- Java并发编程:进程和线程之由来
Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...
随机推荐
- Java 原生日志 java.util.logging
简介 Java 中的 Logging API 让 Java 应用可以记录不同级别的信息,它在debug过程中非常有用,如果系统因为各种各样的原因而崩溃,崩溃原因可以在日志中清晰地追溯,下面让我们来看看 ...
- ARP 地址分类 NAT技术
第1章 OSI回顾 1.1 TCP/IP协议族组成 应用层 主机到主机层 互联网层 网络接入层 1.2 总结应用层掌握的协议与端口号对应关系 http(80) telnet(23) ftp(2 ...
- Qt-网易云音乐界面实现-6 迷你个人中心实现
这个界面除了麻烦耗时,没有啥技术含量.暂时我也就把它称为迷你个人中心,因为后面还有一个个人中心了. 先看下完成品 左侧是我的,右侧是原生 个人感觉还可以吧,哈哈哈.给我自己奖励一个鸡腿. 看下头文件 ...
- 15-RUN vs CMD vs ENTRYPOINT
RUN.CMD 和 ENTRYPOINT 这三个 Dockerfile 指令看上去很类似,很容易混淆.本节将通过实践详细讨论它们的区别. 简单的说: RUN 执行命令并创建新的镜像层,RUN 经常用于 ...
- 九九乘法表的python复习
九九开始的复习 这周复习之前的学的知识关于range函数,gormat函数,print的使用总结一下 从一个小例子开始,开始我的回顾吧, 大家都是从那个九九乘法表开始的数学之旅,从一一得一,开始了我们 ...
- Netty源码分析第3章(客户端接入流程)---->第1节: 初始化NioSockectChannelConfig
Netty源码分析第三章: 客户端接入流程 概述: 之前的章节学习了server启动以及eventLoop相关的逻辑, eventLoop轮询到客户端接入事件之后是如何处理的?这一章我们循序渐进, 带 ...
- WinDbg使用学习
拿到软件崩溃之后产生的crash文件,后缀名为dump 使用winDbg的File-----> Open Crash Dump 打开Crash文件 File---------> Symbo ...
- PytorchZerotoAll学习笔记(一)
Pytorch的安装请参考torch的官方文档,传送门:https://pytorch.org/get-started/locally/ Numpy的复习 如果你之前没有学过Numpy的话,建议去看看 ...
- XSS(Cross Site Script)
类型一:反射型XSS 简单地把用户输入的数据“反射”给浏览器.也就是说,黑客需要诱使用户“点击”一个恶意链接,才能攻击成功. 类型二:存储型XSS 把用户输入的数据“存储”在服务器端.这种XSS具有很 ...
- 入门向:南邮CTF_ReadAsm2_WP
题目链接:http://ctf.nuptzj.cn/challenges#ReadAsm2 我比较菜,所以把思路全部敲上来了. 题目很明确告诉我们,这道题考察阅读汇编代码的能力. 在对编译环境和调用约 ...