线程同步问题

基本的进程线程概念

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

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

image

 

线程解决同步问题

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

2. 继承Thread

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

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

1 public class Count {  
2     private int num;  
3     public void count() {  
4         for(int i = 1; i <= 10; i++) {  
5             num += i;  
6         }  
7         System.out.println(Thread.currentThread().getName() + "-" + num);  
8     }  
9 }  
 1 public class ThreadTest {  
 2     public static void main(String[] args) {  
 3         Runnable runnable = new Runnable() {  
 4             Count count = new Count();  
 5             public void run() {  
 6                 count.count();  
 7             }  
 8         };  
 9 
10         for(int i = 0; i < 10; i++) {  
11             new Thread(runnable).start();  
12         }  
13     }  
14 }  

 

要说明线程同步问题首先要说明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与同步机制都是为了解决多线程中相同变量的访问冲突问题。

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

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

 

转载于:https://www.cnblogs.com/fengwenkai/p/11103330.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值