博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
004-线程同步问题引出、同步问题解决、死锁、生产者与消费者
阅读量:7057 次
发布时间:2019-06-28

本文共 12446 字,大约阅读时间需要 41 分钟。

一、同步问题引出

1、示例:

class MyThread9 implements Runnable {    private int ticket = 5;    @Override    public void run() {
// 主方法 for (int i = 0; i < 10; i++) { if (ticket > 0) System.out.println(Thread.currentThread().getName() + ",ticket=" + (ticket--)); } }}
View Code

main方法

MyThread9 mt = new MyThread9();        Thread threadA = new Thread(mt, "票贩子A");        Thread threadB = new Thread(mt, "票贩子B");        Thread threadC = new Thread(mt, "票贩子C");        threadA.start();        threadB.start();        threadC.start();

输出:

  票贩子B,ticket=5
  票贩子B,ticket=2
  票贩子B,ticket=1
  票贩子C,ticket=3
  票贩子A,ticket=4

2、改造一下,增加卖票延迟

class MyThread9 implements Runnable {    private int ticket = 5;    @Override    public void run() {
// 主方法 for (int i = 0; i < 10; i++) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",ticket=" + (ticket--)); } } }}
View Code

main方法调用

MyThread9 mt = new MyThread9();        Thread threadA = new Thread(mt, "票贩子A");        Thread threadB = new Thread(mt, "票贩子B");        Thread threadC = new Thread(mt, "票贩子C");        threadA.start();        threadB.start();        threadC.start();
View Code

输出:

  票贩子A,ticket=5
  票贩子C,ticket=4
  票贩子C,ticket=2
  票贩子C,ticket=1
  票贩子B,ticket=4
  票贩子A,ticket=3

3、分析

  

  那么这样的操作就属于线程的不同步操作,所以发现多个线程操作时必须要考虑到线程不同步问题。

  一般情况下,如果一个对象的状态是可变的,同时它又是共享的(即至少可被多于一个线程同时访问),则它存在线程安全问题,总结来说:无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。

  避免出现线程安全问题三个方案:1、禁止跨线程访问变量。2、使状态变量为不可变。3、使用同步。(前两个方法实际就是放弃使用多线程,不可取,我们需要解决问题,而非逃避问题)。

 二、线程同步处理

1、实现同步操作

  整个代码发现有个逻辑错误,判断是否有票,休眠,卖票分为三步,那么实际上每一个线程如果要执行卖票的话,其他线程应该等待当前线程执行完毕后才可以进入。

2、问题解决

   

3、线程同步的方法

1.原子性

示例:

public class Generator {      private long value = 1;       public void getValue(){           value++;      }  }

  此处的value++就是非原子操作,它是先取值、再加1、最后赋值的一种机制,是一种“读-写-改”的操作,原子操作需要保证,在对对象进行修改的过程中,对象的状态不能被改变!这个现象我们用一个名词:竞争条件来描述。换句话说,当计算结果的正确性依赖于运行时中相关的时序或者多线程的交替时,会产生竞争条件。(即想得到正确的答案,要依赖于一定的运气。正如value++中的情况,如果我的运气足够好,在对value进行操作时,无其它任何线程同时对其操作)  

1.1.相关的示例如,单例模式【会有线程安全问题】

public static Singleton getInstance() {      if (instance == null) {          instance = new Singleton();      }      return instance;  }

饿汉式单例【可以使用,无线程安全问题】

public class MySingleton {            private static MySingleton instance = new MySingleton();            private MySingleton(){}            public static MySingleton getInstance() {          return instance;      }        }
View Code

懒汉式单例【synchronized 同步方法】

public class MySingleton {            private static MySingleton instance = null;            private MySingleton(){}            public synchronized static MySingleton getInstance() {          try {               if(instance == null){
//懒汉式 //创建实例之前可能会有一些准备性的耗时工作 Thread.sleep(300); instance = new MySingleton(); } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
View Code

  这种实现方式的运行效率会很低。同步方法效率低

懒汉式单例【synchronized 同步代码块】

public class MySingleton {            private static MySingleton instance = null;            private MySingleton(){}            //public synchronized static MySingleton getInstance() {      public static MySingleton getInstance() {          try {               synchronized (MySingleton.class) {                  if(instance == null){
//懒汉式 //创建实例之前可能会有一些准备性的耗时工作 Thread.sleep(300); instance = new MySingleton(); } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
View Code

实现将全部的代码都被锁上了,同样的效率很低下

懒汉模式【Double Check Locking 双检查锁机制(推荐)】

为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成

public class MySingleton {            //使用volatile关键字保其可见性      volatile private static MySingleton instance = null;            private MySingleton(){}             public static MySingleton getInstance() {          try {                if(instance == null){
//懒汉式 //创建实例之前可能会有一些准备性的耗时工作 Thread.sleep(300); synchronized (MySingleton.class) { if(instance == null){
//二次检查 instance = new MySingleton(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
View Code

在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

通过原子变量AtomicLong 

  将其转为线程安全的,在java.util.concurrent.atomic包下有一些将数字和对象引用进行原始状态转换的类,改造这个程序:

public class Generator {      private final AtomicLong value = new AtomicLong(0);            public void getValue(){          value.incrementAndGet();      }  }

2、实现锁,同步代码块或同步方法来解决。内部锁(synchronized)

1.同步代码块

使用synchronized定义的代码块就称为同步代码块,但是在进行同步的时候需要设置一个同步对象,往往可以使用this同步当前对象

2.示例 

class MyThread9 implements Runnable {    private int ticket = 5;    @Override    public void run() {
// 主方法 for (int i = 0; i < 10; i++) { synchronized (this) { if (ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",ticket=" + (ticket--)); } } } }}

main方法

MyThread9 mt = new MyThread9();        Thread threadA = new Thread(mt, "票贩子A");        Thread threadB = new Thread(mt, "票贩子B");        Thread threadC = new Thread(mt, "票贩子C");        threadA.start();        threadB.start();        threadC.start();

输出

  票贩子A,ticket=5
  票贩子C,ticket=4
  票贩子B,ticket=3
  票贩子C,ticket=2
  票贩子C,ticket=1

注意:加入同步之后整个代码执行速度变慢 。

异步操作属于非线程安全操作,而同步操作属于线程安全操作

2.同步方法

示例 

class MyThread10 implements Runnable {    private int ticket = 5;    @Override    public void run() {
// 主方法 for (int i = 0; i < 10; i++) { this.sale(); } } public synchronized void sale() { if (ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ",ticket=" + (ticket--)); } }}
View Code

main方法

MyThread10 mt = new MyThread10();        Thread threadA = new Thread(mt, "票贩子A");        Thread threadB = new Thread(mt, "票贩子B");        Thread threadC = new Thread(mt, "票贩子C");        threadA.start();        threadB.start();        threadC.start();
View Code

输出

  票贩子A,ticket=5
  票贩子C,ticket=4
  票贩子B,ticket=3
  票贩子C,ticket=2
  票贩子C,ticket=1
在多线程访问同一资源时,一定要考虑到数据同步问题,使用synchronized

  Java提供了完善的内置锁机制:synchronized块。在方法前synchronized关键字或者在方法中加synchronized语句块,锁住的都是方法中包含的对象,如果线程想获得所,那么就需要进入有synchronized关键字修饰的方法或块。采用synchronized有时会带来一定的性能下降。但是,无疑synchronized是最简单实用的同步机制,基本可以满足日常需求。内部锁扮演了互斥锁(即mutex)的角色,意味着同一时刻至多只能有一个线程可以拥有锁,当线程A想去请求一个被线程B占用的锁时,必然会发生阻塞,直到B释放该锁,如果B永不释放锁,A将一直等待下去。这种机制是一种基于调用的机制(每调用,即per-invocation),就是说不管哪个线程,如果调用声明为synchronized的方法,就可获得锁(前提是锁未被占用)。

 

三、死锁分析

  死锁是一种不确定的状态,对于死锁的操作应该出现的越少越好。

  多个线程同时访问一个资源可能带来什么问题。以及产生的附加问题
    1.多个线程同时访问一个资源时必须考虑同步,可以使用synchronized定义同步代码块或同步方法
    2.程序中如果过多的同步,那么将产生死锁
  结论,
    如果看见了synchronized声明的 方法,是一个同步方法,属于线程安全的操作
    但是性能不会特别高。

四、生产者与消费者【同步、重复】

线程间的通许问题以及object类的支持

1、基本模型

  希望实现一种数据的生产和取出的操作形式,即:有多个不同的线程,这样的线程对象,分为生产者线程和消费者线程。理想状态是生产者每生产完一条完整数据之后,消费者就要取走这个数据,并且进行消费。

2、示例:

  title="张三",content="帅哥"

  title="李四",content="衰哥"

3、分析

  

4、示例代码,初期实现 

package com.lhx.thread.impl;class Info { private String title; private String content; public String getTitle() {  return title; } public void setTitle(String title) {  this.title = title; } public String getContent() {  return content; } public void setContent(String content) {  this.content = content; }}class Productor implements Runnable { private Info info = null; public Productor(Info info) {  this.info = info; } @Override public void run() {  for (int i = 0; i < 50; i++)   if (i % 2 == 0) {    this.info.setTitle("张三");    try {     Thread.sleep(100);    } catch (InterruptedException e) {     e.printStackTrace();    }    this.info.setContent("衰哥");   } else {    this.info.setTitle("李四");    try {     Thread.sleep(100);    } catch (InterruptedException e) {     e.printStackTrace();    }    this.info.setContent("帅哥");   } }}class Consumer implements Runnable { private Info info = null; public Consumer(Info info) {  this.info = info; } public void run() {  for (int i = 0; i < 50; i++) {   try {    Thread.sleep(100);   } catch (InterruptedException e) {    e.printStackTrace();   }   System.out.println(info.getTitle() + "-->" + info.getContent());  } }}public class TestDemo2 { public static void main(String[] args) throws Exception {  Info info = new Info();  Productor productor = new Productor(info);  Consumer consumer = new Consumer(info);  new Thread(productor).start();  new Thread(consumer).start(); }}
View Code

输出

  李四-->衰哥
  张三-->帅哥
  李四-->衰哥
  张三-->帅哥

5、以上执行存在问题:

  1.数据错位

  2.重复生产,重复取出

6、解决不同步问题

  使用synchronized,操作Info来完成

改进 

package com.lhx.thread.impl;class Info { private String title; private String content; public synchronized void set(String title, String content) {  this.title = title;  try {   Thread.sleep(100);  } catch (InterruptedException e) {   e.printStackTrace();  }  this.content = content; } public synchronized void get() {  System.out.println(title + "-->" + content); }}class Productor implements Runnable { private Info info = null; public Productor(Info info) {  this.info = info; } @Override public void run() {  for (int i = 0; i < 50; i++)   if (i % 2 == 0) {    this.info.set("张三", "衰哥");   } else {    this.info.set("李四", "帅哥");   } }}class Consumer implements Runnable { private Info info = null; public Consumer(Info info) {  this.info = info; } public void run() {  for (int i = 0; i < 50; i++) {   info.get();  } }}public class TestDemo2 { public static void main(String[] args) throws Exception {  Info info = new Info();  Productor productor = new Productor(info);  Consumer consumer = new Consumer(info);  new Thread(productor).start();  new Thread(consumer).start(); }}
View Code

数据的同步操作都交给了同步方法完成,

可以看到同步问题解决了,但是重复操作没有

7、 解决重复操作问题

增加等待与唤醒的处理机制,这样的操作可以使用Object类。Object提供以下操作

  等待:public final void wait() throws InterruptedException
  唤醒第一个等待线程:public final native void notify();
  唤醒全部等待线程: public final native void notifyAll();
代码【标准答案】 

package com.lhx.thread.impl;class Info { private String title; private String content; private boolean flag = true; // flag true 表示生产数据,但是不允许取走数据 // flag false 表示取走数据,但是不允许生产数据 public synchronized void set(String title, String content) {  if (flag == false) {   try {    super.wait();// 等待   } catch (InterruptedException e) {    e.printStackTrace();   }  }  this.title = title;  try {   Thread.sleep(100);  } catch (InterruptedException e) {   e.printStackTrace();  }  this.content = content;  flag = false;  super.notify(); } public synchronized void get() {  if (flag == true) {
// 此时应该生产 try { super.wait();// 等待 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(title + "-->" + content); flag = true; super.notify(); }}class Productor implements Runnable { private Info info = null; public Productor(Info info) { this.info = info; } @Override public void run() { for (int i = 0; i < 50; i++) if (i % 2 == 0) { this.info.set("张三", "衰哥"); } else { this.info.set("李四", "帅哥"); } }}class Consumer implements Runnable { private Info info = null; public Consumer(Info info) { this.info = info; } public void run() { for (int i = 0; i < 50; i++) { info.get(); } }}public class TestDemo2 { public static void main(String[] args) throws Exception { Info info = new Info(); Productor productor = new Productor(info); Consumer consumer = new Consumer(info); new Thread(productor).start(); new Thread(consumer).start(); }}
View Code

解释一下sleep和wait区别

  sleep 是thread类定义的方法,在休眠到一定时间之后将自己唤醒
  wait是object类定义的方法,表示线程等待执行,必须通过notify()、notifyAll()来进行唤醒

 

转载地址:http://zcgol.baihongyu.com/

你可能感兴趣的文章
beyond compare 与git diff整合
查看>>
收集的几篇关于Asp.Net处理原理和URL重写的几篇文章
查看>>
Bootstrap Table总结
查看>>
物联网如何跳出“看起来很美”?
查看>>
linux命令行后台运行与调回
查看>>
TryEnterCriticalSection
查看>>
用 Java 实现断点续传参考 (HTTP)
查看>>
VB6.0 取 毫秒级 时间戳
查看>>
unity KeyCode各键值说明
查看>>
Delphi中编写无输出函数名的DLL文件
查看>>
centos的基本命令04
查看>>
Codeforces Round #313 (Div. 2) D. Equivalent Strings(字符串+递归)
查看>>
20个案例掌握PL/SQL 基础
查看>>
windows下查看端口占用以及进程名称
查看>>
CH 5101 最长公共上升子序列
查看>>
水平分库分表的关键问题及解决思路
查看>>
Spring Boot 探索系列 - 自动化配置篇
查看>>
Jar包转成Dll的方式(带嵌套的jar也能做) (转)
查看>>
Linux-centos-7.2-64bit 安装配置mysql
查看>>
[javaEE] 控制浏览器缓存资源
查看>>