Android 开发经验谈:多线程你了解多少?
i= i+1;
如上代码很简单,在单线程中i就等于i+1,执行不会出问题。
但是在多线程中就会有问题。
在说多线程之前我从别人的博客里摘了一段文字:
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
也就是说有两个线程来执行这段代码,在两个线程中都有缓存,计算时都会把i的值写入到自己的缓存中,计算后再进行同步,这就导致计算结果与预期不符。
为了解决每个线程中都用自己的缓存,于是就采用了关键字volatile
volatile关键字就会强制变量都从主存中获取
但是呢...由于volatile加减并非线程安全,volatile并不适用于计算。
当然也有其专用的使用范围。
volatile(java5):可以保证多线程下的可见性;
读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。
写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。
所以在Android 单例模式中,我大多都采用volatile关键字修饰
当然如何让i=i+1呢?
咱们可以采用如下几个synchronized, AtomicInteger,lock
AtomicInteger:
一个提供原子操作的Integer的类。 一种线程安全的加减操作接口, 相比 synchroized、lock 高效.
例子:
private final AtomicInteger mThreadNumber = new AtomicInteger(1);
mThreadNumber.getAndIncrement()
这便可以保证线程安全。
synchronized:
synchronized是java内置关键字,对象锁,在jvm层面的锁,只能把一块代码锁着,并不能获取到锁的状态。
悲观锁机制,线程获取的是独占锁,当一个线程进入时后,其他线程被阻塞等待。
synchronized修饰方法时要注意以下几点:
1. synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这2. 在定义接口方法时不能使用synchronized关键字。
3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
误区:
非静态:无论你是对方法标注synchronized还是对类标注synchronized,都是对对象的锁。
例:
public class Test {
int i = 0;
public synchronized void methon1(){
i++;
System.out.println("methon1 "+i);
}
public synchronized void methon2(){
i++;
System.out.println("methon2 "+i);
}
}
public static void main(String[] args){
Test test = new Test();
new Thread(new Runnable() {
@Override
public void run() {
int i =0;
do {
test.methon1();
i++;
}while (i<100);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int i =0;
do {
test.methon2();
i++;
}while (i<100);
}
}).start();
}
}
输出:
methon2 1
methon1 2
methon2 3
methon1 4
methon2 5
methon1 6
methon2 7
methon1 8
methon2 9
methon1 10
methon2 11
methon1 12
methon2 13
所以你要是想对某个方法单独进行方法锁,就必须锁另一个的对象,此时你用静态方法可能比较好呢。
例:
Object object = new Object();
public void methon2(){
synchronized (object){
i++;
System.out.println("methon2 "+i);
}
}
lock:
需要指定起始位置与终止位置
lock 与unlock,锁与释放锁
一般为了健壮性在finally中调用unlock
相比synchronized 性能低效,操作比较重量级
乐观锁机制:假设没有锁,如果有冲突失败,则重试。
Lock可以知道线程有没有成功获取到锁。
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
sleep/wait区别
sleep()方法,我们首先要知道该方法是属于Thread类中的native方法。而wait()方法,则是属于Object类中的。
wait方法需要锁来控制
sleep 方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
wait/notify机制
方法名称 | 描述 |
---|---|
notify() | 随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程” |
notifyAll() | 使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现 |
wait() | 使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细力度的控制,可以达到纳秒 |