一,概述
1,什么是线程同步?
当使用多个线程来访问同一个数据时,这个数据在被一个线程访问完成前不允许被其他线程访问。这就叫同步。
2,什么情况下需要同步?
* 当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.
* 如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.
3,多线程同步的方法
多线程同步的方法一共有五个:
1. 使用synchronized 关键字构成同步方法。
2. 使用synchronized 关键字构成同步代码块。
3. 使用ReentrantLock类构成同步代码块。
4. 使用volatile关键字修饰变量。
5. 如果使用ThreadLocal对象管理变量。
以上5种方法中使用最多的是前三种,第5种在Handler机制中有使用,第4种很少使用。下面详细讲述前三种方法。
二,不使用同步时出现的问题
下面写一个demo,模拟线程不同步带来的问题。
首先定义一个成员变量和一个成员方法,如下:
public int num = 100;
public void print(){
if(num >0){
try {
Thread.sleep(1000);//sleep(1000)可以让结果更清晰
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
Log.d(TAG,"num==============="+num);
}
}
代码很简单,这里不做解释。
然后在点击事件中开启两个子线程,都调用print()方法,代码如下:
public void click(View v){
new Thread(new Runnable() {
@Override
public void run() {
while (true){
print();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
print();
}
}
}).start();
}
我们期望的结果中不能出现num=0的情况,而在实际情况中有很大的概率出现了num=0。
问题原因分析:
当有多个线程并发时,哪个线程被执行是随机的。假设此时num=1,线程1抢到了执行权,线程1可以进入到if代码块中,当线程1正在执行sleep(1000)时执行权被线程2抢走了,而此时num仍然等于1,所以线程2也可以进入if代码块中,线程2中代码完全执行,此时num就等于0。此时线程1又获取到了执行权,由于线程1已经进入了if代码块中,此时会继续向下执行num–,所以就出现了num等于0的情况。
显然这是一个严重的问题,假如这是一个卖票系统,当票数等于0时还在卖票,就会出现问题。所以我们必须解决解决办法就是使用同步方法。
三, 使用synchronized 关键字构成同步方法。
改写后的print方法如下:
public synchronized void print(){//使用synchronized 关键字修饰方法
if(num >0){
try {
Thread.sleep(1000);//sleep(1000)可以让结果更清晰
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
Log.d(TAG,"num==============="+num);
}
}
此时再次调用print方法就不会出现num=0的情况。
解析:当方法使用synchronized 关键字修饰后,这个方法称为同步方法。当一个线程调用同步方法时,只有这个方法执行完毕后才会释放执行权。此时就实现了同步的作用。
四,使用synchronized 关键字构成同步代码块
改写后的print方法如下:
public void print(){
synchronized (this) {//添加同步锁
if (num > 0) {
try {
Thread.sleep(1000);//sleep(1000)可以让结果更清晰
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
Log.d(TAG, "num===============" + num);
}
}
}
此时再次调用print方法也不会出现num=0的情况。
解析:此时的方法虽然不是同步方法,但被synchronized 关键字包裹的代码块称为同步代码块。当一个线程调用方法执行到同步代码块时,只有同步代码块执行完毕后才会释放执行权。此时就实现了同步的作用。
五,使用ReentrantLock类构成同步代码块
ReentrantLock是java5.0后提供的一个类,它有两个重要的方法lock和unlock,使用这两个方法也可以给代码块加锁。使用示例如下:
private ReentrantLock lock = new ReentrantLock();
public void print(){
lock.lock();//加锁
if (num > 0) {
try {
Thread.sleep(1000);//sleep(1000)可以让结果更清晰
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
Log.d(TAG, "num===============" + num);
}
lock.unlock();//解锁
}
此时再次调用print方法也不会出现num=0的情况。
解析:ReentrantLock 类具有锁机制,当调用lock方法时这个线程就不会使用执行器,只有遇到unlock时才有可能失去执行权,所以这种方法也可以实现线程的同步。
注:当遇到unlock时这个线程才有可能失去执行权,但不一定会失去,有可能会继续拥有执行权。
六,Synchronized与ReentrantLock的区别
Synchronized是java中提供的一个关键字。ReentrantLock是java5.0后提供的一个类,这个类是java.util.concurrent.locks包中的,具有lock和unlock两个重要的方法。
二者在性能上的区别是:
1,Synchronized可能出现死锁现象。
2,ReentrantLock具有lockInterruptibly方法,可以中断锁等候。若线程一获得了锁,线程1执行的内容又比较多,此时若线程2不想等待,就可以中断锁,可以优化性能。而且不会出现死锁现象。在线程间通讯时ReentrantLock提供了Condition实例对象,让等待唤醒机制操作更加灵活。
七,线程安全
1,什么是线程安全?
线程安全是指:同一个数据在某一个时间段只能被一个线程操作。或者说一个数据被一个线程访问结束后才能被其他线程访问。
2,线程安全与线程同步的区别?
线程同步是一种技术方法。
线程安全是一种实现的效果。
使用了线程同步保证了线程安全。
3,常见的线程安全的例子
- Vector是线程安全的,ArrayList是线程不安全的
- StringBuffer是线程安全的,StringBuilder是线程不安全的
- Hashtable是线程安全的,HashMap是线程不安全的