class Ticket implements Runnable
{
public int sum=10;
public void run()
{
while(true)
{
if(sum>0)
{
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}

如果四个线程同时进入了run方法中,假设当时sum==1,则第一个线程可以进入if块中,但是如果CPU突然切换到了其他线程,那么第一个线程将会等待CPU执行权,但是并没有改变sum的值,此时sum仍然是1;同理,假设极端情况发生了,即第2、3个线程均进入了if块,而且均在改变sum值之前就并指运行,等待CPU执行权,那么第四个线程改变完sum的值称为0之后,其余三个线程会将sum的值变为-1,-2,-3(但是输出只能到-2),很明显的,问题发生了,虽然几率不大,但是一旦发生就是致命的问题。

使用Thread.sleep()方法可以暂停线程的执行,通过输出即可检验。

class Ticket implements Runnable
{
public int sum=10;
public void run()
{
while(true)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
} }
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}

本例中还出现了另一个线程安全性问题:第二条和第三条同时卖出了9号票,这是因为sum--还没来得及自减CPU就切换到了其他线程。

注意使用sleep方法产生的异常只能捕获不能抛出。

1. 线程安全性问题的原因

1).多个线程在操作共享的数据。四个线程操作共享的ticket数据
2).操作共享数据的线程代码有多条。

  当一个线程在执行操作共享数据的多条代码过程中其他线程参与了运算就会导致线程安全问题的产生。

通过以上得分以方法,我们可以发现并解决大多数的线程安全问题。

在本例中,由于操作共享数据ticket的线程有多条,而且每个线程操作共享数据的代码有三条(除去try-catch):

if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}

解决线程安全性问题的分析:

关键问题:两个语句被分开读了。
解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候其他线程是不可以参与运算    的,必须要当前线程把这些代码都执行完毕后其他线程才可以参与运算。

2. 解决线程安全问题方法——同步代码块

使用同步代码块的格式:

synchronized(对象 )
{
  需要被同步的代码。
}

需要说明的是,这里的对象类型是任意的,但是要保证各个线程所使用的对象是统一个对象,可以将此对象定义为Ticket的成员,还可以是其它现有的对象,如this或者字节码文件对象等。

class Ticket implements Runnable
{
Object obj=new Object();
private int sum=10;
public void run()
{
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}

除了改成同步代码块之外,还需要改动其它:在if与while之间加入sleep方法,这样做的目的是为了便于观察多个线程协调运作的情况,否则容易出现单个线程将任务完成的情况,出现这样的原因就是判断同步锁的消耗大于进行下一次循环的消耗,因此,将进行下一次循环所用的时间延长即可轻易解决掉这个问题。

3. 同步的好坏以及其他问题的解决方案

同步的原理:

同步代码块需要的对象相当于一把锁,这把锁称为同步锁。我们可以和火车上的卫生间相比较:当我们需要上卫生间的时候,会先看一下指示灯是否是绿灯,如果是绿灯则表示没有人(这相当于判断锁的过程),这时候我们可以打开门(拿到锁),干活(执行同步代码块代码)。如果这时候外面的人想进来,就会发现灯变红了(这是因为锁的控制权在蹲坑的人手里),所以他们进不来(拿不到锁),等到卫生间里的人出来了(释放锁),外面的人才能进去(拿到锁),这样就保证了卫生间里的人只有一个(保证执行同步代码块的线程只有一个)。

同步的好处:

解决了线程安全性问题。

同步的弊端:

进入同步代码块中的线程不会一直持有CPU执行权,CPU切换到其他线程,判断锁之后又进不去同步代码块,相当于做了无用功。
这样就相对降低了执行效率。

同步中必须有多个线程并使用同一把锁,这是同步的前提。如果出现了即使加上锁仍然出现了线程安全性问题,很有可能是多个线程用的不是同一把锁。

举例:

class Ticket implements Runnable
{ private int sum=10;
public void run()
{
Object obj=new Object();
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}

将同步锁的定义放在了run方法中,这样每个线程都有了自己的锁,线程安全性问题依旧:

4. 解决线程安全问题方法——同步函数

在同步方法的修饰符中添加synchronized关键字即可

储户存钱问题:

    需求:有两个储户,每个都到银行存钱,每次存100,共存三次。

class Bank
{
private int sum;
public void add(int num)
{
sum+=num;
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}

我们运行代码很多次,没有发现线程异常,虽然没有发现异常但是并不代表以后不会发生。假设其中一个线程在sum+=num之后进入堵塞状态,那么肯定就会发生两个线程打印出的银行账目相同的情况。

现在我们模拟sum+=num之后线程堵塞的情况,我们可以通过使用sleep方法实现。

class Bank
{
private int sum;
public void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}

很明显出现了线程安全性问题。

使用同步代码块可以解决这个问题,但是我们可以使用同步方法解决这个问题,因为add方法本身就是一个单独的封装体。

class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}

我们可以发现存入了200但是银行只有100,问题解决了一半。

经过分析我们可以知道问题出在

1 System.out.println(Thread.currentThread().getName()+":存入100");
2 this.bank.add(100);

这两句代码没有同步,也就是说一个线程在执行到第一行代码的时候CPU切换到了其他线程。

我们可以将这两个代码同步起来解决这个问题。

/**
线程安全性问题使用同步方法解决。
*/
class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
synchronized(bank)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}

们发现线程0一直执行,直到循环结束才轮到线程1,运行很多次仍然是这样,出现这样的原因就是判断锁需要的时间过大

我们可以使用sleep方法解决这个问题,以达到交替显示的效果。

class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
synchronized(bank)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}

现在将买票系统的同步代码块改成同步函数:

/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
private int sum=10;
public void run()
{
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}

我们可以看到并没有发生线程安全性问题,改造成功。

现在开始分析同步函数使用的锁是什么锁。

答案:this锁,即本对象。

验证:

为了便于比较和保持代码简洁,现在使用两个线程买票。

两个线程分别使用同步代码块和同步方法,如果没有出现线程安全性问题,则证明同步代码块和同步方法使用的是同一把锁。

同步代码块使用Object锁,同时票的数量改为100。

class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); t.flag=true;
new Thread(t).start();
}
}

现象就是所有的线程都经由同步代码块而没有经过同步方法。

原因就是主方法一口气执行完了,就将flag的值改为了true,这时候两个线程都还没有启动,当启动的时候,发现flag的值为true所以都走了同步代码块。

我们应当找出一种方法,让线程0在启动之后并且进入死循环才让标志变量改变,我们只需要在改变标志变量之前等待一段时间即可,等待的目的就是让线程0启动。

/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
}
t.flag=true;
new Thread(t).start();
}
}

我们发现了线程安全性问题的存在(通过设置延长时间大大增加了发生的几率)。表名使用的不是同一把锁。

现在同步代码块使用的锁改成this。

/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
}
t.flag=true;
new Thread(t).start();
}
}

5. 解决线程安全问题方法——静态同步函数

/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private static int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public static synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{ }
t.flag=true;
new Thread(t).start();
}
}

线程安全性问题又出现了,表名静态同步函数所使用的锁不是本类对象,其原因是显而易见的。

其实,静态同步方法是哦用的锁是字节码文件对象,获取字节码文件对象的方法有两种:

1.对象名.getClass();

2.类名.class;

现改进代码:只是将同步代码块使用的对象锁改成this.getClass或者Ticket.class

/*
将买票系统的同步代码块改造成静态同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private static int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this.getClass())
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public static synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
t.flag=true;
new Thread(t).start();
}
}

经过多次运行程序并验证,可以得到静态同步函数使用的同步锁是本类的字节码文件对象这一结论。

java多线程安全的更多相关文章

  1. 40个Java多线程问题总结

    前言 Java多线程分类中写了21篇多线程的文章,21篇文章的内容很多,个人认为,学习,内容越多.越杂的知识,越需要进行深刻的总结,这样才能记忆深刻,将知识变成自己的.这篇文章主要是对多线程的问题进行 ...

  2. Java多线程基础知识篇

    这篇是Java多线程基本用法的一个总结. 本篇文章会从一下几个方面来说明Java多线程的基本用法: 如何使用多线程 如何得到多线程的一些信息 如何停止线程 如何暂停线程 线程的一些其他用法 所有的代码 ...

  3. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  4. Java多线程系列--“JUC锁”04之 公平锁(二)

    概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...

  5. Java多线程--让主线程等待子线程执行完毕

    使用Java多线程编程时经常遇到主线程需要等待子线程执行完成以后才能继续执行,那么接下来介绍一种简单的方式使主线程等待. java.util.concurrent.CountDownLatch 使用c ...

  6. Java多线程 2 线程的生命周期和状态控制

    一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就 ...

  7. java 多线程 1 线程 进程

    Java多线程(一).多线程的基本概念和使用 2012-09-10 16:06 5108人阅读 评论(0) 收藏 举报  分类: javaSE综合知识点(14)  版权声明:本文为博主原创文章,未经博 ...

  8. 一起阅读《Java多线程编程核心技术》

    目录 第一章 Java多线程技能 (待续...)

  9. 第一章 Java多线程技能

    1.初步了解"进程"."线程"."多线程" 说到多线程,大多都会联系到"进程"和"线程".那么这两者 ...

  10. java从基础知识(十)java多线程(下)

    首先介绍可见性.原子性.有序性.重排序这几个概念 原子性:即一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行. 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到 ...

随机推荐

  1. php返回数据格式

    PHP返回HTML代码:     header('Content-type:text/html; charset=utf-8'); PHP返回xml代码:header('content-type: t ...

  2. 查看当前linux有多少http连接数

    已采纳 1.查看apache当前并发访问数: #对比httpd.conf中MaxClients的数字差距多少.netstat -an | grep ESTABLISHED | wc -l 2.查看ht ...

  3. Linux字符设备简单示例

    1. Linux字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open.close.read和write系统调用.例如:串口.Led.按键等. 2. 通过字符设备文件 ...

  4. caffe 图片数据的转换成lmdb和数据集均值(转)

    转自网站: http://blog.csdn.net/muyiyushan/article/details/70578077 1.准备数据 使用dog/cat数据集,在训练项目根目录下分别建立trai ...

  5. vue.js入门环境搭建

    1.node.js环境(npm包管理器) 2.vue-cli手脚架构建工具 3.cnpm npm的淘宝镜像 安装node.js 从node.js官网下载并安装node,安装过程一路“下一步”就可以 安 ...

  6. 如何使用python将二维数组去重呢?

    二维数组的去重,能和一维的方法类似吗?import numpyc=np.array(((1,2),(3,4),(5,6),(7,8),(7,8),(3,4),(1,2)))print('二维数组:\n ...

  7. socket编程(一)

    因为下载器涉及到socket的知识,就花了一天学习了.因为时间原因分成几部分.(这里记录上的是基于Windows平台的) #include <stdio.h> #include <w ...

  8. Linux大文件快速处理小方法

    背景 工作中使用MapReduce任务导出一批含有路径的文件,共计行数300W+,需要检测文件是否在对应的服务器中存在,而文件所在的服务器并非hadoop集群的服务器,因此打算采用bash脚本进行.具 ...

  9. GitKraken使用教程-基础部分(5)

    7. 提交代码 1) 查看文件改动 修改了某个文件后,在程序右侧会出现已修改文件的列表(如图 1‑1),这里以Test.git 为例,修改了19264.h 的文件编码,将其改为utf8.Unstage ...

  10. mongodb 用户权限控制

    MongoDB已经使用很长一段时间了,基于MongoDB的数据存储也一直没有使用到权限访问(MongoDB默认设置为无权限访问限制),今天特地花了一点时间研究了一下,研究成果如下: 注:研究成果基于W ...