一个程序在运行起来时,会转换为进程,通常含有多个线程。

通常情况下,一个进程中的比较耗时的操作(如长循环、文件上传下载、网络资源获取等),往往会采用多线程来解决。

比如,现实生活中,银行取钱问题、火车票多个窗口售票问题等,通常会涉及并发问题,从而需要用到多线程技术。

当进程中有多个并发线程进入一个重要数据的代码块时,在修改数据的过程中,很有可能引发线程安全问题,从而造成数据异常。例如,正常逻辑下,同一个编号的火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起实际业务异常。

接下来,我以售票问题,来演示多线程问题中对核心数据保护的重要性。我们先来看不对多线程数据进行保护时会引发什么样的状况。

/**
* 售票问题
*/
public class Test1 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
// 未加同步时,产生脏数据
while(tickets>0){
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票结束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test1().new SellTickets();
Thread t1=new Thread(sell, "1号窗口");
Thread t2=new Thread(sell, "2号窗口");
Thread t3=new Thread(sell, "3号窗口");
Thread t4=new Thread(sell, "4号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  上述代码运行后,效果如下:

1号窗口 -->售出第 10 张票
3号窗口 -->售出第 10 张票
2号窗口 -->售出第 10 张票
4号窗口 -->售出第 10 张票
3号窗口 -->售出第 6 张票
2号窗口 -->售出第 6 张票
1号窗口 -->售出第 5 张票
4号窗口 -->售出第 3 张票
3号窗口 -->售出第 2 张票
2号窗口 -->售出第 2 张票
1号窗口 -->售出第 2 张票
4号窗口 -->售票结束!
3号窗口 -->售票结束!
1号窗口 -->售票结束!
2号窗口 -->售票结束!

  上述运行结果中,第10张票被售出多次,显然不符合实际应用中的逻辑。由于多线程调度中的不确定性,读者在演示上述代码时,可能会取得不同的运行结果。

  为了解决上述脏数据的问题,我为大家介绍3种使用比较普遍的三种同步方式。

  第一种,同步代码块。

  有synchronized关键字修饰的语句块,即为同步代码块。同步代码块会被JVM自动加上内置锁,从而实现同步。

  我们来看代码:

/**
* 售票问题
* @author 李章勇
*
*/
public class Test2 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
//同步代码块
while(tickets>0){
synchronized(this){
if(tickets<=0){
break;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");
tickets--;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票结束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test2().new SellTickets();
Thread t1=new Thread(sell, "1号窗口");
Thread t2=new Thread(sell, "2号窗口");
Thread t3=new Thread(sell, "3号窗口");
Thread t4=new Thread(sell, "4号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

 上述代码运行结果:

1号窗口 -->售出第 10 张票
3号窗口 -->售出第 9 张票
4号窗口 -->售出第 8 张票
2号窗口 -->售出第 7 张票
3号窗口 -->售出第 6 张票
4号窗口 -->售出第 5 张票
2号窗口 -->售出第 4 张票
1号窗口 -->售出第 3 张票
4号窗口 -->售出第 2 张票
3号窗口 -->售出第 1 张票
1号窗口 -->售票结束!
2号窗口 -->售票结束!
4号窗口 -->售票结束!
3号窗口 -->售票结束!

  通过运行结果可知,上述运行结果正常。

  第二种,同步方法 。

  即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

  我们来看代码:

/**
* 售票问题
* @author 李章勇
*
*/
public class Test3 {
static int tickets=10;
class SellTickets implements Runnable{
@Override
public void run() {
//同步方法
while(tickets>0){
synMethod();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票结束!");
}
}
//同步方法
synchronized void synMethod(){
synchronized(this){
if(tickets<=0){
return;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");
tickets--;
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test3().new SellTickets();
Thread t1=new Thread(sell, "1号窗口");
Thread t2=new Thread(sell, "2号窗口");
Thread t3=new Thread(sell, "3号窗口");
Thread t4=new Thread(sell, "4号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  上述代码运行结果:

1号窗口 -->售出第 10 张票
4号窗口 -->售出第 9 张票
3号窗口 -->售出第 8 张票
2号窗口 -->售出第 7 张票
1号窗口 -->售出第 6 张票
2号窗口 -->售出第 5 张票
4号窗口 -->售出第 4 张票
3号窗口 -->售出第 3 张票
4号窗口 -->售出第 2 张票
3号窗口 -->售出第 1 张票
1号窗口 -->售票结束!
4号窗口 -->售票结束!
2号窗口 -->售票结束!
3号窗口 -->售票结束!

  上述代码运行结果也正常。

  第三种,Lock锁机制。

  通过创建Lock对象,采用lock()加锁,采用unlock()解锁,来保护指定代码块。我们看如下代码:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 售票问题
* @author 李章勇
*
*/
public class Test4 {
static int tickets=10;
class SellTickets implements Runnable{
Lock lock=new ReentrantLock();
@Override
public void run() {
//Lock锁机制
while(tickets>0){
try{
lock.lock();
if(tickets<=0){
break;
}
System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 张票");
tickets--;
}finally{
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if(tickets<=0){
System.out.println(Thread.currentThread().getName()+" -->售票结束!");
}
}
}
public static void main(String[] args) {
SellTickets sell=new Test4().new SellTickets();
Thread t1=new Thread(sell, "1号窗口");
Thread t2=new Thread(sell, "2号窗口");
Thread t3=new Thread(sell, "3号窗口");
Thread t4=new Thread(sell, "4号窗口");
t1.start();
t2.start();
t3.start();
t4.start();
}
}

  运行结果如下:

1号窗口 -->售出第 10 张票
2号窗口 -->售出第 9 张票
3号窗口 -->售出第 8 张票
4号窗口 -->售出第 7 张票
1号窗口 -->售出第 6 张票
4号窗口 -->售出第 5 张票
2号窗口 -->售出第 4 张票
3号窗口 -->售出第 3 张票
1号窗口 -->售出第 2 张票
2号窗口 -->售出第 1 张票
3号窗口 -->售票结束!
1号窗口 -->售票结束!
2号窗口 -->售票结束!
4号窗口 -->售票结束!

  最后总结:

  由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。

  另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

  补充:  

  在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。
其中,wait()方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁。

  notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码。

  需要注意的是,wait()和notify()必须在synchronized代码块中调用。

  notifyAll()是唤醒所有等待的线程。

  接下来,我们通过下一个程序,使得两个线程交替打印“A”和“B”各10次。请见下述代码:

 

public class Test5 {
static final Object obj=new Object();
//一个子线程
static class ThreadA implements Runnable{
@Override
public void run() {
int count=10;
while(count>0){
synchronized(Test5.obj){
System.out.println("A-->"+count);
count--;
Test5.obj.notify();
try {
Test5.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//另一个子线程
static class ThreadB implements Runnable{
@Override
public void run() {
int count=10;
while(count>0){
synchronized(Test5.obj){
System.out.println("B-->"+count);
count--;
Test5.obj.notify();
try {
Test5.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}

  显示结果如下:

A-->10
B-->10
A-->9
B-->9
A-->8
B-->8
A-->7
B-->7
A-->6
B-->6
A-->5
B-->5
A-->4
B-->4
A-->3
B-->3
A-->2
B-->2
A-->1
B-->1

  

Java之线程安全中的三种同步方式的更多相关文章

  1. Asp.Net中的三种分页方式

    Asp.Net中的三种分页方式 通常分页有3种方法,分别是asp.net自带的数据显示空间如GridView等自带的分页,第三方分页控件如aspnetpager,存储过程分页等. 第一种:使用Grid ...

  2. python中的三种输入方式

    python中的三种输入方式 python2.X python2.x中以下三个函数都支持: raw_input() input() sys.stdin.readline() raw_input( )将 ...

  3. SQL Server中的三种Join方式

      1.测试数据准备 参考:Sql Server中的表访问方式Table Scan, Index Scan, Index Seek 这篇博客中的实验数据准备.这两篇博客使用了相同的实验数据. 2.SQ ...

  4. C++中的三种继承方式

    1,被忽略的细节: 1,冒号( :)表示继承关系,Parent 表示被继承的类,public 的意义是什么? class Parent { }; class Child : public Parent ...

  5. 关于selenium中的三种等待方式与EC模块的知识

    1. 强制等待 第一种也是最简单粗暴的一种办法就是强制等待sleep(xx),强制让闪电侠等xx时间,不管凹凸曼能不能跟上速度,还是已经提前到了,都必须等xx时间. 看代码: 1 2 3 4 5 6 ...

  6. Java开发学习(四)----bean的三种实例化方式

    一.环境准备 准备开发环境 创建一个Maven项目 pom.xml添加依赖 resources下添加spring的配置文件applicationContext.xml 最终项目的结构如下:    二. ...

  7. selenium中的三种等待方式(显示等待WebDriverWait()、隐式等待implicitly()、强制等待sleep())---基于python

    我们在实际使用selenium或者appium时,等待下个等待定位的元素出现,特别是web端加载的过程,都需要用到等待,而等待方式的设置是保证脚本稳定有效运行的一个非常重要的手段,在selenium中 ...

  8. selenium&appium中的三种等待方式---基于python

    我们在实际使用selenium或者appium时,等待下个等待定位的元素出现,特别是web端加载的过程,都需要用到等待,而等待方式的设置是保证脚本稳定有效运行的一个非常重要的手段,在selenium中 ...

  9. Asp.Net中的三种分页方式总结

    本人ASP.net初学,网上找了一些分页的资料,看到这篇文章,没看到作者在名字,我转了你的文章,只为我可以用的时候方便查看,2010的文章了,不知道这技术是否过期. 以下才是正文 通常分页有3种方法, ...

随机推荐

  1. 如何用Python写一个计算器软件 附带效果图

    该计算器使用Python  tkinter模块开发 效果如下图 import tkinter #导入tkinter模块 root = tkinter.Tk() root.minsize(280,500 ...

  2. SQLServer2008数据库连接error40错误

    在连接SQL Server偶尔会遇到报错,如在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误.未找到或无法访问服务器.请验证实例名称是否正确并且 SQL Server 已配置为 ...

  3. 用TensorFlow实现文本分析模型,做个聊天机器人

    用TensorFlow实现文本分析模型,做个聊天机器人 聊天机器人的架构简图用 TensorFlow 实现 Chatbot 的模型如何准备 chatbot 的训练数据Chatbot 源码解读 1. 聊 ...

  4. C++课程设计报告总结

          C++课程设计报告             学院:计算机学院 班级:计科141班 姓名:刘建伟 学号:201400814125 指导老师:王璐 C++课程设计实验报告 学号:2014008 ...

  5. php面向对象(OOP)编程完全教程(转载笔记,有兴趣可以看看))

    http://www.cnblogs.com/xiaochaohuashengmi/archive/2010/09/10/1823042.html

  6. cookie和session有什么区别,请你谈谈cookie的缺点

    1.区别: cookie机制采用的是在客户端保持状态的方案 session机制采用的是在服务端保持状态的方案 2.cookie: 优点: 1> 极高的扩展性和可用性 2> 通过编程方式,控 ...

  7. QT之UDP通信

    前言:前一篇讲了TCP通信,这篇来看看UDP通信. 这里说明一下,UDP通信中分为三种通信分别为单播.组播和广播,下面将一一为大家介绍. 同样的我们都需要在工程文件中添加network QT += c ...

  8. ELK介绍

    为什么用到ELK: 一般我们需要进行日志分析场景:直接在日志文件中 grep.awk 就可以获得自己想要的信息.但在规模较大的场景中,此方法效率低下,需要集中化的日志管理,所有服务器上的日志收集汇总. ...

  9. .NET版支付宝商户会员卡接入

    最近公司计划对接支付宝会员卡功能,而任务恰巧由领导安排给我这边,小弟之前也未做过支付宝接口,研究了三天,终于将支付宝会员卡API接口大体上调通了,现将其整理下,以供参考. 蚂蚁金服开发平台-商户会员卡 ...

  10. JAVA中静态修饰符static的学习(初学)

    静态修饰符static,用于修饰类中的成员变量和成员函数. 用static修饰的成员变量也可叫做类变量. 什么时候使用静态 什么时候定义静态成员变量?     当对象中出现共享数据时,将该数据定义为静 ...