线程同步问题
Posted fengwenkai
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程同步问题相关的知识,希望对你有一定的参考价值。
基本的进程线程概念
线程与进程的区别
拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、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确定唤醒哪个线程,而且不是按优先级。
以上是关于线程同步问题的主要内容,如果未能解决你的问题,请参考以下文章