一、线程安全

1.线程安全的概念

线程安全:某个类被单个线程,或者多个线程同时访问,所表现出来的行为是一致,则可以说这个类是线程安全的。

2.什么情况下会出现线程安全问题

在单线程中不会出现线程安全问题,在多线程编程的情况下,并且多个线程访问同一资源的情况下可能出现线程安全问题。如下面的例子,出现典型的线程安全问题:

 public class BookSaleRunable implements Runnable{
private int bookNum=10;//书的总数为10
@Override
public void run() {
for(int i=0;i<5;i++){
if(bookNum>0){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
}
} } public static void main(String[] args) {
BookSaleRunable booksr = new BookSaleRunable();
//开启3个线程
Thread t1 = new Thread(booksr);
Thread t2 = new Thread(booksr);
Thread t3 = new Thread(booksr);
t1.start();
t2.start();
t3.start();
} }

结果输出:

Thread-1 卖出一本书,剩余书籍:9
Thread-2 卖出一本书,剩余书籍:7
Thread-0 卖出一本书,剩余书籍:8
Thread-1 卖出一本书,剩余书籍:6
Thread-0 卖出一本书,剩余书籍:4
Thread-2 卖出一本书,剩余书籍:5
Thread-1 卖出一本书,剩余书籍:3
Thread-2 卖出一本书,剩余书籍:1
Thread-0 卖出一本书,剩余书籍:2
Thread-2 卖出一本书,剩余书籍:0
Thread-1 卖出一本书,剩余书籍:-1
Thread-0 卖出一本书,剩余书籍:-2

上例出现超卖现象,在生活中不允许,同时如果是一个线程肯定不会出现。那么如何解决这个问题呢?基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,在给定时刻只允许一个任务访问共享资源。

二、解决线程安全问题的方法

1.synchronized关键字

synchronized,顾名思义就是同步的意思,主要用来给方法、代码块加锁。当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。synchronized可以用来修饰方法和代码块。

synchronized修饰方法

当synchronized关键字修饰方法的时候,这个方法我们称为同步方法,同步方法可以控制对类成员变量的方法,synchronized关键字表明方法已经加锁,当任意一个线程访问同步方法的时候都必须 判断这个方法是否被其他线程独占。所有对象自动含有单一的锁,当在对象上调用其任意的synchronized方法的时候,次对象被加锁,这个时候这个对象的其他synchronized方法必须等到前一个同步方法调用完毕并释放锁之后才能被调用。如下:

 public class BookSaleRunable2 implements Runnable{
private int bookNum=20;//书的总数为10
@Override
public void run() {
for(int i=0;i<7;i++){
saleBook();
} } public static void main(String[] args) {
BookSaleRunable2 booksr = new BookSaleRunable2();
//开启3个线程
Thread t1 = new Thread(booksr);
Thread t2 = new Thread(booksr);
Thread t3 = new Thread(booksr);
t1.start();
t2.start();
t3.start();
} private synchronized void saleBook(){
if(bookNum>0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
}
} }

结果输出:

Thread-0 卖出一本书,剩余书籍:19
Thread-0 卖出一本书,剩余书籍:18
Thread-0 卖出一本书,剩余书籍:17
Thread-0 卖出一本书,剩余书籍:16
Thread-0 卖出一本书,剩余书籍:15
Thread-0 卖出一本书,剩余书籍:14
Thread-0 卖出一本书,剩余书籍:13
Thread-2 卖出一本书,剩余书籍:12
Thread-2 卖出一本书,剩余书籍:11
Thread-2 卖出一本书,剩余书籍:10
Thread-2 卖出一本书,剩余书籍:9
Thread-2 卖出一本书,剩余书籍:8
Thread-2 卖出一本书,剩余书籍:7
Thread-2 卖出一本书,剩余书籍:6
Thread-1 卖出一本书,剩余书籍:5
Thread-1 卖出一本书,剩余书籍:4
Thread-1 卖出一本书,剩余书籍:3
Thread-1 卖出一本书,剩余书籍:2
Thread-1 卖出一本书,剩余书籍:1
Thread-1 卖出一本书,剩余书籍:0

注意:在使用并发的时候,将域设置为private是非常重要的,否则,synchronized关键字不能防止其他任务直接访问域,这样讲产生冲突问题。

synchronized与static

关键字synchronized可以修饰static静态方法,这个时候是对当前静态方法所属的Class类进行加锁。与修饰普通方法是有差别的。如下:

 public class ServiceDemo {

     public  static void print1(){
System.out.println(Thread.currentThread().getName()+" print1 start ");
System.out.println(Thread.currentThread().getName()+" print1 end ");
}
public static void print2(){
System.out.println(Thread.currentThread().getName()+" print2 start ");
System.out.println(Thread.currentThread().getName()+" print2 end ");
} public static void main(String[] args) {
ServiceDemo serviceDemo1 = new ServiceDemo();
ServiceDemo serviceDemo2 = new ServiceDemo();
Thread t1 =new Thread(new Thread1(serviceDemo1));
Thread t2 =new Thread(new Thread2(serviceDemo2));
t1.start();
t2.start();
}
} class Thread1 implements Runnable{
private ServiceDemo serviceDemo; Thread1(ServiceDemo serviceDemo){
this.serviceDemo = serviceDemo;
} @Override
public void run() {
serviceDemo.print1();
} }
class Thread2 implements Runnable{
private ServiceDemo serviceDemo; Thread2(ServiceDemo serviceDemo){
this.serviceDemo = serviceDemo;
} @Override
public void run() {
serviceDemo.print2();
} }

输出结果:

Thread-0 print1 start
Thread-1 print2 start
Thread-0 print1 end
Thread-1 print2 end

使用static和synchronized结合的时候,效果完全不同了,如下:

 public class ServiceDemo {

     public synchronized static void print1(){
System.out.println(Thread.currentThread().getName()+" print1 start ");
System.out.println(Thread.currentThread().getName()+" print1 end ");
}
public synchronized static void print2(){
System.out.println(Thread.currentThread().getName()+" print2 start ");
System.out.println(Thread.currentThread().getName()+" print2 end ");
} public static void main(String[] args) {
ServiceDemo serviceDemo1 = new ServiceDemo();
ServiceDemo serviceDemo2 = new ServiceDemo();
Thread t1 =new Thread(new Thread1(serviceDemo1));
Thread t2 =new Thread(new Thread2(serviceDemo2));
t1.start();
t2.start();
}
} class Thread1 implements Runnable{
private ServiceDemo serviceDemo; Thread1(ServiceDemo serviceDemo){
this.serviceDemo = serviceDemo;
} @Override
public void run() {
serviceDemo.print1();
} }
class Thread2 implements Runnable{
private ServiceDemo serviceDemo; Thread2(ServiceDemo serviceDemo){
this.serviceDemo = serviceDemo;
} @Override
public void run() {
serviceDemo.print2();
} }

输出结果:

Thread-0 print1 start
Thread-0 print1 end
Thread-1 print2 start
Thread-1 print2 end

从结果看出,两个线程并未共享同一个对象,但是两个不同对象的静态同步方法发生了同步互斥。如果将ServiceDemo类中的一个方法修改为非静态同步方法,上述代码的执行结果则可能不是上面显示的那样了,那是因为如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

synchronized修饰代码块

使用关键字synchronized声明方法在某些情况下是有弊端的,非常影响效率。如果多个线程在访问一个synchronized方法,那么同一时刻只有一个线程在执行该方法,而其他线程都必须等待,但是如果该方法没有使用synchronized,则所有线程可以在同一时刻执行它,减少了执行的总时间。在这样的情况下,推荐使用同步语句块来解决,同步方法是对当前对象进行加锁,而synchronized代码块是对某一个对象加锁。synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。

 public class BookSaleRunable3 implements Runnable{
private int bookNum=20;//书的总数为10
@Override
public void run() {
for(int i=0;i<7;i++){
synchronized (this) {
if(bookNum>0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum));
}
}
} } public static void main(String[] args) {
BookSaleRunable3 booksr = new BookSaleRunable3();
//开启3个线程
Thread t1 = new Thread(booksr);
Thread t2 = new Thread(booksr);
Thread t3 = new Thread(booksr);
t1.start();
t2.start();
t3.start();
} }

结果输出:

Thread-0 卖出一本书,剩余书籍:19
Thread-0 卖出一本书,剩余书籍:18
Thread-0 卖出一本书,剩余书籍:17
Thread-0 卖出一本书,剩余书籍:16
Thread-0 卖出一本书,剩余书籍:15
Thread-0 卖出一本书,剩余书籍:14
Thread-0 卖出一本书,剩余书籍:13
Thread-2 卖出一本书,剩余书籍:12
Thread-2 卖出一本书,剩余书籍:11
Thread-2 卖出一本书,剩余书籍:10
Thread-2 卖出一本书,剩余书籍:9
Thread-1 卖出一本书,剩余书籍:8
Thread-1 卖出一本书,剩余书籍:7
Thread-1 卖出一本书,剩余书籍:6
Thread-1 卖出一本书,剩余书籍:5
Thread-1 卖出一本书,剩余书籍:4
Thread-1 卖出一本书,剩余书籍:3
Thread-1 卖出一本书,剩余书籍:2
Thread-2 卖出一本书,剩余书籍:1
Thread-2 卖出一本书,剩余书籍:0

总结:

  • 当一个线程访问对象的一个synchronized方法的时候,另外的线程仍然可以访问对象的非synchronized方法。
  • 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
  • 同一时间只有一个线程可以执行synchronized同步代码块中的代码。
  • 如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。
  • synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象

2.Lock锁机制

Lock对象必须被显式的创建、锁定和释放,与jvm内建的锁相比,代码缺乏优雅性,但是对于解决某些特殊类型的问题来说,它更加灵活。使用synchronized关键字,需要些的代码量更少。具体的例子如下:

 public class BookSaleRunable4 implements Runnable {
private int bookNum = 10;// 书的总数为10
private Lock lock = new ReentrantLock();// 重入锁 @Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();// 显示加锁
try {
if (bookNum > 0) {
Thread.sleep(300);
System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:" + (--bookNum));
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//主动释放锁,很容易忘记释放锁
}
} }
public static void main(String[] args) {
BookSaleRunable4 booksr = new BookSaleRunable4();
// 开启3个线程
Thread t1 = new Thread(booksr);
Thread t2 = new Thread(booksr);
Thread t3 = new Thread(booksr);
t1.start();
t2.start();
t3.start();
} }

结果输出:

Thread-0 卖出一本书,剩余书籍:9
Thread-0 卖出一本书,剩余书籍:8
Thread-0 卖出一本书,剩余书籍:7
Thread-2 卖出一本书,剩余书籍:6
Thread-2 卖出一本书,剩余书籍:5
Thread-2 卖出一本书,剩余书籍:4
Thread-1 卖出一本书,剩余书籍:3
Thread-1 卖出一本书,剩余书籍:2
Thread-1 卖出一本书,剩余书籍:1

Java并发基础--线程安全的更多相关文章

  1. Java并发基础--线程通信

    java中实现线程通信的四种方式 1.synchronized同步 多个线程之间可以借助synchronized关键字来进行间接通信,本质上是通过共享对象进行通信.如下: public class S ...

  2. Java 并发基础——线程安全性

    当线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么久称这个类是线程安全的. 在线程 ...

  3. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  4. java并发编程 线程基础

    java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...

  5. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  6. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  7. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

  8. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  9. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

随机推荐

  1. 一、安装 IntelliJ IDEA

    首先,双击打开 IntelliJ IDEA 的快捷方式: 在此,需要说明: 如果咱们的电脑曾经安装过 IntelliJ IDEA,并且你在卸载 IntelliJ IDEA 的时候没有删除其配置文件目录 ...

  2. P1171 售货员的难题 暴力dp

    题面 著名的TSP问题,NPC问题 对于数据大的情况,我们可以使用一系列近似算法进行寻找解. 对于数据规模小的情况,我们可以直接暴力dp 一开始写了一个dfs,然后就被n=20的数据卡爆了 #incl ...

  3. 【洛谷P1169】[ZJOI2007]棋盘制作

    棋盘制作 题目链接 这个题是[USACO5.3]巨大的牛棚Big Barn和玉蟾宫的结合 一道顶两道毒瘤! 题解: 首先,棋盘有两种选法: 1.任意白格(x,y) (x+y)%2=0 ,任意黑格(x, ...

  4. 最短路径问题:弗洛伊德算法(Floyd)

    Floyd算法 1.定义概览 Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被 ...

  5. tomcat的下载和启动

    1.下载和解压 把下载好的压缩包解压,放到想刚的目录里 看一下目录: 目录介绍: 2. 启动和访问 启动步骤: 如果startup.bat 双击执行脚本一闪而过,解决方法: 第一可能是:没有配置JAV ...

  6. UGUI富文本

    <b>text</b>  --粗体 <i>text<i>     --斜体 <size=10>text</size>   --自 ...

  7. checkbox 全选

    <template> <div class="hello"> <table> <tr> <th><input ty ...

  8. 'sessionFactory' or 'hibernateTemplate' is required

    网上都是说在dao中未注入  sessionFactory,然而我有 于是排除 @Autowired public FlightDaoImpl(@Qualifier(value = "ses ...

  9. python pandas库——pivot使用心得

    python pandas库——pivot使用心得 2017年12月14日 17:07:06 阅读数:364 最近在做基于python的数据分析工作,引用第三方数据分析库——pandas(versio ...

  10. 【转载】vue.js实现格式化时间并每秒更新显示功能示例

    引用:https://www.jb51.net/article/143351.htm 这篇文章主要介绍了vue.js实现格式化时间并每秒更新显示功能,结合实例形式分析了vue.js时间格式化显示与基于 ...