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并发编程之线程安全、线程通信的更多相关文章

  1. Java并发编程:如何创建线程?

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  2. Java 并发编程——Executor框架和线程池原理

    Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...

  3. 【转】Java并发编程:如何创建线程?

    一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...

  4. [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors

    [Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...

  5. [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...

    [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...

  6. Java 并发编程——Executor框架和线程池原理

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  7. 2、Java并发编程:如何创建线程

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  8. 原创】Java并发编程系列2:线程概念与基础操作

    [原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...

  9. Java并发编程的艺术(六)——线程间的通信

    多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java ...

  10. Java并发编程:进程和线程之由来

    Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...

随机推荐

  1. STM平台增加性能测试/稳定性测试部分【二】

    [一]方案 基本上测试针对产品的各项方案大体是如下的: 如上所示,针对产品的性能测试主要步骤如下: 1.造数据,在产品业务流上,产生所需的数据,数据量以(稳定性或者压测指标确定) 2.根据步骤1,设定 ...

  2. jenkins使用时出现的问题!

      从安装到日常使用中遇到过的问题和解决方法:   背景/问题:安装时是跳过安装插件过程的,安装好后,我发现里面啥也做不了,连个git的插件都无法下载. 方法:在jenkins的主界面,打开系统管理= ...

  3. flask入门补充

    在上篇文章提到了json的编码问题.那么Flask是国外开发的框架,没有考虑到中文编码,那么我们就需要自己配置 那么在访问页面的时候会有 get 请求和post  请求.在下边我也会提到.以及没有接触 ...

  4. 自己编写的:centos6.6上编译安装apache2.4+php5.6+mysql5.6【亲自】

    在centos6.6上安装apache2.4+php5.6+mysql5.6 关于wget的安装 将之前装系统的.iso文件挂载到光驱 由于我在/home/jinnan/下建立了一个cdrom文件夹 ...

  5. Eclipse web项目更改项目名称

    1. 右键工程:Refactor->Rename,更改项目名称: 2. 修改项目目录下:.project文件 <?xml version="1.0" encoding= ...

  6. Java的POI的封装与应用

    Java对Excel表格的导出一直是对我有种可怕噩梦的东西,每次对要建立行与列,并一个一个放值,我是从心底拒绝的. 处于项目需求,需要导出表格,于是找到网上一版很好的开发, <不想用POI?几行 ...

  7. DNS分离解析IPV6与IPV4用户

    IPV6改造中经常会遇到,网站使用了CDN,但是CDN厂商还不支持IPV6的情况,而AAAA.A.CNAME记录互相冲突,想实现IPV6用户得到AAAA记录,IPV4用户得到CNAME记录的需求. 解 ...

  8. 无法连接 Plugins Market 失效的日子

    一.问题背景 不知道是什么原因,我的 Intellij 连接不上 Plugins Market,这时候我需要使用 @Data 注解来自动生成 Getter.Setter 方法.在添加了相应的依赖之后, ...

  9. ubuntu安装中文输入法必看

    ubuntu安装中文输入法必看以下两篇文章,按照顺序来做: http://www.2cto.com/os/201207/144189.html http://www.cnblogs.com/slide ...

  10. LeetCode 174. Dungeon Game (C++)

    题目: The demons had captured the princess (P) and imprisoned her in the bottom-right corner of a dung ...