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. Codeforces 914 C. Travelling Salesman and Special Numbers (数位DP)

    题目链接:Travelling Salesman and Special Numbers 题意: 给出一个二进制数n,每次操作可以将这个数变为其二进制数位上所有1的和(3->2 ; 7-> ...

  2. Java虚拟机笔记(四):垃圾收集器

    前言 前一篇文章介绍了内存的垃圾收集算法,现在介绍下内存回收的具体实现--垃圾收集器. 由于Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集 ...

  3. while、for循环控制之if、else

    if # score=99 # if score>90: # print('优秀') # elif score<60: # print('不及格') # else: # print('良好 ...

  4. 异步任务(Celery)详解

    一.背景 在开发中,我们可能经常会遇到一些需要执行时间很长的任务,如果放在前端,会让用户一直卡在那儿等待或者一直转圈圈,体验非常不好.为了改善这种体验,我赶紧上网搜索,果然,前人早已有解决办法了.那就 ...

  5. HTTP 请求/响应报文结构

    请求报文和响应报文都是由以下4部分组成: 1.请求行/响应行 2.请求头/响应头 3.空行 4.消息主体(请求体/响应体) 请求报文结构 请求行 格式为:Method Request-URI HTTP ...

  6. Altium软件中Unknowpin的详细解决办法

    1.Altium软件中Unknowpin第一种原因:PCB封装缺失遗漏,直接加入对应的封装即可.点击箭头指示处的Add,接着点击OK之后,再点击图中所示处Browse...选择封装库的封装即可. 2. ...

  7. Python练习——矩形、直角三角形、乘法表

    一.矩形 #******输入行和列,打印相应的矩形******# width = input("宽:") longth = input("长:") if (wi ...

  8. php_package v2.7发布了 宋正河作品

    php_package 是一个面向过程的底层开发框架 http://download.csdn.net/download/songzhengdong82/4974123 欢迎大家下载

  9. Eclipse Photon没有Server选项的问题

    安装Eclipse Photon版本的时候选择了Java开发,导致最后需要本地tomcat调试的时候找不到Server配置选项. ①在软件eclipse下的Help->InstallNew So ...

  10. c语言数字图像处理(八):噪声模型及均值滤波器

    图像退化/复原过程模型 高斯噪声 PDF(概率密度函数) 生成高斯随机数序列 算法可参考<http://www.doc.ic.ac.uk/~wl/papers/07/csur07dt.pdf&g ...