码迷,mamicode.com
首页 > 编程语言 > 详细

线程同步问题

时间:2019-06-28 16:47:35      阅读:129      评论:0      收藏:0      [点我收藏+]

标签:修改   解决   style   unlock   独立   才有   资源   技术   不同   

基本的进程线程概念

线程与进程的区别
  拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
  调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。
  通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

系统中的进程线程模型是这样的:

技术图片

 

线程解决同步问题

线程创建的两种方法:
1. 继承runnable接口传入Thread
实例对象作为单实例传递给Thread 所以创建多个线程共用一个实例对象,里面的属性也都是共享的。

2. 继承Thread

多线程工作原理:
线程1:操作步骤–工作内存–总内存。
线程2:操作步骤–工作内存–总内存。
线程3:操作步骤–工作内存–总内存。

流程:
每个线程从总内存中读取内存到工作内存,然后在操作步骤中对共享数据进行操作,将修改后的数据更新到工作内存,再更新到总内存。

要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。

   多个线程之间是不能直接传递数据进行交互的,它们之间的交互只能通过共享变量来实现。拿上面的例子来说明,在多个线程之间共享了Count类的一个实例,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程栈),工作内存存储了主内存count对象的一个副本,当线程操作count对象时,首先从主内存复制count对象到工作内存中,然后执行代码count.count(),改变了num值,最后用工作内存中的count刷新主内存的 count。当一个对象在多个工作内存中都存在副本时,如果一个工作内存刷新了主内存中的共享变量,其它线程也应该能够看到被修改后的值,此为可见性。

   多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款,此为有序性。

存在成员变量(全局变量)的类用于多线程时是不安全的,不安全体现在这个成员变量可能发生非原子性的操作。

方法一:

volatile:针对工作内存–总内存
    修饰共享变量,保持数据可见性,适用范围:只读取,不修改。
意思:当多线程访问共享数据的时候只是读取,没有修改,这个时候就可以使用volatile。

    只需在共享数据前面加上volatile修饰,即可实现线程同步。

方法二:

synchronized同步方法和同步块
这两种方式都要用到synchronized关键字。
synchronized:锁住代码块,保持顺序性。
意思就是 使得线程内是顺序执行,线程间是随机。
流程:
  每个对象都有一个“锁标志”,当这个对象的一个线程访问这个对象的某个synchronized 数据时,这个对象的所有被synchronized 修饰的数据将被上锁(因为“锁标志”被当前线程拿走了),
  只有当前线程访问完它要访问的synchronized 数据时,当前线程才会释放“锁标志”,这样同一个对象的其它线程才有机会访问synchronized 数据。

  synchronized 关键字用于保护共享数据 目的是使同一个对象的多个线程,在某个时刻只有其中的一个线程可以访问这个对象的synchronized 数据。
  所以尽量只给操作共享数据的代码块加synchronized锁一般使用同步块,锁的内容少,开销小。

方法三:

重入锁ReentrantLock

ReentrantLock() : 创建一个ReentrantLock实例
ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

  lock() : 获得锁
  unlock() : 释放锁

注:关于Lock对象和synchronized关键字的选择:
  a.最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
  b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
  c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
  for (int j = 0; j < 10000; j++) {
    lock.lock(); // 看这里就可以
    //lock.lock(); ①
    try {
      i++;
      } finally {
    lock.unlock(); // 看这里就可以
    //lock.unlock();②
    }
  }
}



注意:需要手动来释放重入锁

方法四:

局部变量实现线程同步(非阻塞型)
使用局部变量实现线程同步:采用以”空间换时间”的方法,与前面方法采用的”时间换空间”不同
ThreadLocal:
  注:ThreadLocal与同步机制
    a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。

  题外话:线程的释放锁重点内容
    wait():使一个线程处于等待状态,并且释放所持有的对象的lock。

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

 

线程同步问题

标签:修改   解决   style   unlock   独立   才有   资源   技术   不同   

原文地址:https://www.cnblogs.com/fengwenkai/p/11103330.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!