5. 线程安全问题与线程同步

多线程编程是有趣且复杂的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在运行过程中偶尔会出现问题,那也是由于我们的代码有问题导致的。当多个线程访问同一个数据时,非常容易出现线程安全问题。

5.1 线程安全问题

所谓线程安全问题,其实就是多线程在并发访问的时候,对共享内存中的共享对象属性进行修改所导致的数据冲突问题
Keyword:

  • 并发访问
  • 共享内存
  • 共享对象 (共享内存中的共享对象)
  • 属性的修改 (方法是在栈中,不会有并发问题)

JVM内存模型:

线程之间可以共享的内存有:
1.堆内存的数据(同一个对象的属性)
2.方法区中的数据(字符串,常量,类的静态属性)
共享数据尽量不要用静态的,因为静态生命周期太长了

解决方法:
1.同步代码块
2.同步方法

5.2 安全问题演示

关于线程安全问题,有一个经典的问题:卖票问题。卖票的基本流程很简单:看是否还有票,如果有就可以卖。

(1)资源类

public class TicketService {public int total = 10;public void saleTicket(){total--;}public boolean hasTicket(){return total > 0;}public int getTotal(){return total;}
}

(2)线程类

//买票窗口类
public class Saler extends Thread{private TicketService ts;//锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象private static final Object obj = new Object();//一个窗口对应一个public Saler(){ts = new TicketService();}//多个窗口对应一个public Saler(TicketService ts) {this.ts = ts;}//todo 1.下面的代码会引发线程安全问题:public void run(){while(ts.hasTicket()){//这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {ts.saleTicket();System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());} catch (Exception e) {System.err.println(e.getMessage());}}System.out.println("没有票了");}
}

Demo1 同一个对象作为共享资源

public class TestSaler {public static void main(String[] args) {TicketService ts = new TicketService();//todo 注意s1和s2两个线程 共享同一个ts对象Saler s1 = new Saler(ts);Saler s2 = new Saler(ts);s1.start();s2.start();}
}
测试结果之一:
Thread-0买了一张票,余票:8
Thread-1买了一张票,余票:8
Thread-1买了一张票,余票:6
Thread-0买了一张票,余票:6
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-0买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-1买了一张票,余票:0
没有票了
Thread-0买了一张票,余票:-1
没有票了

这里就发生了线程安全问题,当多个线程多条语句(size()和remove())访问共享数据(这里TicketService的list存票的集合)时,就会发生线程安全问题。

解释1:“票卖超了”如何发生的,不是先判断是否有票,才买的吗?

在run()中:
ts.saleTicket();
System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());
这两行代码不是原子性的;线程1执行了第一句后,由线程2执行,然后执行了第二句,又切换到线程1执行第二句

Demo2 不同对象的静态属性作为共享资源

public class TestSaler {public static void main(String[] args) {//s3和s4 两个线程对象中各自拥有两个TicketService对象,并且共享资源为total实例属性因此不算线程安全问题// 如果将TicketService类中的total属性改为静态的,仍然是线程安全问题的Saler s3 = new Saler();Saler s4 = new Saler();s3.start();s4.start();}
}

(1)s3和s4 两个线程对象中各自拥有两个TicketService对象,并且共享资源为total实例属性因此不算线程安全问题


Thread-0买了一张票,余票:9
Thread-1买了一张票,余票:9
Thread-1买了一张票,余票:8
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-1买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-1买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-1买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-0买了一张票,余票:3
Thread-0买了一张票,余票:2
Thread-1买了一张票,余票:2
Thread-1买了一张票,余票:1
Thread-0买了一张票,余票:1
Thread-1买了一张票,余票:0
没有票了
Thread-0买了一张票,余票:0
没有票了

(2)如果将TicketService类中的total属性改为静态的,仍然属于线程安全问题


public class TicketService {//改为静态的public static int total = 10;public void saleTicket(){total--;}public boolean hasTicket(){return total > 0;}public int getTotal(){return total;}
}
Thread-0买了一张票,余票:8
Thread-1买了一张票,余票:8
Thread-1买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:2
Thread-0买了一张票,余票:1
没有票了
Thread-1买了一张票,余票:0
没有票了

5.3 安全问题解决

如何解决线程安全问题呢?

1.同步监视器对象(锁对象)

解决思路:将线程中多条操作共享数据的语句封装成一个原子性操作,在线程执行这段原子性操作期间,其他线程不可以参与执行。

为了解决这个问题,Java的多线程支持引入同步监视器来解决这个问题。
使用同步监视器的方式有两种:同步代码块和同步方法。

任何共享资源的操作都需要放进同步代码块/方法中

2.同步代码块

  • 同步代码块的语法格式如下:
synchronized(同步监视器对象){//.....
}

上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。

3.锁对象的选择

(1)任何对象都可以作为同步监视器对象。
(2)只要保证共享资源的这几个线程,只要锁的是同一个同步监视器对象即可
保证是同一个锁对象要么是堆内存中同一个对象;要么是方法区中同一个静态的对象;

3.1 共享资源对象作为同步监视器对象

package com.atguigu.part03;public class Saler extends Thread{private TicketService ts;public Saler(TicketService ts) {super();this.ts = ts;}public void run(){while(true){synchronized (ts) {if(ts.hasTicket()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {String buy = ts.buy();System.out.println("购买票:" + buy);} catch (Exception e) {System.out.println(e.getMessage());}}else{System.out.println("没有票了");break;}}}}
}

要求在创建多个线程的时候,都需要传递同一个TicketService对象作为构造器参数;

3.2 选择this对象作为同步监视器对象

  • 如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。
  • 如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。

(1)任务类

package com.atguigu.part03;public class Window implements Runnable{private TicketService ts;public Window(TicketService ts) {super();this.ts = ts;}public void run(){while(true){synchronized (this) {if(ts.hasTicket()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {String buy = ts.buy();System.out.println("购买票:" + buy);} catch (Exception e) {System.out.println(e.getMessage());}}else{System.out.println("没有票了");break;}}}}
}

(2)

package com.atguigu.part03;public class TestWindow {public static void main(String[] args) {TicketService ts = new TicketService();Window w = new Window(ts);Thread t1 = new Thread(w);Thread t2 = new Thread(w);t1.start();t2.start();}
}

3.3 静态对象作为同步监视器对象

上锁范围
//买票窗口类
public class Saler extends Thread{private TicketService ts;//锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象private static final Object obj = new Object();public Saler(){ts = new TicketService();}public Saler(TicketService ts) {this.ts = ts;}//todo 2.上锁   锁的范围不能太大也不能太小,太大会导致别的线程不容易获得锁,太小无法保证线程安全// 下面这段代码所得范围太大,只能一个线程买票public void run(){synchronized (obj){while(ts.hasTicket()){//这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {ts.saleTicket();System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());} catch (Exception e) {System.err.println(e.getMessage());}}}System.out.println("没有票了");}

执行结果:

package _15_多线程._06_线程安全问题;public class TestSaler {public static void main(String[] args) {Saler s3 = new Saler();Saler s4 = new Saler();s3.start();s4.start();}
}Thread-0买了一张票,余票:9
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-0买了一张票,余票:3
Thread-0买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-0买了一张票,余票:0
没有票了
没有票了

(2)缩小锁定范围:

package _15_多线程._06_线程安全问题;//买票窗口类
public class Saler extends Thread{private TicketService ts;//锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象private static final Object obj = new Object();//一个窗口对应一个public Saler(){ts = new TicketService();}//多个窗口对应一个public Saler(TicketService ts) {this.ts = ts;}//todo 3。缩小锁的范围://       任何关于公共资源的操作都要放进同步代码块中//       但是循环又不能放进同步代码块中,因此切换判断条件,将判断条件放进循环内部!public void run(){//while(ts.hasTicket()){while(true){synchronized (obj){if(ts.hasTicket()){//这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {ts.saleTicket();System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());} catch (Exception e) {System.err.println(e.getMessage());}}else break;}}System.out.println("没有票了");}
}
package _15_多线程._06_线程安全问题;public class TestSaler {public static void main(String[] args) {Saler s3 = new Saler();Saler s4 = new Saler();s3.start();s4.start();}
}Thread-0买了一张票,余票:9
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-0买了一张票,余票:0
没有票了
没有票了

4. 同步方法

概念 Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法;

同步方法用于将对共享数据的操作封装到同步方法中,一般由共享类来提供,比如集合中的Vector和HashTable

同步方法的特点

  • 对于共享对象的synchronized方法 而言,同一时刻只能有一个线程访问
  • 如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法, 其它线程不能同时访问这个对象中任何一个synchronized方法.

这时,不同的对象实例的 synchronized方法是不相干扰的。
也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;

(1) 同步方法的锁对象

对于同步方法而言,无须显式指定同步监视器,也无法指定同步监视器。

  • 静态方法的同步监视器对象是提供此方法的类的Class对象
  • 非静态方法的同步监视器对象是提供此方法的this对象 (A.test() A是方法的提供者对象)

如果要在Thread类中编写同步方法封装对共享资源的操作,必须是静态同步方法;
如果在Runnable实现类中编写同步方法,可以是非静态的;

(1) 案例1 非静态同步方法

package _15_多线程._06_线程安全问题3;public class Test {public static void main(String[] args) {Ticket t1 = new Ticket("t1");Ticket t2 = new Ticket("t2");Ticket t3 = new Ticket("t3");t1.start();t2.start();t3.start();}
}
class Ticket extends Thread {public static int total = 10;public Ticket(String name){super(name);}//todo 同步方法,非静态的同步方法的锁对象为this// 当前为三个线程对象,因此锁对象不是同一个// 因此无法解决线程安全问题// 将此方法改为静态的即可private synchronized void saleOneTicket(){if(total > 0){System.out.println(getName()+"买了一张票");total--;System.out.println("余票"+total);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public void run(){while(total>0){saleOneTicket();}}
}

执行结果:

t1买了一张票
余票9
t2买了一张票
余票8
t3买了一张票
余票7
t1买了一张票
余票6
t3买了一张票
余票5
t2买了一张票
余票4
t2买了一张票
余票3
t1买了一张票
余票2
t3买了一张票
余票1
t1买了一张票
余票0
t2买了一张票
t3买了一张票
余票-2
余票-1

分析:由于是Thread类提供的非静态同步方法,因此锁对象为Thread对象,main方法中创建了三个Thread对象,因此锁对象根本就不是同一个,就无法保证线程安全;

(2)案例2 用静态同步方法解决线程安全问题

package _15_多线程._06_线程安全问题3;public class Test2 {public static void main(String[] args) {Ticket1 t1 = new Ticket1("t1");Ticket1 t2 = new Ticket1("t2");Ticket1 t3 = new Ticket1("t3");t1.start();t2.start();t3.start();}
}class Ticket1 extends Thread {public static int total = 10;public Ticket1(String name){super(name);}//todo 改进:改成静态的方法即可// 同步方法就要求 共享资源必须为静态的private static synchronized void saleOneTicket(){if(total > 0){System.out.println(Thread.currentThread().getName()+"买了一张票");total--;System.out.println("余票"+total);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public void run(){while(total>0){saleOneTicket();}}
}

执行结果:

t1买了一张票
余票9
t3买了一张票
余票8
t3买了一张票
余票7
t3买了一张票
余票6
t3买了一张票
余票5
t3买了一张票
余票4
t3买了一张票
余票3
t2买了一张票
余票2
t3买了一张票
余票1
t3买了一张票
余票0

不要对线程安全类的所有方法都加同步,只对那些会影响竞争资源(即共享资源)的方法进行同步即可。而且也要注意同步方法的默认同步的监视器对象对于多个线程来说是否是同一个。

5. 线程安全的集合

Java提供了很多线程安全的集合;这些集合提供了很多同步方法即有synchronized修饰,以此保证操作集合时线程安全。

6. 释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
1、释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程的同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
  • 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起并释放锁

2、不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
    sleep()和yield()只释放CPU,不释放锁
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。
  • 应尽量避免使用suspend()和resume()这样的过时来控制线程

疑问

package com.atguigu.part03;import java.util.ArrayList;public class TicketService {private ArrayList<String> list;public TicketService(){list = new ArrayList<String>();list.add("01车01A");list.add("01车01B");list.add("01车01C");list.add("01车01D");list.add("01车01E");list.add("01车02A");list.add("01车02B");list.add("01车02C");list.add("01车02D");list.add("01车02E");}public synchronized boolean hasTicket(){return list.size()>0;}public synchronized String buy(){try {return list.remove(0);} catch (IndexOutOfBoundsException e) {throw new RuntimeException("票卖超了");}}
}
package com.atguigu.part03;public class Saler extends Thread{private TicketService ts;public Saler(TicketService ts) {super();this.ts = ts;}public void run(){while(ts.hasTicket()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}try {String buy = ts.buy();System.out.println("购买票:" + buy);} catch (Exception e) {System.err.println(e.getMessage());}}System.out.println("没有票了");}
}package com.atguigu.part03;public class TestSaler {public static void main(String[] args) {TicketService ts = new TicketService();Saler s1 = new Saler(ts);Saler s2 = new Saler(ts);s1.start();s2.start();}
}

上面run()中调用了两个同步方法,但是同步方法作用域不是就一段么,为啥能保证线程同步?

7. 死锁问题

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

package com.atguigu.part03;public class TestDeadLock {public static void main(String[] args) {Object g = new Object();Object m = new Object();Owner s = new Owner(g,m);Customer c = new Customer(g,m);new Thread(s).start();new Thread(c).start();}
}
class Owner implements Runnable{private Object goods;private Object money;public Owner(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (goods) {System.out.println("先给钱");synchronized (money) {System.out.println("发货");}}}
}
class Customer implements Runnable{private Object goods;private Object money;public Customer(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (money) {System.out.println("先发货");synchronized (goods) {System.out.println("再给钱");}}}
}

【Java 并发编程】【05】线程安全问题与线程同步相关推荐

  1. 【Java并发编程】之二:线程中断

    [Java并发编程]之二:线程中断 使用interrupt()中断线程 ​ 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...

  2. Java并发编程进阶——多线程的安全与同步

    多线程的安全与同步 多线程的操作原则 多线程 AVO 原则 A:即 Atomic,原子性操作原则.对基本数据类型变量的读和写是保证原子性的,要么都成功,要么都失败,这些操作不可中断. V:即 vola ...

  3. java并发编程基础—生命周期与线程控制

    一.线程生命周期 线程被创建启动以后,他既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New).就绪(Runnable).运行(Running).阻塞(Bloc ...

  4. 【Java并发编程】面试必备之线程池

    什么是线程池 是一种基于池化思想管理线程的工具.池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需.比如我们的对象池,数据库连接池等. 线程池好处 我们为什么要使用线程池,直接new ...

  5. Java并发编程:面试必备之线程池

    什么是线程池 是一种基于池化思想管理线程的工具.池化技术:池化技术简单点来说,就是提前保存大量的资源,以备不时之需.比如我们的对象池,数据库连接池等. 线程池好处 我们为什么要使用线程池,直接new ...

  6. Java并发编程(2):线程中断(含代码)

    使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回.这 ...

  7. Java并发编程(05):悲观锁和乐观锁机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.资源和加锁 1.场景描述 多线程并发访问同一个资源问题,假如线程A获取变量之后修改变量值,线程C在此时也获取变量值并且修改,两个线程同时并 ...

  8. 【Java并发编程实战14】构建自定义同步工具(Building-Custom-Synchronizers)

    JDK包含许多存在状态依赖的类,例如FutureTask.Semaphore和BlockingQueue,他们的一些操作都有前提条件,例如非空.任务已完成等. 创建状态依赖类的最简单的房就是在JDK提 ...

  9. Java并发编程(十)设计线程安全的类

    待续... 线程安全的类 之前学了很多线程安全的知识,现在导致了我每次用一个类或者做一个操作我就会去想是不是线程安全的.如果每次都这样的考虑的话就很蛋疼了,这里的思路是,将现有的线程安全组件组合为更大 ...

  10. java并发编程实践 part 01 --gt; 线程创建方式

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_26654727/article/details/78013989 最近在尝试重新复习一段关于多 ...

最新文章

  1. C# Socket系列一 简单的创建socket的监听
  2. Oracle RAC在思科UCS上的应用
  3. LVM逻辑卷的管理--创建LVM、扩容,快照实战
  4. 如何将nodejs项目程序部署到阿里云服务器上
  5. 解决Coldfusion连接MySQL数据库的问题
  6. java barrier_Java - Latch和Barrier的区别
  7. scrapy爬取汽车之家宝马5系图片
  8. Maven学习总结(46)——Maven跳过单元测试的两种方法及其区别(-Dmaven.test.skip=true与-DskipTests)
  9. 多段图的动态规划算法(C/C++)
  10. c语言burg算法,用Burg算法提升空间调制傅里叶光谱仪分辨率与定阶方法
  11. 2021华为悦盒EC6110-T-M-拆机-强刷固件及教程
  12. CNKI文献pdf批量下载
  13. 电路串联和并联图解_迷惑我们很久的串联/并联谐振电路(多图、详解)
  14. 合取范式可满足性问题:CDCL(Conflict-Driven Clause Learning)算法详解
  15. MAC怎样显示隐藏文件
  16. android经典项目案例开发
  17. 原生JS实现 ‘Tab栏切换’,‘手风琴’,‘轮播图’效果
  18. 一起了解Windows——用Win10自带工具录制视频
  19. 静态HTML旅行主题网页设计与实现——联途旅游网服务平台网(39页)html css javascript
  20. 应用宝YSDK道具直接支付解决和遇到的坑

热门文章

  1. SQL的update语句
  2. 树莓派获取LAN ip地址并发送到微信
  3. 关于shell unix下,直接执行shell与sh 执行 或加 . 脚本 的区别及含义
  4. Presto Connector 实现原理
  5. python里使用协程和StreamReader、StreamWriter来创建echo服务端
  6. 我的微信扫描二维码实现登录のJava
  7. ROS从入门到精通5-5:局部路径规划插件开发案例(以DWA算法为例)
  8. 嘉为蓝鲸CMP云管平台入选Gartner《中国云管理工具市场指南》
  9. 基于二阶盲源分离方法执行模态识别研究(Matlab代码实现)
  10. Must call super constructor in derived class before accessing or returning from derived const